Introduction

Clear and understandable code is crucial in software development, particularly in complex systems like Microsoft Dynamics 365 CRM. Plugins constitute a vital component of CRM systems. This article explores the benefits of developing a base class for plugins, focusing on how it can enhance code description, readability, and maintenance in Microsoft Dynamics 365 CRM.

Basic Approach

Creating a basic plugin for Microsoft Dynamics 365 CRM involves writing a class in C# that implements the IPlugin interface. Here's an example of a simple basic plugin:

public class BasicPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            //Business logic
        }
    }

 

Once in this form, the plugin can be registered and will work. However, typically, the developer also checks the correctness of the input parameters (whether the plugin is registered correctly):

public class BasicPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            if (context.MessageName == "Create" &&
                context.PrimaryEntityName == "account" &&
                context.Stage == 40 /*Post operation*/)
            {
                //Business logic
            }
        }
    }

 

Additionally, the services for CRM interaction require initialization.

public class BasicPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            if (context.MessageName == "Create" &&
                context.PrimaryEntityName == "account" &&
                context.Stage == 40 /*Post operation*/)
            {
                ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

                IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                IOrganizationService orgService = serviceFactory.CreateOrganizationService(context.UserId);

                //Business logic
            }
        }
    }

 

When scaling a system, a necessity arises to create new plugins and modify existing ones. One of the crucial aspects of this process is maintaining clean code. What problems can arise with such an approach?

•    Repetition of technical code.
•    Context checks that can be missed (which can be critical if the plugin is not registered correctly).
•    Technical code distracts attention from the main business logic.

All the drawbacks mentioned above can be addressed by creating a base class for plugins, which will handle service initialization and necessary checks and eliminate code repetition.

Base Class for Plugins

Once again, briefly, about the basic approach - we implement the IPlugin interface, add our code to the Execute method, and obtain a plugin class, which we register using the Plugin Registration Tool. In the case of the base class, the approach will look slightly different: we inherit the CrmPluginContainer class, add methods with specific signatures, mark them as plugins, and as a result, we get a class with one or several plugins that are registered similarly to the basic approach. What is required for this?

First, we must implement a mechanism that recognizes which class method is a plugin and reads its registration parameters. An excellent solution for solving this task would be attributes:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
    public class CrmPluginAttribute : Attribute
    {
        public string Message { get; set; }

        public string EntityName { get; set; }

        public StageCode? Stage { get; set; }

        public int Order { get; set; }

        public string[] PreImages { get; set; }

        public string[] PostImages { get; set; }

        public CrmPluginAttribute() { }

        public CrmPluginAttribute(string message, string entityName, StageCode stage) : this()
        {
            if (String.IsNullOrEmpty(entityName))
                throw new ArgumentException(nameof(entityName));
            if (String.IsNullOrEmpty(message))
                throw new ArgumentException(nameof(message));

            Message = message;
            EntityName = entityName;
            Stage = stage;
        }

        public CrmPluginAttribute(string message, string entityName) : this()
        {
            if(String.IsNullOrEmpty(message))
                throw new ArgumentException(nameof(message));

            Message = message;
            EntityName = entityName;
        }

        public CrmPluginAttribute(string message, StageCode stage) : this()
        {
            if (String.IsNullOrEmpty(message))
                throw new ArgumentException(nameof(message));

            Message = message;
            Stage = stage;
        }

        public CrmPluginAttribute(string message) : this()
        {
            if (String.IsNullOrEmpty(message))
                throw new ArgumentException(nameof(message));

            Message = message;
        }
    }

 

The next essential mechanism involves searching for methods marked with such attributes and reading registration data.

public interface IPluginSelector
    {
        IReadOnlyCollection<CrmPlugin> Select(MethodInfo method, object instance);
    }

    public class DefaultPluginSelector : IPluginSelector
    {
        public IReadOnlyCollection<CrmPlugin> Select(MethodInfo method, object instance)
        {
            List<CrmPlugin> result = new List<CrmPlugin>();

            IReadOnlyCollection<CrmPluginAttribute> pluginAttributes = method
                .GetCustomAttributes<CrmPluginAttribute>(true)
                .ToList();

            if (method.GetParameters().Length == 1 &&
                method.GetParameters()[0].ParameterType == typeof(CrmPluginContext))
            {
                foreach (CrmPluginAttribute crmPluginAttribute in pluginAttributes)
                {
                    CrmPlugin crmPlugin = new CrmPlugin(
                        method.Name,
                        crmPluginAttribute.Order,
                        CreateCanInvokePredicate(method, instance, crmPluginAttribute),
                        CreateInvokeAction(method, instance, crmPluginAttribute));

                    result.Add(crmPlugin);
                }
            }

            return result;
        }

        private Predicate<CrmPluginContext> CreateCanInvokePredicate(MethodInfo method, object instance, 
            CrmPluginAttribute metadata)
        {
            Predicate<CrmPluginContext> result = (context) =>
            {
                bool canInvoke = false;
                canInvoke = CrmPluginAttributeHelper.CanInvoke(metadata, context);
                return canInvoke;
            };

            return result;
        }

        private Action<CrmPluginContext> CreateInvokeAction(MethodInfo method, object instance, 
            CrmPluginAttribute metadata)
        {
            Action<CrmPluginContext> result = (context) =>
            {
                method.Invoke(instance, new object[] { context });
            };

            return result;
        }
    }

 

The process is straightforward: through Reflection, the registration parameters specified in the attribute are condensed, forming a representation of the plugin, which is then appended to the overall array. The subsequent step involves crafting a class that consolidates all the plugin's input parameters and pre-initialized services for seamless communication with D365 CRM.

public class CrmPluginContext
    {
        public IOrganizationService UserOrganizationService { get; private set; }

        public IOrganizationService SystemOrganizationService { get; private set; }

        public IPluginExecutionContext PluginExecutionContext { get; private set; }

        public ITracingService TracingService { get; private set; }

        internal CrmPluginContext(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
                throw new ArgumentNullException(nameof(serviceProvider));

            PluginExecutionContext = serviceProvider.GetService<IPluginExecutionContext>();
            TracingService = serviceProvider.GetService<ITracingService>(); 

            IOrganizationServiceFactory factory = serviceProvider.GetService<IOrganizationServiceFactory>();
            UserOrganizationService = factory.CreateOrganizationService(PluginExecutionContext.UserId);
            SystemOrganizationService = factory.CreateOrganizationService(null);
        }

        internal CrmPluginContext(IOrganizationService crmService, IPluginExecutionContext pluginExecutionContext)
        {
            if (crmService == null)
                throw new ArgumentNullException(nameof(crmService));
            if (pluginExecutionContext == null)
                throw new ArgumentNullException(nameof(pluginExecutionContext));

            UserOrganizationService = SystemOrganizationService = crmService;
            PluginExecutionContext = pluginExecutionContext;
        }
    }

 

With a list of plugin concepts and the context of the base plugin (a class containing methods marked with an attribute), you can decide which methods need to be called.

public static class CrmPluginAttributeHelper
    {
        public static bool CanInvoke(CrmPluginAttribute attribute, CrmPluginContext context)
        {
            bool result = true;

            if (attribute != null && context != null)
            {
                if (result && !String.IsNullOrWhiteSpace(attribute.Message))
                {
                    result = String.Equals(attribute.Message, context.PluginExecutionContext.MessageName, StringComparison.InvariantCultureIgnoreCase);
                }

                if (result && attribute.Stage.HasValue)
                {
                    result = attribute.Stage.Value == (StageCode)context.PluginExecutionContext.Stage;
                }

                if (result && !String.IsNullOrWhiteSpace(attribute.EntityName))
                {
                    if (String.Equals(context.PluginExecutionContext.MessageName, CrmMessage.ASSOCIATE, StringComparison.InvariantCultureIgnoreCase) ||
                        String.Equals(context.PluginExecutionContext.MessageName, CrmMessage.DISASSOCIATE, StringComparison.InvariantCultureIgnoreCase))
                    {
                        Relationship relationship = (Relationship)context.PluginExecutionContext.InputParameters["Relationship"];
                        
                        result = String.Equals(attribute.EntityName, relationship.SchemaName, StringComparison.InvariantCultureIgnoreCase);
                    }
                    else
                    {
                        result = String.Equals(attribute.EntityName, context.PluginExecutionContext.PrimaryEntityName, StringComparison.InvariantCultureIgnoreCase);
                    }
                }

                if (result && attribute.PreImages != null && attribute.PreImages.Any())
                {
                    result = attribute.PreImages.All(i => context.PluginExecutionContext.PreEntityImages.ContainsKey(i));
                }

                if (result && attribute.PostImages != null && attribute.PostImages.Any())
                {
                    result = attribute.PostImages.All(i => context.PluginExecutionContext.PostEntityImages.ContainsKey(i));
                }
            }

            return result;
        }
    }

 

Now, it remains to unite this into one mechanism.

public class CrmPluginContainer : IPlugin
    {
        private readonly List<IPluginSelector> _pluginSelectors = new List<IPluginSelector>();

        public CrmPluginContainer()
        {
            _pluginSelectors.Add(new DefaultPluginSelector());
        }

        public void Execute(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
                throw new ArgumentNullException(nameof(serviceProvider));

            CrmPluginContext pluginContext = new CrmPluginContext(serviceProvider);

            Action<string> trace = (message) =>
            {
                if (pluginContext.TracingService != null)
                {
                    pluginContext.TracingService.Trace(message);
                }
            };

            trace(string.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.GetType().ToString()));

            try
            {
                IReadOnlyCollection<CrmPlugin> plugins = this.GetType()
                    .GetMethods()
                    .Where(m => m.Name != nameof(Execute))
                    .SelectMany(m =>
                    {
                        List<CrmPlugin> selectedPlugins = new List<CrmPlugin>();
                        foreach (IPluginSelector selector in _pluginSelectors.Where(s => s != null))
                        {
                            selectedPlugins.AddRange(selector.Select(m, this) ?? Enumerable.Empty<CrmPlugin>());
                        }
                        return selectedPlugins;
                    })
                    .OrderBy(plugin => plugin.Order)
                    .ThenBy(plugin => plugin.Name)
                    .ToList();
                
                foreach (CrmPlugin plugin in plugins)
                {
                    if (plugin.CanInvoke(pluginContext))
                    {
                        trace(string.Format(
                            CultureInfo.InvariantCulture,
                            "{0} - {1} is firing for Entity: {2}, Message: {3}",
                            this.GetType().ToString(),
                            plugin.Name,
                            pluginContext.PluginExecutionContext.PrimaryEntityName,
                            pluginContext.PluginExecutionContext.MessageName));

                        plugin.Invoke(pluginContext);
                    }
                }
            }
            catch (FaultException<OrganizationServiceFault> e)
            {
                trace(string.Format(CultureInfo.InvariantCulture, "Exception: {0}", e.ToString()));
                trace(string.Format(CultureInfo.InvariantCulture, "Stack Trace: {0}", e.StackTrace));
                throw;
            }
            finally
            {
                trace(string.Format(CultureInfo.InvariantCulture, "Exiting {0}.Execute()", this.GetType().ToString()));
            }
        }
    }

 

The typical workflow is as follows:

  1. Create a class inheriting from CrmActionContainer.
  2. Add methods with the signature void MethodName(CrmPluginContext context) to the class.
  3. Mark these methods with the attribute [CrmPlugin("Create," "account," StageCodePost)].
  4. Implement necessary business logic within these methods.
  5. Register the container class.
  6. When the trigger is activated, the Execute method is called, initiating the mechanism of the base class created.
  7. Initially, all plugins marked with the attribute are located.
  8. Their registration data is then examined to determine which plugins need to be executed based on the current context.

Examples of usage:

public class AccountPlugins : CrmPluginContainer
    {
        [CrmPlugin(CrmMessage.CREATE, "account", StageCode.PostOperation)]
        [CrmPlugin(CrmMessage.UPDATE, "account", StageCode.PostOperation)]
        public void AutoNumeration(CrmPluginContext context)
        {
            //business logic
        }

        [CrmPlugin(CrmMessage.CREATE, "account", StageCode.PostOperation, PostImages = new string[] { "PostImage" })]
        public void UpdateData(CrmPluginContext context)
        {
            //business logic
        }

        [CrmPlugin(CrmMessage.SET_STATE, "account", StageCode.PostOperation)]
        [CrmPlugin(CrmMessage.SET_STATE_DYNAMIC_ENTITY, "account", StageCode.PostOperation)]
        public void SetInitiator(CrmPluginContext context)
        {
            //business logic
        }
    }

Conclusion

This article extensively discussed the principles and advantages of using a base class for plugin development in Microsoft Dynamics 365 CRM. The key point is that implementing such an approach simplifies and optimizes the development process, making the code cleaner, more understandable, and easier to maintain. 
This method allows developers to focus on the specific logic of the plugin, minimizing repetitive code and reducing the likelihood of errors. It also promotes other developers' better understanding of the code, essential in teamwork. The base class is an excellent foundation for creating structured and efficient plugins. 

Feel free to reach out to us with any questions.