What Is a Dynamics 365 Plugin?
A plugin is a .NET class library assembly that implements theIPlugin interface provided by the Dataverse SDK. When registered against a specific event — for example Create on the Account entity — Dataverse automatically invokes your plugin's Execute method every time that event fires, passing a rich IServiceProvider context you can use to read data, write data, log trace messages, and even abort the operation entirely.Plugins differ fundamentally from JavaScript web resources. Web resources run in the browser and can be bypassed by direct API calls. Plugins run on the Dataverse server, inside a secure sandbox, enforcing your logic no matter whether the change comes from the model-driven UI, a Canvas app, a Power Automate flow, or a raw Web API request.
Understanding the Plugin Execution Pipeline
Before writing a single line of code it is worth understanding where in the lifecycle a plugin fires. Dataverse processes each operation through a multi-stage pipeline:Pre-Validation stage — fires before platform validation, outside the database transaction. Exceptions here do not roll back previously committed work.
Pre-Operation stage — fires after validation but before the database write, inside the transaction. Throwing an
InvalidPluginExecutionException here rolls back everything. This is the most common stage for modifying the Target entity.Post-Operation stage — fires after the database write, still inside the transaction. Ideal for creating child records or updating related entities.
Asynchronous execution — runs in a background queue after the transaction completes. Cannot roll back the original operation but ideal for time-consuming tasks like calling external REST APIs or sending emails.
Step 1: Set Up Your Development Environment
You will need the following tools before you write any code:Visual Studio 2022 (Community edition is free) — install the .NET desktop development workload and the Power Platform Tools extension.
Plugin Registration Tool (PRT) — install via the Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool NuGet package or from XrmToolBox.
Microsoft.CrmSdk.CoreAssemblies NuGet package — provides the
IPlugin interface and all Dataverse SDK types.Step 2: Create a Class Library Project
In Visual Studio, create a new Class Library (.NET Framework 4.6.2) project — targeting 4.6.2 is essential as it is the version supported by the Dataverse sandbox. Then install the SDK package via Package Manager Console:Install-Package Microsoft.CrmSdk.CoreAssemblies
Step 3: Implement the IPlugin Interface
Every plugin must implementIPlugin with a single method: Execute(IServiceProvider serviceProvider). The service provider gives access to four key services:ITracingService — writes messages to the Plugin Trace Log for debugging.
IPluginExecutionContext — gives access to InputParameters (the Target entity), PreEntityImages, PostEntityImages, user ID, message name, and pipeline stage.
IOrganizationServiceFactory — creates an IOrganizationService instance. Pass context.UserId to respect the triggering user's security roles.
IOrganizationService — used to create, read, update, delete, and query CRM records.
Here is a production-quality plugin that auto-populates the Description field on a new Account record:
using System;
using Microsoft.Xrm.Sdk;
namespace Contoso.Plugins
{
public class Account_PreCreate_SetDescription : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Extract services
var tracer = (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);
tracer.Trace("Plugin started. Message: {0}", context.MessageName);
// Guard clauses
if (context.InputParameters == null
|| !context.InputParameters.Contains("Target")
|| !(context.InputParameters["Target"] is Entity))
return;
var target = (Entity)context.InputParameters["Target"];
// Set default description if not already provided
if (!target.Contains("description")
|| string.IsNullOrWhiteSpace(target.GetAttributeValue<string>("description")))
{
target["description"] = $"Account created on {DateTime.UtcNow:yyyy-MM-dd} via Dynamics 365.";
tracer.Trace("Default description applied.");
}
}
}
}
Step 4: Register the Plugin with the Plugin Registration Tool
Once your assembly compiles successfully, open the Plugin Registration Tool and connect to your Dynamics 365 environment. Then follow these steps:1. Register New Assembly — click Register > Register New Assembly. Browse to your compiled DLL. Select "Sandbox" isolation mode and "Database" storage so the assembly is stored inside Dataverse rather than on the server file system.
2. Register New Step — right-click the assembly and choose Register New Step. Fill in: Message = Create, Primary Entity = account, Event Pipeline Stage = Pre-Operation (synchronous).
3. Filtering Attributes — for Update plugins, always specify filtering attributes so your plugin only fires when the relevant fields actually change. Leaving this blank fires the plugin on every field change and hurts performance.
4. Deployment — once registered, the step is live immediately. No IIS restart or deployment pipeline required.
Step 5: Working with Pre-Entity Images
Pre-Entity Images let you see the original field values before the update was applied. This is essential for audit logging, conditional logic, and change detection. Register an image in the Plugin Registration Tool (right-click the step > Register New Image, Type = Pre Image), then access it like this:if (context.PreEntityImages.Contains("PreImage"))
{
var preImage = context.PreEntityImages["PreImage"];
var oldName = preImage.GetAttributeValue<string>("name");
var newName = target.GetAttributeValue<string>("name");
if (oldName != newName)
{
tracer.Trace("Account name changed from '{0}' to '{1}'", oldName, newName);
// Create an audit log record or trigger downstream logic
var auditLog = new Entity("new_auditlog");
auditLog["new_entityname"] = "account";
auditLog["new_oldvalue"] = oldName;
auditLog["new_newvalue"] = newName;
auditLog["new_changedby"] = new EntityReference("systemuser", context.UserId);
service.Create(auditLog);
}
}
Step 6: Throwing Business Errors to the User
To stop an operation and display a user-friendly error message, throw anInvalidPluginExecutionException. This rolls back the entire database transaction and shows the message in the model-driven UI form notification area:var revenue = target.GetAttributeValue<Money>("revenue")?.Value ?? 0;
if (revenue < 0)
{
throw new InvalidPluginExecutionException(
"Annual Revenue cannot be negative. Please enter a valid value.");
}
var creditLimit = target.GetAttributeValue<Money>("creditlimit")?.Value ?? 0;
var accountCategory = target.GetAttributeValue<OptionSetValue>("accountcategorycode")?.Value;
if (accountCategory == 1 && creditLimit < 10000)
{
throw new InvalidPluginExecutionException(
"Preferred customers must have a credit limit of at least $10,000.");
}
Step 7: Calling an External REST API from a Plugin
Asynchronous plugins can call external APIs without blocking the user. Register the step as Asynchronous on Post-Operation. The Dataverse sandbox allows outbound HTTPS calls. Here is a pattern for calling an ERP system when an Account is created:using System;
using System.Net.Http;
using System.Text;
using Microsoft.Xrm.Sdk;
namespace Contoso.Plugins
{
public class Account_PostCreate_SyncToERP : IPlugin
{
private static readonly HttpClient _client = new HttpClient();
public void Execute(IServiceProvider serviceProvider)
{
var tracer = (ITracingService)serviceProvider
.GetService(typeof(ITracingService));
var context = (IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext));
var target = (Entity)context.InputParameters["Target"];
var accountId = target.Id.ToString();
var accountName = target.GetAttributeValue<string>("name") ?? "Unknown";
tracer.Trace("Syncing account {0} to ERP...", accountId);
_client.BaseAddress = new Uri("https://your-erp-api.example.com/");
_client.DefaultRequestHeaders.Add("x-api-key", "YOUR_API_KEY");
var payload = new StringContent(
$"{{\"accountId\":\"{accountId}\",\"accountName\":\"{accountName}\"}}",
Encoding.UTF8,
"application/json");
var response = _client.PostAsync("api/accounts", payload).Result;
if (!response.IsSuccessStatusCode)
{
tracer.Trace("ERP sync failed: {0}", response.StatusCode.ToString());
throw new InvalidPluginExecutionException(
"ERP synchronisation failed. Please contact your administrator.");
}
tracer.Trace("ERP sync successful.");
}
}
}
Step 8: Debugging and Tracing
Plugin Trace Logs are your primary debugging tool. Enable tracing in Settings > Administration > System Settings > Customization tab — set "Enable logging to plug-in trace log" to All. Then calltracer.Trace() liberally throughout your code. After reproducing the issue, navigate to Settings > Plugin Trace Log and filter by your plugin type name to read the output.For a richer debugging experience, the Plugin Profiler (included with the Plugin Registration Tool) lets you capture a live execution context and replay it step-by-step inside Visual Studio with full breakpoint support — without having to trigger the real event in Dynamics again.
Step 9: Plugin Best Practices
Always add guard clauses — check that InputParameters contains "Target" and that the Target is an Entity before casting. Skipping this causes NullReferenceExceptions in edge cases.Never store state in static fields — the plugin class is instantiated once per sandbox worker process and reused across many executions. Static mutable fields cause race conditions and data leakage between users.
Use filtering attributes for Update steps — only fire when the fields your logic depends on actually change. This dramatically reduces unnecessary executions.
Avoid infinite recursion — if your plugin updates a record and triggers itself, use
context.Depth to exit early when depth is greater than 1.Keep synchronous plugins fast — the user waits for Pre-Operation plugins to complete. If you need to call an external API, make the step Asynchronous instead.
Sign your assembly — Dynamics 365 on-premises requires strong-naming. It is good practice for all environments to keep the registration tool happy and to uniquely identify your DLL.
Deploying to Production
When your plugin is ready for production, export it as part of a Managed Solution in Dataverse. The solution packages your plugin assembly, steps, and images together. Import the managed solution into your target environment and the plugin is registered automatically — no need to run the Plugin Registration Tool manually in each environment.For CI/CD pipelines, use the Power Platform Build Tools Azure DevOps extension or the pac solution CLI to export, unpack, version-control, and deploy solutions automatically on each merge to your main branch.
No comments:
Post a Comment