The Dynamics CRM platform can utilize plug-ins, which can be created by a programmer in any .NET language (mostly C#), to perform virtually any business logic operation. Common uses for plug-ins include data validation, data manipulation, maintaining data integrity, calculations, integration with other systems, workflow enhancements and much more. If you're using Dynamics CRM and find yourself saying "I wish, when I save a record, CRM would do X, Y and Z" and the CRM workflow or business rules functionality doesn't provide the functionality you need, then there's a very good chance that you can meet the requirements using a plug-in.

Plug-in Usage Examples

Here are some examples of the usefulness of CRM plug-ins (in no particular order):
  • Perform data validation and throw an exception if conditions are not met.
  • When an e-mail arrives into CRM, create various CRM records based on the to, from, cc, etc.
  • Deactivate a parent account of an opportunity if the opportunity is closed as "Lost"
  • Send the execution context of a plug-in to a Microsoft Azure Service Bus queue. For example, when a new account is created in CRM, send a message to an Azure queue.
  • When the CRM platform retrieves data from the CRM database, use a plug-in to manipulate the data in some way (e.g., convert a date/time to a different time zone).
  • On save of a record that has child records, perform a calculation on the child records and store the derived value on the parent.
  • Apply discounts and taxes for opportunities, quotes, orders, etc.
  • Write a workflow activity type of plug-in to provide data or operation to a workflow process that is not provided using the workflow designer.
  • Create a follow-up Task record after an account is created.
  • Clone a record and its child records.
  • Auto-generate a unique number for an account when it's created.
  • Before an e-mail is sent, dynamically inject HTML to the body of the e-mail. For example, query into CRM to build a list of products and expiration dates and add that data to the e-mail message.
  • Retrieve external data (e.g., get data from an Azure Storage Table via REST API) and update CRM accordingly.
  • Dynamically generate an HTML web resource and save it back to CRM for use on dashboards.

Creating Plug-ins

What you need:
  • Experience writing code in C# or VB.NET or the desire to do so
  • Requirements: What exactly do you want the plug-in to do? How will it benefit the business? What entity should it run for? For which message(s) (event) should it run? Should it run immediately (synchronous) or when it can (asynchronous)?
  • Visual Studio 2015 or newer (a CRM plug-in is a .NET class library so you need to compile to a DLL)
  • Dynamics CRM SDK: The DLLs (i.e., Microsoft.Xrm.Sdk.dll) and knowledge about how plug-ins work. See below for a starting template. You can get these assemblies via Nuget.
  • Plug-in registration tool (also provided in the CRM SDK)
  • Thorough understanding of plug-in registration and the CRM platform. For example, regarding plug-in registration, you must understand Filtering Attributes and Pre- and Post-Images. Also, it's very important that you only update necessary fields within a plug-in (or any application that updates CRM); when you call Update for an entity, the entity must contain only those fields that have a changed value (CRM doesn't do that work for you). Also, only query for the records and fields that are necessary. In other words, don't do the equivalent of SELECT * in your plug-ins (this includes pre- and post-images) if you don't need all field values in the plug-in.
  • Time: Don't rush into plug-in development. It's important to thoroughly test a plug-in because it might be executed thousands or even millions of times. And the actions performed in the plug-in might trigger other workflows and plug-ins resulting in chain reactions. So, again, it's critical that plug-ins are designed carefully and tested thoroughly. Can't say that enough.

See the Misc Notes section below for other plug-in development and registration recommendations.

General development steps:
  1. Create a new Visual Studio 2015 (or newer) Class Library Project.
  2. Set the assembly name to something like MyCorp.CRM.Plugins.
  3. Add a reference to Microsoft.xrm.sdk.dll and Microsoft.crm.sdk.proxy.dll. These are provided with the Dynamics CRM SDK. Or add the references via Nuget.
  4. Add a reference to the .NET 4.5.2 (for CRM 2015/2016) namespaces System.ServiceModel and System.Runtime.Serialization.
  5. Sign the plug-in with a strong key. To do this, go to the Signing tab of the project properties and select New for the Choose strong name drop-down box.
  6. In the plug-in class, add a “using” statement for the Microsoft.Xrm.Sdk namespace.
  7. For the plug-in class, add an inheritance from IPlugin. Finish remainder of plug-in code.
  8. Load the plug-in registration tool (PluginRegistration.exe) that ships with the CRM SDK. Connect to the CRM organization.
  9. Register the assembly.
  10. Register the step(s).
  11. Test, optimize, test some more, try to break it, etc.

Registering Plug-ins

Register plug-ins (DLLs) with the Windows application PluginRegistration.exe. This application is provided in the Dynamics CRM SDK under \SDK\Tools\PluginRegistration\PluginRegistration.exe.

Platform Messages (Events) that trigger Plug-ins

The following are the messages that can be handled by plug-ins for a system (out of box) and custom entities in CRM. There are a few other messages that relate to specific system entities. See page "Supported messages and entities for plug-ins" for the full list.
  • Assign
  • Create
  • Delete
  • GrantAccess
  • ModifyAccess
  • Retrieve
  • RetrieveMultiple
  • RetrievePrincipalAccess
  • RetrieveSharedPrincipalsAndAccess
  • RevokeAccess
  • SetState
  • SetStateDynamicEntity
  • Update

Sample Plug-in (Template)

Below is a starting template for a Dynamics CRM plug-in. Your Visual Studio project needs to reference the Dynamics CRM SDK assemblies Microsoft.Xrm.Sdk.dll and Microsoft.Crm.Sdk.Proxy.dll.
using System;
using System.Reflection;
using Microsoft.Xrm.Sdk;
 
namespace Contoso.CRM.Plugin
{
    /// <summary>
    /// This plug-in...
    /// </summary>
    /// <remarks>
    /// Register this plug-in on the {EVENT NAME} of the {ENTITY NAME} entity, {EVENT PIPELINE STAGE}, {SYNC/ASYNC}.
    /// Set Filtering Attributes to {ATTRIBUTES LIST}
    /// Set to run in context of {Current User or Specify User}
    /// Set execution order to {EXECUTION ORDER}.
    /// Register a pre-image named {PRE-IMAGE NAME} with attributes {ATTRIBUTES LIST}
    /// Register a post-image named {POST-IMAGE NAME} with attributes {ATTRIBUTES LIST}
    /// Register Unsecure configuration containing...
    /// </remarks>
    public sealed class MyPlugin : IPlugin
    {
        // TODO: TEMPLATE: Set this const to the logical name of the expected target entity
        private const string TARGET_ENTITY_NAME = "account";
 
        private const string PRE_IMAGE_ALIAS = "PreImage";
        private const string POST_IMAGE_ALIAS = "PostImage";
 
        private readonly string _unsecureString;
        private readonly string _secureString;
        private readonly string _className;
 
        /// <summary>
        /// Default constructor
        /// </summary>
        /// <param name="unsecureString">Unsecure configuration string</param>
        /// <param name="secureString">Secure configuration string</param>
        public MyPlugin(string unsecureString, string secureString)
        {
            _className = this.GetType().Name;
 
            if (String.IsNullOrWhiteSpace(unsecureString))
            {
                throw new InvalidOperationException("Configuration details were not passed to the plug-in as expected.");
            }
 
            _unsecureString = unsecureString;
            _secureString = secureString;
        }
 
        /// <summary>
        ///
        /// </summary>
        /// <param name="serviceProvider"></param>
        public void Execute(IServiceProvider serviceProvider)
        {
            string tracePrefix = string.Concat(_className, ".", MethodBase.GetCurrentMethod().Name);
 
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
 
            // Obtain the organization service reference.
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
 
            tracingService.Trace("{0}: Begin", tracePrefix);
 
            // TODO: TEMPLATE: Verify that InputParameters should contain "Target" for this plug-in and that
            // the Target must be an Entity for this plug-in or possibly an EntityReference object. See article "Understand the data context passed to a plug-in"
            // in the SDK for more details.
            if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
            {
                tracingService.Trace("{0}: Get target entity", tracePrefix);
                Entity entity = (Entity)context.InputParameters["Target"];
 
                if (entity.LogicalName != TARGET_ENTITY_NAME)
                {
                    tracingService.Trace("{0}: Target entity logical name {1} does not match expected entity name {2}. Exiting plug-in.", tracePrefix, entity.LogicalName, TARGET_ENTITY_NAME);
                    return;
                }
 
                tracingService.Trace("{0}: Get pre-image", _className);
                Entity preImageEntity = (Entity)context.PreEntityImages[PRE_IMAGE_ALIAS];
 
            }
 
            tracingService.Trace("{0}: End", tracePrefix);
        }
 
    }
}
 


PluginContextHelper.cs (Add this class to your plug-in projects to write (trace) out plug-in context details.)

CRM plug-in code typically obtains the "context" of the plug-in. This context contains details such as the name of the message that's being handled (e.g., Create, Update, Delete), the Guid for the user who initiated the event, the primary entity's logical name and Guid, the entity (or a reference to it) with values (e.g., the name of the account being saved), etc.

For a complete list of the context contents, see the CRM SDK page IPluginExecutionContext Members.

You can add the PluginContextHelper class below to your plug-in project to help understand the properties and values included in a plug-in's execution context (IPluginExecutionContext). An example of using the helper class is provided below the class code. NOTE: This code is completely optional; it's not at all required when creating a plug-in. It's provided here for you to use if you need to troubleshoot a plug-in or are curious about the contents of the plug-in context at runtime.
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace MyCorp.Crm.Plugin
{
    internal class PluginContextHelper
    {
        string _executingPluginClassName;
        IPluginExecutionContext _context;
        ITracingService _tracingService;
 
        public PluginContextHelper(IPluginExecutionContext context, string executingPluginClassName, ITracingService tracingService)
        {
            _executingPluginClassName = executingPluginClassName;
            _context = context;
            _tracingService = tracingService;
        }
 
        /// <summary>
        /// Writes out the ExecutionContext to the CRM Trace object.
        /// </summary>
        public StringBuilder TraceOutPluginContext()
        {
            var sb = new StringBuilder();
 
            sb.AppendLine(string.Format("{0}: Begin trace of IPluginExecutionContext", _executingPluginClassName));
 
            sb.AppendLine("----------");
            if (_context == null)
            {
                sb.AppendLine("Context is null.");
                return sb;
            }
 
            sb.AppendLine(string.Format("UserId: {0}", _context.UserId));
            sb.AppendLine(string.Format("OrganizationId: {0}", _context.OrganizationId));
            sb.AppendLine(string.Format("OrganizationName: {0}", _context.OrganizationName));
            sb.AppendLine(string.Format("MessageName: {0}", _context.MessageName));
            sb.AppendLine(string.Format("Stage: {0}", _context.Stage));
            sb.AppendLine(string.Format("Mode: {0}", _context.Mode));
            sb.AppendLine(string.Format("PrimaryEntityName: {0}", _context.PrimaryEntityName));
            sb.AppendLine(string.Format("SecondaryEntityName: {0}", _context.SecondaryEntityName));
            sb.AppendLine(string.Format("BusinessUnitId: {0}", _context.BusinessUnitId));
            sb.AppendLine(string.Format("CorrelationId: {0}", _context.CorrelationId));
            sb.AppendLine(string.Format("Depth: {0}", _context.Depth));
            sb.AppendLine(string.Format("InitiatingUserId: {0}", _context.InitiatingUserId));
            sb.AppendLine(string.Format("IsExecutingOffline: {0}", _context.IsExecutingOffline));
            sb.AppendLine(string.Format("IsInTransaction: {0}", _context.IsInTransaction));
            sb.AppendLine(string.Format("IsolationMode: {0}", _context.IsolationMode));
            sb.AppendLine(string.Format("Mode: {0}", _context.Mode));
            sb.AppendLine(string.Format("OperationCreatedOn: {0}", _context.OperationCreatedOn.ToString()));
            sb.AppendLine(string.Format("OperationId: {0}", _context.OperationId));
            sb.AppendLine(string.Format("PrimaryEntityId: {0}", _context.PrimaryEntityId));
            sb.AppendLine(string.Format("OwningExtension LogicalName: {0}", _context.OwningExtension.LogicalName));
            sb.AppendLine(string.Format("OwningExtension Name: {0}", _context.OwningExtension.Name));
            sb.AppendLine(string.Format("OwningExtension Id: {0}", _context.OwningExtension.Id));
            sb.AppendLine(string.Format("SharedVariables: {0}", (_context.SharedVariables == null
                ? "NULL" : SerializeParameterCollection(_context.SharedVariables))));
            sb.AppendLine(string.Format("InputParameters: {0}", (_context.InputParameters == null
                ? "NULL" : SerializeParameterCollection(_context.InputParameters))));
            sb.AppendLine(string.Format("OutputParameters: {0}", (_context.OutputParameters == null
                ? "NULL" : SerializeParameterCollection(_context.OutputParameters))));
            sb.AppendLine(string.Format("PreEntityImages: {0}", (_context.PreEntityImages == null
                ? "NULL" : SerializeEntityImageCollection(_context.PreEntityImages))));
            sb.AppendLine(string.Format("PostEntityImages: {0}", (_context.PostEntityImages == null
                ? "NULL" : SerializeEntityImageCollection(_context.PostEntityImages))));
            sb.AppendLine(string.Format("----------"));
            sb.AppendLine(string.Format("{0}: End trace of IPluginExecutionContext", _executingPluginClassName));
 
            _tracingService.Trace(sb.ToString());
 
            return sb;
        }
 
        /// <summary>
        /// Writes out the attributes of an entity.
        /// </summary>
        /// <param name="e">The entity to serialize.</param>
        /// <returns>A human readable representation of the entity.</returns>
        public static string SerializeEntity(Entity e)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("{0} LogicalName: {1}{0} EntityId: {2}{0} Attributes: [",
                Environment.NewLine,
                e.LogicalName,
                e.Id);
            foreach (KeyValuePair<string, object> parameter in e.Attributes)
            {
                sb.AppendFormat("{0}: {1}; ", parameter.Key, parameter.Value);
            }
            sb.Append("]");
            return sb.ToString();
        }
 
        /// <summary>
        /// Flattens a collection into a delimited string.
        /// </summary>
        /// <param name="parameterCollection">The values must be of type Entity
        /// to print the values.</param>
        /// <returns>A string representing the collection passed in.</returns>
        private static string SerializeParameterCollection(ParameterCollection parameterCollection)
        {
            StringBuilder sb = new StringBuilder();
            foreach (KeyValuePair<string, object> parameter in parameterCollection)
            {
                Entity e = parameter.Value as Entity;
                if (e != null)
                {
                    sb.AppendFormat("{0}: {1}", parameter.Key, SerializeEntity(e));
                }
                else
                {
                    sb.AppendFormat("{0}: {1}; ", parameter.Key, parameter.Value);
                }
            }
            return sb.ToString();
        }
 
        /// <summary>
        /// Flattens a collection into a delimited string.
        /// </summary>
        /// <param name="entityImageCollection">The collection to flatten.</param>
        /// <returns>A string representation of the collection.</returns>
        private static string SerializeEntityImageCollection(EntityImageCollection entityImageCollection)
        {
            StringBuilder sb = new StringBuilder();
            foreach (KeyValuePair<string, Entity> entityImage in entityImageCollection)
            {
                sb.AppendFormat("{0}{1}: {2}", Environment.NewLine, entityImage.Key,
                    SerializeEntity(entityImage.Value));
            }
            return sb.ToString();
        }
    }
}
 

Here's sample code for using the PluginContextHelper:
var pluginHelper = new PluginContextHelper(pluginExecutionContext, "CombineProductNamesForAccount", tracingService);
pluginHelper.TraceOutPluginContext();
throw new InvalidPluginExecutionException("Tim is testing...");

Pre-Image and Post-Image

Only steps registered on the following messages can have images:
  • Assign
  • Create
  • Delete
  • DeliverIncoming
  • DeliverPromote
  • Merge
  • Route
  • Send
  • SetState
  • SetStateDynamicEntity
  • Update

Miscellaneous Notes & Tips

  • Tracing: A plug-in should including Tracing statements. If an error occurs with a plug-in, a system administrator can review the tracing details to diagnose the problem. In a plug-in, you can create an instance of the ITracingService with a code line similar to the following: ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
  • Constants: As with lots of other types of applications, it's best to use constants (better yet, a dedicated constants class) for values such as entity and field names and picklist id's rather than hard-coding names/values throughout the plug-in code.
  • Pre- and Post Images: When defining pre- and post-images for plug-in steps, include only the field(s) that you need in the plug-in. It should be rare that you need all of an entities fields/attributes in a pre- or post-image. If you configure a plug-in step to retrieve all fields for an entity, this is the equivalent of a SELECT * in SQL Server, which is generally a bad idea in production systems.
  • Updating CRM data: It’s critical that the Entity that’s being updated in CRM (using the IOrganizationService Update method) only contain the fields that need to be updated in CRM. For example, if you register a pre-image with all attributes and in your plug-in you add/change one attribute’s value and then call Update, you’re telling CRM to update all fields for the Entity instance, not just the ones you changed. Worse, by updating fields unnecessarily, your plug-in might trigger other plug-ins or workflows, leading to a cascading effect or a loop. At the very least, you'll be making auditing for real changes very difficult.
  • Filtering Attributes: When registering a plug-in step, for the Filtering Attributes option, select only the attributes that need to be present in the Entity for the plug-in to run. For example, if a plug-in needs to run when someone changes the account type on an account record, then set the account type field as a Filtering Attribute and leave all other attributes unchecked.
  • Tread lightly: In general, make sure the plug-in is doing only what is necessary and no more. Keep in mind that CRM might run the plug-in tens of thousands or even millions of times. Design and code the plug-in to have as low of impact on CRM as possible (e.g., ony query for data you need (including pre- and post-image), only update what’s changed).
  • Visual Studio Solution/Project Structure: There are many different ways to structure plug-in classes in Visual Studio projects. One good practice is to group plug-ins into a VS project by major area of functionality or purpose. For example, if you have a number of plug-ins that relate to relate to data validation (e.g., pre-operation plug-ins that prevent users from entering incorrect data) then you should consider grouping those plug-in classes into a separate VS project. Grouping plug-ins by major functionality or even by entity typically makes it easier to maintain versus putting all plug-ins in one VS project. You can see that Microsoft, for example, grouped plug-ins relating to Activity Feeds together into one assembly; they didn't include those plug-ins with other out-of-box plug-ins into one large generic plug-in assembly.
  • Training: Make sure all plug-in developers read and understand everything here: https://msdn.microsoft.com/en-us/library/gg328490(v=crm.5).aspx
  • ILMerge.exe: Using ILMerge.exe, it's possible to combine third-party .NET assemblies (e.g., ClosedXML, JSON.NET, iTextSharp) with your CRM plug-in assembly. However, not all .NET assemblies can be combined with your plug-in code. You'll want to create and try to deploy a small test plug-in before embarking on a large project using a third-party library in case CRM (particularly CRM Online) prevents the use of one or more of the DLLs you've added to your plug-in with ILMerge.
  • Removing a plug-in from an assembly: If you remove a plug-in class (a class that implements IPlugin) from an assembly that you've previously registered and then try to register the updated assembly, the CRM plug-in registration tool will throw "Unhandled Exception: System.ServiceModel.FaultException...Plug-in assembly does not contain the required types or assembly content cannot be updated.". The easy way to prevent this exception and to allow you to register your new assembly is to first go into the plug-in registration tool and unregister the plug-in that you want to remove from your class library (assembly). Then, register your new assembly and you should no longer get the exception.
  • Create Visio (or similar) diagram for complex plug-ins: It's helpful to have a flowchart of a complex plug-in. This allows you to review the business logic with non-developers and helps you understand what a plug-in does if it's been a while since you looked at the code. One way to correlate a Visio shape with the plug-in's source code line is to create a Shape Data field (named something like "SourceCodeReference") and, for each shape that relates to a code line, copy the source code line to that Shape Data field. This provides a way to find the part of the source code that relates to the Visio shape.

Plug-in Registration Tools - Sample Screens

This section contains screenshots from the plug-in registration tool that Microsoft includes with the Dynamics CRM 2015 SDK. The plug-ins shown are for a new CRM trial organization. Only the main (most often used) screens are shown.

Register New Assembly

wiki_crm_plugin_registration_004.png


Registered Plugins & Custom Workflow Activities

This screen shows the registered plug-in assemblies and child "Step" and "Image" registrations. The lower section, in this example, shows the plug-ins (classes that implement IPlugin) for the highlighted assembly.
wiki_crm_plugin_registration_001.png


This screen below shows the "Properties" view for a highlighted assembly.

wiki_crm_plugin_registration_005.png


Register Step / Update Existing Step

wiki_crm_plugin_registration_002.png


Register Image / Update Existing Image

wiki_crm_plugin_registration_003.png

Viewing Plug-in Details within the Dynamics CRM Web Interface

As shown in the screens below, the Dynamics CRM web UI can list the plug-in assemblies and steps. You can get to these lists by opening the default solution for the organization or can view only those plug-ins that are associated with a custom solution.
wiki_crm_plugin_view_within_crm_001.png

wiki_crm_plugin_view_within_crm_002.png




.