Today marks the go-live of Facteur (French for mailman), a modular mailing kit for .NET. It is a library that allows you to compose e-mails with any service, any template builder, and any technology in .NET. And they are interchangeable, so you can swap and replace parts of your system with the greatest of ease. It adds an extra layer of abstraction to hide the implementation of the e-mail sending process, making your application more robust, easier to maintain, and with fewer dependencies in your application’s core.
The birth of a modular mailing kit
I created this library after an incident with the e-mail service we use(d) for our software Dime.Scheduler. At the time, SendGrid was the only e-mail service available in the product. After having appeared on Spamhaus’ blacklist for weeks on end, we were inundated with support requests as customers couldn’t send and receive e-mails anymore. Unfortunately, there was little we could do, unless we upgraded to a paid program, which we were unwilling to do.
Hence the birth of Facteur, which allows us and our customers to swap e-mail providers without having to change a single line of code. Simple appsettings.json configuration (or any configuration, for that matter) can be used to change the e-mail service to SMTP or MS Graph, just to name a few. The ability to swap components without requiring code changes was an especially important requirement as support tickets can be resolved instantly. The corresponding design is a clear reflection of that concern.
Choose your weapons ⚔️ 🛡️
As the GitHub repo and the docs explain, there are a number of components with their distinctive roles. Each component can be replaced, so long as they implement the underlying interfaces. This design makes Facteur highly modular and extensible.
- Endpoints are the e-mail services that actually send out the mails, such as SendGrid, SMTP, and MS Graph.
- The other components are concerned with creating the body of the e-mail:
- Template providers and resolvers are essentially I/O for template files.
- Resolvers take care of the mapping of template files and the e-mail type.
- Compilers populate the e-mail template with view model objects.
- Composers are used to building the e-mail body.
Here’s an example of how all of these elements work together:
public async Task SendConfirmationMail(string customerMail, string customerName)
{
// Step #1: Set the variables of the e-mail
EmailComposer<TestMailModel> composer = new EmailComposer<TestMailModel>();
EmailRequest<TestMailModel> request = composer
.SetModel(new TestMailModel
{
Email = customerMail,
Name = customerName
})
.SetSubject("Welcome to Contoso Inc.!")
.SetFrom("donotreply@contoso.com")
.SetTo("handsome.b.wonderful@contoso.com")
.SetCc("your.manager@contoso.com")
.SetBcc("hr@contoso.com")
.Build();
// Step #2: Define how the e-mail body should be built
IMailBodyBuilder builder = new MailBodyBuilder(
new ScribanCompiler(),
new AppDirectoryTemplateProvider("Templates", ".sbnhtml"),
new ViewModelTemplateResolver());
// Step #3: populate the mail body and create an e-mail request
EmailRequest populatedRequest = await builder.BuildAsync(request);
// Step #4: send the e-mail
SmtpCredentials credentials = new("smtp.gmail.com", "587", "false", "true", "myuser@gmail.com", "mypassword");
IMailer mailer = new SmtpMailer(credentials);
await mailer.SendMailAsync(populatedRequest);
}
In essence, there are four steps that you need to take to successfully construct and send the e-mail:
- Gather all information that you need to compose the e-mail body
- Choose your weapons, or which tools, languages, and frameworks you will use to compose the body
- Invoke the assembled mail kit and construct the body
- Send the e-mail using the selected e-mail provider
In this example, the weapons of choice include:
- Scriban as the template engine to parse and populate the e-mail HTML templates
- A simple file manager that fetches the templates from the ASP.NET Core application’s application directory (it will look for .sbnhtml files in the Templates directory)
- A template resolver that matches file template names with the name of the view model. In this example, the resolver will tell the file manager (AppDirectoryTemplateProvider) to look for a file named Test.sbnhtml (derived from the class name TestMailModel) file in the Templates directory.
- SMTP as the means to send the e-mail
For each moving part, there are alternatives. For example, you can use SendGrid or Microsoft Graph to send the e-mails, or template engines such as RazorEngine to populate the placeholders in the e-mail body with data you provide in the e-mail view models.
The power of Facteur is that you can assemble your own mailing kit and swap components without having to tamper with the codebase. Having an abstract programming model like this – where most of the coding is done against interfaces – has great benefits when used with dependency injection. Step 2 of the code sample above would typically be configured in the application’s startup, exposing only the IMailBodyBuilder
interface in the app.
As a result, composing a mail body sending the e-mail is extremely lightweight and it gives peace of mind because not only is Facteur not tied to any specific technology, framework, library, or language, it’s free and open-source, so you can always modify the code when it does not fulfil your requirements.
Facteur + ASP.NET Core = ❤️
With (ASP).NET Core, you can obviously use the powerful and elegant IoC capabilities to work only with the interfaces in your application. When used correctly, Facteur has a light footprint and almost certainly will not keep you up at night wondering whether your application’s design will stand the test of time.
A simple extension NuGet package exposes the AddMailer
extension method on the IServicesCollection
interface, allowing you to configure your mailing system:
services.AddMailer<SmtpMailer, ScribanCompiler, AppDirectoryTemplateProvider, ViewModelTemplateResolver>(
mailerFactory: x => new SmtpMailer(credentials),
templateProviderFactory: x => new AppDirectoryTemplateProvider("Templates", ".sbnhtml")
);
With just one instruction, you’ve configured your entire mailing system. The example above does the following:
- Registers the endpoint (good old SMTP)
- Tells which template syntax to use (Scriban)
- Provides the path where the templates are stored (in the app directory, in the templates folder)
- How to locate the right template (using the name of the view model types)
All you need to do next is to inject the necessary interfaces such a
s IMailer
and IBodyBuilder
and you’re good to start sending those e-mails.
Using DI, you eliminate the need to configure the mailing system in any place other than ASP.NET Core’s Startup class. Step 2 and step 4 of the first code sample would become redundant or be reduced to the bare minimum. Sending an e-mail could be as simple as a three line statement: gathering the data, building the body and finally, calling the service to send the mail to the recipients:
public class WelcomeController
{
private readonly IMailer _mailer;
private readonly IMailBodyBuilder _bodyBuilder;
public WelcomeController(IMailer mailer, IMailBodyBuilder bodyBuilder)
{
_mailer = mailer;
_bodyBuilder = bodyBuilder;
}
[HttpPost]
public async ActionResult Index(NewEmployee employee)
{
EmailComposer<NewEmployeeMailModel> composer
= new EmailComposer<NewEmployeeMailModel>();
EmailRequest<NewEmployeeMailModel> request = composer
.SetModel(new NewEmployeeMailModel{ Name = employee.Name })
.SetSubject("Welcome to Contoso Inc.!")
.SetFrom("donotreply@contoso.com")
.SetTo(employee.Email)
.Build();
EmailRequest populatedRequest = await _bodyBuilder.BuildAsync(request);
await _mailer.SendMailAsync(populatedRequest);
}
}
Get started with Facteur
Check out the GitHub repo and the docs for more information about the library and how to get started with the project.