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)— bindsMailOptions, registers one process-wideISendGridClientsingleton (per-send client construction leaks sockets), and a transientIMailServicefactory that returnsSendGridMailServicewhenMailOptions:UseSendGridistrue, otherwiseSmtpMailService.
Service interface
public interface IMailService{ Task SendAsync(MailRequest request, CancellationToken ct);}Request model
MailRequest is a constructor-built class — recipients always come as a Collection<string>:
public class MailRequest( Collection<string> to, string subject, string? body = null, string? from = null, string? displayName = null, string? replyTo = null, string? replyToName = null, Collection<string>? bcc = null, Collection<string>? cc = null, IDictionary<string, byte[]>? attachmentData = null, IDictionary<string, string>? headers = null);The body is treated as HTML by both implementations (MailKit’s BodyBuilder.HtmlBody; SendGrid sends it as both plain-text and HTML content). From/DisplayName on the request override the configured defaults per send.
Implementations
SmtpMailService— uses MailKit + MimeKit. ReadsMailOptions:Smtp:*for host, port, credentials; connects with STARTTLS per send.SendGridMailService— uses the SendGrid SDK via the sharedISendGridClient. One API call perMailRequest; the firstToaddress is the primary recipient,Cc/Bcc/ReplyTo/attachments map onto the SendGrid message.
Options
MailOptions—From,DisplayName,UseSendGrid(bool),Smtp(Host/Port/UserName/Password),SendGrid(ApiKeyplus optionalFrom/DisplayNameoverrides).
How modules consume Mailing
Inject IMailService and call SendAsync — or better, do what Identity does and push the send onto the Hangfire email queue so a slow SMTP server never blocks the request:
var mailRequest = new MailRequest( new Collection<string> { user.Email }, "Confirm Your Email Address", emailBody);
jobService.Enqueue("email", () => mailService.SendAsync(mailRequest, cancellationToken));Identity uses this shape for its email flows: email confirmation, password reset, and the welcome mail.
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" } }}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.Tois always aCollection<string>— even for one recipient. Note the SendGrid path builds a single email toTo[0](extra recipients ride along only as Cc/Bcc); the SMTP path addresses everyone inTo. Bulk lists of 100+ recipients should be split into multipleSendAsynccalls so a single bad address doesn’t fail the whole batch.- No built-in templating. This is by design. If you need it, ship
IMailTemplateRenderernext to your handlers. SmtpMailServiceopens 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.jsonare a footgun. Use environment variables,user-secrets, or your cloud secrets manager. Never checkMailOptions:Smtp:Passwordinto git.
Critical files
src/BuildingBlocks/Mailing/Extensions.cssrc/BuildingBlocks/Mailing/Services/IMailService.cssrc/BuildingBlocks/Mailing/Services/SmtpMailService.cssrc/BuildingBlocks/Mailing/Services/SendGridMailService.cssrc/BuildingBlocks/Mailing/MailOptions.cs
Related
- Web —
FshPlatformOptions.EnableMailingtoggle. - Identity module — the biggest consumer (confirmation, reset, welcome).
- Notifications module — in-app inbox; pair it with email for double-channel delivery.