Notifications Feature Documentation¶
Send and queue application notifications through transport-agnostic contracts with clear delivery boundaries.
Overview¶
The Notifications feature provides an application-level abstraction for sending and queueing notification messages, with the current built-in focus on email. It separates the notification contract from the delivery mechanism so application code can work with INotificationService<TMessage> instead of directly depending on SMTP or storage concerns.
At the core, Application.Notifications defines:
INotificationMessageas the message contractINotificationService<TMessage>as the send and queue APIEmailMessageas the built-in email notification modelINotificationStorageProvideras the persistence abstraction for queued notifications
The feature supports two main operating modes:
- direct sending through an SMTP client
- queued and outbox-style sending through a storage provider plus background worker
Key Capabilities¶
- Email-focused notification model with recipients, headers, priority, reply-to, and attachments
- Real SMTP delivery through MailKit
- Fake SMTP delivery for tests and local verification
- In-memory storage provider by default when no persistent provider is configured
- Optional outbox processing with hosted background delivery
- Immediate processing mode that can wake the outbox worker as soon as a message is queued
- Consistent
Result-based success and failure handling
Core Types¶
Notification Contracts¶
INotificationMessage: minimal message contract with anIdINotificationService<TMessage>: exposesSendAsync(...)andQueueAsync(...)INotificationStorageProvider: persists pending notifications and retrieves batches for processing
Email Model¶
EmailMessage is the built-in notification type and contains:
To,CC, andBCCFromandReplyToSubjectandBodyIsHtmlHeadersAttachmentsPriorityStatusRetryCountCreatedAtandSentAt- a flexible
Propertiesbag for outbox metadata and processing details
Attachments are represented by EmailAttachment and can be regular or embedded attachments.
Basic Setup¶
The base registration entry point is AddNotificationService<TMessage>(...).
using BridgingIT.DevKit.Application.Notifications;
services.AddNotificationService<EmailMessage>(builder.Configuration, o =>
{
o.WithSmtpClient()
.WithSmtpSettings(s =>
{
s.Host = "smtp.example.com";
s.Port = 587;
s.UseSsl = true;
s.Username = "smtp-user";
s.Password = "smtp-password";
s.SenderAddress = "noreply@example.com";
s.SenderName = "Example App";
})
.WithTimeout(TimeSpan.FromSeconds(30));
});
If no storage provider is registered, the feature falls back to the in-memory notification storage provider.
Fake SMTP¶
For local verification or tests, the feature can use FakeSmtpClient instead of a real SMTP server.
services.AddNotificationService<EmailMessage>(builder.Configuration, o =>
{
o.WithFakeSmtpClient()
.WithInMemoryStorageProvider();
});
FakeSmtpClient implements MailKit's ISmtpClient but logs activity instead of delivering mail to a real server. That makes it useful for integration-style tests and local debugging of email flows.
Sending And Queueing¶
Direct Send¶
When outbox processing is not configured, SendAsync(...) sends the email immediately through the configured SMTP client.
public sealed class WelcomeService(INotificationService<EmailMessage> notifications)
{
public async Task SendWelcomeAsync(string email, CancellationToken cancellationToken)
{
var message = new EmailMessage
{
Id = Guid.NewGuid(),
Subject = "Welcome",
Body = "Your account is ready.",
To = [email],
IsHtml = false
};
var result = await notifications.SendAsync(
message,
new NotificationSendOptions { SendImmediately = true },
cancellationToken);
if (result.IsFailure)
{
// inspect result.Errors
}
}
}
Queueing¶
QueueAsync(...) is intended for outbox-backed processing. Without an outbox configuration, queueing does not persist work and only logs a warning. In practice, that means QueueAsync(...) should be used together with a persistent storage provider and WithOutbox<TContext>(...).
Outbox Processing¶
The outbox integration is added by the infrastructure package and turns queued notifications into a background delivery pipeline.
using BridgingIT.DevKit.Application.Notifications;
services.AddNotificationService<EmailMessage>(builder.Configuration, o =>
{
o.WithSmtpClient()
.WithEntityFrameworkStorageProvider<AppDbContext>()
.WithOutbox<AppDbContext>(c => c
.StartupDelay(TimeSpan.FromSeconds(10))
.ProcessingInterval(TimeSpan.FromSeconds(30))
.LeaseDuration(TimeSpan.FromMinutes(5))
.ProcessingMode(OutboxNotificationEmailProcessingMode.Interval)
.ProcessingCount(100)
.RetryCount(3));
});
Once outbox processing is enabled:
- new messages are saved through
INotificationStorageProvider OutboxNotificationEmailServiceruns as a hosted background serviceOutboxNotificationEmailWorkerclaims pending messages in batches by taking a time-bounded lease in storage- each message is sent through
INotificationService<EmailMessage> - status, retry metadata, and lease state are updated after processing
Two processing styles are supported:
Interval: the hosted worker polls on the configured intervalImmediate: queueing can trigger worker processing immediately throughIOutboxNotificationEmailQueue
Storage Providers¶
The notification feature depends on INotificationStorageProvider for queued delivery.
Available providers include:
InMemoryNotificationStorageProviderfor tests, demos, and ephemeral processing-
Entity Framework provider registration from infrastructure for persistent outbox storage
-
EntityFrameworkNotificationStorageProviderfor Entity Framework Core-based persistence, typically used withWithOutbox<TContext>(...) EntityFrameworkNotificationEmailStorageProvider<TContext>for Entity Framework Core-based persistence, typically used withWithOutbox<TContext>(...)
The storage abstraction is intentionally small:
SaveAsync(...)UpdateAsync(...)DeleteAsync(...)GetPendingAsync(...)
That keeps the application layer focused on notification workflows while letting infrastructure choose how messages are stored.
Entity Framework Provider¶
The Entity Framework provider stores queued emails in __Notifications_Emails and attachments in __Notifications_EmailAttachments.
For outbox polling, it is hardened for shared-database and multi-node deployments:
- pending rows are claimed, not just read
- each claim writes
Status=Locked,LockedBy,LockedUntil, and a new provider-neutralConcurrencyVersion - only the worker instance that owns the current lease can persist the final status change for that claimed message
- expired leases can be taken over by another node, so a crashed worker does not strand mail forever
- failed rows remain eligible for retry until
RetryCountreaches the configuredOutboxNotificationEmailOptions.RetryCount
For higher-volume outbox usage, the provider uses a composite polling index on (Status, LockedUntil, CreatedAt) and keeps attachments in a separate table so the hot polling query does not need attachment payload columns.
Because the lease and concurrency columns are part of the EF entity model, consuming applications must add and apply their own migration after upgrading the package.
Delivery Flow¶
sequenceDiagram
participant App as Application Service
participant Notifications as INotificationService<EmailMessage>
participant Storage as INotificationStorageProvider
participant Worker as OutboxNotificationEmailWorker
participant Smtp as ISmtpClient
alt direct send
App->>Notifications: SendAsync(message, SendImmediately=true)
Notifications->>Smtp: Connect / Authenticate / Send / Disconnect
Smtp-->>Notifications: success or failure
else outbox
App->>Notifications: QueueAsync(message)
Notifications->>Storage: Save pending message
Worker->>Storage: GetPendingAsync(batchSize)
Worker->>Notifications: SendAsync(message, SendImmediately=true)
Notifications->>Smtp: Send email
Notifications->>Storage: Update status and processing info
end
Operational Endpoints¶
Presentation.Web.Notifications exposes an operational REST surface for the persisted email outbox. These endpoints are intended for dashboards and support tooling rather than end-user mail composition.
Register them from the fluent notification builder:
services.AddNotificationService<EmailMessage>(builder.Configuration, o =>
{
o.WithEntityFrameworkStorageProvider<AppDbContext>()
.WithOutbox<AppDbContext>()
.AddEndpoints(options => options
.RequireAuthorization()
.GroupPath("/api/_system/notifications/emails"));
});
The endpoint group exposes:
GET /api/_system/notifications/emailsto list persisted emails with filters such asstatus,subject,lockedBy, andtakeGET /api/_system/notifications/emails/statsto retrieve aggregate outbox countsGET /api/_system/notifications/emails/{id}to inspect one emailGET /api/_system/notifications/emails/{id}/contentto fetch the stored bodyPOST /api/_system/notifications/emails/{id}/retryto reset a failed row back to pendingDELETE /api/_system/notifications/emails/{id}to delete one rowDELETE /api/_system/notifications/emailsto purge rows by age and status
For typed operational access inside the application layer, use INotificationEmailOutboxService.
Configuration Notes¶
NotificationServiceOptions groups:
SmtpSettingsOutboxOptionsTimeoutIsOutboxConfigured
OutboxNotificationEmailOptions controls background behavior such as:
StartupDelayProcessingIntervalLeaseDurationProcessingDelayProcessingJitterProcessingModeProcessingCountRetryCountPurgeOnStartupPurgeProcessedOnStartup
Best Practices¶
- Use
SendAsync(...)for simple, synchronous notification flows. - Use
QueueAsync(...)only when an outbox storage provider is configured. - Prefer a persistent provider plus
WithOutbox<TContext>(...)for important business notifications. - Use
FakeSmtpClientin tests and local environments where you want to inspect behavior without real delivery. - Treat handlers and application services as responsible for building
EmailMessagecontent, while the notification feature owns transport and persistence. - Keep attachments modest in size and be deliberate about when you embed binary payloads in queued messages.