Plugins on Dataverse run on an event pipeline with stages, modes, and execution orders. The Plugin Registration Tool exposes the controls; the documentation is scattered. Here is the consolidated mental model.
The four stages
- PreValidation (10): runs before the database transaction starts. Use for input validation that should not roll back a transaction.
- PreOperation (20): runs inside the transaction, before the database write. Use to mutate the Target.
- PostOperation (40): runs inside the transaction, after the write. Use for derived updates that should commit atomically.
- PostOperation Async (40, mode async): runs after the transaction commits, on the async service. Use for fire-and-forget side effects.
Sync vs Async
Sync plugins block the user response until they complete. Async plugins queue and run later. Async failures do not roll back the originating transaction. Pick async only when failure is recoverable later.
The execution order tiebreaker
Multiple plugins on the same stage and message run in ExecutionOrder numerical order, ascending. Ties are broken by registration time. If you depend on order, set explicit numbers (10, 20, 30) rather than leaving them at default. Never assume registration order.
Pre and Post images
Images snapshot the record state for the plugin. PreImage holds the values before the operation; PostImage holds them after. Without an image, your plugin must explicitly retrieve the record, costing an extra database hit.
public void Execute(IServiceProvider sp) {
var ctx = (IPluginExecutionContext)sp.GetService(typeof(IPluginExecutionContext));
var pre = ctx.PreEntityImages.Contains("Image") ?
ctx.PreEntityImages["Image"] : null;
if (pre == null) throw new InvalidPluginExecutionException("Image not registered");
}
Register only the columns you need on the image. Wide images are expensive at scale.
Filtering attributes
The “Filtering Attributes” field on a step limits the plugin to fire only when those attributes change. Without it, the plugin runs on every Update of the entity, even when irrelevant fields change. This is the single biggest performance miss in average plugin code.
The recursion trap
A plugin that updates the same record it triggered on will fire itself recursively. Dataverse stops it after 8 levels deep. Code defensively:
if (ctx.Depth > 1) return; // skip nested execution
Impersonation
Plugins run by default as the user who initiated the action. They can impersonate via IOrganizationServiceFactory.CreateOrganizationService(userId). Pass null for system context. System context bypasses security and audits, which is sometimes what you want and sometimes a privacy violation. Decide consciously.
Tracing is non-negotiable
Production plugins without tracingService.Trace() calls are blind. When they fail at 3am, you have no diagnostic. Trace at entry, at every branch, and at exit. The cost is minimal, the value is enormous.
Sandbox limits to remember
- 2-minute execution timeout.
- 1MB serialized response size.
- Outbound HTTP allowed; outbound to specific Microsoft endpoints is preferred.
- No file system access.
What to do this week
Pull your plugin step inventory. For each step, verify a Filtering Attribute is set unless the plugin truly needs every update. The unfiltered ones are silent CPU consumption.