Back to Blog
7 min read

Dataverse Plugins: Server-Side Business Logic

Dataverse plugins execute custom business logic on the server when specific events occur. They run within the Dataverse execution pipeline, enabling powerful data validation, transformation, and integration scenarios.

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.


References:

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.