Flow Studio
Subscribing to Events
Implement IWorkflowEventHandler<TEvent> and register in DI. The dispatcher resolves all handlers at publish time — no manual wiring required.
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.