Overview
The Mailing building block provides email sending capabilities with support for SMTP and SendGrid providers. It offers a simple, provider-agnostic interface for sending emails.
Switch between SMTP and SendGrid without changing your code — just update configuration.
Key Components
IMailService
Main abstraction for email operations:
namespace FSH . Framework . Mailing . Services ;
public interface IMailService
{
Task SendAsync ( MailRequest request , CancellationToken ct );
}
MailRequest
Email request model:
using System . Collections . ObjectModel ;
namespace FSH . Framework . Mailing ;
public class MailRequest
{
public Collection < string > To { get ; }
public string Subject { get ; }
public string ? Body { get ; }
public string ? From { get ; }
public string ? DisplayName { get ; }
public string ? ReplyTo { get ; }
public string ? ReplyToName { get ; }
public Collection < string > Bcc { get ; }
public Collection < string > Cc { get ; }
public IDictionary < string , byte []> AttachmentData { get ; }
public IDictionary < string , string > Headers { get ; }
public 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 )
{
To = to ;
Subject = subject ;
Body = body ;
From = from ;
DisplayName = displayName ;
ReplyTo = replyTo ;
ReplyToName = replyToName ;
Bcc = bcc ?? new Collection < string >();
Cc = cc ?? new Collection < string >();
AttachmentData = attachmentData ?? new Dictionary < string , byte []>();
Headers = headers ?? new Dictionary < string , string >();
}
}
MailOptions
Configuration for mail providers:
namespace FSH . Framework . Mailing ;
public class MailOptions
{
public bool UseSendGrid { get ; set ; }
public string ? From { get ; set ; }
public string ? DisplayName { get ; set ; }
public SmtpOptions ? Smtp { get ; set ; }
public SendGridOptions ? SendGrid { get ; set ; }
}
public class SmtpOptions
{
public string ? Host { get ; set ; }
public int Port { get ; set ; }
public string ? UserName { get ; set ; }
public string ? Password { get ; set ; }
}
public class SendGridOptions
{
public string ? ApiKey { get ; set ; }
public string ? From { get ; set ; }
public string ? DisplayName { get ; set ; }
}
Registration
Program.cs (Services)
appsettings.json (SMTP Configuration)
appsettings.Production.json (SendGrid)
using FSH . Framework . Mailing ;
builder . Services . AddHeroMailing ();
// Or via platform registration
builder . AddHeroPlatform ( options =>
{
options . EnableMailing = true ; // Enables mailing
});
Usage Examples
Basic Email
SendWelcomeEmailHandler.cs
using FSH . Framework . Mailing ;
using FSH . Framework . Mailing . Services ;
using System . Collections . ObjectModel ;
public sealed class SendWelcomeEmailHandler : ICommandHandler < SendWelcomeEmailCommand >
{
private readonly IMailService _mailService ;
public SendWelcomeEmailHandler ( IMailService mailService )
{
_mailService = mailService ;
}
public async ValueTask < Unit > Handle ( SendWelcomeEmailCommand cmd , CancellationToken ct )
{
var mailRequest = new MailRequest (
to : new Collection < string > { cmd . Email },
subject : "Welcome to FSH!" ,
body : $"<h1>Welcome { cmd . Name } !</h1><p>Thanks for joining us.</p>"
);
await _mailService . SendAsync ( mailRequest , ct );
return Unit . Value ;
}
}
Email with Template
OrderConfirmationService.cs
using FSH . Framework . Mailing ;
using FSH . Framework . Mailing . Services ;
using System . Collections . ObjectModel ;
public sealed class OrderConfirmationService
{
private readonly IMailService _mailService ;
public async Task SendOrderConfirmationAsync (
string customerEmail ,
string orderNumber ,
decimal totalAmount ,
CancellationToken ct )
{
var htmlBody = $@"
<html>
<body>
<h1>Order Confirmation</h1>
<p>Thank you for your order!</p>
<p><strong>Order Number:</strong> { orderNumber } </p>
<p><strong>Total Amount:</strong> $ { totalAmount : N2 } </p>
<p>We'll send you another email when your order ships.</p>
</body>
</html>" ;
var mailRequest = new MailRequest (
to : new Collection < string > { customerEmail },
subject : $"Order Confirmation - { orderNumber } " ,
body : htmlBody
);
await _mailService . SendAsync ( mailRequest , ct );
}
}
Email with Attachments
using FSH . Framework . Mailing ;
using FSH . Framework . Mailing . Services ;
using System . Collections . ObjectModel ;
public sealed class InvoiceEmailService
{
private readonly IMailService _mailService ;
public async Task SendInvoiceAsync (
string customerEmail ,
string invoiceNumber ,
byte [] pdfBytes ,
CancellationToken ct )
{
var attachments = new Dictionary < string , byte []>
{
[ "invoice.pdf" ] = pdfBytes
};
var mailRequest = new MailRequest (
to : new Collection < string > { customerEmail },
subject : $"Invoice { invoiceNumber } " ,
body : $"<p>Please find your invoice attached.</p>" ,
attachmentData : attachments
);
await _mailService . SendAsync ( mailRequest , ct );
}
}
Email with CC and BCC
using FSH . Framework . Mailing ;
using FSH . Framework . Mailing . Services ;
using System . Collections . ObjectModel ;
public sealed class NotificationService
{
private readonly IMailService _mailService ;
public async Task NotifyTeamAsync (
string primaryEmail ,
string subject ,
string body ,
CancellationToken ct )
{
var mailRequest = new MailRequest (
to : new Collection < string > { primaryEmail },
subject : subject ,
body : body ,
cc : new Collection < string >
{
"[email protected] " ,
"[email protected] "
},
bcc : new Collection < string >
{
"[email protected] "
}
);
await _mailService . SendAsync ( mailRequest , ct );
}
}
Password Reset Email
SendPasswordResetEmailHandler.cs
using FSH . Framework . Mailing ;
using FSH . Framework . Mailing . Services ;
using System . Collections . ObjectModel ;
public sealed class SendPasswordResetEmailHandler
: ICommandHandler < SendPasswordResetEmailCommand >
{
private readonly IMailService _mailService ;
private readonly IConfiguration _configuration ;
public async ValueTask < Unit > Handle (
SendPasswordResetEmailCommand cmd ,
CancellationToken ct )
{
var appUrl = _configuration [ "AppUrl" ] ?? "https://localhost:5001" ;
var resetUrl = $" { appUrl } /reset-password?token= { cmd . Token } " ;
var htmlBody = $@"
<html>
<body>
<h2>Password Reset Request</h2>
<p>Click the link below to reset your password:</p>
<p><a href=' { resetUrl } '>Reset Password</a></p>
<p>This link will expire in 24 hours.</p>
<p>If you didn't request this, please ignore this email.</p>
</body>
</html>" ;
var mailRequest = new MailRequest (
to : new Collection < string > { cmd . Email },
subject : "Password Reset Request" ,
body : htmlBody
);
await _mailService . SendAsync ( mailRequest , ct );
return Unit . Value ;
}
}
Background Job Integration
Always send emails in background jobs to avoid blocking user requests.
using FSH . Framework . Jobs . Services ;
using FSH . Framework . Mailing ;
using FSH . Framework . Mailing . Services ;
public sealed class SendEmailJob
{
private readonly IMailService _mailService ;
public SendEmailJob ( IMailService mailService )
{
_mailService = mailService ;
}
public async Task ExecuteAsync ( MailRequest request , CancellationToken ct )
{
await _mailService . SendAsync ( request , ct );
}
}
// Enqueue from handler
public sealed class CreateOrderHandler : ICommandHandler < CreateOrderCommand , Guid >
{
private readonly IJobService _jobService ;
public async ValueTask < Guid > Handle ( CreateOrderCommand cmd , CancellationToken ct )
{
// ... create order logic
// Enqueue email job
var mailRequest = new MailRequest (
to : new Collection < string > { cmd . CustomerEmail },
subject : "Order Confirmation" ,
body : "Your order has been confirmed."
);
_jobService . Enqueue < SendEmailJob >( job => job . ExecuteAsync ( mailRequest , ct ));
return orderId ;
}
}
Provider Configuration
SMTP (Gmail)
Enable 2FA
Enable two-factor authentication on your Gmail account.
Generate App Password
Go to Google Account → Security → App Passwords and generate a password.
Configure appsettings.json
{
"MailOptions" : {
"UseSendGrid" : false ,
"From" : "[email protected] " ,
"DisplayName" : "Your App" ,
"Smtp" : {
"Host" : "smtp.gmail.com" ,
"Port" : 587 ,
"UserName" : "[email protected] " ,
"Password" : "your-16-char-app-password"
}
}
}
SMTP (Outlook)
{
"MailOptions" : {
"UseSendGrid" : false ,
"From" : "[email protected] " ,
"DisplayName" : "Your App" ,
"Smtp" : {
"Host" : "smtp-mail.outlook.com" ,
"Port" : 587 ,
"UserName" : "[email protected] " ,
"Password" : "your-password"
}
}
}
SendGrid
Generate API Key
Go to Settings → API Keys → Create API Key
Verify Sender Identity
Add and verify your sender email or domain.
Configure appsettings.json
{
"MailOptions" : {
"UseSendGrid" : true ,
"SendGrid" : {
"ApiKey" : "SG.xxxxxxxxxxxxxxxxxxxxx" ,
"From" : "[email protected] " ,
"DisplayName" : "Your Company"
}
}
}
Best Practices
Use Background Jobs
Always send emails asynchronously via background jobs to avoid blocking requests.
HTML Templates
Store email templates in separate files or use a templating engine like Razor.
Handle Failures Gracefully
Wrap email sending in try-catch and log failures. Don’t fail the entire operation if email fails.
Rate Limiting
Be aware of provider rate limits (e.g., SendGrid free tier: 100 emails/day).
Error Handling
using FSH . Framework . Mailing ;
using FSH . Framework . Mailing . Services ;
using Microsoft . Extensions . Logging ;
public sealed class SafeEmailService
{
private readonly IMailService _mailService ;
private readonly ILogger < SafeEmailService > _logger ;
public async Task < bool > TrySendAsync (
MailRequest request ,
CancellationToken ct )
{
try
{
await _mailService . SendAsync ( request , ct );
_logger . LogInformation ( "Email sent successfully to {Recipients}" ,
string . Join ( ", " , request . To ));
return true ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Failed to send email to {Recipients}" ,
string . Join ( ", " , request . To ));
return false ;
}
}
}
Testing
Local SMTP Server (Mailhog)
services :
mailhog :
image : mailhog/mailhog:latest
ports :
- "1025:1025" # SMTP
- "8025:8025" # Web UI
appsettings.Development.json
{
"MailOptions" : {
"UseSendGrid" : false ,
"From" : "dev@localhost" ,
"Smtp" : {
"Host" : "localhost" ,
"Port" : 1025 ,
"UserName" : "" ,
"Password" : ""
}
}
}
Access Mailhog UI at http://localhost:8025 to view sent emails.
Mock for Unit Tests
using FSH . Framework . Mailing . Services ;
using NSubstitute ;
public class EmailServiceTests
{
[ Fact ]
public async Task Should_Send_Welcome_Email ()
{
// Arrange
var mockMailService = Substitute . For < IMailService >();
var handler = new SendWelcomeEmailHandler ( mockMailService );
var command = new SendWelcomeEmailCommand ( "[email protected] " , "John" );
// Act
await handler . Handle ( command , CancellationToken . None );
// Assert
await mockMailService . Received ( 1 ). SendAsync (
Arg . Is < MailRequest >( r => r . To . Contains ( "[email protected] " )),
Arg . Any < CancellationToken >()
);
}
}
Troubleshooting
SMTP Authentication Failed
Verify username and password
Check if 2FA is enabled (Gmail requires app passwords)
Ensure “Less secure app access” is enabled (if applicable)
SendGrid 403 Forbidden
Verify API key is correct
Check sender email is verified in SendGrid
Ensure API key has “Mail Send” permission
Emails Going to Spam
Configure SPF, DKIM, and DMARC records for your domain
Use a verified sender domain
Avoid spammy subject lines and content
Package Reference
< ItemGroup >
< ProjectReference Include = "..\..\BuildingBlocks\Mailing\FSH.Framework.Mailing.csproj" />
</ ItemGroup >
Jobs Building Block Send emails via background jobs
SendGrid Docs Official SendGrid documentation
SMTP Testing Mailhog for local email testing
Email Best Practices Email design and deliverability tips