5.2 Execution Context & Implementing Business Logic in Plug-ins
Key Takeaways
- IServiceProvider.GetService retrieves IPluginExecutionContext, ITracingService, and IOrganizationServiceFactory -- the three services nearly every plug-in needs.
- InputParameters["Target"] holds an Entity (Create/Update) or EntityReference (Delete/Retrieve) containing only the attributes the caller actually sent, not the full record.
- context.Depth increments each time an operation re-enters the pipeline; checking it prevents infinite recursive loops when a plug-in writes back to the table it's handling.
- Throwing InvalidPluginExecutionException rolls back the transaction and surfaces the message text to the calling client -- the standard way to reject an operation from business logic.
- A plug-in class instance is cached and reused across many requests, so Execute must not store request-specific state in instance-level fields.
Every plug-in's Execute(IServiceProvider serviceProvider) method receives a single object -- IServiceProvider -- that is the gateway to everything the plug-in needs: the details of the request that triggered it, a way to call back into Dataverse, and a way to log diagnostic output. Getting the right services out of that provider, and using the execution context correctly, is the mechanical core of every plug-in you'll write for PL-400.
Pulling Services from IServiceProvider
Three services matter for almost every plug-in:
public void Execute(IServiceProvider serviceProvider)
{
IPluginExecutionContext context =
(IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
ITracingService tracing =
(ITracingService)serviceProvider.GetService(typeof(ITracingService));
IOrganizationServiceFactory factory =
(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = factory.CreateOrganizationService(context.UserId);
// ...
}
IOrganizationServiceFactory and the IOrganizationService it produces are covered fully in 5.3; this section focuses on IPluginExecutionContext and ITracingService.
IPluginExecutionContext: What the Plug-in Knows
The execution context describes the request that triggered the plug-in. The properties tested most often on the exam:
MessageName-- the SDK message that fired the pipeline ("Create","Update","Delete", or a custom message name). A single plug-in class can be registered against multiple messages, so checkingMessageNamelets one class branch its logic.PrimaryEntityNameandPrimaryEntityId-- the logical name and GUID of the table and record the message targets.InputParameters-- aParameterCollection(essentially a dictionary) holding the data sent by the caller. ForCreateandUpdate, the key"Target"holds anEntitywith only the attributes the caller actually set. ForDeleteandRetrieve,"Target"holds anEntityReferenceinstead, since there's no attribute payload.OutputParameters-- a collection a plug-in can write to so calling code (or a custom API response, 5.5) receives data back. ForRetrieve, the platform populatesOutputParameters["BusinessEntity"]with the retrieved record.StageandMode-- the numeric pipeline stage (10/20/30/40) and execution mode (0 = synchronous, 1 = asynchronous) the plug-in is currently running under, useful when one class is registered at more than one stage.Depth-- an integer that starts at 1 and increments each time the current operation triggers another operation that re-enters the pipeline (for example, a plug-in that callsIOrganizationService.Updateon the same table it's handlingUpdatefor). Dataverse enforces a maximum depth and throws once it's exceeded, but well-written plug-ins don't rely on hitting that ceiling -- they checkcontext.Depth > 1(or a stricter threshold appropriate to the design) near the top ofExecuteand return early to avoid unnecessary recursive work and needless risk of the platform's own loop-protection firing.UserIdandInitiatingUserId-- the security context the operation is running under, versus the user who originally initiated the request chain.SharedVariables-- a dictionary for passing data between plug-ins registered on different stages of the same pipeline execution (a pre-operation step can stash a value that a post-operation step reads).
Reading and Writing Business Logic
The most common pattern is: read Target from InputParameters, inspect or validate its attributes, and -- if running at pre-operation (stage 20) -- write corrected or defaulted values back onto Target so they're included in the save:
Entity target = (Entity)context.InputParameters["Target"];
if (target.Contains("creditlimit") &&
target.GetAttributeValue<Money>("creditlimit").Value < 0)
{
throw new InvalidPluginExecutionException("Credit limit cannot be negative.");
}
target["statuscode"] = new OptionSetValue(100000001);
Entity here is late-bound: attributes are accessed by string logical name through an indexer, with no compile-time checking. Many teams instead generate early-bound entity classes (via the Plug-in Registration Tool's code generation or pac modelbuilder) that expose strongly typed properties (target.CreditLimit) -- better IntelliSense and compile-time safety, at the cost of needing to regenerate the classes whenever the schema changes. The exam expects you to recognize both styles.
Throwing InvalidPluginExecutionException with a clear message is the standard way for a plug-in to reject an operation: the platform rolls back the transaction and surfaces the message text to the calling client, visible as an error dialog in a model-driven app or as the error in a failed Web API response.
Tracing and Statelessness
ITracingService.Trace("message", args) writes diagnostic output captured in the plug-in trace log -- enabled per-organization or per-step -- the primary debugging tool for plug-ins, since you can't attach a live debugger to the sandboxed execution environment.
One design point the exam likes to probe: a plug-in class is instantiated once and reused across many executions for performance. That means Execute must not store request-specific data in instance (class-level) fields -- anything specific to one invocation belongs in local variables inside Execute, or it will leak between unrelated requests running on the same cached instance.
A plug-in registered on the Update message needs to read the values the caller actually submitted for the record being updated. Which execution context member holds that data?
A plug-in class is registered once, but Dataverse reuses the same instance to handle many separate Update requests over time. What is the correct design implication?