Transactional email on .net core with Sendwithus

Transactional email on .net core with Sendwithus
💡
This article is from 2016. Code samples may no longer work.

Transactional emails can be a real bugbear for business and for marketers. They are often buried in source code, require dev involvement to update, and lack powerful tracking, analytics and segmentation that we now take for granted in e-mail marketing.

There must be a better way

There is! Enter sendwithus

Sendwithus is a layer on top of your transactional mailer like Sendgrid, Mailgun, etc. Here are the supported mailers.

Sendwithus supports:

  • Versioned templates in multiple locales/languages
  • A/B testing, segmentation, drip campaigns
  • Powerful analytics, reporting and per-recipient auditing
  • Automation on email events (such as bounces) which can be important in transactional emails, but often overlooked
  • Automation on segment.com events - which can allow you to remove the email code from your solution entirely - we won't cover this here but it's pretty cool

OK cool, let's try it

Firstly, you'll need to sign up with sendwithus for free. Be sure to complete the on-boarding when signing up so that it creates a test email template.

Optionally you may signup for and register an email service provider now, but it's not required right away if we're just doing a quick test.

Once that's done, we can write a .net core client.

The client

We're assuming integration into an existing asp.net core project. We'll need:

  • Configuration (to store our API keys etc)
  • A simple REST client to send emails
  • Some objects to encapsulate the request and response, and a way to serialize into sendwithus compatible JSON

Configuration in .net core

web.config is no more. We'll have to make an appsettings.json file. Here's ours:

{
  "SendWithUs": {
    "LiveApiKey": "<your live api key>",
    "TestApiKey": "<your test api key>",
    "ApiBaseUrl": "https://api.sendwithus.com/api/v1/"
  }
}
JSON
💡
A note on test API keys: the default test key will not send an email. It may be better to create an override key to redirect all email to a single email address of your choosing.

To load the config, I prefer to load into a class using IOptions<T>. Create a class "SendWithUsConfig.cs"

public class SendWithUsConfig
{
    public string LiveApiKey { get; set; }
    public string TestApiKey { get; set; }
    public string ApiBaseUrl { get; set; }
}
C#

We need to load the config (and inject it into the IOC container) in Startup.cs - I've only included the bits you need for config here... cherry pick into your own Startup.cs file.

public class Startup
{
    readonly IConfigurationRoot _config;

    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json");
        _config = builder.Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions();
        // this requires Microsoft.Extensions.Options.ConfigurationExtensions
        services.Configure<SendWithUsConfig>(_config.GetSection("SendWithUs"));
    }
}
C#

A simple REST client in .net core

At the time of writing WebClient hasn't been ported to .net core, so we need to use HttpClient instead. We will use this in combination with Newtonsoft.Json and a custom contract resolver to map our PascalCase property names to snake_case.

The API

Here's the REST method: https://www.sendwithus.com/docs/api#sending-emails - a sample request might look like:

{
    "template": "tem_A5RHVP6CnRbS34UysLjYHx",
    "recipient": {
        "name": "John",
        "address": "user@email.com"
    },
    "template_data": { "amount": "$12.00" },
    "cc": [
        {"address": "cc_one@email.com"},
        {"address": "cc_two@email.com"}
    ],

    "bcc": [
        {"address": "bcc_one@email.com"},
        {"address": "bcc_two@email.com"}
    ],
    "sender": {
        "name": "Company",
        "address": "company@company.com",
        "reply_to": "info@company.com"
    },
    "locale": "en-US",
}
JSON

The Proxy classes

We'll need to make some classes, here's the Request classes (note I've shortened it a bit for this example):

public class SendRequest
{
    public string Template { get; set; }
    public Recipient Recipient { get; set; }
    public List<Recipient> Cc { get; set; }
    public List<Recipient> Bcc { get; set; }
    public Sender Sender { get; set; }
    public dynamic TemplateData { get; set; }
    public string Locale { get; set; }
}

public class Recipient
{
    public string Address { get; set; }
    public string Name { get; set; }
}

public class Sender
{
    public string Address { get; set; }
    public string ReplyTo { get; set; }
    public string Name { get; set; }
}
C#

And here is the Response:

public class SendResponse
{
    public bool Success { get; set; }
    public string Status { get; set; }
    public string ReceiptId { get; set; }
    public EmailSent Email { get; set; }
}

public class EmailSent
{
    public string Locale { get; set; }
    public string Name { get; set; }
    public string VersionName { get; set; }
}
C#

Next... Json Serialization

We're using good old Newtonsoft.Json for this. Apart from the fact that our .net classes are PascalCase and the JSON needs to be snake_case, the property names are identical. The solution is to write a ContractResolver with an elegant regex replace.

public class SendwithUsContractResolver : DefaultContractResolver
{
    protected override string ResolvePropertyName(string propertyName)
    {
        // convert Pascal/Camel case to underscored lower case with regex
        var pattern = "([a-z])([A-Z]+)";
        var replacement = "$1_$2";

        return Regex.Replace(propertyName, pattern, replacement).ToLower();
    }
}
C#

Lastly, we need a couple of extension methods for serialization that utilize our contract resolver.

public static string ToSendWithUsJson(this object objectToSerialize)
{
    return JsonConvert.SerializeObject(
        objectToSerialize,
        new JsonSerializerSettings
        {
            ContractResolver = new SendwithUsContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        });
}

public static T FromSendWithUsJson<T>(this string json)
{
    return JsonConvert.DeserializeObject<T>(
        json,
        new JsonSerializerSettings
        {
            ContractResolver = new SendwithUsContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        });
}
C#

The REST Client

public class SendWithUsClient
{
    readonly SendWithUsConfig _config;
    readonly HttpClient _httpClient;
    public SendWithUsClient(IOptions<SendWithUsConfig> config)
    {
        _config = config.Value;
        _httpClient = new HttpClient
        {
            BaseAddress = new Uri(_config.ApiBaseUrl)
        };
    }

    private AuthenticationHeaderValue GetAuthorizationHeaderValue(bool isTestMode)
    {
        var apiKey = isTestMode ? _config.TestApiKey : _config.LiveApiKey;
        var authHeaderBytes = ASCIIEncoding.UTF8.GetBytes($"{apiKey}:");
        var authHeaderEncoded = Convert.ToBase64String(authHeaderBytes);

        return new AuthenticationHeaderValue("Basic", authHeaderEncoded);
    }

    public async Task<SendResponse> SendEmail(SendRequest sendRequest, bool isTestMode)
    {
        var requestJson = sendRequest.ToSendWithUsJson();

        _httpClient.DefaultRequestHeaders.Authorization = GetAuthorizationHeaderValue(isTestMode);

        HttpResponseMessage response = await _httpClient.PostAsync("send", new StringContent(requestJson));
        string responseJson = await response.Content.ReadAsStringAsync();

        return responseJson.FromSendWithUsJson<SendResponse>();
    }
}
C#

You'll note that the API uses basic auth, which is just a base64 encoded username:password - the username is the API key and the password is blank in this case.

The send method uses our 2 new extension methods to convert into API friendly JSON and back again, and the response is returned.

Let's test

Ordinarily you might call this in a controller, but for the sake of simplicity, I'm just adding it to Startup.cs - this is my "Configure" method:

💡
Warning: this sample code uses the live API key.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.Run(async (context) =>
    {
        var config = app.ApplicationServices.GetService<IOptions<SendWithUsConfig>>();
        var sendWithUsClient = new SendWithUsClient(config);

        var sendRequest = new SendRequest
        {
            Template = "<Your template ID>",
            Recipient = new Recipient { Address = "<Your email here>", Name = "<Your name here>" },
            TemplateData = new { FirstName = "<Your first name>", ButtonText = "Go!" }
        };

        var sendResponse = await sendWithUsClient.SendEmail(sendRequest, false);
        await context.Response.WriteAsync(JsonConvert.SerializeObject(sendResponse));
    });
}
C#

That's it! It's not a full implementation but it is a good indication of how easy it is to write a JSON client, and then integrate into your .net core solution.