Dynamics 365

Dynamics 365 Plugin Development: Patterns That Scale

10 min read
#dynamics-365#plugins#dotnet#testing
Georg Pfeiffer
Georg Pfeiffer
Senior Solution Architect

After building and maintaining Dynamics 365 plugins across multiple enterprise projects, I've accumulated a list of patterns that consistently lead to maintainable, testable code — and an equally long list of anti-patterns that cause late-night debugging sessions.

Plugin Architecture Basics

Every Dynamics 365 plugin implements `IPlugin` with a single `Execute` method. The challenge is keeping that method clean as business logic grows:

csharp
public class CreateInvoicePlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider
            .GetService(typeof(IPluginExecutionContext));
        var factory = (IOrganizationServiceFactory)serviceProvider
            .GetService(typeof(IOrganizationServiceFactory));
        var service = factory.CreateOrganizationService(context.UserId);

        if (context.InputParameters["Target"] is not Entity target)
            return;

        // business logic starts here...
    }
}

This boilerplate is fine for a single plugin, but it doesn't scale across dozens of them.

The Plugin Base Class Pattern

Extract the boilerplate into a base class that provides typed access to context and services:

csharp
public abstract class PluginBase<TEntity> : IPlugin
    where TEntity : Entity
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var ctx = new PluginContext<TEntity>(serviceProvider);
        try
        {
            ExecutePlugin(ctx);
        }
        catch (Exception ex) when (ex is not InvalidPluginExecutionException)
        {
            ctx.Trace(ex.ToString());
            throw new InvalidPluginExecutionException(
                $"An error occurred in {GetType().Name}.", ex);
        }
    }

    protected abstract void ExecutePlugin(PluginContext<TEntity> context);
}

Now your actual plugin logic is focused and testable:

csharp
public class ValidateOrderPlugin : PluginBase<Order>
{
    protected override void ExecutePlugin(PluginContext<Order> ctx)
    {
        var order = ctx.Target;
        if (order.TotalAmount <= 0)
            throw new InvalidPluginExecutionException(
                "Order total must be greater than zero.");
    }
}

Testing Plugins Without Dynamics

The biggest mistake I see teams make is not testing plugins at all because "you need a Dynamics instance." You don't. Mock the `IOrganizationService`:

csharp
[Fact]
public void ValidateOrder_RejectsZeroTotal()
{
    var service = Substitute.For<IOrganizationService>();
    var context = new FakePluginContext<Order>
    {
        Target = new Order { TotalAmount = 0 },
        OrganizationService = service
    };

    var plugin = new ValidateOrderPlugin();

    Assert.Throws<InvalidPluginExecutionException>(
        () => plugin.ExecutePlugin(context));
}

This catches logic errors minutes after writing the code, not days later in a shared dev environment.

Common Anti-Patterns

**1. Querying inside loops.** Every `service.Retrieve` call is a network roundtrip. Use `QueryExpression` or FetchXML to batch:

csharp
// bad: N+1 queries
foreach (var id in contactIds)
    service.Retrieve("contact", id, columns);

// good: single query
var query = new QueryExpression("contact")
{
    ColumnSet = columns,
    Criteria = { Conditions = {
        new ConditionExpression("contactid",
            ConditionOperator.In, contactIds.ToArray())
    }}
};
service.RetrieveMultiple(query);

**2. Ignoring execution depth.** Plugins can trigger other plugins. Without a depth check, you get infinite loops:

csharp
if (ctx.ExecutionContext.Depth > 2)
    return; // prevent recursive execution

**3. Hardcoding entity logical names.** Use early-bound types or constants. Typos in string-based field access are silent failures.

Key Takeaways

  • Use a plugin base class to eliminate boilerplate and enforce error handling
  • Mock `IOrganizationService` for unit tests — don't skip testing
  • Batch queries, respect execution depth, and use early-bound types
  • Log generously via `ITracingService` — it's your only debugger in production

Share this article

Georg Pfeiffer

About the Author

Georg is a senior solution architect specializing in .NET, Azure, and Dynamics 365. He helps organizations design and build scalable, maintainable enterprise systems. When he's not writing code, he's writing about it here.

Learn more about Georg

© 2026 georgpfeiffer.dev. All rights reserved.

Built with SvelteKit & Tailwind CSS