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.
Last updated: July 2026

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 checking MessageName lets one class branch its logic.
  • PrimaryEntityName and PrimaryEntityId -- the logical name and GUID of the table and record the message targets.
  • InputParameters -- a ParameterCollection (essentially a dictionary) holding the data sent by the caller. For Create and Update, the key "Target" holds an Entity with only the attributes the caller actually set. For Delete and Retrieve, "Target" holds an EntityReference instead, 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. For Retrieve, the platform populates OutputParameters["BusinessEntity"] with the retrieved record.
  • Stage and Mode -- 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 calls IOrganizationService.Update on the same table it's handling Update for). Dataverse enforces a maximum depth and throws once it's exceeded, but well-written plug-ins don't rely on hitting that ceiling -- they check context.Depth > 1 (or a stricter threshold appropriate to the design) near the top of Execute and return early to avoid unnecessary recursive work and needless risk of the platform's own loop-protection firing.
  • UserId and InitiatingUserId -- 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.

Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D