Writing Plug-ins: Business Logic, Service & Registration
Write production-quality Dataverse plug-ins in C#. Learn to use the Organisation Service for CRUD operations, optimise performance, and register plug-ins with the Plug-in Registration Tool.
Building real plug-ins
Think of a plug-in as a quality inspector on a factory line.
As each product passes through, the inspector checks it (validation), stamps it with a batch number (data enrichment), and records the inspection (audit logging). If the product fails inspection, the inspector stops the line (throws an exception).
Your Dataverse plug-in does the same for data: it uses the Organisation Service to read, create, update, and delete records. It uses tracing to log what it is doing. And it must be fast β synchronous plug-ins block the user, so every millisecond counts.
Using the Organisation Service
The Organisation Service is your plug-inβs gateway to Dataverse data:
public class ShipmentProcessor : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Get services
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = serviceFactory.CreateOrganizationService(context.UserId);
var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
// Get the target entity (being created/updated)
Entity target = (Entity)context.InputParameters["Target"];
tracingService.Trace("Processing shipment: " + target.Id);
// READ β retrieve a related record
Entity account = service.Retrieve("account",
target.GetAttributeValue<EntityReference>("customerid").Id,
new ColumnSet("name", "creditlimit"));
// VALIDATE β check business rules
decimal creditLimit = account.GetAttributeValue<Money>("creditlimit")?.Value ?? 0;
decimal shipmentValue = target.GetAttributeValue<Money>("totalvalue")?.Value ?? 0;
if (shipmentValue > creditLimit)
{
throw new InvalidPluginExecutionException(
$"Shipment value ({shipmentValue:C}) exceeds customer credit limit ({creditLimit:C}).");
}
// ENRICH β add calculated data to the target before save
target["estimateddelivery"] = DateTime.UtcNow.AddDays(3);
target["processedby"] = context.UserId.ToString();
// CREATE β a related audit record
Entity auditRecord = new Entity("shipment_audit");
auditRecord["shipmentid"] = new EntityReference("shipment", target.Id);
auditRecord["action"] = "Created";
auditRecord["timestamp"] = DateTime.UtcNow;
service.Create(auditRecord);
tracingService.Trace("Shipment processing complete.");
}
}
Common Organisation Service operations
| Operation | Method | Example |
|---|---|---|
| Create | service.Create(entity) | Create an audit log record |
| Retrieve | service.Retrieve(name, id, columns) | Load a related account |
| Update | service.Update(entity) | Update a field on another record |
| Delete | service.Delete(name, id) | Remove a temporary record |
| RetrieveMultiple | service.RetrieveMultiple(query) | Query records with FetchXML or QueryExpression |
| Execute | service.Execute(request) | Run specialised requests (Assign, SetState, etc.) |
Performance optimisation
| Practice | Why | Bad Example | Good Example |
|---|---|---|---|
| Minimise queries | Each query = network round-trip | 3 separate Retrieve calls | One RetrieveMultiple with filter |
| Use ColumnSet wisely | Loading all columns is slow | new ColumnSet(true) β all columns | new ColumnSet("name", "email") β only needed |
| Avoid loops with individual operations | N creates = N round-trips | foreach with service.Create() | ExecuteMultipleRequest for batch |
| Check Depth | Prevent infinite recursion | No depth check | if (context.Depth > 2) return; |
| Use tracing, not exceptions for logging | Exceptions are expensive | throw for non-critical warnings | tracingService.Trace() for diagnostics |
| Keep synchronous plug-ins fast | 2-minute timeout, blocks user | Call external API synchronously | Move external calls to async plug-in or Azure Function |
Exam tip: Keep plug-ins lean β no batch requests
If your plug-in needs to create, update, or delete multiple records, keep each operation direct and focused. Do not use ExecuteMultipleRequest inside plug-ins β Microsoft explicitly advises against batch request types in plug-in/workflow code because they add overhead within the pipeline transaction.
Instead, keep plug-ins lean:
- Limit the number of service calls (aim for under 5)
- If you need to process many records, move that work to an Azure Function or an asynchronous pattern (post-operation async plug-in, queue, external processor)
- For external clients (console apps, integrations),
ExecuteMultipleRequestIS recommended β just not inside plug-ins
The exam may present a slow plug-in scenario. The correct fix is usually: reduce the number of operations, move heavy work out of the plug-in, or switch to async execution.
Plug-in Registration Tool (PRT)
The PRT registers your compiled plug-in assembly in Dataverse and configures when it fires.
Registration steps
- Register Assembly β Upload the compiled DLL to Dataverse
- Register Step β Configure the message (Create, Update), entity (shipment), stage (Pre/Post), and execution mode (sync/async)
- Register Image β Configure Pre/Post images with specific attributes
Step configuration
| Setting | Options | Impact |
|---|---|---|
| Message | Create, Update, Delete, Retrieve, etc. | Which operation triggers the plug-in |
| Primary Entity | Any Dataverse table | Which table the plug-in monitors |
| Stage | Pre-validation, Pre-operation, Post-operation | When in the pipeline it runs |
| Execution Mode | Synchronous, Asynchronous | Blocks the user or runs in background |
| Filtering Attributes | Specific columns (Update only) | Only trigger when these fields change |
| Execution Order | Number (1, 2, 3β¦) | Order when multiple plug-ins fire on the same step |
Filtering attributes save performance
For Update messages, always set filtering attributes. Without them, the plug-in fires on EVERY update β even if the user only changed the Description field and your plug-in only cares about Status.
With filtering attributes set to βstatuscodeβ, the plug-in only fires when Status changes. This dramatically reduces unnecessary executions and improves overall system performance.
Kai's synchronous plug-in creates 50 audit records when a shipment is processed. It works but takes 30 seconds, frustrating users. What is the most effective approach?
π¬ Video coming soon
Next up: Custom APIs & Business Events β creating reusable Dataverse endpoints and configuring event publishing.