Domain 5 β€” Module 2 of 9 22%
16 of 26 overall
Domain 5: Extend the Platform Free ⏱ ~15 min read

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

Simple explanation

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

OperationMethodExample
Createservice.Create(entity)Create an audit log record
Retrieveservice.Retrieve(name, id, columns)Load a related account
Updateservice.Update(entity)Update a field on another record
Deleteservice.Delete(name, id)Remove a temporary record
RetrieveMultipleservice.RetrieveMultiple(query)Query records with FetchXML or QueryExpression
Executeservice.Execute(request)Run specialised requests (Assign, SetState, etc.)

Performance optimisation

PracticeWhyBad ExampleGood Example
Minimise queriesEach query = network round-trip3 separate Retrieve callsOne RetrieveMultiple with filter
Use ColumnSet wiselyLoading all columns is slownew ColumnSet(true) β€” all columnsnew ColumnSet("name", "email") β€” only needed
Avoid loops with individual operationsN creates = N round-tripsforeach with service.Create()ExecuteMultipleRequest for batch
Check DepthPrevent infinite recursionNo depth checkif (context.Depth > 2) return;
Use tracing, not exceptions for loggingExceptions are expensivethrow for non-critical warningstracingService.Trace() for diagnostics
Keep synchronous plug-ins fast2-minute timeout, blocks userCall external API synchronouslyMove 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), ExecuteMultipleRequest IS 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

  1. Register Assembly β€” Upload the compiled DLL to Dataverse
  2. Register Step β€” Configure the message (Create, Update), entity (shipment), stage (Pre/Post), and execution mode (sync/async)
  3. Register Image β€” Configure Pre/Post images with specific attributes

Step configuration

SettingOptionsImpact
MessageCreate, Update, Delete, Retrieve, etc.Which operation triggers the plug-in
Primary EntityAny Dataverse tableWhich table the plug-in monitors
StagePre-validation, Pre-operation, Post-operationWhen in the pipeline it runs
Execution ModeSynchronous, AsynchronousBlocks the user or runs in background
Filtering AttributesSpecific columns (Update only)Only trigger when these fields change
Execution OrderNumber (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.

Question

How do you throw a user-friendly error from a plug-in?

Click or press Enter to reveal answer

Answer

Throw an InvalidPluginExecutionException with a clear message: throw new InvalidPluginExecutionException('Shipment value exceeds credit limit.'). This displays the message to the user in the app. Regular exceptions show a generic error. NEVER use InvalidPluginExecutionException for logging β€” use ITracingService.Trace() instead.

Click to flip back

Question

What is the 2-minute rule for synchronous plug-ins?

Click or press Enter to reveal answer

Answer

Synchronous plug-ins have a 2-minute execution timeout. If your plug-in takes longer than 2 minutes, Dataverse cancels it and rolls back the transaction. This is why synchronous plug-ins must be fast β€” no external API calls, no large batch operations, minimal queries.

Click to flip back

Question

What are filtering attributes on a plug-in step?

Click or press Enter to reveal answer

Answer

Filtering attributes (Update message only) specify which columns trigger the plug-in. Without them, the plug-in fires on every update to the entity. With filtering set to 'statuscode, priority', it only fires when those specific fields change. Always set them for Update steps.

Click to flip back

Knowledge Check

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?

Next up: Custom APIs & Business Events β€” creating reusable Dataverse endpoints and configuring event publishing.