DEV Community

Jeremy
Jeremy

Posted on • Edited on • Originally published at Medium

Transactional Emails: A long term view

Sending emails has become essential for almost all modern applications and games. The most common type of emails in your first app are transactional. Examples of transactional communications include order confirmations, password reset requests, account notifications, and appointment reminders. Luckily, Microsoft has made this task pretty simple out of the box. So much so that you may not put much thought into how you’re sending out your app’s communications. And that’s okay — we all have deadlines to meet.

Getting Started

Once you’ve identified the required transactional communications for your app, you need to decide how to send them. Again, you’re in luck here, too. You have a few different options when deciding how to send your emails. To keep things simple, you can start by using an established SMTP server. There are countless examples of how to use Gmail, Zoho, and other third-party SMTP services. It’s simple enough and will get you through development and even to the MVP phase.

As your app grows, you might notice you’re being rate-limited by the SMTP service, transient errors are creeping in, and your simple email solution is no longer holding up to the stress of your growing app. First of all, congrats! Your app is growing! Second, it’s time to revisit our communications strategy. You’re in luck here as well. Numerous third-party APIs make it even easier to send emails. Services such as SendGrid, Mailgun, and ZeptoMail all come with their own pros and cons and a wide range of features. Now, you can do some really neat things with your emails. Again, your communications strategy has evolved, and so has the complexity.

Smooth Sailing…

You’ve chosen a third-party service you love, and it’s working great. You’ve created an email service using their SDK. Now you can get back to growing your app, team, and business. Unfortunately, you receive an email from your third-party service. They are increasing their prices… again. Or worse, they’ve moved a feature you’ve been using heavily behind an expensive paywall.

It’s time to change your service… again. It’s generally not too big of a deal, right? …Right?! Integrating a new email service only took a day. But the app is growing quickly, we have tech debt throughout, and not all of our emails are consistent. It wasn’t a big deal, but the new service doesn’t handle our older, legacy formats.

While it may only be a mild annoyance that only rears its ugly head every few years, you’ve got a successful app. You don’t have time to learn a new service, a new API, all the quirks of a new service, and regression test all of your communications… again. This is why the open-source Transmitly communications library was created.

Transmit-Who?

The Transmitly library is an open-source library designed to manage and control outgoing transactional communications, including emails, SMS, and push notifications. It offers centralized communication management, allowing developers to handle multiple communication channels from a single platform.

Transmitly provides flexible configuration, enabling easy switching between SMTP (MailKit) and service providers like SendGrid, Twilio, and Infobip without altering domain code. It simplifies message composition and delivery and integrates seamlessly with various dependency injection containers and template engines.

And Now for Something Completely Different

Transmitly is built on concepts pioneered by Serilog, Microsoft, and many other extensibility-first libraries. At its core, Transmitly enables you to choose as much or as little integration as you have the time or effort to use.

Love the idea? Convert your whole codebase in a day or two. Not sure? Try it on only new transactional communications to start. It’s up to you.

All this extensibility doesn’t come without some downsides. There’s going to be a ‘different’ way of doing things. And with different, there come some new terms we need to understand before we can really grasp the full scope of what’s possible.

Channels

A channel is the base of all communications. Out of the box, Transmitly supports email, SMS, and push notifications. But you’re not limited to these channels. Transmitly is built with extensibility in mind at all times. This means that if you have a need for any kind of channel, such as physical mail, fax, or Slack, practically any kind of channel can be built in.

Channel Providers

Channel Providers do the heavy lifting for your Channels. A Channel Provider can support one to many channels. Examples include MailKit (SMTP Email), SendGrid (Email), Infobip (Email & SMS), or Firebase (iOS/Android/App push notifications).

Pipelines

Pipelines are where the real power of Transmitly starts to materialize. With a pipeline, you gain control of managing communications in a single location. You can also manage which communications are available and which channel providers are allowed to send them.

That’s it! For now, at least. There are a few more advanced concepts that make the Transmitly transition even sweeter. For now, we’re sticking to the basics to get you up to speed ASAP. After all, you have a fast-growing application to maintain!

Writing Code

First, you’ll need to install the Transmitly NuGet library in your app:

# Install the base Transmitly package
dotnet install Transmitly

# To make our lives easier, we'll use the optional MS DI extensions 
dotnet install Transmitly.Microsoft.Extensions.DependencyInjectionbash
Enter fullscreen mode Exit fullscreen mode

Next, we’ll continue with our email theme by recreating a welcome email that is sent to a newly signed-up user of your app.

Startup.cs

using Transmitly;

var builder = WebApplication.CreateBuilder(args);
var emailConfig = builder.Configuration.GetRequiredSection("EmailSettings").Get<EmailSettingsConfiguration>();

builder.Services.AddTransmitly(tly =>
{
    // Pipelines allow you to define your communications
    // as a domain action. This allows your domain code to 
    // stay agnostic to the details of how you
    // may send out a transactional communication.
    tly.AddPipeline("WelcomeKit", pipeline =>
    {
        // AsIdentityAddress() is also a convenience method that helps us create an audience identity
        // Identity addresses can be anything: email, phone, or even a device/app ID for push notifications!
        pipeline.AddEmail("welcome@my.app".AsIdentityAddress("Welcome Committee"), email =>
        {
            // Transmitly is a bit different. Most channel content is configured by using 
            // templates. The immediate benefit out of the box is being able to configure
            // culture-based templates. 
            // For this example, we'll keep things simple and send a static message.
            // But as you may have guessed, Transmitly also has Template Engine plugins as well.
            email.Subject.AddStringTemplate("Thanks for creating an account!");
            email.HtmlBody.AddStringTemplate("Check out the <a href=\"https://my.app/getting-started\">Getting Started</a> section to see all the cool things you can do!");
            email.TextBody.AddStringTemplate("Check out the Getting Started (https://my.app/getting-started) section to see all the cool things you can do!");
        });
    });
Enter fullscreen mode Exit fullscreen mode

Next, we can trigger this welcome kit to happen in the same place we used to send our email. Note how simple your domain logic becomes when we remove communications details.

Using Transmitly:

public class RegistrationService
{
    private readonly ICommunicationsClient _commsClient;

    public RegistrationService(ICommunicationsClient commsClient)
    {
        _commsClient = commsClient ?? throw new ArgumentNullException(nameof(commsClient));
    }

    public async Task<User> RegisterUser(string email, AccountDetail detail)
    {
        // Create user
        var newUser = new User(email);
        // Trigger the welcome kit email
        var result = await _commsClient.DispatchAsync("WelcomeKit", email, TransactionalModel.Create(new { }));
        if (result.IsSuccessful)
            return newUser;
        throw new RegistrationException("Registration failed. Rollback!");
    }
}
Enter fullscreen mode Exit fullscreen mode

If you run this new code… technically, it will work. However, you’ll be disappointed that you’re no longer sending emails. This is because we have not yet defined the ‘how’ of sending the emails out. With Transmitly, the “how” or services are referred to as Channel Providers. Channel Providers are what the name implies: they are providers for any given channel (email, SMS, push, etc.).

# We want to continue using SMTP for now, so we'll add the MailKit extension
dotnet install Transmitly.ChannelProvider.MailKit
Enter fullscreen mode Exit fullscreen mode

Now we can configure the SMTP server settings Transmitly will use for our WelcomeKit.

using Transmitly;

var builder = WebApplication.CreateBuilder(args);
var emailConfig = builder.Configuration.GetRequiredSection("EmailSettings").Get<EmailSettingsConfiguration>();

builder.Services.AddTransmitly(tly =>
{
    // Configure the MailKit Channel Provider Settings
    tly.AddMailKitSupport(mailkit =>
    {
        mailkit.Host = emailConfig.Host;
        mailkit.UseSsl = emailConfig.UseSsl;
        mailkit.Port = emailConfig.Port;
        mailkit.UserName = emailConfig.Username;
        mailkit.Password = emailConfig.Password;
    })
    // Pipelines allow you to define your communications
    // as a domain action. This allows your domain code to 
    // stay agnostic to the details of how you
    // may send out a transactional communication.
    .AddPipeline("WelcomeKit", pipeline =>
    {
        // AsIdentityAddress() is also a convenience method that helps us create an audience identity
        // Identity addresses can be anything: email, phone, or even a device/app ID for push notifications!
        pipeline.AddEmail("welcome@my.app".AsIdentityAddress("Welcome Committee"), email =>
        {
            // Transmitly is a bit different. Most channel content is configured by using 
            // templates. The immediate benefit out of the box is being able to configure
            // culture-based templates. 
            // For this example, we'll keep things simple and send a static message.
            // But as you may have guessed, Transmitly also has Template Engine plugins as well.
            email.Subject.AddStringTemplate("Thanks for creating an account!");
            email.HtmlBody.AddStringTemplate("Check out the <a href=\"https://my.app/getting-started\">Getting Started</a> section to see all the cool things you can do!");
            email.TextBody.AddStringTemplate("Check out the Getting Started (https://my.app/getting-started) section to see all the cool things you can do!");
        });
    });
Enter fullscreen mode Exit fullscreen mode

After deploying your code, sending a simple email can remain just as easy as it was before.

If SMTP doesn’t quite fit the bill for your app anymore, try a different channel provider.

Adding SMS or push notifications? Communication preferences? Modify your pipelines to effortlessly add new channels. Handling your user communication preferences are just as easy.

Conclusion

As your app grows and evolves, having a flexible and scalable communication strategy is crucial. Transmitly not only simplifies the process of sending transactional communications but also provides a robust framework for handling various channels and channel providers. This reduces the time spent managing communication logic and enables you to focus on building and improving your core application.

Check out the Transmitly Github repo if you're interested in learning more!

Happy holidays from the C# Advent Calendar 2024!

Top comments (0)