Skip to content
fullstackhero

Reference

Mailing building block

SMTP or SendGrid email abstraction behind a single IMailService, with multi-recipient delivery and HTML body support.

views 0 Last updated

The Mailing block is the email abstraction the kit’s transactional emails go through. Identity uses it for email confirmation, password reset, and welcome flows; any other module that needs to send mail does so via IMailService. Two implementations ship — SMTP via MailKit / MimeKit, and SendGrid via the official SDK — picked at startup from a single UseSendGrid flag.

What it ships

Extension

  • AddHeroMailing(services) — reads MailOptions:UseSendGrid. If true, registers SendGridMailService. Else SmtpMailService. Both implement IMailService.

Service interface

public interface IMailService
{
Task SendAsync(MailRequest request, CancellationToken ct = default);
}

Request model

public sealed record MailRequest
{
public required object To { get; init; } // string OR IReadOnlyCollection<string>
public required string Subject { get; init; }
public required string Body { get; init; }
public bool BodyAsHtml { get; init; } = true;
public IReadOnlyCollection<string>? Cc { get; init; }
public IReadOnlyCollection<string>? Bcc { get; init; }
public IReadOnlyCollection<MailAttachment>? Attachments { get; init; }
}

Implementations

  • SmtpMailService — uses MailKit + MimeKit. Reads MailOptions:Smtp:* for host, port, credentials, TLS.
  • SendGridMailService — uses the SendGrid SDK. One API call per MailRequest.

Options

  • MailOptionsFrom, DisplayName, UseSendGrid (bool), Smtp (Host / Port / UserName / Password / EnableSSL), SendGrid (ApiKey).

How modules consume Mailing

Inject IMailService and call SendAsync:

public sealed class ConfirmEmailCommandHandler(IMailService mail, IUserService users)
: ICommandHandler<ConfirmEmailCommand, Unit>
{
public async ValueTask<Unit> Handle(ConfirmEmailCommand cmd, CancellationToken ct)
{
var user = await users.GetByIdAsync(cmd.UserId, ct).ConfigureAwait(false);
var token = /* generate confirm token */;
var link = $"https://app.example.com/confirm?token={token}";
await mail.SendAsync(new MailRequest
{
To = user.Email,
Subject = "Confirm your email",
Body = $"<p>Welcome! <a href=\"{link}\">Confirm your email</a> to finish setup.</p>",
BodyAsHtml = true,
}, ct).ConfigureAwait(false);
return Unit.Value;
}
}

Identity uses this exact shape for the four flows that need email: confirm, forgot-password, reset-password, and welcome.

Configuration

SMTP

{
"MailOptions": {
"From": "no-reply@example.com",
"DisplayName": "fullstackhero",
"UseSendGrid": false,
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"UserName": "smtp-user",
"Password": "set-via-secrets",
"EnableSSL": true
}
}
}

SendGrid

{
"MailOptions": {
"From": "no-reply@example.com",
"DisplayName": "fullstackhero",
"UseSendGrid": true,
"SendGrid": {
"ApiKey": "set-via-secrets"
}
}
}

How to extend

Add an SES (or Mailgun, Postmark…) provider

Implement IMailService against the SDK of your choice and register it in place of the existing implementations. Both SmtpMailService and SendGridMailService are templates of “translate MailRequest to the provider’s API.”

Add templating

Build a separate IMailTemplateRenderer service. Resolve a Razor or Scriban template, render to HTML, then call IMailService.SendAsync. Keep templating outside IMailService so you can swap providers without rewriting templates.

Add a retry policy

Microsoft.Extensions.Http.Resilience (already a dep of the Web block) lets you wrap any HttpClient (SendGrid uses one) with a Polly v8 pipeline. For SMTP, wrap SmtpMailService with a decorator that retries on transient failures (timeout, 4xx with try-again codes).

Gotchas

  • MailRequest.To is flexible. It’s object typed to accept either a single string or an IReadOnlyCollection<string>. The service handles both; be deliberate about which one you pass. Bulk lists of 100+ recipients should be split into multiple SendAsync calls so a single bad address doesn’t fail the whole batch.
  • No built-in templating. This is by design. If you need it, ship IMailTemplateRenderer next to your handlers.
  • SmtpMailService opens a fresh connection per send. Heavy traffic? Switch to SendGrid or wrap with a pool. The kit doesn’t ship a long-lived SMTP client.
  • SendGrid charges per API call. Bulk personalisation via the SendGrid Personalisations API is faster and cheaper for newsletter-style use. The kit’s wrapper is the transactional path; replace it for marketing.
  • Plaintext credentials in appsettings.json are a footgun. Use environment variables, user-secrets, or your cloud secrets manager. Never check MailOptions:Smtp:Password into git.

Critical files

  • src/BuildingBlocks/Mailing/Extensions.cs
  • src/BuildingBlocks/Mailing/Services/IMailService.cs
  • src/BuildingBlocks/Mailing/Services/SmtpMailService.cs
  • src/BuildingBlocks/Mailing/Services/SendGridMailService.cs
  • src/BuildingBlocks/Mailing/MailOptions.cs
  • WebFshPlatformOptions.EnableMailing toggle.
  • Identity module — the biggest consumer (confirmation, reset, welcome).
  • Notifications module — in-app inbox; pair it with email for double-channel delivery.