> For the complete documentation index, see [llms.txt](https://docs.ezra360.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ezra360.com/customization/plugins.md).

# Plugins

## 1. Overview: Plugins

### 1.1 What is a Plugin?

A Plugin is a class that implements the IXrmPlugin interface. It is triggered automatically by the&#x20;Ezra360 platform when a specific event occurs on a specific entity — for example, when a record is&#x20;created, updated, or deleted.\
Plugins run as part of the entity lifecycle. They receive a **PluginExecutionContext** object that provides&#x20;access to the triggering record and platform services.

#### Common use cases for plugins:

* Auto-creating related child records when a parent is saved
* Enforcing business rules before a record is committed to the database
* Sending notifications or emails when a record reaches a certain state
* Calculating rollup or summary values on related records
* Preventing deletion of records that have active dependencies

#### Plugin (IXrmPlugin) Characteristics

* [ ] Triggered automatically by entity events
* [ ] Implements IXrmPlugin
* [ ] Return type: void
* [ ] Execution step: PostUpdate, PreCreate, etc.
* [ ] Access: PluginExecutionContext
* [ ] Example: Enforce approval rules on save

## 2. Prerequisites & NuGet Packages

Before writing any code, ensure your development environment is properly set up.

### 2.1 Required Software

* Visual Studio 2022 (Community, Professional, or Enterprise edition)
* .NET Core 3.1 SDK — download from <https://dotnet.microsoft.com/download>
* Git (optional, for source control)

### 2.2 Creating the Project

In Visual Studio, create a new Class Library project targeting .NET Core 3.1:

1. Open Visual Studio 2022
2. Click Create a new project
3. Search for "Class Library" and select Class Library (.NET Core)
4. Name the project using the convention: CompanyName.Module.Feature — e.g. ERP.Ezra360.Package
5. Set the framework to .NET Core 3.1
6. Click Create

### 2.3 Required NuGet Packages

Add the following packages via NuGet Package Manager (Tools > NuGet Package Manager > Manage&#x20;&#x20;NuGet Packages for Solution):

<figure><img src="/files/7e0F1U2YVWgYGATU5HSM" alt=""><figcaption></figcaption></figure>

## 3. Project Structure in Visual Studio

A well-organised Ezra360 plugin project follows a consistent folder structure. Each folder serves a&#x20;specific purpose and maps to a distinct category of code responsibility.

### 3.1 Recommended Folder Layout

YourProject/\\

├── Models/ ← C# classes mapping to SQL query results\
│ ├── Directives.cs\
│ └── DirectiveSubmissions.cs\
├── Plugins/ ← Event-driven logic (IXrmPlugin)\
│ ├── CreateDirectiveSubmissions.cs\
│ └── SendConfirmationEmail.cs\\

### 3.2 Folder Descriptions

#### Plugins/&#xD;

Contains all event-driven plugin classes. Each file implements IXrmPlugin. These are triggered&#x20;automatically by entity lifecycle events such as PostUpdate, PreCreate, PostDelete, etc.

#### Models/&#xD;

Contains plain C# classes (POCOs) that map to the results of your SQL queries. When you execute a&#x20;SQL query through context.BusinessActions.ExecuteListQuery(), the SDK maps each result row to&#x20;an instance of your model class. Column names in the SQL SELECT must match property names on&#x20;the model.

## 4. Creating a Plugin (IXrmPlugin)

A plugin class must be public and implement the IXrmPlugin interface from the Xrm.Soft.Aer.SDK&#x20;package. There is one required method: ExecutePlugin(PluginExecutionContext context).

{% stepper %}
{% step %}

### Add a New Class File

Right-click the Plugins folder in Solution Explorer, select Add > Class, and give it a descriptive name&#x20;that reflects what the plugin does. For example:\
• CalculatePackageTotals.cs\
• CreateDirectiveSubmissions.cs
{% endstep %}

{% step %}

### Implement IXrmPlugin

Your class must use the correct using directives and implement the required interface. Below is the&#x20;standard boilerplate structure:

```cs
using Xrm.Soft.Aer.SDK;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;

namespace ERP.Ezra360.Package.Plugins
{
    public class CalculatePackageTotals : IXrmPlugin
    {
        private const decimal MaxTotalAmount = 1000000m;

        public void ExecutePlugin(PluginExecutionContext context)
        {
            var stopwatch = Stopwatch.StartNew();
            context.Trace("CalculatePackageTotals: Execution started.");

            try
            {
                // Validate
                ValidateContext(context);
                var packageLineId = GetPackageLineId(context);
                context.Trace($"CalculatePackageTotals: Processing PackageLine ID: {packageLineId}");

                // Get and validate lines
                var lines = GetPackageLines(context, packageLineId);
                if (!lines.Any())
                {
                    context.Trace("CalculatePackageTotals: No package lines found. Skipping update.");
                    return;
                }

                // Calculate and validate total
                var total = CalculateTotal(lines);
                ValidateTotal(total);

                // Update package
                UpdatePackage(context, lines.First().PackageId.Value, total);

                stopwatch.Stop();
                context.Trace($"CalculatePackageTotals: Completed successfully in {stopwatch.ElapsedMilliseconds}ms.");
            }
            catch (Exception ex)
            {
                context.Trace($"CalculatePackageTotals ERROR: {ex.Message}");
                context.Trace($"CalculatePackageTotals Stack Trace: {ex.StackTrace}");

                if (ex.InnerException != null)
                {
                    context.Trace($"CalculatePackageTotals Inner Exception: {ex.InnerException.Message}");
                }

                throw new InvalidPluginExecutionException($"CalculatePackageTotals failed: {ex.Message}", ex);
            }
        }

        private void ValidateContext(PluginExecutionContext context)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));

            if (context.Target == null)
                throw new InvalidOperationException("Target record is required.");
        }

        private Guid GetPackageLineId(PluginExecutionContext context)
        {
            var target = context.Target as BusinessEntity;
            if (target == null || !target.Id.HasValue || target.Id.Value == Guid.Empty)
                throw new InvalidOperationException("Valid target ID is required.");

            return target.Id.Value;
        }

        private List<PackageLine> GetPackageLines(PluginExecutionContext context, Guid packageLineId)
        {
            var query = PackageSQL.GetPackageLines.Replace("@PackageLineId", $"'{packageLineId}'");
            context.Trace($"CalculatePackageTotals: Executing query: {query}");

            return context.BusinessActions
                .ExecuteListQuery<PackageLine>(query)
                .ToList();
        }

        private decimal CalculateTotal(List<PackageLine> lines)
        {
            if (lines == null || lines.Count == 0)
                throw new InvalidOperationException("No package lines to calculate.");

            return Math.Round(lines.Sum(l => l.Amount), 2);
        }

        private void ValidateTotal(decimal total)
        {
            if (total < 0)
                throw new InvalidOperationException($"Total amount cannot be negative: {total:C}");

            if (total > MaxTotalAmount)
                throw new InvalidOperationException($"Total amount {total:C} exceeds maximum allowed {MaxTotalAmount:C}");
        }

        private void UpdatePackage(PluginExecutionContext context, Guid packageId, decimal total)
        {
            context.Trace($"CalculatePackageTotals: Updating Package ID: {packageId} with total: {total:C}");

            var package = new BusinessEntity("Package")
            {
                Id = packageId,
                Attributes = new List<EntityAttribute>
                {
                    new EntityAttribute 
                    { 
                        SchemaName = "totalamount", 
                        Value = total 
                    }
                }
            };

            context.BusinessActions.Update(package);
            context.Trace("CalculatePackageTotals: Package updated successfully.");
        }
    }

    public class PackageLine
    {
        public Guid Id { get; set; }
        public Guid? PackageId { get; set; }
        public decimal Amount { get; set; }
        public string ProductName { get; set; }
        public int Quantity { get; set; }
    }

    public static class PackageSQL
    {
        public const string GetPackageLines = @"
            SELECT 
                Id,
                PackageId,
                ISNULL(Amount, 0) AS Amount,
                ProductName,
                Quantity
            FROM PackageLine
            WHERE Id = @PackageLineId";
    }
}
```

{% endstep %}

{% step %}

### Key Context Methods

All platform operations are accessed through context.BusinessActions. Below are the most commonly&#x20;used methods:

<figure><img src="/files/9UmiXwFixZROMjKc3YMW" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Tip: Always wrap your plugin logic in a try/catch block. Unhandled exceptions in plugins will surface\
as generic platform errors and will not include your error message.
{% endhint %}
{% endstep %}
{% endstepper %}

### 4.1 Accessing the Triggering Record

The PluginExecutionContext exposes a Target property that represents the record that triggered the&#x20;event. Cast it to BusinessEntity to access its fields:

```c#
// Get the target record (the one that was created/updated/deleted) 
var target = (BusinessEntity)context.Target; 
 
// Access its Id 
Guid recordId = target.Id.Value; 
 
// Access an attribute value (if it was included in the trigger) 
var amount = target.Attributes 
.FirstOrDefault(a => a.SchemaName == "Amount")?.Value;
```

{% hint style="info" %}
Note: The Target entity only contains attributes that were changed in the triggering operation, not&#x20;the full record. To access fields that were not changed, use context.BusinessActions.RetrieveRecord()&#x20;to fetch the full record from the database.
{% endhint %}

## 5. Building the DLL in Visual Studio

Once your plugin code is complete, you need to compile the project to produce a&#x20;DLL file that will be uploaded to Ezra360.

### 5.1 Build the Solution

Go to the menu bar and select Build > Build Solution, or press Ctrl + Shift + B. Visual Studio will&#x20;compile all projects in the solution.

{% hint style="info" %}
The build must complete with zero errors. Warnings are acceptable. If there are&#x20;errors, the DLL file will not be generated and any existing DLL at the output path may&#x20;be stale or absent.
{% endhint %}

### 5.2 Locate the Output DLL

After a successful build, the compiled DLL is placed in:

*<mark style="color:$primary;">ProjectFolder\bin\Debug\netcoreapp3.1\YourProjectName.dll</mark>*

Navigate to this folder in Windows Explorer. The DLL filename matches your project name exactly. You&#x20;will also see a .pdb file (debug symbols) and a .deps.json file — only the .dll needs to be uploaded.

{% hint style="info" %}
✔ Tip: Always build in Debug mode when uploading to a sandbox or UAT environment. Use Release&#x20;mode only for production deployments. Debug builds include additional diagnostic information that&#x20;helps trace issues in Plugin Trace Logs.
{% endhint %}

### 5.3 Build Configuration Reference

<figure><img src="/files/smZUcAlas7Qaq7KkouOV" alt=""><figcaption></figcaption></figure>

## 6. Uploading the DLL to Ezra360

After building the DLL, it must be uploaded to Ezra360 as an Xrm Assembly record. The assembly&#x20;record holds the compiled code and serves as the container for all plugin classes and plugin message&#x20;registrations.

{% stepper %}
{% step %}

### Navigate to Xrm Assemblies&#x20;

Log into your Ezra360 environment. From the top navigation bar, open the Customizations menu. Scroll&#x20;down to Xrm Assemblies and click it.

<figure><img src="/files/mZlxkCYfLeMnsrO7mAwN" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/CSOPc4oT4Eg5w2DadrRI" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Create or Update the Assembly

If this is the first time you are uploading this DLL, click New to create a new assembly record. If the&#x20;assembly already exists and you are updating it after a code change, click on the existing record.

The assembly record form contains:

* File — a file upload control to select your .dll file
* Assembly Name — auto-populated from the DLL metadata after upload  &#x20;
* Version — auto-populated with your build version number (e.g. 2026.5.28.1422)

{% hint style="info" %}
The version number in Ezra360 reflects your assembly version attribute in the .csproj&#x20;file. It typically follows the format **Year.Month.Day.BuildNumber.** You can confirm you&#x20;have the right version after upload.
{% endhint %}
{% endstep %}

{% step %}

### Upload the DLL File

Click the Choose File button in the assembly form, navigate to your bin/Debug/netcoreapp3.1/ folder,&#x20;and select the .dll file. The Assembly Name and Version fields will auto-populate once the file is&#x20;selected.

Click Save & Close to complete the upload. The assembly is now registered in Ezra360 and ready for&#x20;plugin message registration.

<figure><img src="/files/uImFFLk4a7SOQlibBTBR" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

## 7. Registering Plugin Classes

After uploading your assembly, you must register each plugin class you want to use. Plugin classes are&#x20;listed under the XRM Plugin Classes tab on the assembly record. This step tells Ezra360 which C#&#x20;classes in your DLL are valid plugins.

### 7.1 Adding a New Plugin Class

On the assembly record, click the XRM Plugin Classes tab. Click Add New to open the Quick Create&#x20;dialog.

<figure><img src="/files/vKp1d0cVOAfyOabLyrPL" alt=""><figcaption></figcaption></figure>

Fill in the following fields:

* Name — must match the class name exactly as it appears in your C# code&#x20;
* Plugin Type — select the appropriate type for  &#x20;plugins
* Assembly — pre-filled with the current assembly record

<figure><img src="/files/2V1dyM72n3JlraqSrGtk" alt=""><figcaption></figcaption></figure>

## 8. Registering Plugin Messages

A Plugin Message is the configuration record that tells Ezra360 when to fire a specific plugin class. It&#x20;binds a plugin class to an entity and an execution step.\
Plugin messages are registered under the Plugin Messages tab on the assembly record.

### 8.1 Viewing Existing Plugin Messages

<figure><img src="/files/NDvnLRsjfgwuU4WuUJfB" alt=""><figcaption></figcaption></figure>

Each row in the Plugin Messages table represents one registration — one plugin bound to one&#x20;entity and one execution step.

### 8.2 Adding a New Plugin Message

Click Add New on the Plugin Messages tab to open the Quick Create dialog:

<figure><img src="/files/ypDjJyj5gHp6YEST9YLc" alt=""><figcaption></figcaption></figure>

Fill in all required fields (marked with a red asterisk):

<figure><img src="/files/tQCuSZGBeswHYsnNEehQ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/q7Hz71PmBfIcK6yLJLhz" alt=""><figcaption></figcaption></figure>

## 9. Updating an Existing Assembly&#x20;

After your initial deployment, subsequent code changes require re-uploading the DLL. The process is&#x20;similar to the initial upload but with a few important differences.

### 8.1 Re-Upload Process&#x20;

1. Make your code changes in Visual Studio&#x20;
2. Rebuild the solution (Ctrl + Shift + B) and confirm zero errors&#x20;
3. Navigate to Customizations > Xrm Assemblies in Ezra360&#x20;
4. Click on the existing assembly record&#x20;
5. Click the upload/attachment icon next to the Name field (the paperclip icon)&#x20;
6. Select the updated .dll from your bin/Debug/netcoreapp3.1/ folder&#x20;
7. The Version field will update to reflect the new build version&#x20;
8. Click Save

{% hint style="info" %}
✔ Tip: You do not need to re-register plugin messages after re-uploading a DLL. Your existing plugin&#x20;message registrations continue to work as long as the class names remain the same. Only add new&#x20;registrations when you add new plugin classes.
{% endhint %}

## 10. Execution Steps Reference

Execution steps define the precise moment in the entity's lifecycle when a plugin fires. Choosing the&#x20;wrong execution step is a common source of bugs — for example, using PreUpdate when you need the&#x20;record to already be saved before reading it back.

### 10.1 Create Events

<figure><img src="/files/4d3Mrk0LXWaQINqk4ZLI" alt=""><figcaption></figcaption></figure>

### 10.2 Update Events

<figure><img src="/files/mBYbdjQGN6wo2wrhqZdb" alt=""><figcaption></figcaption></figure>

### 10.3 Retrieve Events

<figure><img src="/files/CdaDjnlG4tC755LzuhLX" alt=""><figcaption></figcaption></figure>

### 10.4 Delete Events

<figure><img src="/files/cCQc4YhxSAg3848JyBSC" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ezra360.com/customization/plugins.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
