DEV Community

Cover image for Stop Writing Plugins Like It’s 2011: Modern Architecture Guide
Mohamed ELgharably
Mohamed ELgharably

Posted on • Edited on

Stop Writing Plugins Like It’s 2011: Modern Architecture Guide

🌟 Modern Dataverse Plugin Architecture (2025 Edition)

A Clean, Testable, Maintainable, and DI-Friendly Template for Power Platform Developers

A complete, ready-to-use architecture template you can drop into your next Dataverse / Dynamics 365 project.


πŸ”₯ Why This Article?

Most Dataverse plugins still follow the old 2011 pattern:

  • Logic inside Execute()
  • Hard-coded field names
  • No testability
  • Zero separation of concerns
  • Hard to extend
  • Not reusable outside plugins
  • Difficult to maintain

This article gives you a modern, scalable, testable plugin architecture with:

βœ” Clean separation

βœ” Supports multi-project structure

βœ” Minimal DI (no heavy libraries)

βœ” Test-friendly

βœ” Reusable in Azure Functions / Custom APIs

βœ” NuGet-based plugin deployment

βœ” No system-specific logic

βœ” Perfect as a starter template


🧱 Architecture Overview

/PluginSolution
β”‚
β”œβ”€β”€ Core
β”‚   β”œβ”€β”€ Interfaces
β”‚   β”œβ”€β”€ Models
β”‚   └── Enums
β”‚
β”œβ”€β”€ Infrastructure
β”‚   β”œβ”€β”€ Repositories
β”‚   └── Services
β”‚
β”œβ”€β”€ Plugins
β”‚   β”œβ”€β”€ PluginBase.cs  (from CLI template)
β”‚   └── SamplePlugin.cs
β”‚
└── Startup
    └── PluginFactory.cs
Enter fullscreen mode Exit fullscreen mode

πŸ— Layer Explanation

1. Core Layer (Pure, CRM-agnostic)

Contains:

  • Interfaces
  • Lightweight models
  • Enums
  • Zero dependency on Microsoft.Xrm.Sdk Benefits:
  • 100% testable
  • Reusable in Azure Functions / Custom APIs
  • Pure C# domain layer
2. Infrastructure Layer

Contains:

  • Repositories
  • Dataverse operations
  • FetchXML logic
  • Business services This layer knows about Dataverse so the rest of the system doesn’t have to.
3. Plugins Layer

Responsible for:

  • Orchestration
  • Extracting context
  • Mapping Entity β†’ Core Model
  • Calling services The plugin stays thin and easy to reason about.
4. Startup / Factory Layer (Minimal DI)

Instead of heavy DI (which causes sandbox issues), we use a simple factory pattern:

  • No dependency conflicts
  • No BCL async interface issues
  • No Microsoft.Extensions.* packages needed Small. Fast. Compatible with Sandbox isolation.

⚑ Modern Deployment: PAC CLI + .nupkg Package

In 2025, plugins should not be deployed as DLLs.
Microsoft now provides:

pac plugin init --outputDirectory . --skip-signing
Enter fullscreen mode Exit fullscreen mode

This command:

  • Creates a structured plugin project
  • Includes PluginBase + ILocalPluginContext
  • Supports NuGet packaging
  • Removes need for manual DLL signing

🎯 Why --skip-signing?

Because NuGet-based plugin deployment does not require strong naming.
Benefits:

  • No shared signing keys
  • No assembly conflicts
  • Smooth CI/CD
  • Faster team collaboration

🧩 Minimal DI (Factory Pattern)

Heavy DI causes:

  • Slow plugin execution
  • Version conflicts
  • Sandbox restrictions
  • Hard-to-debug runtime errors

So we use:

Plugin β†’ Factory β†’ Services β†’ Repositories
Enter fullscreen mode Exit fullscreen mode

This gives you DI benefits without DI overhead.


🧩 Optional: Using Early-Bound Classes (Highly Recommended)

Although the template in this article uses a lightweight EntityModel for simplicity,
the architecture is fully compatible with Early-Bound classes

Note:

The Power Platform CLI can now generate Early-Bound classes for you automatically using:

pac modelbuilder build --outputDirectory Models

Just drop the generated models into a separate project and reference it from your Plugin + Infrastructure layers.


πŸ“ Template Code (Copy/Paste)

A completely generic, reusable template.


πŸ“¦ Core: Model
namespace PluginTemplate.Core.Models
{
    public class EntityModel
    {
        public Guid Id { get; set; }
        public string LogicalName { get; set; }
        public IDictionary<string, object> Attributes { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode
πŸ“¦ Core: Interface
namespace PluginTemplate.Core.Interfaces
{
    public interface IEntityValidationService
    {
        void Validate(EntityModel model, Guid userId);
    }
}
Enter fullscreen mode Exit fullscreen mode
πŸ“¦ Infrastructure: Repository Template
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using PluginTemplate.Core.Interfaces;

namespace PluginTemplate.Infrastructure.Repositories
{
    public interface ISampleRepository
    {
        Entity RetrieveEntity(Guid id);
    }

    public class SampleRepository : ISampleRepository
    {
        private readonly IOrganizationService _service;

        public SampleRepository(IOrganizationService service)
        {
            _service = service;
        }

        public Entity RetrieveEntity(Guid id)
        {
            return _service.Retrieve(
                "xyz_customtable",
                id,
                new ColumnSet("xyz_textfield"));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
πŸ“¦ Infrastructure: Service Template
using PluginTemplate.Core.Interfaces;
using PluginTemplate.Core.Models;

namespace PluginTemplate.Infrastructure.Services
{
    public class EntityValidationService : IEntityValidationService
    {
        public void Validate(EntityModel model, Guid userId)
        {
            // Add validation logic (optional)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
βš™οΈ Factory (Minimal DI)
using Microsoft.Xrm.Sdk;
using PluginTemplate.Core.Interfaces;
using PluginTemplate.Infrastructure.Repositories;
using PluginTemplate.Infrastructure.Services;

namespace PluginTemplate.Startup
{
    public static class PluginFactory
    {
        public static IEntityValidationService CreateValidationService(
            IOrganizationService service,
            ITracingService tracing)
        {
            var repository = new SampleRepository(service);

            return new EntityValidationService();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
πŸ”Œ Plugin Template
using System;
using Microsoft.Xrm.Sdk;
using PluginTemplate.Startup;
using PluginTemplate.Core.Models;
using PluginTemplate.Core.Interfaces;

public class SamplePlugin : PluginBase
{
    public SamplePlugin(string unsecure, string secure)
        : base(typeof(SamplePlugin)) { }

    protected override void ExecuteDataversePlugin(ILocalPluginContext ctx)
    {
        var context = ctx.PluginExecutionContext;
        var tracing = ctx.TracingService;
        var org = ctx.OrgSvcFactory.CreateOrganizationService(context.UserId);

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

        var model = new EntityModel
        {
            Id = target.Id,
            LogicalName = target.LogicalName,
            Attributes = target.Attributes.ToDictionary(a => a.Key, a => a.Value)
        };

        var service = PluginFactory.CreateValidationService(org, tracing);
        service.Validate(model, context.UserId);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“ Architecture Diagram

+-----------------------+
|        Plugins        |
|  (thin orchestration) |
+-----------+-----------+
            |
            v
+-----------+-----------+
|        Factory         |
|   (minimal DI layer)  |
+-----------+-----------+
            |
            v
+-----------+-----------+
|     Infrastructure    |
| Repositories/Services |
+-----------+-----------+
            |
            v
+-----------+-----------+
|      Core Layer       |
|  Interfaces + Models  |
+-----------------------+
Enter fullscreen mode Exit fullscreen mode

✨ Benefits of This Architecture

πŸ”Ή 1. Testable

Core + Infrastructure can reach 100% test coverage.

πŸ”Ή 2. Clean Separation

Plugin β†’ Service β†’ Repository.

πŸ”Ή 3. Reusable

The same services can be used in:

  • Plugins
  • Custom APIs
  • Azure Functions
  • Virtual Tables
πŸ”Ή 4. Minimal Dependencies

No need for:

  • Microsoft.Extensions.DependencyInjection
  • Async Interfaces
  • External DI frameworks

Top comments (6)

Collapse
 
cgoconseils profile image
Christophe Gondouin • Edited

In that philosophy, you could use the Open Source Framework developped by me and DIMSI : XrmFramework (aka.ms/XrmFramework).

It contains all you want in your modern plugin architecture post :

  • Metadata files generation
  • Dependency injection of services in plugins
  • Plugin step metadata in source code
  • Deploying tool without the need to use Plugin Registration Tool
  • Management of Custom Api Metadata and automatic deployment and registration
  • Remote debugging (debugging of plugins from the user desktop without the need to redeploy the DLL)
  • and many more functionalities
Collapse
 
mohamed_elgharably profile image
Mohamed ELgharably

Thanks for sharing this β€” XrmFramework is a solid piece of work!
It definitely delivers a lot of what modern plugin projects look for: metadata generation, DI support, custom API registration, deployment tooling, and especially the remote debugging feature β€” that’s a huge time-saver in real projects.

My post was focused on showing how to achieve a clean architecture without introducing a full framework, since many teams prefer to keep their footprint minimal or build on top of native tooling (PAC CLI + NuGet packaging). But for teams that want a more feature-rich, opinionated framework on top of Dataverse, XrmFramework is definitely worth looking into.

Thanks again for mentioning it β€” it’s great for readers to be aware of the options available in the community.

Collapse
 
thomas_vandyck_753d47628 profile image
Thomas Van Dyck

When starting a new project or setting up a new environment, this architectural approach is excellent, and I really appreciate the way you described it. However, we’re dealing with several years of legacy code: over 100 separate plugin files, some reused across multiple steps, with multiple images, and so on.

Is there a good way to β€œstart fresh” with this improved architecture in mind, without having to rebuild everything from scratch?

Are there any tools that can help us transition from our current project structure (A) to an improved structure (B)? For example, we are still using traditional DLLsβ€”how can we migrate those to NuGet packages? Any tips on how to approach this journey would be greatly appreciated.

Thanks in advance!

Collapse
 
mohamed_elgharably profile image
Mohamed ELgharably

If you have questions about DI, plugin packaging, or Dataverse architecture, drop them here β€” I’ll answer everything.

Collapse
 
dynamics_padawan_c6d9c925 profile image
Dynamics PadaWan

Hi Mohamed! Great article - this modern architecture is exactly what the Dynamics community needs! πŸŽ‰

I'm implementing this in my project and have two questions:

  1. Deployment Between Environments:
    You mention that "plugins should not be deployed as DLLs" and the PAC CLI supports NuGet packaging. However, when moving plugins between environments (DEV β†’ UAT β†’ PROD), do we still export/import the plugin as part of a Dataverse Solution (the traditional way)? Or is there a new deployment approach using .nupkg packages that I'm missing?

  2. EntityModel Bug:
    In the SamplePlugin.cs example, line 29 assigns target.Attributes (which is AttributeCollection) directly to EntityModel.Attributes (which is IDictionary). This causes a compile error CS0029. Should we add .ToDictionary(a => a.Key, a => a.Value) to convert it, or did you intend something different?

Thanks again for sharing this clean architecture approach! The separation of concerns with Core/Infrastructure layers is brilliant for testability.

Collapse
 
mohamed_elgharably profile image
Mohamed ELgharably • Edited

Thanks so much for the kind words β€” really appreciate it! πŸ™
I’m glad the architecture resonated with you.
Great questions as well β€” let me tackle them one by one:

1. Deployment Between Environments (DEV β†’ UAT β†’ PROD)

Yes, you still move your plugins between environments as part of a Dataverse Solution β€” nothing changes there.
So the lifecycle is:

Local Dev / Build:
pac plugin build β†’ produces .nupkg
pac plugin push β†’ registers the plugin in DEV (you can register using the plugin registration tool normally but use add new package instead of adding DLL ( assembly))

Environment Promotion:
Export Solution β†’ Import to UAT/PROD (as usual)

There is no new cross-environment deployment model for .nupkg yet


2. EntityModel Bug in the Template Example

Great catch! Yes β€” AttributeCollection cannot be assigned directly to IDictionary.

The correct version should convert it:
Attributes = target.Attributes.ToDictionary(a => a.Key, a => a.Value)

I kept the EntityModel deliberately simple so people could swap in Early-Bound or their own mapping approach, but you’re absolutely right that the template line should be updated for correctness. I’ll adjust it in the article β€” thanks for spotting it!


If you have more questions while implementing this pattern, feel free to reach out β€” always happy to help!