Noundry.Sanquhar

Provider-agnostic email library for ASP.NET Core with Razor templating, multi-provider support (SMTP, SendGrid, Mailgun), resilience policies, and OpenTelemetry metrics.

v1.0.1 .NET 8.0 / 9.0 / 10.0 MIT License
# Install via NuGet
$ dotnet add package Noundry.Sanquhar
# Optional providers
$ dotnet add package Noundry.Sanquhar.SendGrid
$ dotnet add package Noundry.Sanquhar.Mailgun

Quick Start

1. Register Services

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));

2. Configure appsettings.json

{
 "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"
 }
 }
}

3. Send an Email

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);
 }
}

Core API

IMailService

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);
}

MailMessage Model

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; }
}

Template System

Sanquhar uses Razor templates for email body, subject, and plain text content.

File Naming Convention

FilePurposeRequired
{Name}.cshtmlHTML body contentYes
{Name}_Subject.cshtmlEmail subject lineNo
{Name}_Text.cshtmlPlain text alternativeNo

Example Template

// 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!

Sending Patterns

One-liner with Template

await _mailService.SendFromTemplateAsync("Welcome", model, email);

Two-step with Customization

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);

With Attachments

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);

Provider Comparison

FeatureSMTPSendGridMailgun
Click TrackingNoYesYes
Open TrackingNoYesYes
Sandbox ModeNoYesNo
API-basedNoYesYes

Advanced Features

Resilience Policies (Polly)

Automatic retry with exponential backoff, circuit breaker (opens after 5 failures), and configurable timeout. Enabled via Sanquhar:Resilience config section.

Template Caching

In-memory template caching with configurable expiration and size limits. Manage via ITemplateCacheManager for invalidation and clearing.

Email Validation

RFC 5322 compliant email validation via IEmailValidator. Checks format, domain, length, and character validity.

OpenTelemetry Metrics

Automatic metrics: emails sent/failed, send duration, templates rendered, and cache hit/miss counters.

Health Checks

Built-in SMTP health check for monitoring. Register via AddSanquharSmtpCheck().

Structured Logging

Detailed event IDs for send start/success/fail, template rendering, and validation events with correlation IDs.

Resilience Configuration

{
 "Sanquhar": {
 "Resilience": {
 "Enabled": true,
 "MaxRetryAttempts": 3,
 "CircuitBreakerThreshold": 5,
 "CircuitBreakerBreakDurationSeconds": 30,
 "TimeoutSeconds": 30
 }
 }
}

Error Handling

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);
}

Related Packages