π 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 slow DI container startup
- No Microsoft.Extensions.* packages needed Small. Fast. Compatible with Sandbox.
β‘ 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
};
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 (1)
If you have questions about DI, plugin packaging, or Dataverse architecture, drop them here β Iβll answer everything.