web
You’re offline. This is a read only version of the page.
close
Skip to main content

Announcements

News and Announcements icon
Community site session details

Community site session details

Session Id :

Mastering Dynamics 365 Plugin Development: A Complete Step-by-Step Guide

Siddique mahsud Profile Picture Siddique mahsud 467
If you have been working with Microsoft Dynamics 365 or the Power Platform for any length of time, you have almost certainly hit a scenario where out-of-the-box configuration is not enough. That is exactly where plugins come in. Plugins let you execute rich, custom server-side business logic at precisely the right moment — before a record saves, after it saves, or asynchronously in the background — and they fire regardless of how the data change was triggered. In this comprehensive guide we cover everything from setting up your environment to writing, registering, testing, and deploying production-ready plugins.

What Is a Dynamics 365 Plugin?

A plugin is a .NET class library assembly that implements the IPlugin 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 implement IPlugin 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 an InvalidPluginExecutionException. 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 call tracer.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.

Conclusion

Dynamics 365 plugins are one of the most powerful and flexible tools in a CRM developer's toolkit. Once you understand the execution pipeline, master the IPlugin interface, and follow the best practices outlined above, you will be able to enforce complex business rules, integrate with external systems, and automate workflows with complete confidence. Start with a simple Pre-Operation plugin on a non-critical entity, iterate quickly using the Plugin Profiler, and gradually build up to more advanced patterns like asynchronous API calls and Pre-Entity Image comparisons. Happy coding!

This was originally posted here.

Comments

*This post is locked for comments