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í
- běží na
Updateentityaccount, - doporučená fáze je
PostOperation/ stage40, synchronně, - nic neaktualizuje — pouze čte hodnotu a případně vyhodí výjimku,
- protože běží v synchronní PostOperation pipeline, vyhozená výjimka zruší transakci,
- hodnotu
accountnumberhledá v pořadí: Target, pokud atribut přišel v update payloadu,PostImage, pokud je zaregistrovaný,Retrieveaktuálního account záznamu jako fallback.
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.