1 min read
Dataverse Plugins: Server-Side Business Logic
I wrote “Dataverse Plugins: Server-Side Business Logic” to share practical, production-minded guidance on this topic.
Plugin Development Basics
Set up a plugin project:
<!-- Plugin.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CrmSdk.CoreAssemblies" Version="9.0.2.*" />
<PackageReference Include="Microsoft.CrmSdk.Workflow" Version="9.0.2.*" />
</ItemGroup>
</Project>
Basic Plugin Structure
using Microsoft.Xrm.Sdk;
using System;
namespace MyPlugins
{
public class AccountValidationPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Get execution context
var context = (IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext));
// Get tracing service for debugging
var tracingService = (ITracingService)serviceProvider
.GetService(typeof(ITracingService));
// Get organization service factory
var serviceFactory = (IOrganizationServiceFactory)serviceProvider
.GetService(typeof(IOrganizationServiceFactory));
// Create organization service
var service = serviceFactory.CreateOrganizationService(context.UserId);
tracingService.Trace("Plugin execution started");
try
{
// Ensure we have a target entity
if (!context.InputParameters.Contains("Target"))
return;
var target = context.InputParameters["Target"] as Entity;
if (target == null)
return;
// Execute business logic
ValidateAccount(target, tracingService);
tracingService.Trace("Plugin execution completed successfully");
}
catch (InvalidPluginExecutionException)
{
throw; // Re-throw validation exceptions
}
catch (Exception ex)
{
tracingService.Trace($"Error: {ex.Message}");
throw new InvalidPluginExecutionException(
$"An error occurred in the plugin: {ex.Message}", ex);
}
}
private void ValidateAccount(Entity account, ITracingService trace)
{
trace.Trace("Validating account data");
// Validate account name
var name = account.GetAttributeValue<string>("name");
if (string.IsNullOrWhiteSpace(name))
{
throw new InvalidPluginExecutionException(
"Account name is required.");
}
if (name.Length < 3)
{
throw new InvalidPluginExecutionException(
"Account name must be at least 3 characters.");
}
// Validate revenue
var revenue = account.GetAttributeValue<Money>("revenue");
if (revenue != null && revenue.Value < 0)
{
throw new InvalidPluginExecutionException(
"Revenue cannot be negative.");
}
// Validate email format
var email = account.GetAttributeValue<string>("emailaddress1");
if (!string.IsNullOrEmpty(email) && !IsValidEmail(email))
{
throw new InvalidPluginExecutionException(
"Please enter a valid email address.");
}
trace.Trace("Account validation passed");
}
private bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
}
}
Pre-Operation Plugin
Modify data before it’s saved:
public class OrderCalculationPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext));
var trace = (ITracingService)serviceProvider
.GetService(typeof(ITracingService));
if (context.Stage != 20) // Pre-operation
return;
if (!context.InputParameters.Contains("Target"))
return;
var target = (Entity)context.InputParameters["Target"];
// Calculate order totals
CalculateOrderTotals(target, context, trace);
}
private void CalculateOrderTotals(
Entity order,
IPluginExecutionContext context,
ITracingService trace)
{
var serviceFactory = (IOrganizationServiceFactory)context
.GetType()
.GetProperty("ServiceProvider")
.GetValue(context);
trace.Trace("Calculating order totals");
decimal subtotal = 0;
decimal discount = 0;
decimal tax = 0;
// If updating, get existing line items
if (context.MessageName == "Update")
{
var orderId = order.Id != Guid.Empty
? order.Id
: ((EntityReference)context.InputParameters["Target"]).Id;
// Query line items
// Calculate totals from lines
}
// Get discount percentage
var discountPercent = order.GetAttributeValue<decimal?>("discountpercentage") ?? 0;
discount = subtotal * (discountPercent / 100);
// Calculate tax (example: 10%)
var taxRate = 0.10m;
tax = (subtotal - discount) * taxRate;
// Set calculated fields
order["discountamount"] = new Money(discount);
order["tax"] = new Money(tax);
order["totalamount"] = new Money(subtotal - discount + tax);
trace.Trace($"Calculated totals - Subtotal: {subtotal}, Discount: {discount}, Tax: {tax}");
}
}
Post-Operation Plugin
Execute logic after data is saved:
public class OpportunityWonPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext));
var trace = (ITracingService)serviceProvider
.GetService(typeof(ITracingService));
var serviceFactory = (IOrganizationServiceFactory)serviceProvider
.GetService(typeof(IOrganizationServiceFactory));
var service = serviceFactory.CreateOrganizationService(context.UserId);
if (context.Stage != 40) // Post-operation
return;
var target = (Entity)context.InputParameters["Target"];
var preImage = context.PreEntityImages.Contains("PreImage")
? context.PreEntityImages["PreImage"]
: null;
// Check if opportunity was just won
var newStatus = target.GetAttributeValue<OptionSetValue>("statecode")?.Value;
var oldStatus = preImage?.GetAttributeValue<OptionSetValue>("statecode")?.Value;
if (newStatus == 1 && oldStatus != 1) // Won
{
trace.Trace("Opportunity won - executing post-win actions");
// Create follow-up task
CreateFollowUpTask(service, target, context);
// Update account statistics
UpdateAccountStatistics(service, target, trace);
// Send notification
SendWinNotification(service, target, trace);
}
}
private void CreateFollowUpTask(
IOrganizationService service,
Entity opportunity,
IPluginExecutionContext context)
{
var task = new Entity("task");
task["subject"] = "Follow up on won opportunity";
task["description"] = $"Congratulations on winning this opportunity! " +
$"Please follow up with the customer to ensure smooth onboarding.";
task["regardingobjectid"] = opportunity.ToEntityReference();
task["ownerid"] = opportunity.GetAttributeValue<EntityReference>("ownerid");
task["scheduledend"] = DateTime.Now.AddDays(7);
task["prioritycode"] = new OptionSetValue(2); // High
service.Create(task);
}
private void UpdateAccountStatistics(
IOrganizationService service,
Entity opportunity,
ITracingService trace)
{
var accountRef = opportunity.GetAttributeValue<EntityReference>("parentaccountid");
if (accountRef == null) return;
var account = service.Retrieve(
"account",
accountRef.Id,
new ColumnSet("numberofemployees", "revenue"));
// Update won opportunities count (custom field)
var currentWon = account.GetAttributeValue<int?>("cr_wonopportunities") ?? 0;
var updateAccount = new Entity("account", accountRef.Id);
updateAccount["cr_wonopportunities"] = currentWon + 1;
service.Update(updateAccount);
trace.Trace($"Updated account statistics for {accountRef.Name}");
}
private void SendWinNotification(
IOrganizationService service,
Entity opportunity,
ITracingService trace)
{
// Create email notification
var email = new Entity("email");
email["subject"] = $"Opportunity Won: {opportunity.GetAttributeValue<string>("name")}";
email["description"] = $"Great news! The opportunity has been won.\n\n" +
$"Value: {opportunity.GetAttributeValue<Money>("estimatedvalue")?.Value:C}";
// Get owner email
var ownerRef = opportunity.GetAttributeValue<EntityReference>("ownerid");
var from = new Entity("activityparty");
from["partyid"] = new EntityReference("systemuser", Guid.Empty); // System
var to = new Entity("activityparty");
to["partyid"] = ownerRef;
email["from"] = new EntityCollection(new[] { from });
email["to"] = new EntityCollection(new[] { to });
email["regardingobjectid"] = opportunity.ToEntityReference();
var emailId = service.Create(email);
// Send the email
var sendRequest = new OrganizationRequest("SendEmail");
sendRequest["EmailId"] = emailId;
sendRequest["IssueSend"] = true;
service.Execute(sendRequest);
trace.Trace("Win notification sent");
}
}
Plugin with External Service Call
Integrate with external APIs:
public class AddressValidationPlugin : IPlugin
{
private readonly string _apiKey;
public AddressValidationPlugin(string unsecureConfig, string secureConfig)
{
// Get API key from secure configuration
_apiKey = secureConfig;
}
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext));
var trace = (ITracingService)serviceProvider
.GetService(typeof(ITracingService));
var target = (Entity)context.InputParameters["Target"];
// Get address fields
var street = target.GetAttributeValue<string>("address1_line1");
var city = target.GetAttributeValue<string>("address1_city");
var state = target.GetAttributeValue<string>("address1_stateorprovince");
var postal = target.GetAttributeValue<string>("address1_postalcode");
var country = target.GetAttributeValue<string>("address1_country");
if (string.IsNullOrEmpty(street) && string.IsNullOrEmpty(city))
return;
trace.Trace("Validating address with external service");
try
{
var validatedAddress = ValidateAddressAsync(
street, city, state, postal, country).Result;
if (validatedAddress != null)
{
// Update with validated/standardized address
target["address1_line1"] = validatedAddress.Street;
target["address1_city"] = validatedAddress.City;
target["address1_stateorprovince"] = validatedAddress.State;
target["address1_postalcode"] = validatedAddress.PostalCode;
target["address1_country"] = validatedAddress.Country;
target["address1_latitude"] = validatedAddress.Latitude;
target["address1_longitude"] = validatedAddress.Longitude;
trace.Trace("Address validated and standardized");
}
}
catch (Exception ex)
{
trace.Trace($"Address validation failed: {ex.Message}");
// Don't block the operation, just log the error
}
}
private async Task<ValidatedAddress> ValidateAddressAsync(
string street, string city, string state, string postal, string country)
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", _apiKey);
var request = new
{
street,
city,
state,
postalCode = postal,
country
};
var response = await client.PostAsJsonAsync(
"https://api.addressvalidation.com/validate",
request);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<ValidatedAddress>();
}
return null;
}
}
public class ValidatedAddress
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
}
Plugin Registration
Register plugins using the Plugin Registration Tool or code:
// Registration helper
public class PluginRegistration
{
public void RegisterAccountValidationPlugin(IOrganizationService service)
{
// Register assembly
var assembly = new Entity("pluginassembly");
assembly["name"] = "MyPlugins";
assembly["content"] = Convert.ToBase64String(File.ReadAllBytes("MyPlugins.dll"));
assembly["isolationmode"] = new OptionSetValue(2); // Sandbox
assembly["sourcetype"] = new OptionSetValue(0); // Database
var assemblyId = service.Create(assembly);
// Register plugin type
var pluginType = new Entity("plugintype");
pluginType["name"] = "MyPlugins.AccountValidationPlugin";
pluginType["typename"] = "MyPlugins.AccountValidationPlugin";
pluginType["friendlyname"] = "Account Validation Plugin";
pluginType["pluginassemblyid"] = new EntityReference("pluginassembly", assemblyId);
var typeId = service.Create(pluginType);
// Register step
var step = new Entity("sdkmessageprocessingstep");
step["name"] = "Account Validation - Create";
step["plugintypeid"] = new EntityReference("plugintype", typeId);
step["sdkmessageid"] = GetMessageId(service, "Create");
step["sdkmessagefilterid"] = GetFilterId(service, "account", "Create");
step["stage"] = new OptionSetValue(20); // Pre-operation
step["mode"] = new OptionSetValue(0); // Synchronous
step["rank"] = 1;
service.Create(step);
}
private EntityReference GetMessageId(IOrganizationService service, string messageName)
{
// Query sdkmessage table
return new EntityReference("sdkmessage", Guid.Empty);
}
private EntityReference GetFilterId(
IOrganizationService service, string entity, string message)
{
// Query sdkmessagefilter table
return new EntityReference("sdkmessagefilter", Guid.Empty);
}
}
Summary
Dataverse plugins provide:
- Server-side validation and logic
- Pre and post-operation execution
- External service integration
- Transaction participation
- Secure configuration storage
Build robust business logic that enforces data integrity and automates processes.