Dataverse Plugin Development: Simplify Your Plugin Code Using This Way!

When creating Dataverse Plugin, sure we always encounter a scenario where we need to retrieve attribute values of the main entity. But we want to take either from Target‘s attribute or PreImage/PostImage/Database‘s attribute. For example, if we have an entity (table) that contains QtyPrice, and Total. If the user changed the Qty attribute, we want to calculate the Total using Target.Qty * Database.Price. Same with the scenario if the user just changed the Price attribute, then we need to calculate the Total using Database.Qty * Target.Price. These scenarios are very common. Yet the implementation of the code is always complicated in the real life. How can we make a TRUTHFUL object?

Simplify the plugin set or get attribute

From the diagram above, we need to have 3 new objects (data type Entity):

  • Target: The target Entity from UI (for Create/Update Message)
  • Current: Entity that we can get from PreImage or PostImage or Retrieve from the database.
  • Latest: Combination of the Current state + Target state (the order is important!).

For the implementation of the code, you can try running the command below:

pac plugin init

The Powerapps CLI command will give you a scaffolding plugin project that for me already sufficient for our demonstration purpose. In the ILocalPluginContext, I will add several new properties that will reflect the above design, and we will add one method name Set. Here is the full code of the PluginBase.cs (the highlighted lines are my changes):

using Microsoft.Xrm.Sdk;
using System;
using System.Linq;
using System.ServiceModel;

namespace DemoPlugin
{
    /// <summary>
    /// Base class for all plug-in classes.
    /// Plugin development guide: https://docs.microsoft.com/powerapps/developer/common-data-service/plug-ins
    /// Best practices and guidance: https://docs.microsoft.com/powerapps/developer/common-data-service/best-practices/business-logic/
    /// </summary>    
    public abstract class PluginBase : IPlugin
    {
        protected string PluginClassName { get; }

        /// <summary>
        /// Initializes a new instance of the <see cref="PluginBase"/> class.
        /// </summary>
        /// <param name="pluginClassName">The <see cref=" cred="Type"/> of the plugin class.</param>
        internal PluginBase(Type pluginClassName)
        {
            PluginClassName = pluginClassName.ToString();
        }

        /// <summary>
        /// Main entry point for he business logic that the plug-in is to execute.
        /// </summary>
        /// <param name="serviceProvider">The service provider.</param>
        /// <remarks>
        /// </remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Execute")]
        public void Execute(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new InvalidPluginExecutionException("serviceProvider");
            }

            // Construct the local plug-in context.
            var localPluginContext = new LocalPluginContext(serviceProvider);

            localPluginContext.Trace($"Entered {PluginClassName}.Execute() " +
                 $"Correlation Id: {localPluginContext.PluginExecutionContext.CorrelationId}, " +
                 $"Initiating User: {localPluginContext.PluginExecutionContext.InitiatingUserId}");

            try
            {
                // Invoke the custom implementation 
                ExecuteCdsPlugin(localPluginContext);

                // Now exit - if the derived plugin has incorrectly registered overlapping event registrations, guard against multiple executions.
                return;
            }
            catch (FaultException<OrganizationServiceFault> orgServiceFault)
            {
                localPluginContext.Trace($"Exception: {orgServiceFault.ToString()}");

                throw new InvalidPluginExecutionException($"OrganizationServiceFault: {orgServiceFault.Message}", orgServiceFault);
            }
            finally
            {
                localPluginContext.Trace($"Exiting {PluginClassName}.Execute()");
            }
        }

        /// <summary>
        /// Placeholder for a custom plug-in implementation. 
        /// </summary>
        /// <param name="localPluginContext">Context for the current plug-in.</param>
        protected virtual void ExecuteCdsPlugin(ILocalPluginContext localPluginContext)
        {
            // Do nothing. 
        }

    }

    /// <summary>
    /// This interface provides an abstraction on top of IServiceProvider for commonly used PowerPlatform Dataverse Plugin development constructs
    /// </summary>
    public interface ILocalPluginContext
    {
        // The PowerPlatform Dataverse organization service for current user account
        IOrganizationService CurrentUserService { get; }

        // The PowerPlatform Dataverse organization service for system user account
        IOrganizationService SystemUserService { get; }

        // IPluginExecutionContext contains information that describes the run-time environment in which the plugin executes, information related to the execution pipeline, and entity business information
        IPluginExecutionContext PluginExecutionContext { get; }

        // Synchronous registered plugins can post the execution context to the Microsoft Azure Service Bus.
        // It is through this notification service that synchronous plug-ins can send brokered messages to the Microsoft Azure Service Bus
        IServiceEndpointNotificationService NotificationService { get; }

        // Provides logging run time trace information for plug-ins. 
        ITracingService TracingService { get; }

        // Writes a trace message to the Dataverse trace log
        void Trace(string message);

        Entity Target { get; }
        Entity Current { get; }
        Entity Latest { get; }

        void Set(string attribute, object value);
    }

    /// <summary>
    /// Plug-in context object. 
    /// </summary>
    public class LocalPluginContext : ILocalPluginContext
    {
        internal IServiceProvider ServiceProvider { get; }

        /// <summary>
        /// The PowerPlatform Dataverse organization service for current user account.
        /// </summary>
        public IOrganizationService CurrentUserService { get; }

        /// <summary>
        /// The PowerPlatform Dataverse organization service for system user account.
        /// </summary>
        public IOrganizationService SystemUserService { get; }

        /// <summary>
        /// IPluginExecutionContext contains information that describes the run-time environment in which the plug-in executes, information related to the execution pipeline, and entity business information.
        /// </summary>
        public IPluginExecutionContext PluginExecutionContext { get; }

        /// <summary>
        /// Synchronous registered plug-ins can post the execution context to the Microsoft Azure Service Bus. <br/> 
        /// It is through this notification service that synchronous plug-ins can send brokered messages to the Microsoft Azure Service Bus.
        /// </summary>
        public IServiceEndpointNotificationService NotificationService { get; }

        /// <summary>
        /// Provides logging run-time trace information for plug-ins. 
        /// </summary>
        public ITracingService TracingService { get; }

        /// <summary>
        /// Helper object that stores the services available in this plug-in.
        /// </summary>
        /// <param name="serviceProvider"></param>
        public LocalPluginContext(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new InvalidPluginExecutionException("serviceProvider");
            }

            PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            TracingService = new LocalTracingService(serviceProvider);

            NotificationService = (IServiceEndpointNotificationService)serviceProvider.GetService(typeof(IServiceEndpointNotificationService));

            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

            CurrentUserService = factory.CreateOrganizationService(PluginExecutionContext.UserId);

            SystemUserService = factory.CreateOrganizationService(null);
        }

        /// <summary>
        /// Writes a trace message to the CRM trace log.
        /// </summary>
        /// <param name="message">Message name to trace.</param>
        public void Trace(string message)
        {
            if (string.IsNullOrWhiteSpace(message) || TracingService == null)
            {
                return;
            }

            if (PluginExecutionContext == null)
            {
                TracingService.Trace(message);
            }
            else
            {
                TracingService.Trace(
                    "{0}, Correlation Id: {1}, Initiating User: {2}",
                    message,
                    PluginExecutionContext.CorrelationId,
                    PluginExecutionContext.InitiatingUserId);
            }
        }

        private Entity _target;
        public Entity Target
        {
            get
            {
                if (_target == null)
                {
                    _target = GetTarget();
                }
                return _target;
            }
        }

        private Entity _current;
        public Entity Current
        {
            get
            {
                if (_current == null)
                {
                    _current = GetCurrent();
                }
                return _current;
            }
        }

        private Entity _latest;
        public Entity Latest
        {
            get
            {
                if (_latest == null)
                {
                    _latest = GetLatest();
                }
                return _latest;
            }
        }

        private Entity GetLatest()
        {
            var result = new Entity(Target.LogicalName, Target.Id);
            Current.Attributes.ToList().ForEach(attr => result[attr.Key] = attr.Value);
            Target.Attributes.ToList().ForEach(attr => result[attr.Key] = attr.Value);
            return result;
        }

        private Entity GetTarget()
        {
            switch (PluginExecutionContext.MessageName)
            {
                case "Create":
                case "Update":
                    var target = (Entity)PluginExecutionContext.InputParameters["Target"];
                    return target;
                case "Delete":
                    var entityRef = (EntityReference)PluginExecutionContext.InputParameters["Target"];
                    var entity = CurrentUserService.Retrieve(entityRef.LogicalName, entityRef.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true));
                    return entity;
                default:
                    throw new InvalidPluginExecutionException($"Target accessor for {PluginExecutionContext.MessageName} not supported.");
            }
        }
        private Entity GetCurrent()
        {
            var image = PluginExecutionContext.Depth == 20 && PluginExecutionContext.PreEntityImages.ContainsKey("Image") ? PluginExecutionContext.PreEntityImages["Image"]
                : PluginExecutionContext.Depth == 40 && PluginExecutionContext.PostEntityImages.ContainsKey("Image") ? PluginExecutionContext.PostEntityImages["Image"] :
                CurrentUserService.Retrieve(Target.LogicalName, Target.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true));

            return image;
        }

        public void Set(string attribute, object value)
        {
            Target[attribute] = value;
            Latest[attribute] = value;
        }
    }

    /// <summary>
    /// Specialized ITracingService implementation that prefixes all traced messages with a time delta for Plugin performance diagnostics
    /// </summary>
    public class LocalTracingService : ITracingService
    {
        private readonly ITracingService _tracingService;

        private DateTime _previousTraceTime;

        public LocalTracingService(IServiceProvider serviceProvider)
        {
            DateTime utcNow = DateTime.UtcNow;

            var context = (IExecutionContext)serviceProvider.GetService(typeof(IExecutionContext));

            DateTime initialTimestamp = context.OperationCreatedOn;

            if (initialTimestamp > utcNow)
            {
                initialTimestamp = utcNow;
            }

            _tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

            _previousTraceTime = initialTimestamp;
        }

        public void Trace(string message, params object[] args)
        {
            var utcNow = DateTime.UtcNow;

            // The duration since the last trace.
            var deltaMilliseconds = utcNow.Subtract(_previousTraceTime).TotalMilliseconds;

            _tracingService.Trace($"[+{deltaMilliseconds:N0}ms)] - {message}");

            _previousTraceTime = utcNow;
        }
    }
}

Then for the demonstration purpose, I’ll create a simple scenario that I already explained earlier.

Simple scenario to demonstrate the code

Users can change Qty/Price attribute. Then the plugin logic will help to calculate the Total using the formula Qty * Price.

Here is the plugin logic (Plugin1.cs):

using Microsoft.Xrm.Sdk;
using System;

namespace DemoPlugin
{
    public class Plugin1 : PluginBase
    {
        public Plugin1(string unsecureConfiguration, string secureConfiguration)
            : base(typeof(Plugin1))
        {
        }

        protected override void ExecuteCdsPlugin(ILocalPluginContext localPluginContext)
        {
            if (localPluginContext == null)
            {
                throw new ArgumentNullException("localPluginContext");
            }

            // Checking if Target.cr4c6_qty or Target.cr4c6_price got changes, then run the logic
            if (localPluginContext.Target.Contains("cr4c6_qty") || localPluginContext.Target.Contains("cr4c6_price"))
            {
                // Getting latest data to get the value
                var qty = localPluginContext.Latest.GetAttributeValue<int?>("cr4c6_qty").GetValueOrDefault();
                var price = (localPluginContext.Latest.GetAttributeValue<Money>("cr4c6_price") ?? new Money(0)).Value;
                var result = qty * price;

                // Set the Target + Latest
                localPluginContext.Set("cr4c6_total", result);
            }
        }
    }
}

Once we created the logic, we can register the plugin like the below setting:

Register the plugin step

Here is the demonstration in the UI:

Demo plugin

Summary

This feature is one of the features that Niam.Xrm.Framework provided. But the implementation on the framework more complex + cover more scenario. You can check the Niam.Xrm.Framework here.

What do you think?

13 thoughts on “Dataverse Plugin Development: Simplify Your Plugin Code Using This Way!

  1. I’ve been using a very similar approach for years, although with support for early binding. Makes plugins way cleaner to write! I will say your method for coalescing the attributes is the cleanest implementation I’ve seen. You may want to think about including other information as well, such as the formatted values.

    One suggestion, don’t force the use of pre/post image names. Just check to see if there is one, and if there is, use it. Why allow a potential bug with a mis spelled image name?

    Like

    1. Hi Daryl!

      It’s an honor to me to receive that comment from you!

      The implementation is still a bit off for me because we need to always depend on the localPluginContext. What I think making an abstract Business class and creating a shortcut for Input, Initial, and Current will be better.

      This feature idea is copied from Niam.Xrn.Framework( https://github.com/khairuddinniam/Niam.XRM.Framework) which I understand many developers will not use because it means they need time to re-learn again. 😭

      And thanks for the suggestion! That’s a brilliant idea!

      Like

  2. Hi Temmy,

    Amazing article and I appreciate the Plugins blog you have wriiten !

    I am trying to implement the same in my VS Code and I used the pac plugin init command and it created base class and plugin1. However, when i build the project it gives error on using Microsoft.Xrm.Sdk saying Namespace doesn’t exist. I know on VS 2017, i can add Microsoft.CrmSdk.CoreAssemblies using Nuget Manager. How do i do the same in VS Code ?

    Like

      1. Hi Temmy,

        The command did the trick for me. Really appreciate your prompt response. Just one additional question on the same topic, if I need to register the DLL with Plugin Registration Tool, where can I find the DLL file if I am using VS Code with the above files?

        Like

      2. To build the project, you can run command “dotnet build”. Then after you run that, you can see it will create bin directory. Same like when you using visual studio. 🤗

        Like

  3. How do I maintain n number of plugins? If I have to write 10 plug-ins for different entity means I have to create 10 plugins project or I have use single project for all.

    Like

    1. Microsoft recommendation is 1 plugin project per entity. The more plugin steps you registered on 1 entity, the more complex it is. Same like workflow. Avoid too many workflow, just 1 workflow for multiple business logic.

      Like

      1. Thanks for your immediate response. I really appreciate it. Could you please provide any reference for better understanding?

        Like

      2. I try to find it later ya. That document was internal document that Microsoft provide as part of remediation steps to upgrade.

        From official documentation: “From a performance perspective, is it better to create a single long workflow or is it better to have multiple child workflows and call them in one parent workflow? The child workflow approach achieves lower throughput, but it is more manageable if you frequently change your workflow definition.” https://docs.microsoft.com/en-us/dynamics365/customerengagement/on-premises/developer/best-practices-sdk?view=op-9-1

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.