Email Service

Email Service

Not long ago I wrote about a hosted alert handler I sometimes use. That handler sends emails for error alerts. It does that through the use of an email service that I wrote, in the CODEGATOR CG.Email NUGET package. I though I might write about that service today.

The service starts off with an interface. This one looks like this:

public interface IEmailService : IService
{
    Task<EmailResult> SendAsync(
        string fromAddress,
        IEnumerable<string> toAddresses,
        IEnumerable<string> ccAddresses,
        IEnumerable<string> bccAddresses,
        IEnumerable<string> attachments,
        string subject,
        string body,
        bool bodyIsHtml = false,
        CancellationToken token = default(CancellationToken)
        );
}

As we can see, this type of object can asynchronously send an email message to a list of people, with an optional CC and BCC list. The body can be HTML, or not. The email can have a subject line. All standard email stuff, right?

The class that implements the IEmailService interface is called, no surprise, EmailService. Here is what that looks like:

public class EmailService : ServiceBase, IEmailService
{
    private IEmailStrategy EmailStrategy { get; }

    public EmailService(
        IEmailStrategy emailStrategy
        )
    {
        Guard.Instance().ThrowIfNull(emailStrategy, nameof(emailStrategy));
        EmailStrategy = emailStrategy;
    }

    public async Task<EmailResult> SendAsync(
        string fromAddress,
        IEnumerable<string> toAddresses,
        IEnumerable<string> ccAddresses,
        IEnumerable<string> bccAddresses,
        IEnumerable<string> attachments,
        string subject,
        string body,
        bool bodyIsHtml = false,
        CancellationToken token = default(CancellationToken)
        )
    {
        Guard.Instance().ThrowIfNullOrEmpty(fromAddress, nameof(fromAddress))
            .ThrowIfNull(toAddresses, nameof(toAddresses))
            .ThrowIfNull(ccAddresses, nameof(ccAddresses))
            .ThrowIfNull(bccAddresses, nameof(bccAddresses))
            .ThrowIfNull(attachments, nameof(attachments));

        try
        {
            var retValue = await EmailStrategy.SendAsync(
                fromAddress,
                toAddresses,
                ccAddresses,
                bccAddresses,
                attachments,
                subject,
                body,
                bodyIsHtml,
                token
                ).ConfigureAwait(false);

            return retValue;
        }
        catch (Exception ex)
        {
            throw new EmailException(
                message: string.Format(
                    Resources.EmailService_SendAsync,
                    EmailStrategy.GetType().Name
                    ),
                innerException: ex
                );
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            EmailStrategy?.Dispose();
        }

        base.Dispose(disposing);
    }
}

So, the email service wraps an object of type IEmailStrategy, and defers to that object whenever the SendAsync method is called. The IEmailStrategy object is passed in, as a parameter, on the constructor. From there the object is saved to the EmailStrategy property. On cleanup, the Dispose method of the EmailService class ensures that we cleanup the strategy along with the service class.

Ok, so, I covered the IEmailService and EmailService types. I also introduced the IEmailServiceStrategy type, but, I’ve yet to look at that. Let’s do that now:

public interface IEmailStrategy : IStrategy
{
    Task<EmailResult> SendAsync(
        string fromAddress,
        IEnumerable<string> toAddresses,
        IEnumerable<string> ccAddresses,
        IEnumerable<string> bccAddresses,
        IEnumerable<string> attachments,
        string subject,
        string body,
        bool bodyIsHtml,
        CancellationToken token
        );
}

The IEmailStrategy type is just an algorithm for sending an email. Using the bit of indirection, like this, makes it easy to swap out the actual logic for sending emails, at runtime. That makes everything more flexible and easier to test. Both of those are good things.

Since almost any email logic is going to require some sort of options, for things like, server name, port, credentials, etc., I’ve also created a generic type of IEmailStrategy that accepts an options type. That interface looks like this:

public interface IEmailStrategy<TOptions> : IEmailStrategy
    where TOptions : EmailStrategyOptions, new()
{
        
}

So, here I’m just saying that an email strategy might have some sort of options type associated with it.

As I usually do, when working with interfaces, I’ve also created a corresponding base class called EmailStrategyBase. Here is what that looks like:

public abstract class EmailStrategyBase : StrategyBase
{
    public abstract Task<EmailResult> SendAsync(
        string fromAddress,
        IEnumerable<string> toAddresses,
        IEnumerable<string> ccAddresses,
        IEnumerable<string> bccAddresses,
        IEnumerable<string> attachments,
        string subject,
        string body,
        bool bodyIsHtml,
        CancellationToken token
        );
}


public abstract class EmailStrategyBase<TOptions> :
    StrategyBase<TOptions>,
    IEmailStrategy<TOptions>
    where TOptions : EmailStrategyOptions, new()
{
    protected EmailStrategyBase(
        IOptions<TOptions> options
        ) : base(options)
    {

    }

    public abstract Task<EmailResult> SendAsync(
        string fromAddress,
        IEnumerable<string> toAddresses,
        IEnumerable<string> ccAddresses,
        IEnumerable<string> bccAddresses,
        IEnumerable<string> attachments,
        string subject,
        string body,
        bool bodyIsHtml,
        CancellationToken token
        );
}

Now that I’ve introduced the strategy types, let’s use them to build an quick SMS email strategy – since SMS is probably the most common way of sending emails.

The SMTP email strategy is implemented using the SmtpEmailStrategy class, which looks like this:

internal class SmtpEmailStrategy : 
    EmailStrategyBase<SmtpEmailStrategyOptions>, 
    IEmailStrategy<SmtpEmailStrategyOptions>
{
    protected SmtpClient Client { get; private set; }

    public SmtpEmailStrategy(
        IOptions<SmtpEmailStrategyOptions> options
        ) : base(options)
    {

    }

    public override Task<EmailResult> SendAsync(
        string fromAddress,
        IEnumerable<string> toAddresses,
        IEnumerable<string> ccAddresses,
        IEnumerable<string> bccAddresses,
        IEnumerable<string> attachments,
        string subject,
        string body,
        bool bodyIsHtml,
        CancellationToken token
        )
    {
        var message = BuildAMessage(
            fromAddress,
            toAddresses,
            ccAddresses,
            bccAddresses,
            attachments,
            subject,
            body,
            bodyIsHtml
            );

        if (Client == null)
        {
            var address = Options.Value.ServerAddress;
            var port = Options.Value.ServerPort;
            var userName = Options.Value.UserName;
            var password = Options.Value.Password;

            Client = new SmtpClient(address, port);

            if (!string.IsNullOrWhiteSpace(userName) &&
                !string.IsNullOrWhiteSpace(password)
                )
            {
                Client.UseDefaultCredentials = false;
                Client.Credentials = new NetworkCredential(
                    userName,
                    password
                    );
            }
            else
            {
                Client.UseDefaultCredentials = true;
            }
        }

        Client.Send(message);

        var retValue = new EmailResult()
        {
            EmailId = $"{Guid.NewGuid():N}"
        };

        return Task.FromResult(retValue);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            Client?.Dispose();
            Client = null;
        }
        base.Dispose(disposing);
    }

    private MailMessage BuildAMessage(
        string fromAddress,
        IEnumerable<string> toAddresses,
        IEnumerable<string> ccAddresses,
        IEnumerable<string> bccAddresses,
        IEnumerable<string> attachments,
        string subject,
        string body,
        bool bodyIsHtml
        )
    {
        var message = new MailMessage()
        {
            From = new MailAddress(fromAddress)
        };

        foreach (var toAddress in toAddresses)
        {
            message.To.Add(new MailAddress(toAddress.Trim()));
        }
        foreach (var ccAddress in ccAddresses)
        {
            message.CC.Add(new MailAddress(ccAddress.Trim()));
        }
        foreach (var bccAddress in bccAddresses)
        {
            message.Bcc.Add(new MailAddress(bccAddress.Trim()));
        }
        foreach (string attachment in attachments)
        {
            message.Attachments.Add(new Attachment(attachment));
        }

        message.Subject = subject;
        message.Body = body;
        message.IsBodyHtml = bodyIsHtml;

        return message;
    }
}

Walking through the code, we see that the class derives from the strategy base types we talked about, earlier in this post. The class also implements the generic IEmailStrategy variant, with a SmtpEmailStrategyOptions type argument. Those options are passed into the constructor, as a parameter. From there, I pass the options off to the EmailStrategyBase class, where it’s ultimately stored in that classes Options property. We’ll cover the SmtpEmailStrategyOptions class shortly. For now though, let’s stay focused on the SMTP strategy.

The class has a Client property that contains a reference to an SmtpClient object. SmtpClient is a standard part of .NET, and it’s the object we’ll leverage, to send our emails.

The SendAsync method calls the BuildAMessage method to create the MailMessage object that the SmtpClient wants to work with. From there, I check to see of the Client property has been initialized, or not. If not, I pull various properties out of the Options property, then use that information to create the SmtpClient object. If a user name and password were specified, in the Options, I set the SmtpClient object up to use custom credentials. SmtpClient defaults to using standard credentials, otherwise. Finally, I store the SmtpClient object in the Client property.

Once I have the Client property initialized, all I have to do is call the Send method, to send the email message.

Once I’ve sent the email, I then need to create an EmailResult instance, since the SendAsync method wants to return an object of that type. For this particular strategy, that EmailResult message will contain a GUID that we’ll make ourselves. That’s because, SMTP itself doesn’t give us anything that can uniquely identify this email operation. So, we’ll fudge things and make up a unique number instead.

The BuildAMessage method starts by creating a MailMessage object. From there, I add any ‘to’ addresses, ‘cc’ addresses, and ‘bcc’ addresses to that object. After that I set the subject, body and the IsBodyHTML flag, for the message.

The options that are used for the SmtpEmailStrategy class are called SmtpEmailStrategyOptions Here is what it looks like:

public class SmtpEmailStrategyOptions : EmailStrategyOptions
{
    [Required]
    public string ServerAddress { get; set; }

    public int ServerPort { get; set; }

    [Required]
    public string UserName { get; set; }

    [ProtectedProperty]
    public string Password { get; set; }

    public SmtpEmailStrategyOptions()
    {
        // Set default values here.
        ServerPort = 25;
    }
}

The class derives from the EmailStrategyOptions class, which I’ll cover shortly. This class contains all the setup information needed to create the SmtpClient instance, inside the SmtpEmailStrategy class.

The EmailStrategyOptions class looks like this:

public class EmailStrategyOptions : StrategyOptions
{
    public string Name { get; set; }

    public string Assembly { get; set; }
}

I’ll be candid, this part of my library is likely to change, at some point. As I write this, I’m working through some changes to the CODEGATOR CG.Business NUGET package, which is where the IStrategy and StrategyBase and StrategyOptions types come from. One of those changes will, hopefully, pull some of the code I’m about to cover, for setting up strategies and options, from libraries like this one, back into my CG.Business library. For now, I’ll just mention that and move on with the code that I have.

The EmailStrategyOptions class contains a Name and Assembly property. Those properties are used by the code that actually wires up a concrete strategy with the service, and makes all this work together. The Name property must contain the name of an email strategy. The Assembly property is optional, and may be used to dynamically load an assembly that contains an email strategy.

Let’s stop and regroup. So far, I’ve laid out an email service that’s designed to contain an email strategy. That strategy contains the actual logic for sending emails. Each strategy type is designed to contain an options object with all the properties required to setup the emailing operation, at runtime.

So where do we go from here? Well, everything I’ve covered, so far, would work just fine the way it is, but, I like to include an extension method, to wire everything up nicely. That way, I don’t leave all those (sometimes complicated) steps to whoever comes behind me. Also, I hate to repeat myself and, without that extension method, I’d have to write the same startup logic every time I wanted to use an email service.

Based on that, the only real thing left is to go look at my startup extension method. That’s called AddEmail and it looks like this:

public static IServiceCollection AddEmail(
    this IServiceCollection serviceCollection,
    IConfiguration configuration
    )
{
    Guard.Instance().ThrowIfNull(serviceCollection, nameof(serviceCollection))
        .ThrowIfNull(configuration, nameof(configuration));

    serviceCollection.ConfigureOptions<EmailServiceOptions>(
        configuration,
        out var emailServiceOptions
        );

    serviceCollection.AddSingleton<IEmailService, EmailService>();

    if (!string.IsNullOrEmpty(emailServiceOptions.Strategy.Assembly))
    {
        try
        {
            _ = Assembly.Load(
                emailServiceOptions.Strategy.Assembly
                );
        }
        catch
        {
            // Just eat the error.
        }
    }

    var methodName = "";

    if (string.IsNullOrEmpty(emailServiceOptions.Strategy.Name))
    {
        methodName = $"AddDoNothingStrategy";
    }
    else
    {
        methodName = $"Add{emailServiceOptions.Strategy.Name}Strategy";
    }

    var methods = AppDomain.CurrentDomain.ExtensionMethods(
        typeof(IEmailStrategyBuilder),
        methodName,
        new Type[] { typeof(IConfiguration) }
        );

    if (false == methods.Any())
    {
        // Panic!
        throw new MissingMethodException(
            message: string.Format(
                Resources.ServiceCollectionExtensions_MethodNotFound,
                methodName
                )
            );
    }

    var strategyBuilder = new EmailStrategyBuilder()
    {
        Services = serviceCollection
    };

    var method = methods.First();

    method.Invoke(
        null,
        new object[] { strategyBuilder, configuration }
        );

    return serviceCollection;
}

AddEmail starts by verifying the incoming parameters. After that, I use the ConfigureOptions method to load my options, validate them, and ensure they are registered with the DI container. Next, I register the EmailService types with the DI container. Next, I check to see if an assembly was specified, in the options. If so, I then try to load the assembly.

Why load an assembly here? Well, this approach means you don’t have to create a reference to whatever assembly you’re using, for you email strategy, at compile time. It also works around an issue created by the way that ASP.NET trims it’s assembly list, at startup.

If you’ve ever added a reference to an assembly, in an ASP.NET project, only to discover that it isn’t loaded in the app domain, at runtime, then you’ve run afoul of ASP.NET’s assembly trimming algorithm. The only two ways I know, to work around that behavior, is to explicitly add a type reference, some in your code, to something in the assembly – such as:

var blah = typeof(MyClass);

Where, the variable blah will remain in scope for the lifetime of the website. Or, the other way to deal with this issue, is to simply load the assembly yourself, at runtime. My extension method uses that last route to ensure that whatever assembly contains the email strategy we’re about to use, is still loaded and waiting for us.

Once I know the strategy’s assembly is loaded. I then check the options for the strategy name. Assuming a strategy was specified, I use it to form the name of an extension method. If a strategy wasn’t specified, I default to using a “do nothing” email strategy, that allows me to prevent ubiquitous checks, in the calling code, for whether the service is configured, or not.

Once I have the name of an extension method, I then search through all the loaded assemblies, for that method, using my ExtensionMethods method. This method is part of the CODEGATOR CG.Reflection NUGET package. It’s implementation is outside the scope of this blog post. For now, just know that it will find the extension we’re looking for, and return a MethodInfo object, for that method, so we can invoke it.

If the extension method search fails to return a match, we throw an exception. If it does return a match, we create an instance of EmailStrategyBuilder, which is my type for creating email strategies using the DI container. The EmailStrategyBuilder type itself, is very simple:

public interface IEmailStrategyBuilder
{
    IServiceCollection Services { get; set; }
}

internal class EmailStrategyBuilder : IEmailStrategyBuilder
{
     public IServiceCollection Services { get; set; }
}

So an EmailStrategyBuilder object simply wraps the DI container, which is passed around in .NET projects as the IServiceCollection type. I use the EmailStrategyBuilder type to further reduce the likely hood that any of my extension methods will class with other, non CODEGATOR related extension methods on the IServiceCollection type.

Just trying to do my part to reduce namespace pollution and create a safer, better, greener world for all of us to live in … My name is Martin Cook, I’m not running for political office and I approved this message.

Ok, back to the code. We know have a MethodInfo object representing an extension method hung off the EmailStrategyBuilder type. Only thing left to do is call it, which we do.

Why does my AddEmail extension method reach out and call another extension method? Well, let’s think about what we just did. We used configuration settings that were given to us as an EmailServiceOptions object, to dynamically locate and call an extension method. What I haven’t said, so far, and probably should have, is that this other extension method is the thing that’s going to register our email strategy. Since we didn’t hard code the call to that, other, extension method, that means the entire process is nicely dynamic and configurable from the application’s standard configuration.

Next thing to look at is that “other” extension method. Since we’ve covered the SMTP email strategy, let’s assume we’ll call that one and cover it next. Here is what the AddSmtpStrategy method looks like:

public static IEmailStrategyBuilder AddSmtpStrategy(
    this IEmailStrategyBuilder emailStrategyBuilder,
    IConfiguration configuration
    )
{
    Guard.Instance().ThrowIfNull(emailStrategyBuilder, nameof(emailStrategyBuilder))
        .ThrowIfNull(configuration, nameof(configuration));

    emailStrategyBuilder.Services.ConfigureOptions<SmtpEmailStrategyOptions>(
        configuration.GetSection("Smtp")
        );

    emailStrategyBuilder.Services.AddSingleton<IEmailStrategy, SmtpEmailStrategy>();

    return emailStrategyBuilder;
}

AddSmtpStrategy begins by validating the incoming parameters. After that, it calls ConfigureOptions to ensure that our SMTP specific options, SmtpEmailStrategyOptions, are loaded, validated, and registered as a service with the DI container. Finally, the method registers the SMTP strategy, with the DI container.

Notice that the method reads the SMTP settings by looking for the “Smtp” section, in the configuration. That means the configuration my email is looking for, assuming the use of the SMTP strategy, is something like this:

{
    "Email": {
       "Strategy" : "Smtp",
       "Assembly" : "",
       "Smtp": {
       "ServerAddress": "localhost",
       "ServerPort": 25,
       "UserName": "[email protected]",
       "Password": "locally encrypted password"
       }
    }
}

And, using AddEmail to register an email service, using the SMTP strategy, can be done like this:

static void Main(string[] args)
{
   var services = new ServiceCollection();
   var configuration = // todo get your configuration here.

   services.AddEmail(configuration.GetSection("Email"));         
}

Other types of email strategies can be employed simply by changing the Name property, in the configuration, and supplying the corresponding section, with appropriate properties. Of course, the appropriate properties will depend on the email strategy you’ve chosen to use.

Now that we know how to wire everything up, let’s look at how to use the service, to send emails. That SendAsync method, shown again here:

Task<EmailResult> SendAsync(
    string fromAddress,
    IEnumerable<string> toAddresses,
    IEnumerable<string> ccAddresses,
    IEnumerable<string> bccAddresses,
    IEnumerable<string> attachments,
    string subject,
    string body,
    bool bodyIsHtml = false,
    CancellationToken token = default(CancellationToken)
    );

Has quite a few parameters to deal with. In my experience, most of those parameters won’t be needed for most email operations. What’s needed, are some overloads with fewer parameters, for the more likely usage scenarios. It just so happens, I created several extension methods for just that purpose. They all defer to the SendAsync method, internally, they each just expose a different set of parameters. Rather than display all twenty bazillion variations, here is what one of them looks:

public static EmailResult Send(
    this IEmailService service,
    string fromAddress,
    string toAddress,
    string subject,
    string body,
    bool bodyIsHtml = false
    )
{
    Guard.Instance().ThrowIfNull(service, nameof(service))
        .ThrowIfNullOrEmpty(fromAddress, nameof(fromAddress));

    return service.Send(
        fromAddress,
        toAddress.Split(','),
        new string[0],
        new string[0],
        new string[0],
        subject,
        body,
        bodyIsHtml
        );
}

Using these wrapper methods, every conceivable is covered so you won’t have to deal with parameters you don’t need, or deal with parameter types that don’t meet your needs.

That is really about it. Oh wait! I also have a hosting related extension method, hung off the IHostBuilder type, that I use when I’m using my hosting extensions, including my hosted alert handler. That method is also called AddEmail, and it looks like this:

public static IHostBuilder AddEmail(
    this IHostBuilder hostBuilder
    )
{
    Guard.Instance().ThrowIfNull(hostBuilder, nameof(hostBuilder));

    hostBuilder.ConfigureServices((hostBuilderContext, serviceCollection) =>
    {
        var section = hostBuilderContext.Configuration.GetSection(
            "Services:Email"
            );

        if (section.GetChildren().Any())
        {
            serviceCollection.AddEmail(section);
        }
        else
        {
            serviceCollection.AddEmail(options =>
            {
                options.AddDoNothingStrategy(section);
            });
        }
    });

    return hostBuilder;
}

This method calls the ConfigureServices method, on the IHostBuilder object. That method has a delegate that then checks for the “Services:Email” section, in the host’s configuration. If that section isn’t there, or doesn’t contain anything, then the method registers the “do nothing” email strategy. That covers the scenario where emails are required, say, during development, or in a QA setting, On the other hand, assuming the “Services:Email” does exist, and is populated, then the method defers to the AddEmail extension method, on the IServiceCollection type, that we just covered in this blog post.

The end result is, when using hosting extensions, I can configure the email service under the “Services” section of the host’s configuration. That means I can group all my services together, in one place, so there’s never any confusion about where a service should be configured in one of my hosted applications.

That’s it! My email service in a nutshell. There are certainly other ways to send emails, but I don’t think any of them are any more flexible. Also, none of them integrate with the other CODEGATOR extension any better.

Thanks for reading.

Photo by Brett Jordan on Unsplash