" MicromOne: Dynamic Email Placeholder Replacement – PreCreate Plugin Example

Pagine

Dynamic Email Placeholder Replacement – PreCreate Plugin Example

In Microsoft Dynamics 365 / Dataverse, email templates are often
static and difficult to adapt to different business scenarios.
A common requirement is to dynamically inject data such as:

Recipient names

Related record information

Conditional fields (shown only when data exists)

This article presents a PreCreate Email plugin that replaces
placeholders inside the email body before the Email record is created,
using clean and reusable logic.



Plugin Overview

Execution details

Entity: email

Message: Create

Stage: Pre-Operation (20)

Purpose: Replace placeholders in email.description

Supported placeholders

PlaceholderSource
##ToRecipient##Names of recipients (To field)
##account.custom_taxcode##Account custom field
##opportunity.custom_recordurl##Opportunity record URL


Complete Plugin Code (Generic & Safe for Sharing)

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Text.RegularExpressions;

namespace Plugins.Email
{
public class PreCreateEmailReplacePlaceholders : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var tracing = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

var serviceUser = factory.CreateOrganizationService(context.InitiatingUserId);
var serviceAdmin = factory.CreateOrganizationService(null);

if (context.Stage != 20 ||
context.MessageName != "Create" ||
context.PrimaryEntityName != "email")
{
throw new InvalidPluginExecutionException("Plugin not registered correctly.");
}

if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity target)
{
ExecuteLogic(serviceAdmin, serviceUser, target, tracing);
}
}

private void ExecuteLogic(
IOrganizationService serviceAdmin,
IOrganizationService serviceUser,
Entity target,
ITracingService tracing)
{
tracing?.Trace("START PreCreateEmailReplacePlaceholders");

try
{
if (!target.Contains("description"))
return;

var description = target.GetAttributeValue<string>("description");

#region TO RECIPIENTS

if (target.Contains("to"))
{
var parties = target.GetAttributeValue<EntityCollection>("to")?.Entities;
var recipientNames = string.Empty;

foreach (var party in parties)
{
if (!party.Contains("partyid")) continue;

var reference = party.GetAttributeValue<EntityReference>("partyid");
if (reference == null) continue;

string primaryName;

switch (reference.LogicalName)
{
case "account": primaryName = "name"; break;
case "contact": primaryName = "fullname"; break;
case "systemuser": primaryName = "fullname"; break;
case "queue": primaryName = "name"; break;
default: continue;
}

var entity = serviceAdmin.Retrieve(
reference.LogicalName,
reference.Id,
new ColumnSet(primaryName));

var name = entity.GetAttributeValue<string>(primaryName);

recipientNames = string.IsNullOrEmpty(recipientNames)
? name
: $"{recipientNames}, {name}";
}

description = description.Replace("##ToRecipient##", recipientNames);
}

#endregion

#region REGARDING RECORD

if (target.Contains("regardingobjectid"))
{
var regarding = target.GetAttributeValue<EntityReference>("regardingobjectid");

if (regarding != null)
{
switch (regarding.LogicalName)
{
case "opportunity":
var opportunity = serviceAdmin.Retrieve(
"opportunity",
regarding.Id,
new ColumnSet("custom_recordurl"));

description = description.Replace(
"##opportunity.custom_recordurl##",
opportunity.GetAttributeValue<string>("custom_recordurl"));
break;

case "account":
var account = serviceAdmin.Retrieve(
"account",
regarding.Id,
new ColumnSet("custom_taxcode"));

var pattern = @"<li\s*>\s*Tax Code:\s*##account\.custom_taxcode##\s*</li>";
var taxCode = account.GetAttributeValue<string>("custom_taxcode");

var replacement = !string.IsNullOrWhiteSpace(taxCode)
? $"<li>Tax Code: {taxCode}</li>"
: string.Empty;

description = Regex.Replace(
description,
pattern,
replacement,
RegexOptions.IgnoreCase | RegexOptions.Singleline);
break;
}
}
}

#endregion

target["description"] = description;
}
catch (Exception ex)
{
tracing?.Trace(ex.ToString());
throw new InvalidPluginExecutionException(ex.Message, ex);
}
finally
{
tracing?.Trace("END PreCreateEmailReplacePlaceholders");
}
}
}
}



Example Email Template (HTML)

<p>Dear <strong>##ToRecipient##</strong>,</p>

<p>
A new request has been created in the system.
</p>

<ul>
<li>Tax Code: ##account.custom_taxcode##</li>
</ul>

<p>
You can open the related opportunity here:
</p>

<p>
<a href="##opportunity.custom_recordurl##">Open Opportunity</a>
</p>

<p>
Kind regards,<br/>
CRM Team
</p>



How It Works

1. Recipient Resolution

Reads all records in the To field (activityparty)

Supports users, contacts, accounts, and queues

Joins names into a single string

Replaces ##ToRecipient##



2. Conditional HTML Replacement

For Account-related emails:

If the tax code exists → <li> is rendered

If empty → <li> is completely removed via Regex

This keeps the email HTML clean and professional.



3. Why Pre-Operation?

Executing in PreCreate ensures:

Email body is already finalized when saved

No async jobs

No client-side JavaScript

Works with templates, workflows, Power Automate