0

I have an application. It needs to send emails. We've all been there, done that, got the t-shirt.

I've got an IEmailMessage interface:

public interface IEmailMessage
{
    string From { get; }

    string To { get; }

    string Subject { get; }

    string Body { get; }

    bool IsBodyHtml { get; }
}

with a readonly implementation:

public class DefaultEmailMessage : IEmailMessage
{
    public DefaultEmailMessage(string from, string to, string subject, string body, bool isBodyHtml)
    {
        // you can probably guess what this does
    }

    public string From { get; }

    public string To { get; }

    public string Subject { get; }

    public string Body { get; }

    public bool IsBodyHtml { get; }
}

There's a service to actually send an email (implementations use actual email providers like SendGrid):

public interface IEmailSender
{
    Task<bool> SendAsync(IEmailMessage email, CancellationToken cancellationToken = default(CancellationToken));
}

and finally, a service that handles pulling the messages from a queue, transforms them into generic IEmailMessages, then uses an instance of the above interface to actually send them off:

public interface IEmailDeliveryService
{
    Task SendOutstandingEmailsAsync(int batchSize);
}

public class EmailDeliveryService : IEmailDeliveryService
{
    public EmailDeliveryService(IEmailSender emailClient)
    {
        ...
    }

    public async Task SendOutstandingEmailsAsync(int batchSize)
    {
        IEnumerable<IEmailMessage> serviceEmails = GetListOfEmailsFromDb(batchSize)
            .ConvertAll(dbEmail => ToIEmailMessage(dbEmail));

        foreach (var email in serviceEmails)
            await _emailClient.SendAsync(email);
    }
}

The reason for this somewhat convoluted chain is to allow the actual email sending provider IEmailSender to be substituted (this is a PoC and SendGrid is unlikely to be the final choice).


In our dev environment we don't want test emails to potentially go out to clients, so we need to modify the "To" field to set it to a dev mailbox. For this purpose I've written the following visitor:

public interface IEmailTransformer
{
    void Transform(IEmailMessage email);
}

The intention is that I will have 0...N instances of IEmailTransformer injected into the EmailDeliveryService, and each instance will get every email passed to it so it can do whatever it needs:

public interface IEmailDeliveryService
{
    Task SendOutstandingEmailsAsync(int batchSize);
}

public class EmailDeliveryService : IEmailDeliveryService
{
    public EmailDeliveryService(IEmailSender emailClient, IEnumerable<IEmailTransformer> transformers)
    {
        ...
    }

    public async Task SendOutstandingEmailsAsync(int batchSize)
    {
        IEnumerable<IEmailMessage> serviceEmails = GetListOfEmailsFromDb(batchSize)
            .ConvertAll(dbEmail => ToIEmailMessage(dbEmail));

        foreach (var email in serviceEmails)
        {
            foreach (var transformer in _transformers)
                transformer.Transform(email);

            await _emailClient.SendAsync(email);
        }
    }
}

I'll then have an implementation that sets the "To" field, instantiated only in the dev environment:

public class EmailToTransformer : IEmailTransformer
{
    public void Transform(IEmailMessage email)
    {
        email.To = _emailTo;
    }
}

Problem

The above obviously doesn't work since the interface's properties are readonly.


Solution #1

Make the properties writable.

Issues #1

The properties shouldn't be writable because IEmailSender shouldn't be able to mutate them.


Solution #2

Define an intermediate interface, IWritableEmailMessage, that inherits from IEmailMessage:

public interface IWritableEmailMessage : IEmailMessage
{
    string From { get; set; }

    string To { get; set; }

    string Subject { get; set; }

    string Body { get; set; }

    bool IsBodyHtml { get; set; }
}

Change EmailDeliveryService, DefaultEmailMessage and IEmailTransformer to use IWritableEmailMessage.

Issues #2

Feels like using abstraction to achieve the same thing as #1, just in an unnecessarily convoluted manner.


Question

Neither of the above solutions is optimal, I feel like there must be a better way to achieve this, but I don't know what it is. I feel like I'm either overlooking something simple, or unnecessarily overcomplicating this. Any suggestions (especially pointing to particular design patterns) are highly appreciated!

Apologies for the large amount of code, but I felt it's necessary to fully explain the problem. I tried to follow the advice in this Meta answer.

4
  • 1
    "we don't want test emails to potentially go out to clients, so we need to modify the "From" field to set it to a dev mailbox" That would be the "to" field then.
    – Flater
    Mar 12, 2020 at 11:46
  • Derp! Yes, you are 100% correct, I'm playing with multiple things at the moment - fixed.
    – Ian Kemp
    Mar 12, 2020 at 11:50
  • How about creating new message, copying all fields except the one you want to change? Efectively changing the Transformer interface to return new message instead of void? Another option could be to provide sender implementation which ignores that field. It May actually lead to the same thing but maybe making it a decorator would be more flexible than having transformers.
    – slepic
    Mar 12, 2020 at 11:53
  • 1
    To downvoters, please advise what is wrong with this question? As someone who spends a lot of time curating questions on Stack Overflow, I don't want to be asking bad questions anywhere.
    – Ian Kemp
    Mar 12, 2020 at 12:25

2 Answers 2

2

The visitor pattern you're suggesting isn't impossible, but if rewriting the "to" field is the only concern, it comes across as a bit overengineered, in my opinion.

Starting from scratch, what you want is effectively something like this:

public class DevelopmentEmailMessage : IEmailMessage
{
    public DevelopmentEmailMessage(string from, string subject, string body, bool isBodyHtml)
    {
        // note there is no "to" parameter now
    }

    public string From { get; }

    public string To => "[email protected]"; // can of course be parametrized

    public string Subject { get; }

    public string Body { get; }

    public bool IsBodyHtml { get; }
}  

This neatly solves the issue without requiring the domain logic to actually behave differently. This solves the problem for the mail sending logic.

For the mail generating logic, it's a bit more contextual. One way would be to have a mail factory which creates the email message for you, and having two different factories (the real one and the dev one):

public interface IEMailMessageFactory
{
    IEmailMessage Create(string from, string to, string subject, string body, bool isBodyHtml);
}

public class RealEmailMessageFactory : IEMailMessageFactory
{
    public IEmailMessage Create(string from, string to, string subject, string body, bool isBodyHtml)
    {
        return new DefaultEmailMessage(...);
    }
}

public class DevelopmentEmailMessageFactory : IEMailMessageFactory
{
    public IEmailMessage Create(string from, string to, string subject, string body, bool isBodyHtml)
    {
        // we just ignore the `to` parameter completely

        return new DevelopmentEmailMessage(...);
    }
}

This means that you can change the behavior of your application using your DI framework, which can in turn respond to e.g. an environment value.

If you take the factory route, you could decide the recipient's (development) email address in the development factory itself and not create a DevelopmentEmailMessage class by itself, but I'd be inclined to keep these two email message classes, in case you want to make more distinctions between real and development emails in the future.

I would, for example, mention the original recipient in the development email, for debugging purposes:

public class DevelopmentEmailMessage : IEmailMessage
{
    public DevelopmentEmailMessage(string from, string to, string subject, string body, bool isBodyHtml)
    {
        this.Subject = $"[intended for {to}] {subject}";
        // and then set the other values, but not "to".
    }

    public string From { get; }

    public string To => "[email protected]"; // can of course be parametrized

    public string Subject { get; }

    public string Body { get; }

    public bool IsBodyHtml { get; }
}  

By prepending the original recipient to the mail subject, you can still confirm that the email would've been sent out to the correct recipient. You could similarly also add other information, such as the name of the current application (in cases where multiple applications send mails to this mailbox), or detailed debug information.

1

There is a very simple solution to your specific need: change the IEmailTransformer interface:

public interface IEmailTransformer
{
    IEmailMessage Transform(IEmailMessage email);
}

And your transformer then becomes:

public class EmailToTransformer : IEmailTransformer
{
    ...

    public IEmailMessage Transform(IEmailMessage email)
        => new DefaultEmailMessage(
            email.From, 
            _emailTo,
            email.Subject,
            email.Body,
            email.IsBodyHtml);
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.