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




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 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
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
        };

        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



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *