Problem
TemplatedEmailSender.SendSafeAsync swallows SMTP failures and logs them - the email is silently lost. For transactional emails (password reset, email verification, invitation), this means users see no error but never receive the email, with no automatic recovery.
Proposed Solution
Leverage the existing Hangfire infrastructure as a delivery queue. Enqueue email sends as background jobs to get automatic retries with exponential backoff - no custom outbox table needed.
Approach:
- Add
BackgroundEmailService : IEmailService that wraps sends in backgroundJobClient.Enqueue<SmtpEmailService>(...) instead of sending synchronously
SmtpEmailService stays as-is (pure SMTP send, used as the job executor)
- Hangfire provides retry with configurable attempts and backoff (e.g.
[AutomaticRetry(Attempts = 5)])
- Failed emails visible in the Hangfire dashboard as failed jobs (manual retry possible)
- DI registration: when both
Email:Enabled and JobScheduling:Enabled are true, register BackgroundEmailService; otherwise fall back to SmtpEmailService (direct) or NoOpEmailService (disabled)
Benefits over a custom outbox:
- Zero new infrastructure - Hangfire's PostgreSQL storage is the outbox
- Retry, backoff, dead-letter, and dashboard visibility already built in
- Consistent with existing job patterns in the codebase
Affected Areas
MyProject.Infrastructure/Features/Email/Services/ - new BackgroundEmailService
MyProject.Infrastructure/Features/Email/Extensions/ServiceCollectionExtensions.cs - conditional registration
MyProject.Component.Tests/Services/ - BackgroundEmailServiceTests
Problem
TemplatedEmailSender.SendSafeAsyncswallows SMTP failures and logs them - the email is silently lost. For transactional emails (password reset, email verification, invitation), this means users see no error but never receive the email, with no automatic recovery.Proposed Solution
Leverage the existing Hangfire infrastructure as a delivery queue. Enqueue email sends as background jobs to get automatic retries with exponential backoff - no custom outbox table needed.
Approach:
BackgroundEmailService : IEmailServicethat wraps sends inbackgroundJobClient.Enqueue<SmtpEmailService>(...)instead of sending synchronouslySmtpEmailServicestays as-is (pure SMTP send, used as the job executor)[AutomaticRetry(Attempts = 5)])Email:EnabledandJobScheduling:Enabledare true, registerBackgroundEmailService; otherwise fall back toSmtpEmailService(direct) orNoOpEmailService(disabled)Benefits over a custom outbox:
Affected Areas
MyProject.Infrastructure/Features/Email/Services/- newBackgroundEmailServiceMyProject.Infrastructure/Features/Email/Extensions/ServiceCollectionExtensions.cs- conditional registrationMyProject.Component.Tests/Services/-BackgroundEmailServiceTests