Notification Channels — Email, SMS, Push, Realtime
Every delivery channel implements INotificationChannel. The engine resolves all registered channels and routes delivery commands by name. See email-specific details and SMS, push, and real-time channels for provider configuration.
INotificationChannel
Section titled “INotificationChannel”Every delivery channel implements this interface. The engine resolves all
registered INotificationChannel services and routes by Name:
public interface INotificationChannel{ string Name { get; } Task SendAsync(NotificationDeliveryContext context, CancellationToken cancellationToken = default);}Channels not registered are silently skipped with a warning log (graceful degradation pattern).
Well-known channels
Section titled “Well-known channels”public static class NotificationChannels{ public const string InApp = "InApp"; public const string SignalR = "SignalR"; public const string Email = "Email"; public const string Sms = "Sms"; public const string WhatsApp = "WhatsApp"; public const string Push = "Push"; // W3C Web Push public const string MobilePush = "MobilePush"; public const string Sse = "Sse"; public const string Zulip = "Zulip";}Channel registration
Section titled “Channel registration”| Channel | Registration | Provider resolution |
|---------|-------------|---------------------|
| InApp | Built-in (auto-registered) | N/A |
| Email | AddGranitNotificationsEmail() | Keyed Service: "Smtp", "Brevo", "AzureCommunicationServices", "Scaleway", "SendGrid" |
| SMS | AddGranitNotificationsSms() | Keyed Service: "Brevo", "AzureCommunicationServices", "AwsSns", "Twilio" |
| WhatsApp | AddGranitNotificationsWhatsApp() | Keyed Service: "Brevo", "Twilio" |
| Mobile Push | AddGranitNotificationsMobilePush() | Keyed Service: "GoogleFcm", "AzureNotificationHubs", "AwsSns" |
| SignalR | AddGranitNotificationsSignalR() | Direct (NotificationHub) |
| Web Push | AddGranitNotificationsPush() | VAPID (Lib.Net.Http.WebPush) |
| SSE | AddGranitNotificationsSse() | Native .NET 10 SSE |
| Zulip | AddGranitNotificationsZulip() | Zulip Bot API |
Implementing a custom channel
Section titled “Implementing a custom channel”public class TeamsNotificationChannel(IRecipientResolver resolver) : INotificationChannel{ public string Name => "Teams";
public async Task SendAsync( NotificationDeliveryContext context, CancellationToken cancellationToken = default) { RecipientInfo? recipient = await resolver .ResolveAsync(context.RecipientUserId, cancellationToken) .ConfigureAwait(false); if (recipient is null) return;
// Send via Microsoft Graph API... }}
// Registerservices.AddSingleton<INotificationChannel, TeamsNotificationChannel>();IRecipientResolver
Section titled “IRecipientResolver”This interface resolves contact information from user identifiers. By default the
application implements it — each application knows its own user model. Apps built on
Granit.Identity can instead opt into the ready-made resolver from
Granit.Identity.Notifications.
public interface IRecipientResolver{ Task<RecipientInfo?> ResolveAsync( string userId, CancellationToken cancellationToken = default);}public sealed record RecipientInfo{ public required string UserId { get; init; } public string? Email { get; init; } public string? PhoneNumber { get; init; } // E.164 format public string? PreferredCulture { get; init; } // BCP 47 public string? DisplayName { get; init; }}Ready-made resolver: Granit.Identity.Notifications
Section titled “Ready-made resolver: Granit.Identity.Notifications”Apps on Granit.Identity no longer have to hand-roll a resolver. The opt-in
Granit.Identity.Notifications package ships IdentityRecipientResolver, a single
adapter over IIdentityUserReader — the unified identity read abstraction — that
covers local (OpenIddict) and every federated provider (Keycloak, Entra ID,
Cognito, Google) with no per-provider code. In federated mode the reader already
resolves through the user cache,
so there is no extra round-trip to the external IdP per notification.
Reference the module and register the resolver:
[DependsOn(typeof(GranitIdentityNotificationsModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGranitIdentityRecipientResolver(); }}The resolver registers with TryAddScoped, so an application-supplied
IRecipientResolver always takes precedence — adopting the bridge never blocks a
later custom override.
Field mapping
Section titled “Field mapping”| RecipientInfo field | Source |
|-----------------------|--------|
| Email | The identity user’s Email |
| DisplayName | Canonical User.DisplayName; otherwise FirstName + LastName, falling back to Username |
| PhoneNumber | Canonical User.PhoneNumber when present; otherwise the first non-empty provider metadata key |
| PreferredCulture | Canonical User.PreferredLocale when present; otherwise the first non-empty metadata key, then DefaultCulture |
When the identity backend returns the canonical
User aggregate (ADR-051),
its first-class PhoneNumber / PreferredLocale win. Otherwise the resolver probes a
configurable, ordered list of provider metadata keys and takes the first non-empty
value.
Configuration
Section titled “Configuration”Bound to the Identity:RecipientResolver section via IdentityRecipientResolverOptions:
| Setting | Default | Purpose |
|---------|---------|---------|
| PhoneNumberMetadataKeys | ["phoneNumber", "phone_number"] | Metadata keys probed in order for the phone number (Keycloak / Cognito) |
| PreferredCultureMetadataKeys | ["locale", "preferredLanguage"] | Metadata keys probed in order for the BCP-47 culture (Keycloak / Cognito / Graph) |
| DefaultCulture | null | Culture applied when none is resolved; null leaves it to the pipeline |
{ "Identity": { "RecipientResolver": { "PhoneNumberMetadataKeys": ["phoneNumber", "phone_number"], "PreferredCultureMetadataKeys": ["locale", "preferredLanguage"], "DefaultCulture": null } }}See also
Section titled “See also”- Overview — fan-out engine, channels, dependency graph
- Fan-out engine — dispatch mechanics and
INotificationPublisher - Email channel — SMTP, SendGrid, Azure, Scaleway, Brevo provider configuration
- SMS, push & real-time — WhatsApp, FCM, SignalR, SSE, Web Push, Zulip
- Data model — entities, CQRS stores, preferences
- Configuration — options and health checks
- Identity module — user lookup for
IRecipientResolverimplementations