Xperience by Kentico's templated email functionality, enabled through email channels is great for marketers that want to author emails, use the structured content they've already created, and send them out to recipients without having to write any code!

We love when marketers can do everything themselves. But, what if we have a more advanced scenario - like sending an email to a recipient when a webhook in our solution is called from an external service, some data is imported through an integration, or some customer data is submitted to a website as part of a complex business process?

Thankfully, we can use Xperience's APIs to send out an autoresponder email to a recipient while also using an email already authored by a marketer! We can even include dynamic data using tokens that a marketer places in the email content 😲.

This post was written for Xperience by Kentico v27.0.2. Please consult the documentation for any feature or API changes when using a different version. Be sure to check the System Requirements in the product documentation.

Please note that this solution uses several APIs from internal namespaces, even though the C# types are public. Internal APIs are not guaranteed to be stable over time and may change when applying a Hotfix or Refresh.

# Defining our dependencies

Let's assume we trigger the email send through ASP.NET Core API Endpoint. First, we'll create a Controller and action method:

Today, in v27 of Xperience by Kentico there are quite a few services and APIs we need to juggle to achieve our result of sending an email through C#, but hopefully in the future there will be improvements to make this easier for developers!

[ApiController]
[Route("[controller]/[action]")]
public class EmailController : Controller
{
    private readonly IEmailConfigurationInfoProvider emailConfigurationProvider;
    private readonly IEmailContentResolver emailContentResolver;
    private readonly IEmailTemplateMergeService mergeService;
    private readonly IEmailChannelLanguageRetriever languageRetriever;
    private readonly IContentItemDataInfoRetriever dataRetriever;
    private readonly IEmailChannelSenderEmailProvider senderInfoProvider;
    private readonly IEmailService emailService;
    private readonly IInfoProvider<EmailChannelInfo> emailChannels;
    private readonly IInfoProvider<ContentItemInfo> contentItems;
    private readonly IInfoProvider<EmailChannelSenderInfo> emailChannelSenders;

    public EmailController(
        IEmailConfigurationInfoProvider emailConfigurationProvider,
        IEmailContentResolver emailContentResolver,
        IEmailService emailService,
        IEmailTemplateMergeService mergeService,
        IEmailChannelLanguageRetriever languageRetriever,
        IContentItemDataInfoRetriever dataRetriever,
        IEmailChannelSenderEmailProvider senderInfoProvider,
        IInfoProvider<ContentItemInfo> contentItems,
        IInfoProvider<EmailChannelInfo> emailChannels,
        IInfoProvider<EmailChannelSenderInfo> emailChannelSenders
    )
    {
        this.emailConfigurationProvider = emailConfigurationProvider;
        this.emailContentResolver = emailContentResolver;
        this.emailService = emailService;
        this.contentItems = contentItems;
        this.languageRetriever = languageRetriever;
        this.dataRetriever = dataRetriever;
        this.senderInfoProvider = senderInfoProvider;
        this.emailChannels = emailChannels;
        this.emailChannelSenders = emailChannelSenders;
        this.mergeService = mergeService;
    }

Now that we have all the required services available, we can start composing our email:

[HttpGet]
public async Task<IActionResult> Index()
{
    /*
     * Retrieve these values from the endpoint request or db
     */
    var recipient = new Recipient
    {
        FirstName = "Dana",
        LastName = "Scully",
        Email = "[email protected]"
    };

    var dataContext = new CustomValueDataContext
    {
        Recipient = recipient,
        Items = new()
        {
            /*
             * Define any tokens you want to replace
             */
            {
                "TOKEN_DynamicGatedAssetURL", 
                "https://www.youtube.com/watch?v=TYdC1uwO4UI" 
            }
        }
    };

Our CustomValueDataContext is a simple class that inherits from FormAutoresponderEmailDataContext since our email will be an autoresponder email:

public class CustomValueDataContext : FormAutoresponderEmailDataContext
{
    public Dictionary<string, string> Items { get; set; } = new();
}

It has a collection of key/value pairs that we can populate with whatever we want. This is what lets us have dynamic email content that we define. For our use-case, we're doing to replace a TOKEN_DynamicGatedAssetURL token in the email with a hard-coded URL, but this URL could come from the database and be based on the information submitted to the API endpoint.

We'll return later to this data context object to define how the token replacement works.

# Generating an email programmatically

Now, we can begin to use all our dependencies to generate the email body and retrieve the sender information:

var emailConfig = await emailConfigurationProvider
    .GetAsync("DancingGoatAutoresponder-v6dz12bu");

We first retrieve our EmailConfigurationInfo which we are calling emailConfig.

Don't confuse this with an EmailInfo... in Xperience by Kentico, EmailInfo represents a packaged email to a specific recipient ready to be sent (typically from the email queue).

So what is an EmailConfigurationInfo? Well, that represents the email that a marketer creates in an email channel. For example, in the Dancing Goat sample solution email channel, it represents one of the items in this image.

Email channel email item list

We then use our IEmailTemplateMergeService to merge the email and all of its structured content with its associated email template. We end up with a string that includes our non-personalized or dynamic email body.

string mergedTemplate = await mergeService
    .GetMergedTemplateWithEmailData(emailConfig, false);

Then we use the IEmailContentResolver to resolve all the email content, populating personalization tokens and our custom data tokens (like TOKEN_DynamicGatedAssetURL).

string emailBody = await emailContentResolver.Resolve(
    emailConfig, 
    mergedTemplate, 
    EmailContentFilterType.Sending, 
    dataContext);

Next, we need to get all the related email configuration that is part of our email channel configuration. This requires a few steps, but it ensures that our email sending address works exactly how a marketer would expect it to.

var contentItem = await contentItems
    .GetAsync(emailConfig.EmailConfigurationContentItemID);
var contentLanguage = await languageRetriever
    .GetEmailChannelLanguageInfoOrThrow(emailConfig.EmailConfigurationEmailChannelID);
var data = await dataRetriever
    .GetContentItemData(contentItem, contentLanguage.ContentLanguageID, false);
var emailFieldValues = new EmailContentTypeSpecificFieldValues(data);

What we end up with here is an Xperience EmailContentTypeSpecificFieldValues object that looks like this:

public sealed class EmailContentTypeSpecificFieldValues 
    : IEmailContentTypeSpecificFieldValues
{
    public string EmailSubject { get; }
    public string EmailPreviewText { get; }
    public string EmailPlainText { get; }
    public int EmailSenderID { get; }
    public int EmailTemplateID { get; }
}

This object includes our EmailSenderID which is what we use to get the rest of our email configuration:

var emailChannel = (await emailChannels.Get()
    .WhereEquals(
        nameof(EmailChannelInfo.EmailChannelChannelID), 
        emailConfig.EmailConfigurationEmailChannelID)
    .GetEnumerableTypedResultAsync())
    .FirstOrDefault();

var sender = await emailChannelSenders
    .GetAsync(emailData.EmailSenderID);
string senderEmail = await senderInfoProvider
    .GetEmailAddress(emailChannel.EmailChannelID, sender.EmailChannelSenderName);

Our senderEmail is the full email address for the sender configured for this autoresponder email.

We're finally ready to author and send the email message!

    var emailMessage = new EmailMessage
    {
        From = $"\"{sender.EmailChannelSenderDisplayName}\" <{senderEmail}>",
        Recipients = recipient.Email,
        Subject = emailData.EmailSubject,
        Body = emailBody,
        PlainTextBody = emailData.EmailPlainText,
        EmailConfigurationID = emailConfig.EmailConfigurationID,
        MailoutGuid = dataContext.MailoutGuid
    };

    await emailService.SendEmail(emailMessage);

    return Ok();
}

We use the address specification for the From property so that we can include the email address and display name.

If we were to make a request to our HTTP endpoint now, we'd successfully send an email to our recipient, but our dynamic content would not be inserted into the email. For that, we need to create a custom IEmailContentFilter.

# Email content filters

To transform our token value TOKEN_DynamicGatedAssetURL into the URL value we provided to our CustomValueDataContext requires we define and register an IEmailContentFilter.

First, we'll create a CustomValueFilter class:

public class CustomValueFilter : IEmailContentFilter
{
    public Task<string> Apply(
        string text, 
        EmailConfigurationInfo email, 
        IEmailDataContext dataContext)
    {
        if (dataContext is CustomValueDataContext customValueContext)
        {
            foreach (var (key, val) in customValueContext.Items)
            {
                text = text.Replace(key, val);
            }
        }

        return Task.FromResult(text);
    }
}

This class is really simple! It will be instantiated and the Apply() method will be executed for each email sent out by Xperience.

We have access to the EmailConfigurationInfo and IEmailDataContext which means we can make sure our filter only does its work for emails that can use it. By patterning matching on dataContext we can get access to a strongly typed CustomValueDataContext.

We then loop through all the keys in the Items dictionary, doing a simple string replace for the values.

The last step is to register our filter with Xperience, which we need to do in a custom module class:

[assembly: RegisterModule(typeof(DancingGoatModule))]

namespace DancingGoat;

public class DancingGoatModule : Module
{
    public DancingGoatModule() : base(nameof(DancingGoatModule)) { }

    protected override void OnInit()
    {
        base.OnInit();

        EmailContentFilterRegister.Instance
            .Register(
                () => new CustomValueFilter(), 
                EmailContentFilterType.Sending, 
                100);
    }
}

With our filter registered, a marketer can now insert the dynamic token value anywhere in the email content:

Highlighted token text used for an anchor link in Email HTML

Pretty neat!

Now when we hit our API endpoint, we'll see a link in our email that is tailored for our recipient!

Rendered email screenshot

If you have any ideas on how to build on this idea and help marketers achieve even more, share your thoughts in the comments!