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 Qty, Price, 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?

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"] :
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.

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:

Here is the demonstration in the UI:

Summary
What do you think?
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?
LikeLike
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!
LikeLike
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 ?
LikeLike
Hi Nikhil. I believe you need to run on command prompt/terminal “dotnet restore” so the dotnet will download the nuget packages.
LikeLike
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?
LikeLike
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. 🤗
LikeLike
Thanks for your help. The command works like magic and really appreciate all your help 🙂 Can’t wait to explore more on VS code.
LikeLiked by 1 person
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.
LikeLike
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.
LikeLike
Thanks for your immediate response. I really appreciate it. Could you please provide any reference for better understanding?
LikeLike
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
LikeLike
Thank you temmy for spending sometime for my query. I will go-through the recommendation.
LikeLike
Hello, I’m testing your code and works perfectly with update. But if I try to use with Create message into Step, and exeption ocurs like
“Microsoft.Xrm.Sdk.InvalidPluginExecutionException: ‘OrganizationServiceFault: cr0e6_ofertas With Id = f109e9b2-f912-ed11-b83d-000d3a3a2c96 Does Not Exist'”
We test with PreValidation, PreOperartion and PostOperation, Any suggestions?
Thank you in advance,
LikeLike
Hi JP, saw the issue with the code. I add changes to the GetCurrent method to use the “Target” object when on create. Let me know if it works!
LikeLike
Hi, now works fine, without errors, but the setter attributes not saved 😦
The pluguin CODE:
public class AddOferta : PluginBase
{
public AddOferta(string unsecureConfiguration, string secureConfiguration)
: base(typeof(AddOferta))
{
}
}
The step properties are:
Message: Create and the Event: PosOperation.
Thank you!
LikeLike
Hi JP. For post, you can’t use Set method. Instead you need to create something like:
var update = new Entity(“entityname”){Id = Target.Id};
update[“yourattr”]=value;
Service.Update(update);
LikeLike
Hi, works fine using PreOperation Event and assign pluguin variables like:
localPluginContext.Target[“cr0e6_numerooferta”] = 1655;
localPluginContext.Target[“cr0e6_versionoferta”] = 1;
localPluginContext.Target[“cr0e6_name”] = “1655 – 1”;
Thank’s!
LikeLike
Yes pre-val, pre-op you can use set. The only exception is only for post as the “Target” object already committed from database stand point. That’s why you need to do Save manually. You can refer to my blog post https://wp.me/p9MTqq-1x
LikeLike