Skip to content

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.

CategoryKey typesPackage
ModuleGranitNotificationsModule, GranitNotificationsEntityFrameworkCoreModule, GranitNotificationsWolverineModule
PublisherINotificationPublisher, NotificationType<TData>Granit.Notifications
ChannelsINotificationChannel, NotificationChannels, NotificationDeliveryContextGranit.Notifications
DefinitionsNotificationDefinition, INotificationDefinitionProvider, INotificationDefinitionContext, INotificationDefinitionStoreGranit.Notifications
EntitiesUserNotification, NotificationDeliveryAttempt, NotificationPreference, NotificationSubscription, UserNotificationStateGranit.Notifications
CQRSIUserNotificationReader, IUserNotificationWriter, INotificationPreferenceReader, INotificationPreferenceWriter, INotificationSubscriptionReader, INotificationSubscriptionWriter, INotificationDeliveryWriterGranit.Notifications
RecipientIRecipientResolver, RecipientInfoGranit.Notifications
Recipient resolver (Identity bridge — default path)GranitIdentityNotificationsModule, IdentityRecipientResolverOptions, AddGranitIdentityRecipientResolver()Granit.Identity.Notifications
Entity trackingITrackedEntity, TrackedPropertyConfig, EntityStateChangedData, EntityReferenceGranit.Notifications
MessagesNotificationTrigger, DeliverNotificationCommandGranit.Notifications
HandlersNotificationFanoutHandler, NotificationDeliveryHandlerGranit.Notifications
OptionsNotificationsOptions, EmailChannelOptions, SmtpOptions, BrevoOptions, ScalewayEmailOptions, SendGridEmailOptions, AcsEmailOptions, SmsChannelOptions, AcsSmsOptions, AwsSnsSmsOptions, TwilioOptions, MobilePushChannelOptions, GoogleFcmOptions, AzureNotificationHubsOptions, AwsSnsMobilePushOptions, SignalRChannelOptions, WebPushChannelOptions, SseChannelOptions, ZulipChannelOptions, ZulipBotOptionsvarious
EmailIEmailSender, EmailMessageGranit.Notifications.Email, Granit.Notifications.AzureCommunicationServices, Granit.Notifications.Scaleway, Granit.Notifications.SendGrid
SMSISmsSender, SmsMessageGranit.Notifications.Sms, Granit.Notifications.AwsSns, Granit.Notifications.Twilio
WhatsAppIWhatsAppSender, WhatsAppMessageGranit.Notifications.WhatsApp, Granit.Notifications.Twilio
Mobile PushIMobilePushSender, MobilePushMessage, IMobilePushTokenReader, IMobilePushTokenWriter, MobilePushTokenInfo, MobilePlatformGranit.Notifications.MobilePush, Granit.Notifications.AwsSns
SignalRNotificationHub, SignalRNotificationMessageGranit.Notifications.SignalR
Web PushIWebPushSubscriptionReader, IWebPushSubscriptionWriter, WebPushSubscriptionInfoGranit.Notifications.WebPush
SSEISseConnectionManager, SseConnection, SseNotificationMessageGranit.Notifications.Sse
ZulipIZulipSender, ZulipMessageGranit.Notifications.Zulip
ExceptionsNotificationDeliveryExceptionGranit.Notifications
EndpointsNotificationsEndpointsOptions, MobilePushTokenEndpointsGranit.Notifications.Endpoints
ExtensionsAddGranitNotifications(), 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