Dynamics 365 plugin – povinný accountnumber na Account po Update

Tento plugin validuje po aktualizaci záznamu account, že pole accountnumber je vyplněné. Pokud hodnota chybí, je null, prázdná nebo obsahuje jen mezery, vyhodí přesně tuto chybu:

AccountNumber je povinný!

Chování

Doporučená registrace plugin step

Vlastnost Hodnota
Message Update
Primary Entity account
Stage PostOperation (40)
Execution Mode Synchronous
Run in User Context Calling User
Filtering Attributes nechat prázdné, pokud má validace proběhnout při každém update accountu

Doporučená Post Image

Vlastnost Hodnota
Image Type Post Image
Alias PostImage
Attributes accountnumber

PostImage je doporučená kvůli výkonu. Kód ale funguje i bez ní, protože jako fallback udělá Retrieve.

Zdrojový kód

Soubor: src/AccountNumberRequiredPostUpdatePlugin.cs

using System;
using Microsoft.Xrm.Sdk;

namespace D365.Plugins.Account
{
    /// <summary>
    /// Plugin: validates that the <c>accountnumber</c> field on the Account entity is filled.
    /// Stage: PostOperation (40), Message: Update, Entity: account, Mode: Synchronous.
    /// Required PostImage alias: "PostImage" with attribute "accountnumber".
    /// </summary>
    public sealed class AccountNumberRequiredPostUpdatePlugin : IPlugin
    {
        private const string TargetEntityLogicalName = "account";
        private const string AccountNumberAttribute = "accountnumber";
        private const string PostImageAlias = "PostImage";
        private const string ValidationErrorMessage = "AccountNumber je povinný!";

        public void Execute(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new ArgumentNullException(nameof(serviceProvider));
            }

            var tracing = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            tracing.Trace(
                "AccountNumberRequiredPostUpdatePlugin: start. Message={0}, Stage={1}, Mode={2}, PrimaryEntityName={3}, Depth={4}",
                context.MessageName,
                context.Stage,
                context.Mode,
                context.PrimaryEntityName,
                context.Depth);

            if (!string.Equals(context.MessageName, "Update", StringComparison.OrdinalIgnoreCase))
            {
                tracing.Trace("Message is not 'Update'. Exiting without action.");
                return;
            }

            if (!string.Equals(context.PrimaryEntityName, TargetEntityLogicalName, StringComparison.OrdinalIgnoreCase))
            {
                tracing.Trace("PrimaryEntityName is not 'account'. Exiting without action.");
                return;
            }

            // No Depth short-circuit is needed here: this plugin does not write data,
            // so it cannot recursively trigger itself. Validation should run even when
            // the account update was initiated by another plugin/workflow.
            string accountNumber = ResolveAccountNumber(context, serviceProvider, tracing);

            if (string.IsNullOrWhiteSpace(accountNumber))
            {
                tracing.Trace("Validation failed: 'accountnumber' is missing/null/empty/whitespace. Throwing InvalidPluginExecutionException.");
                throw new InvalidPluginExecutionException(ValidationErrorMessage);
            }

            tracing.Trace("Validation passed. accountnumber='{0}'.", accountNumber);
        }

        private static string ResolveAccountNumber(
            IPluginExecutionContext context,
            IServiceProvider serviceProvider,
            ITracingService tracing)
        {
            if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity target)
            {
                if (target.Contains(AccountNumberAttribute))
                {
                    var value = target.GetAttributeValue<string>(AccountNumberAttribute);
                    tracing.Trace("'accountnumber' resolved from Target: '{0}'.", value ?? "<null>");
                    return value;
                }

                tracing.Trace("Target does not contain 'accountnumber'. Falling back to PostImage.");
            }
            else
            {
                tracing.Trace("Target is not present or is not an Entity. Falling back to PostImage.");
            }

            if (context.PostEntityImages != null
                && context.PostEntityImages.Contains(PostImageAlias)
                && context.PostEntityImages[PostImageAlias] != null)
            {
                var postImage = context.PostEntityImages[PostImageAlias];
                if (postImage.Contains(AccountNumberAttribute))
                {
                    var value = postImage.GetAttributeValue<string>(AccountNumberAttribute);
                    tracing.Trace("'accountnumber' resolved from PostImage: '{0}'.", value ?? "<null>");
                    return value;
                }

                tracing.Trace("PostImage does not contain 'accountnumber'. Falling back to Retrieve.");
            }
            else
            {
                tracing.Trace("PostImage alias '{0}' not registered or empty. Falling back to Retrieve.", PostImageAlias);
            }

            return RetrieveAccountNumberFromService(context, serviceProvider, tracing);
        }

        private static string RetrieveAccountNumberFromService(
            IPluginExecutionContext context,
            IServiceProvider serviceProvider,
            ITracingService tracing)
        {
            var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service = factory.CreateOrganizationService(context.UserId);

            tracing.Trace("Retrieving account {0} to read 'accountnumber'.", context.PrimaryEntityId);

            var account = service.Retrieve(
                TargetEntityLogicalName,
                context.PrimaryEntityId,
                new Microsoft.Xrm.Sdk.Query.ColumnSet(AccountNumberAttribute));

            var value = account?.GetAttributeValue<string>(AccountNumberAttribute);
            tracing.Trace("'accountnumber' resolved from Retrieve: '{0}'.", value ?? "<null>");
            return value;
        }
    }
}

Poznámka k Depth

Plugin nemá Depth > 1 stopku, protože sám neprovádí žádný Update, takže se nemůže zacyklit. Díky tomu validuje i situace, kdy účet aktualizuje jiný plugin nebo workflow.