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

When creating a Dataverse Plugin, 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 Qty, Price, and Total. If the user changes 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 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 Retrievefrom 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 CLIcommand 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.Stage == 20 && PluginExecutionContext.PreEntityImages.ContainsKey("Image") ? PluginExecutionContext.PreEntityImages["Image"]
                : PluginExecutionContext.Stage == 40 && PluginExecutionContext.PostEntityImages.ContainsKey("Image") ? PluginExecutionContext.PostEntityImages["Image"] : 			PluginExecutionContext.MessageName == "Create" ? (Entity)PluginExecutionContext.InputParameters["Target"] :
                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/Priceattribute. Then the plugin logic will help to calculate the Totalusing 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?

Leave a comment

Your comment is sent privately to the author and isn't published on the site.