Notification Recipes — Workflow, Authz, Encryption
These recipes show how to combine Granit.Notifications with other framework modules for common real-world scenarios.
Workflow + Notifications: notify on state transition
Section titled “Workflow + Notifications: notify on state transition”When an entity transitions in a workflow, publish a notification automatically.
The WorkflowStateChangedEvent is published by Wolverine — handle it in a
notification handler:
A notification is declared once as a NotificationType<TData> singleton, then
published against it — the publisher is generic over the payload, so the data
shape is checked at compile time:
// Declared once, next to the module that owns the notification.public sealed class InvoiceApprovalRequiredNotificationType : NotificationType<InvoiceApprovalRequiredData>{ public static readonly InvoiceApprovalRequiredNotificationType Instance = new();
public override string Name => "invoicing.approval_requested"; public override NotificationSeverity DefaultSeverity => NotificationSeverity.Warning; public override IReadOnlyList<string> DefaultChannels { get; } = [NotificationChannels.InApp, NotificationChannels.Email];}
public sealed record InvoiceApprovalRequiredData(string InvoiceNumber);
public static class InvoiceApprovalNotificationHandler{ public static async Task Handle( WorkflowStateChangedEvent @event, IInvoiceReader invoices, INotificationPublisher publisher, CancellationToken ct) { if (@event.NewState != nameof(InvoiceStatus.PendingApproval)) return;
Invoice invoice = await invoices.GetAsync(@event.EntityId, ct);
await publisher.PublishAsync( InvoiceApprovalRequiredNotificationType.Instance, new InvoiceApprovalRequiredData(invoice.Number), recipientUserIds: [invoice.ApproverId], relatedEntity: new EntityReference(@event.EntityType, @event.EntityId), cancellationToken: ct); }}WorkflowStateChangedEvent carries EntityType, EntityId, PreviousState,
NewState and TransitionedBy — it has no free-form property bag, so resolve
anything else from your own store, as above.
Authorization + Notifications: permission-gated channels
Section titled “Authorization + Notifications: permission-gated channels”Use IPermissionChecker to restrict notification channels based on the sender’s
permissions (e.g., only managers can trigger SMS notifications):
INotificationDeliveryGate is consulted per recipient and per channel — the
gate sees the user, the notification type, the channel, and the tenant, and
returns whether that specific delivery should go out.
public sealed class SmsPermissionGate(IPermissionChecker permissionChecker) : INotificationDeliveryGate{ public async Task<bool> ShouldDeliverAsync( string userId, string notificationTypeName, string channelName, Guid? tenantId, CancellationToken cancellationToken) { if (!string.Equals(channelName, "Sms", StringComparison.Ordinal)) return true;
return await permissionChecker .IsGrantedAsync("Notifications.Sms.Send") .ConfigureAwait(false); }}Encryption + Notifications: protect PII in notification payloads
Section titled “Encryption + Notifications: protect PII in notification payloads”The InApp channel persists UserNotification.Data — a JsonElement payload —
in the notifications database. There is no payload-mutation hook on the publish
path, so the rule is do not put PII in the payload. Send identifiers and let
the renderer resolve display values at render time:
// Publish an identifier, not the name.await publisher.PublishAsync( NotificationTypes.AppointmentConfirmed, new AppointmentConfirmedData(AppointmentId: appointment.Id), recipientUserIds: [appointment.PatientUserId], cancellationToken);When a column genuinely must hold PII, encrypt it at rest on your own entity
with [Encrypted] — ApplyEncryptionConventions wires the EF Core value
converter through IStringEncryptionService:
public sealed class Patient : AggregateRoot{ [Encrypted] public string FullName { get; private set; } = string.Empty;}NotificationsDbContext already applies the same convention to its own
encrypted columns — see Encryption.
Identity + Notifications: zero-boilerplate recipient resolution
Section titled “Identity + Notifications: zero-boilerplate recipient resolution”Apps on Granit.Identity get a deliverable pipeline without writing an
IRecipientResolver. The opt-in Granit.Identity.Notifications bridge is the default
path: it resolves recipient contact details straight from the identity layer — local
(OpenIddict) and every federated provider (Keycloak, Entra ID, Cognito, Google)
through a single adapter.
[DependsOn(typeof(GranitIdentityNotificationsModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGranitIdentityRecipientResolver(); }}It registers with TryAddScoped, so a custom IRecipientResolver still wins if you
later need one. See
Channels → ready-made resolver
for the field mapping and the Identity:RecipientResolver options.
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Module | GranitNotificationsModule, GranitNotificationsEntityFrameworkCoreModule, GranitNotificationsWolverineModule | — |
| Publisher | INotificationPublisher, NotificationType<TData> | Granit.Notifications |
| Channels | INotificationChannel, NotificationChannels, NotificationDeliveryContext | Granit.Notifications |
| Definitions | NotificationDefinition, INotificationDefinitionProvider, INotificationDefinitionContext, INotificationDefinitionStore | Granit.Notifications |
| Entities | UserNotification, NotificationDeliveryAttempt, NotificationPreference, NotificationSubscription, UserNotificationState | Granit.Notifications |
| CQRS | IUserNotificationReader, IUserNotificationWriter, INotificationPreferenceReader, INotificationPreferenceWriter, INotificationSubscriptionReader, INotificationSubscriptionWriter, INotificationDeliveryWriter | Granit.Notifications |
| Recipient | IRecipientResolver, RecipientInfo | Granit.Notifications |
| Recipient resolver (Identity bridge — default path) | GranitIdentityNotificationsModule, IdentityRecipientResolverOptions, AddGranitIdentityRecipientResolver() | Granit.Identity.Notifications |
| Entity tracking | ITrackedEntity, TrackedPropertyConfig, EntityStateChangedData, EntityReference | Granit.Notifications |
| Messages | NotificationTrigger, DeliverNotificationCommand | Granit.Notifications |
| Handlers | NotificationFanoutHandler, NotificationDeliveryHandler | Granit.Notifications |
| Options | NotificationsOptions, EmailChannelOptions, SmtpOptions, BrevoOptions, ScalewayEmailOptions, SendGridEmailOptions, AcsEmailOptions, SmsChannelOptions, AcsSmsOptions, AwsSnsSmsOptions, TwilioOptions, MobilePushChannelOptions, GoogleFcmOptions, AzureNotificationHubsOptions, AwsSnsMobilePushOptions, SignalRChannelOptions, WebPushChannelOptions, SseChannelOptions, ZulipChannelOptions, ZulipBotOptions | various |
IEmailSender, EmailMessage | Granit.Notifications.Email, Granit.Notifications.AzureCommunicationServices, Granit.Notifications.Scaleway, Granit.Notifications.SendGrid | |
| SMS | ISmsSender, SmsMessage | Granit.Notifications.Sms, Granit.Notifications.AwsSns, Granit.Notifications.Twilio |
IWhatsAppSender, WhatsAppMessage | Granit.Notifications.WhatsApp, Granit.Notifications.Twilio | |
| Mobile Push | IMobilePushSender, MobilePushMessage, IMobilePushTokenReader, IMobilePushTokenWriter, MobilePushTokenInfo, MobilePlatform | Granit.Notifications.MobilePush, Granit.Notifications.AwsSns |
| SignalR | NotificationHub, SignalRNotificationMessage | Granit.Notifications.SignalR |
| Web Push | IWebPushSubscriptionReader, IWebPushSubscriptionWriter, WebPushSubscriptionInfo | Granit.Notifications.WebPush |
| SSE | ISseConnectionManager, SseConnection, SseNotificationMessage | Granit.Notifications.Sse |
| Zulip | IZulipSender, ZulipMessage | Granit.Notifications.Zulip |
| Exceptions | NotificationDeliveryException | Granit.Notifications |
| Endpoints | NotificationsEndpointsOptions, MobilePushTokenEndpoints | Granit.Notifications.Endpoints |
| Extensions | AddGranitNotifications(), AddGranitNotificationsEntityFrameworkCore(), AddGranitNotificationsEmail(), AddGranitNotificationsSmtp(), AddGranitNotificationsAcsEmail(), AddGranitNotificationsAcsSms(), AddGranitNotificationsScaleway(), AddGranitNotificationsSendGrid(), AddGranitNotificationsBrevo(), AddGranitNotificationsAwsSes(), AddGranitNotificationsSms(), AddGranitNotificationsAwsSnsSms(), AddGranitNotificationsTwilio(), AddGranitNotificationsWhatsApp(), AddGranitNotificationsMobilePush(), AddGranitNotificationsGoogleFcm(), AddGranitNotificationsAzureNotificationHubs(), AddGranitNotificationsAwsSnsMobilePush(), AddGranitNotificationsSignalR(), AddGranitNotificationsWebPush(), AddGranitNotificationsSse(), AddGranitNotificationsSseRedisBackplane(), AddGranitNotificationsZulip(), AddNotificationDefinitions<T>(), MapGranitNotifications(), MapGranitMobilePushTokens() | various |
See also
Section titled “See also”- Overview: Package structure, dependency graph, and setup
- Fan-Out Engine: Dispatch mechanics and INotificationPublisher
- Channels: Channel abstraction and custom channel implementation
- Email Channel: Email providers, plain text generation, MJML
- SMS, Push & Real-Time: SMS, WhatsApp, FCM, SignalR, SSE, Web Push
- Data Model: Entities, CQRS stores, preferences, entity tracking
- Endpoints: REST API for inbox, preferences, subscriptions
- Wolverine Integration: Durable outbox and retry
- Configuration: Provider options, health checks, telemetry
- Set up notifications guide — step-by-step configuration walkthrough
- Templating module — Scriban engine, MJML, post-render transformers
- Wolverine module — Durable messaging, transactional outbox
- Persistence module —
AuditedEntity,Entity, interceptors - Identity module — User lookup for
IRecipientResolverimplementation - Templating module — Scriban templates for email/SMS content
- Workflow module — FSM engine with transition events
- Core module —
Entity,AuditedEntity,IMultiTenant - API Reference (auto-generated from XML docs)