Portal Community

Basic Registration

// In Program.cs or a dedicated extension method
builder.Services.AddTransient<INodeEventSubscriber, AuditNodeSubscriber>();
builder.Services.AddTransient<INodeEventSubscriber, MetricsNodeSubscriber>();
builder.Services.AddTransient<INodeEventSubscriber, ErrorAlertSubscriber>();

Lifetime Considerations

LifetimeWhen to UseGotcha
TransientStateless subscribers (write to repo, increment metric)A new instance per node event — no shared state between events
ScopedSubscribers that need a scoped service (e.g., EF DbContext)Scope is the execution scope, not the HTTP request scope
SingletonSubscribers with in-memory state across executions (e.g., circuit breaker)Must be thread-safe; all executions share the instance

Registration Order = Execution Order

Subscribers are called in the exact order they are registered. Register more critical subscribers first (e.g., audit logging before alerting) so that even if alerting fails, the audit record is still written.

// Recommended order: audit first, then metrics, then alerting
services.AddTransient<INodeEventSubscriber, AuditNodeSubscriber>();
services.AddTransient<INodeEventSubscriber, MetricsNodeSubscriber>();
services.AddTransient<INodeEventSubscriber, SlackAlertSubscriber>();

Extension Method Pattern (Recommended)

For subscribers shipped in a library or module, expose a registration extension:

// In your library's ServiceCollectionExtensions.cs
public static class NodeObservabilityExtensions
{
    public static IServiceCollection AddNodeObservability(
        this IServiceCollection services,
        Action<NodeObservabilityOptions>? configure = null)
    {
        var options = new NodeObservabilityOptions();
        configure?.Invoke(options);

        services.AddSingleton(options);
        services.AddTransient<INodeEventSubscriber, AuditNodeSubscriber>();

        if (options.EnableMetrics)
            services.AddTransient<INodeEventSubscriber, MetricsNodeSubscriber>();

        if (options.EnableAlerts)
            services.AddTransient<INodeEventSubscriber, ErrorAlertSubscriber>();

        return services;
    }
}

// Usage in Program.cs
builder.Services.AddNodeObservability(opts =>
{
    opts.EnableMetrics = true;
    opts.EnableAlerts  = true;
});

Conditional Registration (Feature Flags)

var config = builder.Configuration;

if (config.GetValue<bool>("Features:NodeAuditLog"))
    builder.Services.AddTransient<INodeEventSubscriber, AuditNodeSubscriber>();

if (config.GetValue<bool>("Features:NodeMetrics"))
    builder.Services.AddTransient<INodeEventSubscriber, MetricsNodeSubscriber>();

Testing a Subscriber

[Fact]
public async Task AuditSubscriber_WritesRecord_OnBeforeExecute()
{
    // Arrange
    var repo = new FakeAuditRepository();
    var subscriber = new AuditNodeSubscriber(repo);
    var args = new NodeExecutionEventArgs
    {
        NodeId      = "node-1",
        NodeType    = "HttpRequest",
        ExecutionId = "exec-123",
        ProcessId   = "proc-456",
        TenantId    = "tenant-789",
        StartedAt   = DateTimeOffset.UtcNow,
        Context     = FakeNodeContext.Create()
    };

    // Act
    await subscriber.OnBeforeExecuteAsync(args, CancellationToken.None);

    // Assert
    Assert.Single(repo.Entries);
    Assert.Equal(AuditStatus.InProgress, repo.Entries[0].Status);
    Assert.Equal("node-1", repo.Entries[0].NodeId);
}
No attribute scanning: Subscribers are not discovered by attribute — they must be explicitly registered in DI. This is intentional: it ensures you always know exactly which subscribers are active in a given deployment.