---
name: build-plugins
description: Scaffolds, builds, registers, updates, debugs, and troubleshoots Dataverse plugins (server-side C# code). Use when creating new plugin projects, writing plugin code, updating existing plugins, fixing plugin errors, reviewing plugin architecture, or working with plugin registration steps and images. Covers project scaffolding, PluginBase patterns, Literals/constants, CRUD separation, tracing, and MCP registration tools.
argument-hint: "[environment-url] [solution-name] [optional: --table TABLE]"
---

# Build Plugins Skill

Guide the user through scaffolding, building, compiling, registering, and managing Dataverse plugins (server-side C# code that runs on data events).

## Overview

Dataverse plugins are C# classes implementing `IPlugin` that execute on server-side events (Create, Update, Delete, etc.). This skill covers the full lifecycle: project scaffolding, code writing, local compilation, registration via MCP tools, step configuration, and debugging.

## Pre-Flight

Before building or modifying a plugin:
- Confirm the target environment, solution name, and publisher prefix
- If modifying an existing plugin: `dataverse_list_plugin_assemblies` to find the assembly, `dataverse_list_plugin_steps` to understand current registrations
- If creating a new plugin: verify the target table exists (`dataverse_get_table`) and understand its columns and relationships
- Verify the .NET development environment is available (dotnet CLI, correct SDK version)
- Plugins are ALWAYS C# (.NET). Never scaffold Python, Node.js, Java, or any other runtime.

## Scaffolding a New Plugin Project

When creating a new plugin project from scratch, scaffold the full project structure. This ensures proper architecture from the start — thin plugin classes, centralized constants, separated business logic, and production-grade tracing.

### Project Structure

```
{ProjectName}/
  {ProjectName}.sln
  Plugin/
    {ProjectName}.csproj
    key.snk                      # Strong name key (required for Sandbox)
    PluginBase.cs                # Abstract base class + LocalPluginContext
    ServiceConnection.cs         # Context/Trace/Service wrapper for helpers
    Literals.cs                  # Entity/field name constants (single source of truth)
    Enums.cs                     # PluginStage + domain option set enums
    {PluginClassName}.cs         # Plugin class(es) — thin, delegates to helpers
    CRUD/
      RetrieveOperations.cs      # All Dataverse retrieve/query operations
      CreateOperations.cs        # All Dataverse create operations
      UpdateOperations.cs        # All Dataverse update operations
    Helpers/
      {Feature}Helper.cs         # Business logic (one helper per feature/domain)
    Infrastructure/
      ServiceContainer.cs        # Lazy-init container for CRUD ops + services
```

### Step 1: Create Solution and Project

```bash
# Create solution
mkdir {ProjectName} && cd {ProjectName}
dotnet new sln -n {ProjectName}

# Create plugin class library
mkdir Plugin && cd Plugin
dotnet new classlib -n {ProjectName} --framework net462
cd ..

# Add project to solution
dotnet sln add Plugin/{ProjectName}.csproj

# Generate strong name key
# Find sn.exe in Windows SDK tools:
"C:/Program Files (x86)/Microsoft SDKs/Windows/v10.0A/bin/NETFX 4.8 Tools/sn.exe" -k Plugin/key.snk
```

### Step 2: Configure .csproj

Replace the generated `.csproj` with:

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net462</TargetFramework>
    <RootNamespace>{ProjectNamespace}</RootNamespace>
    <AssemblyName>{ProjectName}</AssemblyName>
    <SignAssembly>true</SignAssembly>
    <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CrmSdk.CoreAssemblies" Version="9.0.2.56" />
    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net462" Version="1.0.3" PrivateAssets="all" />
  </ItemGroup>
</Project>
```

The `Microsoft.NETFramework.ReferenceAssemblies.net462` NuGet package provides the net462 targeting pack without needing the .NET Framework Developer Pack installed.

### Step 3: Create PluginBase.cs

The abstract base class handles service resolution, tracing, and error handling so plugin classes stay thin:

```csharp
using System;
using System.Diagnostics;
using Microsoft.Xrm.Sdk;

namespace {ProjectNamespace}
{
    /// <summary>
    /// Base class for all plugins. Handles service resolution, tracing, and error handling.
    /// Plugin classes inherit this and implement ExecutePlugin().
    /// </summary>
    public abstract class PluginBase : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            var context = (IPluginExecutionContext)
                serviceProvider.GetService(typeof(IPluginExecutionContext));
            var tracingService = (ITracingService)
                serviceProvider.GetService(typeof(ITracingService));
            var factory = (IOrganizationServiceFactory)
                serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service = factory.CreateOrganizationService(context.UserId);

            var sw = Stopwatch.StartNew();
            tracingService.Trace("[+0ms] {0} triggered: {1} on {2} (Depth={3})",
                GetType().Name, context.MessageName, context.PrimaryEntityName, context.Depth);

            try
            {
                var cnx = new ServiceConnection(context, tracingService, service);
                ExecutePlugin(cnx);
                tracingService.Trace("[+{0}ms] {1} completed successfully",
                    sw.ElapsedMilliseconds, GetType().Name);
            }
            catch (InvalidPluginExecutionException)
            {
                throw; // Already a user-facing error, re-throw as-is
            }
            catch (Exception ex)
            {
                tracingService.Trace("[+{0}ms] ERROR: {1}", sw.ElapsedMilliseconds, ex.ToString());
                throw new InvalidPluginExecutionException(
                    "An error occurred in " + GetType().Name + ". See trace logs for details.", ex);
            }
        }

        /// <summary>
        /// Override this in each plugin class with your business logic.
        /// </summary>
        protected abstract void ExecutePlugin(ServiceConnection cnx);
    }
}
```

### Step 4: Create ServiceConnection.cs

A lightweight wrapper that bundles context, tracing, and service for passing to helpers:

```csharp
using Microsoft.Xrm.Sdk;

namespace {ProjectNamespace}
{
    /// <summary>
    /// Bundles plugin execution services for passing to helper classes.
    /// Keeps helper method signatures clean (one parameter instead of three).
    /// </summary>
    public class ServiceConnection
    {
        public IPluginExecutionContext Context { get; }
        public ITracingService Trace { get; }
        public IOrganizationService Service { get; }

        public ServiceConnection(IPluginExecutionContext context,
            ITracingService trace, IOrganizationService service)
        {
            Context = context;
            Trace = trace;
            Service = service;
        }
    }
}
```

### Step 5: Create Literals.cs

The single source of truth for all entity and field logical names. **Never use magic strings in plugin code** — always reference this file:

```csharp
namespace {ProjectNamespace}
{
    /// <summary>
    /// Centralized entity and field logical name constants.
    /// Add a static class per entity with all fields the plugin touches.
    /// Never hardcode entity/field names elsewhere — always reference Literals.
    /// </summary>
    public static class Literals
    {
        // Add one static class per entity your plugin interacts with.
        // Example:
        //
        // public static class Account
        // {
        //     public const string EntityName = "account";
        //     public const string Id = "accountid";
        //     public const string Name = "name";
        //     public const string PrimaryContactId = "primarycontactid";
        //     public const string StatusCode = "statuscode";
        // }
    }
}
```

### Step 6: Create Enums.cs

Option set integer values and plugin stage constants:

```csharp
namespace {ProjectNamespace}
{
    /// <summary>
    /// Plugin execution stages for context validation.
    /// </summary>
    public enum PluginStage
    {
        PreValidation = 10,
        PreOperation = 20,
        PostOperation = 40
    }

    /// <summary>
    /// Standard Dataverse state codes.
    /// </summary>
    public enum StateCode
    {
        Active = 0,
        Inactive = 1
    }

    // Add domain-specific option set enums here.
    // IMPORTANT: Dataverse choices and C# enums are NOT automatically synced.
    // Creating a choice value in Dataverse does NOT create the C# enum.
    // Keep them in sync manually.
    //
    // Example:
    // public enum ApplicationStatus
    // {
    //     Draft = 100000000,
    //     Submitted = 100000001,
    //     Approved = 100000002,
    //     Rejected = 100000003
    // }
}
```

### Step 7: Create CRUD Operation Classes

Centralize all Dataverse data access. This prevents scattered `service.Retrieve()` calls and makes queries easy to find and maintain.

**CRUD/RetrieveOperations.cs:**

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace {ProjectNamespace}.CRUD
{
    /// <summary>
    /// All Dataverse retrieve/query operations. Centralizes data access
    /// so field references are easy to find and maintain.
    /// </summary>
    public class RetrieveOperations
    {
        private readonly ServiceConnection _cnx;

        public RetrieveOperations(ServiceConnection cnx)
        {
            _cnx = cnx;
        }

        // Add retrieve methods here. Always specify ColumnSet explicitly.
        // Example:
        //
        // public Entity GetAccount(Guid accountId)
        // {
        //     return _cnx.Service.Retrieve(
        //         Literals.Account.EntityName,
        //         accountId,
        //         new ColumnSet(Literals.Account.Name, Literals.Account.StatusCode));
        // }
    }
}
```

Create `CRUD/CreateOperations.cs` and `CRUD/UpdateOperations.cs` with the same pattern.

### Step 8: Create ServiceContainer.cs

Lazy-init container that creates CRUD operations and helpers on demand:

```csharp
using {ProjectNamespace}.CRUD;

namespace {ProjectNamespace}.Infrastructure
{
    /// <summary>
    /// Lazy-initialized service container. Creates CRUD operations and helpers
    /// on first access within a single plugin execution. Each execution gets
    /// its own container — no cross-execution state.
    /// </summary>
    public class ServiceContainer
    {
        private readonly ServiceConnection _cnx;
        private RetrieveOperations _retrieve;
        private CreateOperations _create;
        private UpdateOperations _update;

        public ServiceContainer(ServiceConnection cnx)
        {
            _cnx = cnx;
        }

        public RetrieveOperations Retrieve =>
            _retrieve ?? (_retrieve = new RetrieveOperations(_cnx));
        public CreateOperations Create =>
            _create ?? (_create = new CreateOperations(_cnx));
        public UpdateOperations Update =>
            _update ?? (_update = new UpdateOperations(_cnx));
    }
}
```

### Step 9: Create Your Plugin Class

Plugin classes should be thin — validate context, create the service container, delegate to a helper:

```csharp
using Microsoft.Xrm.Sdk;
using {ProjectNamespace}.Helpers;
using {ProjectNamespace}.Infrastructure;

namespace {ProjectNamespace}
{
    /// <summary>
    /// Post-Update on Account: recalculates related totals when status changes.
    /// </summary>
    public class AccountPostUpdate : PluginBase
    {
        protected override void ExecutePlugin(ServiceConnection cnx)
        {
            // Context validation
            if (cnx.Context.Depth > 1) return;
            if (cnx.Context.MessageName != "Update") return;

            var target = (Entity)cnx.Context.InputParameters["Target"];
            if (!target.Contains(Literals.Account.StatusCode)) return;

            cnx.Trace.Trace("Processing status change for account {0}", cnx.Context.PrimaryEntityId);

            // Delegate to helper
            var container = new ServiceContainer(cnx);
            var helper = new AccountHelper(cnx, container);
            helper.RecalculateTotals(cnx.Context.PrimaryEntityId);
        }
    }
}
```

### Step 10: Build and Verify

```bash
cd {ProjectName}
dotnet restore
dotnet build Plugin -c Release
```

The compiled DLL will be at `Plugin/bin/Release/net462/{ProjectName}.dll`.

## Common Plugin Patterns

**Pre-Validation (stage 10):** Validate data before the platform validates it. Throw `InvalidPluginExecutionException` to block the operation with a user-facing message.

**Pre-Operation (stage 20):** Modify the Target entity before it's saved. Changes to `Target` are included in the database transaction.

**Post-Operation (stage 40):** React after the record is saved. Use for creating related records, sending notifications, or triggering integrations. Access pre-image for "before" values.

**Accessing Pre/Post Images:**
```csharp
Entity preImage = cnx.Context.PreEntityImages["PreImage"];
Entity postImage = cnx.Context.PostEntityImages["PostImage"];
string oldName = preImage.GetAttributeValue<string>("name");
```

**Early-bound vs Late-bound:** Late-bound (`Entity` class) is simpler for MCP-driven development since it doesn't require generated proxy classes.

## Architecture Best Practices

### Thin Plugins, Fat Helpers

Keep plugin classes thin — they should orchestrate, not compute. All business logic goes in Helper classes under `Helpers/`. Plugin classes should only: resolve services, validate context, create the ServiceContainer, and call a helper method.

### Context Validation

Always validate execution context before processing. This prevents infinite loops, wrong-stage execution, and unnecessary processing:

```csharp
// In your plugin's ExecutePlugin method:
if (cnx.Context.Depth > 1) return;                    // Prevent infinite recursion
if (cnx.Context.MessageName != "Update") return;       // Verify correct message
if (!target.Contains(Literals.Account.StatusCode)) return; // Verify relevant attributes
```

### Constants / Literals Pattern

All entity names, field names, and relationship names go in `Literals.cs`. All option set integer values go in `Enums.cs`. **Never hardcode these values anywhere else.** When adding a new field reference, add it to Literals first, then use the constant in your code.

### N+1 Query Prevention

When processing entities in a loop, never query inside the loop. Pre-fetch with a batch query and cache in a Dictionary:

```csharp
// BAD: N+1 queries
foreach (var item in items)
{
    var related = cnx.Service.Retrieve("relatedtable",
        item.GetAttributeValue<EntityReference>("lookupid").Id, cols);
}

// GOOD: Pre-fetch + Dictionary lookup
var relatedIds = items
    .Select(i => i.GetAttributeValue<EntityReference>("lookupid").Id)
    .Distinct().ToList();
var query = new QueryExpression("relatedtable") { ColumnSet = cols };
query.Criteria.AddCondition("relatedtableid", ConditionOperator.In,
    relatedIds.Cast<object>().ToArray());
var allRelated = cnx.Service.RetrieveMultiple(query).Entities.ToDictionary(e => e.Id);

foreach (var item in items)
{
    if (allRelated.TryGetValue(
        item.GetAttributeValue<EntityReference>("lookupid").Id, out var related))
    {
        // process with zero additional queries
    }
}
```

## Tracing Best Practices

### Delta-Time Tracing

Include elapsed time in trace output for performance diagnostics:

```csharp
var sw = System.Diagnostics.Stopwatch.StartNew();
cnx.Trace.Trace("[+{0}ms] Phase 1: Starting record processing", sw.ElapsedMilliseconds);
// ... do work ...
cnx.Trace.Trace("[+{0}ms] Phase 1: Processed {1} records", sw.ElapsedMilliseconds, count);
```

### Leveled Tracing

Use trace levels to manage output volume — especially important for plugins processing many records (trace buffer has a size limit):

- **Summary**: High-level status — "Processed 45 records, 3 errors"
- **Info**: Key values and decision points — "Record X routed to path B because status=Active"
- **Verbose**: Detailed field values — only enable during debugging
- **TimedOperation**: Wrap expensive operations with start/complete + elapsed time

### What to Trace

At minimum, trace at these points:
1. **Entry**: Plugin triggered, message name, entity, depth
2. **Context validation**: Why processing was skipped (if applicable)
3. **Key decisions**: Which branch was taken, what values drove the decision
4. **Query results**: Record counts returned (not full records)
5. **Exit**: Success/failure, total elapsed time

## Key Constraints

- **Must target net462** — Dataverse online rejects assemblies targeting net472 or higher (`0x8004420b`)
- **Must be signed** — unsigned assemblies fail in Sandbox isolation (`0x80048472`)
- **2-minute timeout** — Sandbox plugins are killed after 120 seconds. Batch large operations to stay within this limit.
- **Never use `ColumnSet(true)`** — always specify the columns you need. `ColumnSet(true)` retrieves all columns, causing performance issues and unnecessary data transfer.
- **PostImage attributes must be explicitly registered** — if your code reads an attribute from `PostEntityImages`, that attribute MUST be listed in the step registration's image attributes, or it will be null at runtime.
- **Race conditions in parallel async plugins** — when multiple async plugin instances trigger simultaneously (e.g., bulk update), check-then-set patterns are NOT atomic. Between checking "does this exist?" and creating it, another instance may already have created it. For critical exactly-once operations, use Dataverse optimistic concurrency (`RowVersion` + `ConcurrencyBehavior.IfRowVersionMatches`).
- **Plugins are stateless** — each execution creates a new instance with fresh memory. Static fields reset between executions. The only valid caching scope is within a single plugin execution.

## Registration via MCP

After building the DLL locally, register it in Dataverse using the MCP tools.

### 1. Upload Assembly

Read the DLL, base64-encode it, and register. **You MUST provide the plugin_types parameter** — the Web API does NOT auto-discover IPlugin classes (unlike the Plugin Registration Tool GUI).

```
# Read and encode the DLL
base64_content = base64_encode(read_file("Plugin/bin/Release/net462/{ProjectName}.dll"))

# Register assembly — list ALL IPlugin classes in plugin_types
dataverse_register_plugin_assembly(
  name: "{ProjectName}",
  content: base64_content,
  plugin_types: "{ProjectNamespace}.AccountPostUpdate,{ProjectNamespace}.ContactPreCreate",
  isolation_mode: "Sandbox",  # Required for online
  solution_name: "MySolution"
)
# Returns: assembly_id, created plugin_types[] with type_ids
```

> **IMPORTANT**: The `plugin_types` parameter is required. The Dataverse Web API does not inspect the DLL to find IPlugin implementations. You must explicitly list every class that implements `PluginBase` (which implements `IPlugin`) as a comma-separated string. The tool creates `plugintype` records for each.

### 2. Register Steps

For each plugin type, register the event trigger:

```
dataverse_register_plugin_step(
  plugin_type: "{ProjectNamespace}.AccountPostUpdate",
  message: "Update",
  table_name: "account",
  stage: "PostOperation",
  mode: "Synchronous",
  filtering_attributes: "statuscode",  # Optional: only for Update message
  solution_name: "MySolution"
)
```

### 3. Add Images (if needed)

```
dataverse_add_plugin_step_image(
  step_name_or_id: "{ProjectNamespace}.AccountPostUpdate.Update.account",
  image_type: "PreImage",
  entity_alias: "PreImage",
  attributes: "name,statuscode"
)
```

## Updating Plugins

When code changes, rebuild and push the new DLL. Step registrations persist — only the assembly content needs updating:

```
# Rebuild locally
dotnet build Plugin -c Release

# Upload new DLL via MCP
dataverse_update_plugin_assembly(
  assembly_name_or_id: "{ProjectName}",
  content: base64_encode(read_file("Plugin/bin/Release/net462/{ProjectName}.dll"))
)
```

## Debugging

- **Plugin Trace Logs:** Use `cnx.Trace.Trace()` in code. View traces in Power Platform Admin Center > Environments > [env] > Settings > Audit and logs > Plugin trace log.
- **Enable trace logging:** Settings > Administration > System Settings > Customization tab > Enable logging to plugin trace log = "All".
- **Common errors:**
  - `InvalidPluginExecutionException` — user-facing error message, shown in the UI
  - `0x80040265` — plugin assembly not found (registration issue)
  - `0x8004418b` — sandbox timeout (2 minutes max execution)
  - `0x80048472` — assembly must be signed for Sandbox isolation
  - `0x8004420b` — wrong target framework (must be net462, not net472+)

## MCP Tools Reference

| Tool | Purpose |
|------|---------|
| `dataverse_list_plugin_assemblies` | List registered assemblies |
| `dataverse_get_plugin_assembly` | Get assembly details + plugin types |
| `dataverse_register_plugin_assembly` | Upload DLL + register plugin types |
| `dataverse_update_plugin_assembly` | Update DLL content or description |
| `dataverse_delete_plugin_assembly` | Delete assembly (must remove steps first) |
| `dataverse_list_plugin_steps` | List step registrations |
| `dataverse_get_plugin_step` | Get step details + images |
| `dataverse_register_plugin_step` | Register step with friendly names |
| `dataverse_update_plugin_step` | Update step configuration |
| `dataverse_enable_plugin_step` | Enable a disabled step |
| `dataverse_disable_plugin_step` | Disable a step |
| `dataverse_delete_plugin_step` | Delete step + images |
| `dataverse_add_plugin_step_image` | Attach pre/post image |
| `dataverse_delete_plugin_step_image` | Remove an image |

## Validation

After registering or updating a plugin:
- Verify the assembly appears in the solution: `dataverse_list_plugin_assemblies` filtered by solution
- Verify all expected steps are registered: `dataverse_list_plugin_steps` for the assembly
- For each step, confirm: correct message (Create/Update/Delete), correct stage (PreValidation/PreOperation/PostOperation), correct entity, correct filtering attributes
- Test by performing the triggering action and checking the plugin trace log for expected output
- Report: assembly name, step count, messages registered, filtering attributes per step
