Skip to content

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>() on WorkflowsClientModuleOptions. 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
<ProjectReference Include="$(ServicesPath)Workflows/ITsynch.Suite.Workflows.ClientModule.Subscriptions/ITsynch.Suite.Workflows.ClientModule.Subscriptions.csproj" />

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:

C#
public override void SetupModule(IModuleBuilder builder)
{
    base.SetupModule(builder);

    builder.DependsOn<WorkflowsClientModule, WorkflowsClientModuleOptions>(opts =>
    {
        opts.HasApprovals<Claim>();
    });

    builder.DependsOn<WorkflowsSubscriptionModule, WorkflowsSubscriptionModuleOptions>(opts =>
    {
        // configuration goes here, see below
    });
}

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#
ApprovalSubscriptionEntityOptions<TEntity> ConfigureEntity<TEntity>(string entityWellKnownName)
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, see LoadWith) 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#
1
2
3
4
5
new
{
    SubjectArguments = new { ApplicationName = "Warranty Claims", Subject = "Claim rejected" },
    BodyArguments = new { Subject = "Claim rejected", Body = "Claim C01 was rejected." },
}

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:

C#
public class ClaimsModule : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        base.SetupModule(builder);

        // Regular workflow + approvals setup.
        builder.DependsOn<WorkflowsClientModule, WorkflowsClientModuleOptions>(opts =>
        {
            opts.HasApprovals<Claim>();
        });

        // Register a custom template for the "rejected" event.
        builder.DependsOn<SubscriptionsClientModule, SubscriptionsClientModuleOptions>(opts =>
        {
            opts.RegisterTemplate(
                Templates.ClaimApprovalRejectedTemplateName,
                Templates.ClaimApprovalRejectedTemplatePath,
                Channel.Email);
        });

        // Wire up notifications.
        builder.DependsOn<WorkflowsSubscriptionModule, WorkflowsSubscriptionModuleOptions>(opts =>
        {
            opts.ConfigureEntity<Claim>("WarrantyClaims_Claim")
                .LoadWith(new ClaimWithApproversSpecification())
                .OnApprovalAdded(
                    null,
                    (claim, context, services, cancellationToken) => new ValueTask<object>(new
                    {
                        SubjectArguments = new { ApplicationName = "Warranty Claims", Subject = "Approval requested" },
                        BodyArguments = new { Subject = "Approval requested", Body = $"Claim {claim.Code} needs your approval." },
                    }))
                .OnApprovalApproved(
                    null,
                    (claim, context, services, cancellationToken) => new ValueTask<object>(new
                    {
                        SubjectArguments = new { ApplicationName = "Warranty Claims", Subject = "Claim approved" },
                        BodyArguments = new { Subject = "Claim approved", Body = $"Claim {claim.Code} was approved." },
                    }))
                .OnApprovalRejected(
                    Templates.ClaimApprovalRejectedTemplateName,
                    (claim, context, services, cancellationToken) => new ValueTask<object>(new
                    {
                        Claim = new { claim.Code, claim.Subject },
                        Reason = context.Reason,
                    }));
        });
    }
}

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/ApprovalAdded
  • workflows/approvals/warrantyclaims_claim/ApprovalApproved
  • workflows/approvals/warrantyclaims_claim/ApprovalRejected

These are regular Subscriptions topics, so users are notified through them the same way described in Subscribing.

See also