Approval notifications¶
Entities supporting approvals often need to keep people in the loop over email: an approver should know they have something pending, and the requester should know once their request has been approved or rejected. Rather than every bounded context wiring this up on its own, the Workflows client module ships an integration with the Subscriptions service that takes care of it end to end: you describe what data goes into the notification, and delivery (recipients, template rendering, and sending) is handled for you.
How it works¶
Once an entity is opted into approval notifications, three topics are created for it, one per approval event: an approver was assigned, an approval was approved, and an approval was rejected. Whenever one of these events happens for the entity, the corresponding topic receives it and the Subscriptions service notifies whoever is meant to hear about it.
You don't need to manage subscribers yourself for this to work: the module resolves the right recipients automatically depending on the event, pending approvers (plus the requester) when an approval is added, and the requester once it's approved or rejected.
Modules you depend on¶
Approval notifications build on top of the regular approvals setup, so before going any further your bounded context must already:
- Depend on
WorkflowsClientModule, as described in Integration. - Register the entity with
HasApprovals<TEntity>()onWorkflowsClientModuleOptions. That registration remains the single place where "this entity supports approvals" is declared; the notifications module only adds behavior on top of it, it does not replace it.
With that in place, add a reference to the notifications module itself:
| XML | |
|---|---|
And depend on WorkflowsSubscriptionModule. This module already brings in
SubscriptionsClientModule for you (and registers its own minimal fallback
template on it), so you don't have to reference the Subscriptions client
module yourself just to get notifications flowing:
The only reason to depend on SubscriptionsClientModule directly is if you
also want to register your own templates, using its regular
RegisterTemplate API, exactly as you would for any other integration with
Subscriptions, see Templates below.
Configuring an entity¶
Everything else is configured through WorkflowsSubscriptionModuleOptions.
Call ConfigureEntity<TEntity> once per entity you want notifications for,
passing a stable, unique name for it:
| C# | |
|---|---|
| Input | Description |
|---|---|
TEntity |
The entity type notifications are being configured for. It must be the same type passed to HasApprovals<TEntity>(). |
entityWellKnownName |
A stable name identifying the entity, used to build its notification topics. See Subscribing to approval notifications. |
Calling ConfigureEntity<TEntity> again for the same entity, anywhere,
returns the same options instance rather than creating a new one, as long as
you pass the same well-known name every time; passing a different one
throws. This lets more than one module contribute configuration for the same
entity, for example one module wiring the events and another disabling them
for a given environment, without one overwriting the other.
ConfigureEntity<TEntity> returns an ApprovalSubscriptionEntityOptions<TEntity>
exposing the following members:
| Member | Input | Purpose |
|---|---|---|
OnApprovalAdded |
templateName (nullable string), buildData (Func<TEntity, ApprovalSubscriptionContext, IServiceProvider, CancellationToken, ValueTask<object>>) |
Configures the notification sent when an approver is assigned. |
OnApprovalApproved |
Same as above | Configures the notification sent when an approval is approved. |
OnApprovalRejected |
Same as above | Configures the notification sent when an approval is rejected. |
LoadWith |
params IncludeSpecification<TEntity>[] |
Include specifications used to hydrate the entity before buildData runs, same as you would use to query it elsewhere. |
SetEnabled |
bool |
Turns notifications for this entity on (the default) or off, without removing the rest of the configuration. |
All three events must be configured for every entity you opt in, otherwise the
application will fail to start; use SetEnabled(false) instead if you want
to keep an entity's configuration around but disabled for now.
buildData inputs¶
buildData is where you turn the entity (and the event's context) into the
model your template expects. It receives:
TEntity— the (optionally hydrated, seeLoadWith) entity the approval belongs to.ApprovalSubscriptionContext— event-specific details, described below.IServiceProvider— in case building the model needs to pull in additional services.CancellationToken— the ambient cancellation token for the operation.
And returns a ValueTask<object> with the model to render.
ApprovalSubscriptionContext carries the following, some of it only
meaningful for certain events:
| Property | Added | Approved | Rejected |
|---|---|---|---|
EntityId |
id of the entity | id of the entity | id of the entity |
TriggeredByUserIds |
ids of the newly assigned approvers | id of who approved | id of who rejected |
ApprovalDisplayName |
display name of the approval | not set | not set |
ApprovalCode |
code of the approval | not set | not set |
Reason |
not set | not set | rejection reason |
Templates¶
Templates work exactly as described in
Subscriptions integration:
register them through SubscriptionsClientModuleOptions.RegisterTemplate in
your application module, then pass their name to OnApprovalAdded,
OnApprovalApproved or OnApprovalRejected. Since the same template can be
reused across several entities and events, this module never registers
templates on your behalf, it only assigns the name you give it to the right
topic — so nothing stops you from sharing one template across every event, or
even across several entities, as long as the model each buildData produces
matches what the template expects.
If you don't have a custom template ready yet, pass null instead of a
template name to fall back to a minimal built-in template. Its model contract
is intentionally small, so when relying on it your buildData callback must
return an object shaped like this:
| C# | |
|---|---|
Putting it together¶
A generic module registering a custom template for one event and falling back to the default template for the other two looks like this:
Subscribing to approval notifications¶
Notification topics follow the pattern
workflows/approvals/{entityWellKnownName}/{event}, using the well-known name
you gave the entity in ConfigureEntity, lowercased. For example, opting in
Claim as WarrantyClaims_Claim exposes:
workflows/approvals/warrantyclaims_claim/ApprovalAddedworkflows/approvals/warrantyclaims_claim/ApprovalApprovedworkflows/approvals/warrantyclaims_claim/ApprovalRejected
These are regular Subscriptions topics, so users are notified through them the same way described in Subscribing.