Notifications

The Rule Repository raises a notification whenever something happens that people outside the repository's screen may need to hear about: someone asked for approval of a rule version, a request was approved or declined, a version was made live or taken offline. Your application receives each one through the Notify delegate of the request configuration and decides who is told, and how.

You need it because the repository keeps no list of who should hear about what, and sends no email of its own. Your application knows its users, their roles and how to reach them: by email, in a chat channel, through a ticket, or on a dashboard. Typical uses:

  • Email the publishers when an author requests approval, so requests do not sit unnoticed.
  • Tell the author when their request is declined, with the reason the approver gave.
  • Post to a team channel whenever production changes.
  • Reload your application's cached rules when a rule version goes live or is taken offline.
  • Write production changes to your own audit system.

The server-side Notify delegate has nothing to do with the client's notifyCallback option of $repo.init(..), which only lets your page show the repository's on-screen messages in its own notification style. Nothing raised through Notify reaches the browser.

Setting It Up

Set Notify on the Configuration you build for each request:

using CodeEffects.Rule.Repository;
using CodeEffects.Rule.Repository.Common;

var config = new Configuration
{
	CurrentUserId = user.Id,
	CurrentUserName = user.Name,
	AccessMode = permissions.Mode,
	ActionList = permissions.Actions,

	Notify = (action, data) => logger.LogInformation(
		"Rule Repository: {Action} -- {Message}", action, data.Message)
};

Leave it null and nothing is raised.

When It Is Called

NotificationAction Raised when Usually told
ApprovalRequested (1) Approval of a rule version is requested: at its save, later from its dialog, or by any save an AI agent or service makes The users who may approve (RuleVersionApproveAndDecline)
ApprovalDeclined (2) A request is declined. Reason holds the approver's reason, when given The user who asked
RuleVersionMadeLive (3) A rule version is made live: Go Live, Save & Go Live, Approve & Go Live, or the replacement of a Take Offline. ReplacedVersionId names the version it replaced Your choice: the team, the rule's author, your own systems
RuleVersionTakenOffline (4) A rule's live version is taken offline Your choice
SourceVersionMadeLive (5) A person makes a source version live in the repository Your choice
SourceVersionTakenOffline (6) A person takes a source object's live version offline Your choice
ApprovalGranted (7) A request is approved The user who asked, and those who may make it live

Details worth knowing:

  • Approve & Go Live raises two notifications: ApprovalGranted first, then RuleVersionMadeLive.
  • Take Offline with a replacement raises two: first RuleVersionMadeLive for the replacement (with ReplacedVersionId naming the version taken offline), then RuleVersionTakenOffline for the old version. The same order applies to source versions.
  • The source sync raises nothing. A source version made live by SourceSettings.NewVersionsGoLive is recorded in the activity log, but no notification is raised; your sync code already knows it did it, from SyncResult.
  • New NotificationAction values may be added in later releases; their numbers are never reused. Ignore values your code does not know.

What Each Notification Carries

NotificationData has one shape for every action. What does not apply is null: a source version's notification leaves the rule members null.

Property Meaning
Timestamp When it happened, in UTC
UserId, UserName, UserType Who did it: the request's CurrentUserId, and whether that is a person, an AI agent or a service
AgentId, AgentName The AI agent that acted for the user, or null when the user acted directly
SourceId, SourceName, SourceVersionId The source object, and the source version involved (for a rule version, the one it is written against)
RuleId, RuleName The rule
RuleVersionId, RuleVersionName The rule version
ReplacedVersionId For a version made live, the version that was live before it, if any
Reason Why a request was declined, as the approver wrote it
Message What happened, as one readable English sentence: the same text the activity log records

A host that only forwards text can send Message as it is.

How It Runs

  • After the change is committed. The notification reports something that has already happened. It cannot stop or undo it.
  • On the request's own thread. A slow delegate makes the user's click slow. Queue anything that takes time (sending email, calling another service) and return at once.
  • Exceptions are caught and ignored. A failure in your delegate never fails the user's action, and the user is not told. Log your own failures inside the delegate.
  • Only in the process that handled the request. In a web farm, each server raises the notifications of the requests it handles.

Example: Emailing Approvers in ASP.NET Core

The delegate puts each notification on a queue and returns. A background service reads the queue, finds the right people in your own user directory, and sends the email.

using System.Threading.Channels;
using CodeEffects.Rule.Common.Models;
using CodeEffects.Rule.Repository;
using CodeEffects.Rule.Repository.Common;

/// <summary>Holds notifications until the background sender takes them.</summary>
public sealed class RuleNotificationQueue
{
	private readonly Channel<(NotificationAction Action, NotificationData Data)> channel =
		Channel.CreateUnbounded<(NotificationAction, NotificationData)>();

	/// <summary>The delegate the request configuration gets: never blocks.</summary>
	public void Enqueue(NotificationAction action, NotificationData data) => this.channel.Writer.TryWrite((action, data));

	public IAsyncEnumerable<(NotificationAction Action, NotificationData Data)> ReadAllAsync(CancellationToken token) =>
		this.channel.Reader.ReadAllAsync(token);
}

/// <summary>Your application's own services: who has which role, and how to send mail.</summary>
public interface IUserDirectory
{
	Task<IReadOnlyList<string>> GetEmailsOfUsersWithAsync(ActionPermissionType permission);
	Task<string> GetEmailAsync(string userId);
}

public interface IMailSender
{
	Task SendAsync(IEnumerable<string> to, string subject, string body);
}

public sealed class RuleNotificationSender : BackgroundService
{
	private readonly RuleNotificationQueue queue;
	private readonly IServiceScopeFactory scopes;
	private readonly ILogger<RuleNotificationSender> logger;

	public RuleNotificationSender(RuleNotificationQueue queue, IServiceScopeFactory scopes, ILogger<RuleNotificationSender> logger)
	{
		this.queue = queue;
		this.scopes = scopes;
		this.logger = logger;
	}

	protected override async Task ExecuteAsync(CancellationToken stoppingToken)
	{
		await foreach(var (action, data) in this.queue.ReadAllAsync(stoppingToken))
		{
			try
			{
				using var scope = this.scopes.CreateScope();
				var users = scope.ServiceProvider.GetRequiredService<IUserDirectory>();
				var mail = scope.ServiceProvider.GetRequiredService<IMailSender>();

				switch(action)
				{
					case NotificationAction.ApprovalRequested:
						await mail.SendAsync(
							await users.GetEmailsOfUsersWithAsync(ActionPermissionType.RuleVersionApproveAndDecline),
							"Approval requested: " + data.RuleName,
							data.Message + "\n\nOpen the Rule Repository to approve or decline it.");
						break;

					case NotificationAction.ApprovalDeclined:
					case NotificationAction.ApprovalGranted:
						// The person who asked is the author of the version; your application may keep
						// track of requests itself, or read the author from the repository's activity
						break;

					case NotificationAction.RuleVersionMadeLive:
					case NotificationAction.RuleVersionTakenOffline:
						this.logger.LogInformation("Production change: {Message}", data.Message);
						break;
				}
			}
			catch(Exception ex)
			{
				this.logger.LogError(ex, "Could not deliver the Rule Repository notification {Action}.", action);
			}
		}
	}
}

Register the queue and the sender once, and hand the queue's method to every request's configuration:

// Program.cs
builder.Services.AddSingleton<RuleNotificationQueue>();
builder.Services.AddHostedService<RuleNotificationSender>();

// In the repository endpoint
app.MapPost("/api/rules", async (HttpRequest request, RuleNotificationQueue notifications) =>
{
	var config = new Configuration
	{
		// ... the user and permissions
		Notify = notifications.Enqueue
	};

	// ... Context.GetResponse(config, dbProvider, requestJson)
}).RequireAuthorization();

Who asked for approval. ApprovalDeclined and ApprovalGranted name the approver in UserId, not the person who asked. To tell the author, keep the ApprovalRequested notification's UserId against its RuleVersionId when it arrives, and look it up when the answer comes.

Example: Reloading Cached Rules

When your application caches loaded rules (see Loading the Live Rules for Evaluation), reload them when production changes:

Notify = (action, data) =>
{
	if(action == NotificationAction.RuleVersionMadeLive || action == NotificationAction.RuleVersionTakenOffline)
		orderRules.Reload();
}

In a web farm, the other servers do not see this notification: publish it to them through your own message bus or cache.

In a .NET Framework Application

The delegate is the same. Queue the work with Task.Run or HostingEnvironment.QueueBackgroundWorkItem, so the request is not held up:

Notify = (action, data) => HostingEnvironment.QueueBackgroundWorkItem(token => SendNotification(action, data))

Ask on Stack Overflow - Yes, it's still active
Ask in GitHub Discussions

p102

l097 --

l102

p101

×