π 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
π 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
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
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 ModelsJust 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; }
}
}
π¦ Core: Interface
namespace PluginTemplate.Core.Interfaces
{
public interface IEntityValidationService
{
void Validate(EntityModel model, Guid userId);
}
}
π¦ 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"));
}
}
}
π¦ 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)
}
}
}
βοΈ 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();
}
}
}
π 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);
}
}
π Architecture Diagram
+-----------------------+
| Plugins |
| (thin orchestration) |
+-----------+-----------+
|
v
+-----------+-----------+
| Factory |
| (minimal DI layer) |
+-----------+-----------+
|
v
+-----------+-----------+
| Infrastructure |
| Repositories/Services |
+-----------+-----------+
|
v
+-----------+-----------+
| Core Layer |
| Interfaces + Models |
+-----------------------+
β¨ 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)
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 :
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.
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!
If you have questions about DI, plugin packaging, or Dataverse architecture, drop them here β Iβll answer everything.
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:
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?
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.
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!