Portal Community

Implementing a Handler

public class WorkflowAuditHandler : IWorkflowEventHandler<WorkflowStarted>
{
    private readonly IWorkflowAuditRepository _repo;

    public WorkflowAuditHandler(IWorkflowAuditRepository repo) => _repo = repo;

    public async Task HandleAsync(WorkflowStarted @event, CancellationToken ct)
    {
        await _repo.InsertAsync(new WorkflowAuditRecord
        {
            EventId     = @event.EventId,
            ExecutionId = @event.ExecutionId,
            ProcessId   = @event.ProcessId,
            TenantId    = @event.TenantId,
            Event       = "WorkflowStarted",
            TriggeredBy = @event.TriggeredBy,
            OccurredAt  = @event.OccurredAt
        }, ct);
    }
}

Registering the Handler

// Register for a specific event type
builder.Services.AddTransient<
    IWorkflowEventHandler<WorkflowStarted>,
    WorkflowAuditHandler>();

// One class can handle multiple event types
builder.Services.AddTransient<
    IWorkflowEventHandler<WorkflowCompleted>,
    WorkflowAuditHandler>();
builder.Services.AddTransient<
    IWorkflowEventHandler<WorkflowFailed>,
    WorkflowAuditHandler>();

Handler That Implements Multiple Event Types

public class WorkflowLifecycleAuditHandler :
    IWorkflowEventHandler<WorkflowStarted>,
    IWorkflowEventHandler<WorkflowCompleted>,
    IWorkflowEventHandler<WorkflowFailed>,
    IWorkflowEventHandler<WorkflowPaused>,
    IWorkflowEventHandler<WorkflowResumed>
{
    private readonly IWorkflowAuditRepository _repo;

    public WorkflowLifecycleAuditHandler(IWorkflowAuditRepository repo) => _repo = repo;

    public Task HandleAsync(WorkflowStarted @event, CancellationToken ct)
        => WriteAsync(@event.EventId, @event.ExecutionId, @event.TenantId, "started", ct);

    public Task HandleAsync(WorkflowCompleted @event, CancellationToken ct)
        => WriteAsync(@event.EventId, @event.ExecutionId, @event.TenantId, "completed", ct);

    public Task HandleAsync(WorkflowFailed @event, CancellationToken ct)
        => WriteAsync(@event.EventId, @event.ExecutionId, @event.TenantId, "failed", ct);

    public Task HandleAsync(WorkflowPaused @event, CancellationToken ct)
        => WriteAsync(@event.EventId, @event.ExecutionId, @event.TenantId, "paused", ct);

    public Task HandleAsync(WorkflowResumed @event, CancellationToken ct)
        => WriteAsync(@event.EventId, @event.ExecutionId, @event.TenantId, "resumed", ct);

    private async Task WriteAsync(string eventId, string executionId,
        string tenantId, string eventName, CancellationToken ct)
    {
        await _repo.InsertAsync(new WorkflowAuditRecord
        {
            EventId     = eventId,
            ExecutionId = executionId,
            TenantId    = tenantId,
            Event       = eventName,
            OccurredAt  = DateTimeOffset.UtcNow
        }, ct);
    }
}

Handler Execution Order

When multiple handlers are registered for the same event type, they execute in DI registration order. All handlers run regardless of whether earlier handlers fail (exceptions are isolated).

Scoped vs. Transient: Use Transient unless your handler needs a scoped service (e.g., EF DbContext). The bus resolves handlers fresh for each event publish, so Transient is the safest default.