Writing Your First Dataverse Plug-in in C#: A Complete Walkthrough
Your first Dataverse plug-in usually starts as a small validation and becomes production code faster than expected. The platform gives you a powerful execution pipeline, but it also gives you enough rope to create recursion, slow saves, and vague support tickets. This walkthrough builds the right mental model before the first assembly is registered.
A plug-in is a pipeline participant, not a background script
A C# plug-in implements IPlugin and runs inside the Dataverse event pipeline. It responds to messages such as Create, Update, Delete, Assign, or custom actions. The code is invoked by the platform with an IServiceProvider, which gives access to execution context, tracing, and organization service factories.
The key shift is that plug-ins are not form scripts. They run for model-driven apps, Power Automate, imports, SDK calls, integrations, and anything else that triggers the registered message. That makes them the right place for server-side data integrity rules.
Because plug-ins run in the platform pipeline, they must be fast, deterministic, and stateless. Do not cache user-specific state in instance fields. Do not wait for external systems during a synchronous save unless the business explicitly accepts the latency and failure coupling.
That discipline matters from day one because plug-ins become shared infrastructure. A small shortcut in the first version is copied into every later validation.
The pipeline stage decides what your code can safely do
Dataverse exposes several stages that matter for day-to-day plug-in work. Pre-validation happens before the main database transaction in many scenarios. Pre-operation happens inside the transaction before the platform writes the row. Post-operation happens after the core operation and can run synchronously or asynchronously.
| Stage | Typical use | Transaction behavior | Common mistake |
|---|---|---|---|
| Pre-validation | Reject invalid requests early | Often before transaction | Reading values that are not loaded |
| Pre-operation | Set fields before save | Inside transaction | Calling Update on the same target |
| Post-operation sync | Create dependent data immediately | Inside transaction | Doing slow external calls |
| Post-operation async | Non-critical follow-up | Outside user wait path | Assuming instant completion |
For a first plug-in, pre-operation on Update is a good training ground. You can modify the incoming Target entity without making a separate service update, which avoids extra pipeline executions and reduces recursion risk.
Context, Target, and services are the core objects
IPluginExecutionContext tells you the message, stage, depth, user, correlation ID, and input parameters. For Create and Update, InputParameters usually contains Target, which is an Entity. On Update, Target includes only changed columns, not the full row.
That last detail is where many first plug-ins fail. If you need old values or unchanged columns, register a pre-image or post-image. Do not assume the attribute exists just because the table has the column.
IOrganizationServiceFactory creates an IOrganizationService. Passing context.UserId gives you a service that runs as the calling user. Passing a different user ID is possible in some designs, but should be explicit and justified. ITracingService is your production lifeline, especially in sandboxed online environments.
using System;
using Microsoft.Xrm.Sdk;
public sealed class SetAccountReviewCategoryPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var tracing = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = factory.CreateOrganizationService(context.UserId);
if (context.Depth > 1)
{
tracing.Trace("Skipping to avoid recursive execution. Depth: {0}", context.Depth);
return;
}
if (!string.Equals(context.MessageName, "Update", StringComparison.OrdinalIgnoreCase))
{
return;
}
if (!context.InputParameters.Contains("Target") ||
!(context.InputParameters["Target"] is Entity target) ||
target.LogicalName != "account")
{
return;
}
if (!target.Contains("revenue"))
{
tracing.Trace("Revenue was not changed; no category update required.");
return;
}
var revenue = target.GetAttributeValue<Money>("revenue")?.Value ?? 0m;
target["new_reviewcategory"] = revenue >= 1000000m
? new OptionSetValue(100000000)
: new OptionSetValue(100000001);
tracing.Trace("Review category set for account {0}. Revenue: {1}", target.Id, revenue);
}
}
This plug-in expects registration on account Update in pre-operation, filtered to the revenue column. It sets a custom option set value directly on Target, so Dataverse writes it as part of the same update.
Registration turns code into platform behavior
Build the assembly, then register it with the Plugin Registration Tool or equivalent deployment automation. For a first manual registration, connect to the environment, register the assembly, register a step, and set the message, primary table, stage, execution mode, and filtering attributes.
Message: Use Update for this example.
Primary table: Use account.
Stage: Use pre-operation.
Mode: Use synchronous only because the field should be set during the save.
Filtering attributes: Include revenue so the plug-in does not run on every account update.
Images: Add a pre-image only if your logic needs old or unchanged values.
Assembly: Contoso.Plugins
Type: SetAccountReviewCategoryPlugin
Message: Update
Primary table: account
Stage: Pre-operation
Execution mode: Synchronous
Filtering attributes: revenue
After registration, test through a model-driven form and through an API or import path if that is how the table is used. A plug-in that only works from the form is usually relying on client behavior by accident.
Production plug-ins are small, traced, and defensive
Keep plug-ins stateless. Dataverse may reuse class instances, so instance fields can become shared state across executions. Put constants, immutable configuration names, and helper methods in code, but do not store per-run values outside the Execute method scope.
Use depth checks, but do not use depth as a lazy fix for bad design. If your plug-in updates the same row in post-operation and then exits when depth is greater than one, you may hide a recursion problem instead of designing around it. Prefer pre-operation Target updates when changing the same row.
Never use Thread.Sleep in a plug-in. It blocks platform resources and punishes the user. If you need delayed work, use async plug-ins, Power Automate, Azure Functions, or a queue-based integration.
Trace deliberately. Include correlation-friendly messages and important decision points, but do not log sensitive values. When the support ticket arrives, a few precise trace lines are better than a wall of object dumps.
<secureconfig>
<setting name="EnableVerboseTracing" value="false" />
</secureconfig>
The first plug-in sets your team standard
A first Dataverse plug-in should not be clever. It should show the pattern your team wants repeated: validate context, check message and table, inspect Target, use images when needed, trace decisions, and avoid unnecessary service calls. Register narrowly and test from more than one entry point.
Once that standard exists, plug-ins become a dependable part of your architecture instead of mysterious code that runs somewhere in the platform. The discipline is simple: small synchronous rules in the transaction, heavier work outside the user wait path, and enough tracing that production behavior is explainable.
Keep reading
Dual-Write vs Virtual Entities vs OData: Choosing the Right F&O–Dataverse Pattern
Compare dual-write, virtual entities, OData, and DMF for Finance and Operations to Dataverse integration with latency and failure tradeoffs.
Low-Code Plug-ins in Dataverse: Server-Side Logic Without C#
Learn when to use Dataverse low-code plug-ins with Power Fx for transactional server-side logic, and when C# plug-ins or flows still win.
Business Rules vs Power Fx vs Plug-ins: Where to Put Dataverse Logic
A practical Dataverse logic placement guide comparing business rules, Power Fx, JavaScript form scripts, and C# plug-ins for scale.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
No comments yet. Be the first.