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?
context.OutputParameters["Target"]
context.PostEntityImages["PostImage"]
context.SharedVariables["Target"]
context.InputParameters["Target"]
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?
The Execute method must avoid storing request-specific data in instance-level fields, since those fields persist across unrelated invocations of the shared instance
The plug-in should cache the IOrganizationService in a static field so every request reuses the same connection
The plug-in should store the current context.Depth in an instance field so later requests can check it
The plug-in should always throw InvalidPluginExecutionException if it detects it is running on a cached instance
Sections you finish are checked off in the contents.