skills/infra/email-notifications/SKILL.md
Use when implementing email sending with templates, SendGrid, or SES integration.
npx skillsauth add faysilalshareef/dotnet-ai-kit email-notificationsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
IEmailService abstracts email sending from provider implementationnamespace {Company}.{Domain}.Application.Interfaces;
public interface IEmailService
{
Task SendAsync(EmailMessage message, CancellationToken ct = default);
Task SendTemplatedAsync(string templateName, object model,
string toEmail, string? toName = null, CancellationToken ct = default);
}
public sealed record EmailMessage(
string ToEmail,
string ToName,
string Subject,
string HtmlBody,
string? PlainTextBody = null);
namespace {Company}.{Domain}.Infrastructure.Email;
public sealed class SendGridEmailService(
IOptions<EmailOptions> options,
ILogger<SendGridEmailService> logger) : IEmailService
{
private readonly SendGridClient _client = new(options.Value.ApiKey);
public async Task SendAsync(EmailMessage message, CancellationToken ct)
{
var msg = new SendGridMessage
{
From = new EmailAddress(options.Value.FromEmail, options.Value.FromName),
Subject = message.Subject,
HtmlContent = message.HtmlBody,
PlainTextContent = message.PlainTextBody
};
msg.AddTo(new EmailAddress(message.ToEmail, message.ToName));
var response = await _client.SendEmailAsync(msg, ct);
if (!response.IsSuccessStatusCode)
{
var body = await response.Body.ReadAsStringAsync(ct);
logger.LogError("SendGrid error: {Status} {Body}",
response.StatusCode, body);
}
}
public async Task SendTemplatedAsync(string templateName, object model,
string toEmail, string? toName, CancellationToken ct)
{
var template = await LoadTemplateAsync(templateName);
var html = RenderTemplate(template, model);
var subject = ExtractSubject(template, model);
await SendAsync(new EmailMessage(toEmail, toName ?? "", subject, html), ct);
}
private static string RenderTemplate(string template, object model)
{
var result = template;
foreach (var prop in model.GetType().GetProperties())
{
var value = prop.GetValue(model)?.ToString() ?? "";
result = result.Replace($"{{{{{prop.Name}}}}}", value);
}
return result;
}
}
public sealed class EmailOptions
{
public const string SectionName = "Email";
[Required]
public string Provider { get; init; } = "SendGrid"; // SendGrid, SES, SMTP
[Required]
public string ApiKey { get; init; } = string.Empty;
[Required, EmailAddress]
public string FromEmail { get; init; } = string.Empty;
public string FromName { get; init; } = "{Company}";
}
templates/
order-confirmation.html
welcome.html
password-reset.html
<!-- templates/order-confirmation.html -->
<html>
<body>
<h1>Order Confirmation</h1>
<p>Dear {{CustomerName}},</p>
<p>Your order #{{OrderNumber}} has been confirmed.</p>
<p>Total: {{Total}}</p>
</body>
</html>
services.AddOptions<EmailOptions>()
.BindConfiguration(EmailOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddScoped<IEmailService, SendGridEmailService>();
| Anti-Pattern | Correct Approach | |---|---| | Direct SendGrid calls in handlers | Use IEmailService abstraction | | Synchronous email sending in request | Fire-and-forget via background job | | Hardcoded email content | Use HTML templates with placeholders | | Missing error handling | Log failures, don't throw to caller |
# Find email service
grep -r "IEmailService\|EmailService" --include="*.cs" src/
# Find SendGrid/SES
grep -r "SendGrid\|SES\|SmtpClient" --include="*.cs" src/
# Find email templates
find . -name "*.html" -path "*/templates/*"
IEmailService — extend, don't replacedata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.