DataVerse

Building Custom Dynamics 365 Plugins with Claude Code: Email Anonymisation & Network Activities

One of the best ways to learn Dynamics 365 plugin development is to build something real. In this post, I’ll walk through two plugins I built for Dynamics 365 Sales — an Email Anonymisation Plugin and a Network Activities Plugin — both developed entirely with the help of Claude Code, Anthropic’s CLI coding assistant.

Why Plugins?

Dynamics 365 plugins let you inject custom business logic directly into the platform’s event pipeline. They run server-side, inside the Dataverse sandbox, and can intercept creates, updates, retrieves, and more. If you need behaviour that Power Automate flows can’t easily deliver — like rewriting queries on the fly or transforming data before it’s saved — plugins are the answer.

Plugin 1: AnonymiseEmailPlugin

The Problem

When working with test or demo data, you often don’t want real email addresses sitting in the system. I needed a way to automatically generate a safe, deterministic email address for every contact based on their name.

How It Works

The plugin registers on the Pre-Operation stage of Create and Update messages for the contact entity. Whenever a contact is created or their name changes, the plugin:

  1. Reads the firstname and lastname from the target entity (falling back to PreImage on updates where only one name changed)
  2. Sanitises each name — lowercasing, replacing spaces with dots, stripping special characters
  3. Constructs an email like firstname.lastname@carrots.com
  4. Sets emailaddress1 on the target before it’s saved

Key Design Decisions

  • PreImage fallback — On an update where only the first name changes, the last name isn’t in the Target entity. The plugin checks PreImage to get the unchanged field. This is a common gotcha in Dynamics plugin development.
  • Sanitisation — Names like “Mary-Jane O’Brien” become maryjane.obrien. The regex [^a-z0-9.] strips anything that isn’t a letter, digit, or dot.
  • Guard clause — If both names are empty, the plugin exits without setting an email, avoiding nonsensical addresses like @carrots.com.
private static string Sanitise(string name)
{
    if (string.IsNullOrWhiteSpace(name))
        return string.Empty;

    string result = name.Trim().ToLowerInvariant().Replace(" ", ".");
    result = Regex.Replace(result, @"[^a-z0-9.]", "");
    return result;
}

Simple, predictable, and it runs in the sandbox without any external dependencies.

Plugin 2: NetworkActivitiesPlugin

The Problem

Out of the box, when you look at a contact’s activity timeline in Dynamics 365, you only see activities directly related to that contact via the regardingobjectid field. But in real sales scenarios, you want to see the bigger picture — emails sent to colleagues at the same company, meetings booked with the parent account, calls involving sibling contacts. I wanted a “network view” of activities.

How It Works

This plugin registers on RetrieveMultiple of activitypointer at the Pre-Operation stage. It intercepts the query before Dynamics executes it and rewrites it to include the contact’s entire network:

  1. Extract the contact ID from the incoming query (handling both QueryExpression and FetchExpression formats)
  2. Build the network — look up the contact’s parent account, find all accounts where the contact is the primary contact, and find all sibling contacts under the same parent account
  3. Rewrite the query — replace the simple “regarding = this contact” filter with an OR condition: regardingobjectid IN (all network IDs) OR activityparty.partyid IN (all network IDs)
  4. Return the modified query to the platform, which executes it and returns the expanded results

The Tricky Bits

This plugin was significantly more complex than the email one. Here are the challenges we hit and solved:

FetchXML vs QueryExpression

Dynamics sometimes sends queries as FetchExpression instead of QueryExpression. The plugin handles both by converting FetchXML to QueryExpression using FetchXmlToQueryExpressionRequest.

The LinkEntity Pattern

When a subgrid sends FetchXML, converting it can produce a LinkEntity join (activitypointer → contact) instead of a direct filter condition. The plugin checks both query.Criteria and query.LinkEntities[].LinkCriteria to find the contact ID.

ConditionOperator.In vs Equal

You’d expect Dynamics to use ConditionOperator.Equal for a single GUID filter, but subgrids actually use ConditionOperator.In with a single value. The plugin checks for both operators — a subtle gotcha that caused real head-scratching during development.

ActivityParty Relationships

The regardingobjectid field only captures one relationship. But contacts are often linked to activities as From, To, CC, or BCC participants via the activityparty table. The plugin adds a left outer join to activityparty and includes partyid in the OR filter to catch these relationships too.

Saved View Configuration

Getting the custom view to display in a subgrid required specific XML configuration. The <grid> element must have an object attribute set to the entity type code (4200 for activitypointer), and the subgrid needs a <RelationshipName> to provide parent record context.

Deployment

Both plugins live in the same assembly (ContactEmailPlugin.dll), built targeting .NET Framework 4.6.2 for Dataverse sandbox compatibility. Deployment is handled via PowerShell scripts that:

  1. Build the project with dotnet build -c Release
  2. Base64-encode the DLL
  3. PATCH the assembly to Dataverse via the Web API
  4. Register plugin steps, saved views, and form customisations as needed

The assembly is signed with a strong name key (.snk file), which is a Dataverse sandbox requirement.

What I Learned

Building these plugins with Claude Code was a genuinely collaborative process. Here are the key takeaways:

  • Plugin trace logging is essential — Setting plugintracelogsetting to 2 (All) during development and reading traces via the API saved hours of debugging
  • PreImage configuration matters — If you forget to register PreImage with the right attributes, your plugin silently gets null values
  • Query rewriting is powerful but fragile — The NetworkActivities plugin had to handle multiple query formats, and each Dynamics UI component (timeline, subgrid, associated view) sends queries differently
  • Test incrementally — Each iteration of the network plugin revealed a new edge case. Building and deploying frequently via the PowerShell scripts kept the feedback loop tight
  • Claude Code handles the boilerplate — The repetitive parts (API calls, XML manipulation, base64 encoding) were generated instantly, letting me focus on the actual business logic

Wrapping Up

These two plugins demonstrate the range of what’s possible with Dynamics 365 server-side logic — from simple data transformation (email anonymisation) to complex query rewriting (network activities). The combination of C# plugins and PowerShell deployment scripts, built with Claude Code’s assistance, made the whole process surprisingly approachable.

If you’re getting started with Dynamics 365 plugin development, I’d recommend starting with something simple like the email plugin, getting comfortable with the deployment pipeline, and then tackling more ambitious scenarios like query interception.

Leave a Comment

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