Provider-agnostic email library for ASP.NET Core with Razor templating, multi-provider support (SMTP, SendGrid, Mailgun), resilience policies, and OpenTelemetry metrics.
using Noundry.Sanquhar.Extensions;
var builder = WebApplication.CreateBuilder(args);
// SMTP (default)
builder.Services.AddSanquhar(builder.Configuration);
// Or with SendGrid
builder.Services.AddSanquhar(builder.Configuration, options =>
options.UseSendGrid(builder.Configuration));
// Or with Mailgun
builder.Services.AddSanquhar(builder.Configuration, options =>
options.UseMailgun(builder.Configuration));
{
"Sanquhar": {
"TemplatePath": "EmailTemplates",
"EnableTemplateCache": true,
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "user@example.com",
"Password": "your-password",
"EnableSsl": true
},
"Mail": {
"DefaultFrom": "noreply@example.com",
"DefaultFromName": "My Application"
}
}
}
public class UserService
{
private readonly IMailService _mailService;
public UserService(IMailService mailService) => _mailService = mailService;
public async Task SendWelcomeEmailAsync(string email, string username)
{
var model = new WelcomeEmailModel { Username = username, Email = email };
await _mailService.SendFromTemplateAsync("Welcome", model, email);
}
}
The primary service for sending emails. Inject via dependency injection.
public interface IMailService
{
// Create email from template (does NOT send)
Task<MailMessage> CreateFromTemplateAsync<TModel>(string templateName, TModel model);
// Send pre-built email
Task SendAsync(MailMessage message);
// Create from template AND send in one call
Task SendFromTemplateAsync<TModel>(string templateName, TModel model, params string[] recipients);
}
public class MailMessage
{
public string Subject { get; set; }
public string Body { get; set; } // Plain text
public string HtmlBody { get; set; } // HTML content
public List<string> To { get; set; }
public List<string> Cc { get; set; }
public List<string> Bcc { get; set; }
public string From { get; set; } // Auto-filled from config
public string? ReplyTo { get; set; }
public MailPriority Priority { get; set; }
public List<MailAttachment> Attachments { get; set; }
public Dictionary<string, string> Headers { get; set; }
}
Sanquhar uses Razor templates for email body, subject, and plain text content.
| File | Purpose | Required |
|---|---|---|
| {Name}.cshtml | HTML body content | Yes |
| {Name}_Subject.cshtml | Email subject line | No |
| {Name}_Text.cshtml | Plain text alternative | No |
// EmailTemplates/Welcome.cshtml
@model WelcomeEmailModel
<!DOCTYPE html>
<html>
<body>
<h1>Welcome, @Model.Username!</h1>
<p>Thank you for registering on @Model.RegistrationDate.ToString("D").</p>
</body>
</html>
// EmailTemplates/Welcome_Subject.cshtml
@model WelcomeEmailModel
Welcome to Our Service, @Model.Username!
await _mailService.SendFromTemplateAsync("Welcome", model, email);
var message = await _mailService.CreateFromTemplateAsync("Welcome", model);
message.To.Add(email);
message.Cc.Add("manager@example.com");
message.Priority = MailPriority.High;
await _mailService.SendAsync(message);
var message = new MailMessage { Subject = "Report", HtmlBody = "<p>See attached.</p>" };
message.To.Add(to);
message.Attachments.Add(new MailAttachment
{
FileName = "report.pdf",
Content = fileBytes,
ContentType = "application/pdf"
});
await _mailService.SendAsync(message);
| Feature | SMTP | SendGrid | Mailgun |
|---|---|---|---|
| Click Tracking | No | Yes | Yes |
| Open Tracking | No | Yes | Yes |
| Sandbox Mode | No | Yes | No |
| API-based | No | Yes | Yes |
Automatic retry with exponential backoff, circuit breaker (opens after 5 failures), and configurable timeout. Enabled via Sanquhar:Resilience config section.
In-memory template caching with configurable expiration and size limits. Manage via ITemplateCacheManager for invalidation and clearing.
RFC 5322 compliant email validation via IEmailValidator. Checks format, domain, length, and character validity.
Automatic metrics: emails sent/failed, send duration, templates rendered, and cache hit/miss counters.
Built-in SMTP health check for monitoring. Register via AddSanquharSmtpCheck().
Detailed event IDs for send start/success/fail, template rendering, and validation events with correlation IDs.
{
"Sanquhar": {
"Resilience": {
"Enabled": true,
"MaxRetryAttempts": 3,
"CircuitBreakerThreshold": 5,
"CircuitBreakerBreakDurationSeconds": 30,
"TimeoutSeconds": 30
}
}
}
try
{
if (!_validator.IsValid(to))
return EmailResult.Failed("Invalid email address");
await _mailService.SendFromTemplateAsync(template, model, to);
return EmailResult.Success();
}
catch (FileNotFoundException ex)
{
_logger.LogError(ex, "Template {Template} not found", template);
return EmailResult.Failed($"Template not found");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send email to {To}", to);
return EmailResult.Failed(ex.Message);
}