# Dynamic emails with Handlebars.Net

Handlebars.Net is a port of the Handlebars.js library that compiles Handlebars templates directly into IL bytecode. It enables quick and easy creation of dynamic email templates, including variables, iterations, conditional blocks, and much, much more.

```csharp
string source =
@"<div class=""entry"">
  <h1>{{title}}</h1>
  <div class=""body"">
    {{body}}
  </div>
</div>";

var template = Handlebars.Compile(source);

var data = new {
    title = "My new post",
    body = "This is my first post!"
};

var result = template(data);

/* Would render:
<div class="entry">
  <h1>My New Post</h1>
  <div class="body">
    This is my first post!
  </div>
</div>
*/
```

In this article, I will present an example of implementing Handlebars into a project.

## Installation

1. Add [Handlebars.Net](https://www.nuget.org/packages/Handlebars.Net/) from Nuget.
    

```bash
dotnet add package Handlebars.Net
```

2. Register Handlebars in `Services`:
    

```csharp
builder.Services.AddSingleton(Handlebars.Create());
```

## Creating HTML template

Simply add a `.html` file and all the possibilities of Handleras are open to you!

### Variables

```xml
<h1>Hi {{UserName}}!</h1>
```

### Iteration

```xml
{{#each Products}}
    <div>
        {{Name}} - {{Quantity}}x {{Price}} PLN
    </div>
{{/each}}
```

### Conditional statements

```xml
{{#if isActive}}
    <img src="star.gif" alt="Active" />
{{/if}}
```

You will find all the syntax in the [documentation](https://handlebarsjs.com/guide/).

## Creating TXT template

Creating `txt` files is similar to `HTML` files and does not require any additional configuration.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Remember to set files property <code>Copy to output directory</code> to <code>Copy always</code>. Otherwise, files will not appear with the build.</div>
</div>

```csharp
    <ItemGroup>
      <None Update="Features\Emails\Templates\Order\OrderEmail.txt">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      </None>
      <None Update="Features\Emails\Templates\Order\OrderEmail.html">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      </None>
    </ItemGroup>
```

## Template model

We already have `html` and `txt` templates. In my case, these were templates for order emails. Now we need to collect the variables we use into a single, consistent data model. This is how it looked for me:

```csharp
public record OrderTemplateModel(
    string UserName,
    string OrderNumber,
    string OrderDate,
    List<OrderItem> Products,
    decimal ShippingPrice,
    string PaymentLink,
    decimal TotalPrice,
    string OrderUrl);

public record OrderItem(string Name, int Quantity, decimal Price);
```

## Templates provider

Now we need to create a method that will render our templates.

```csharp
public class TemplatesProvider(IHandlebars handlebars)
{
    public (string Html, string Text) RenderTemplate(object model, string htmlPath, string txtPath)
    {
        var templateHtml = handlebars.Compile(File.ReadAllText(htmlPath));
        var html = templateHtml(model);    
        
        var templateText = handlebars.Compile(File.ReadAllText(txtPath));
        var text = templateText(model);
        
        return (html,text);
    }
}
```

Our method compiles the files indicated by the paths and then fills in the templates with values from the passed model. However, it has two problems. First, the object is too broad a type and does not secure our method in any way. Second, specifying the paths each time will be inconvenient and prone to errors. Let us solve these problems one by one.

### ITemplateModel

Let's start by getting rid of the `object`. Create an interface that we will use to mark template models:

```csharp
public interface ITemplateModel;
```

Next, add it to our created model:

```csharp
public record OrderTemplateModel(
    string UserName,
    string OrderNumber,
    string OrderDate,
    List<OrderItem> Products,
    decimal ShippingPrice,
    string PaymentLink,
    decimal TotalPrice,
    string OrderUrl) : ITemplateModel;

public record OrderItem(string Name, int Quantity, decimal Price);
```

Now we can refactor `TemplatesProvider`:

```csharp
public class TemplatesProvider(IHandlebars handlebars)
{
    public (string Html, string Text) RenderTemplate(ITemplateModel model, string htmlPath, string txtPath)
    {
        var templateHtml = handlebars.Compile(File.ReadAllText(htmlPath));
        var html = templateHtml(model);    
        
        var templateText = handlebars.Compile(File.ReadAllText(txtPath));
        var text = templateText(model);
        
        return (html,text);
    }
}
```

### Paths attribute

File paths can be organised in many ways, e.g. through a static class with constants. I decided to use attributes so that later, when adding new templates, I could limit the number of places where I would have to make changes.

```csharp
[AttributeUsage(AttributeTargets.Class)]
public class PathsAttribute(string htmlPath, string textPath) : Attribute
{
    public string HtmlPath { get; } = htmlPath;
    public string TextPath { get; } = textPath;
}
```

Return to our template's model and add an `attribute`:

```csharp
[Paths(htmlPath:"Features/Emails/Templates/Order/OrderEmail.html", textPath:"Features/Emails/Templates/Order/OrderEmail.txt")]
public record OrderTemplateModel(
    string UserName,
    string OrderNumber,
    string OrderDate,
    List<OrderItem> Products,
    decimal ShippingPrice,
    string PaymentLink,
    decimal TotalPrice,
    string OrderUrl) : ITemplateModel;

public record OrderItem(string Name, int Quantity, decimal Price);
```

Now we can remove the arguments from the method in `TemplatesProvider`:

```csharp
public class TemplatesProvider(IHandlebars handlebars)
{
    public (string Html, string Text) RenderTemplate(ITemplateModel model)
    {
        var pathsAttribute = model.GetType().GetCustomAttribute<PathsAttribute>();
        if (pathsAttribute == null) throw new InvalidOperationException($"{nameof(PathsAttribute)} not found in {model.GetType().Name}. {nameof(PathsAttribute)} is required for templates");
        
        var templateHtml = handlebars.Compile(File.ReadAllText(pathsAttribute.HtmlPath));
        var html = templateHtml(model);    
        
        var templateText = handlebars.Compile(File.ReadAllText(pathsAttribute.TextPath));
        var text = templateText(model);
        
        return (html,text);
    }
}
```

### TemplatesProvider - final touches

To enable the injection of `TemplatesProvider`, add its interface:

```csharp
public interface ITemplatesProvider
{
    (string Html, string Text) RenderTemplate(ITemplateModel model);
}
```

Make `TemplatesProvider` inherits from it:

```csharp
public class TemplatesProvider(IHandlebars handlebars) : ITemplatesProvider
{
    public (string Html, string Text) RenderTemplate(ITemplateModel model)
    {
        var pathsAttribute = model.GetType().GetCustomAttribute<PathsAttribute>();
        if (pathsAttribute == null) throw new InvalidOperationException($"{nameof(PathsAttribute)} not found in {model.GetType().Name}. {nameof(PathsAttribute)} is required for templates");
        
        var templateHtml = handlebars.Compile(File.ReadAllText(pathsAttribute.HtmlPath));
        var html = templateHtml(model);    
        
        var templateText = handlebars.Compile(File.ReadAllText(pathsAttribute.TextPath));
        var text = templateText(model);
        
        return (html,text);
    }
}
```

And register it in the DI container:

```csharp
builder.Services.AddSingleton<ITemplatesProvider, TemplatesProvider>();
```

### Usage

The templates are ready to use! It looks like this:

```csharp
[ApiController]
[Route("[controller]")]
public class EmailsController(IEmailService emailService, ITemplatesProvider templatesProvider) : ControllerBase
{
    [HttpPost("order")]
    public async Task<SendOrderEmailResult> SendOrderEmail(SendOrderEmailRequest request)
    {
        var data = new OrderTemplateModel(
            UserName: "Hubert",
            OrderNumber: "12345",
            OrderDate: DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
            Products: [
                new OrderItem("Test product", 1, 249.90m)
            ],
            ShippingPrice: 14.90m,
            PaymentLink: "testUrl",
            TotalPrice: 264.80m,
            OrderUrl: "testUrl"
        );
        
        var (html, text) = templatesProvider.RenderTemplate(data);
        
        var email = new Email
        {
            BodyHtml = html,
            BodyText = text,
            Subject = "Order",
            SenderAddress = "noreply@poc.com",
            ToAddresses = ["test@test.com"]
        };
        
        var sendEmailResult = await emailService.SendEmailAsync(email, HttpContext.RequestAborted);
        
        return new SendOrderEmailResult
        {
            IsSuccess = sendEmailResult.IsSent
        };
    }
}
```

### Reuse of templates

Handlebars allows for template reuse through partials. Partials are normal Handlebars templates that may be called directly by other templates. You can read more about it [here](https://handlebarsjs.com/guide/partials.html).

We can use partials to separate the header and footer of the email.

Let's start by creating the appropriate `html` and `txt` files. Using my header as an example:

```xml
<table style="background-color: black; padding: 10px; height: 100%; width: 100%;">
    <tr>
        <td style="background-color: black;">
            <img src="myLogo.jpg" alt="Logo" style="width: 100%;">
        </td>
    </tr>
</table>
```

Partials, just like regular templates, can accept model templates and use all the features offered by Handlebars.

Use a partial in your templates:

```xml
<div>
    {{> Header}}
</div>
```

For partials to work, they must be registered in your Handlebars instance:

```csharp
builder.Services.AddSingleton<IHandlebars>(_ =>
{
    var handlebars = Handlebars.Create();
    handlebars.RegisterTemplate("Footer", File.ReadAllText(Path.Combine("Features", "Emails", "Templates", "Footer", "Footer.html")));
    handlebars.RegisterTemplate("Header", File.ReadAllText(Path.Combine("Features", "Emails", "Templates", "Header", "Header.html")));

    return handlebars;
});
```

If we use more parties, we will have more code here. That is why I decided to separate Handlebars configuration into a separate class - `HandlebarsFactory`.

### Handlebars factory

```csharp
public static class HandlebarsFactory
{
    public static IHandlebars Create()
    {
        var handlebars = Handlebars.Create();
        handlebars.Configuration.TextEncoder = new HtmlEncoder();
        handlebars.RegisterTemplate("Footer", File.ReadAllText(Path.Combine("Features", "Emails", "Templates", "Footer", "Footer.html")));
        handlebars.RegisterTemplate("Header", File.ReadAllText(Path.Combine("Features", "Emails", "Templates", "Header", "Header.html")));

        return handlebars;
    }
}
```

In the factor, you can register partials and add other configurations, such as TextEncoder, which supports special characters in templates.

Usage:

```csharp
builder.Services.AddSingleton<IHandlebars>(_ => HandlebarsFactory.Create());
```

## Bonus

The source code is available [here](https://github.com/Katarzyna-Kadziolka/MailingPoC) :)
