WorkflowForge Event System
Comprehensive guide to the WorkflowForge event system for workflow monitoring and observability.
Table of Contents
- Overview
- Event System Design
- Event Interfaces
- Event Args
- Subscribing to Events
- Event Patterns
- Best Practices
Overview
WorkflowForge provides a comprehensive event system for monitoring workflow execution, tracking operations, and handling compensation scenarios. The event system follows the Single Responsibility Principle (SRP) with three focused interfaces.
Why Events?
- Observability: Monitor workflow execution in real-time
- Auditing: Track all workflow activities for compliance
- Error Handling: React to failures and compensation
- Metrics: Collect performance data
- Integration: Connect to external monitoring systems
Event System Design
SRP-Compliant Architecture
WorkflowForge evolved from a single monolithic event interface to three focused interfaces, each with a single responsibility:
Old Design (Violates SRP):
IWorkflowEvents
├── WorkflowStarted
├── WorkflowCompleted
├── OperationStarted
├── OperationCompleted
├── CompensationTriggered
└── ... (all events mixed together)
New Design (SRP-Compliant):
├── IWorkflowLifecycleEvents (Workflow-level events)
├── IOperationLifecycleEvents (Operation-level events)
└── ICompensationLifecycleEvents (Compensation events)
Interface Mapping
| Component | Implements | Events Exposed |
|---|---|---|
IWorkflowSmith |
IWorkflowLifecycleEvents, ICompensationLifecycleEvents |
Workflow and compensation events |
IWorkflowFoundry |
IOperationLifecycleEvents |
Operation execution events |
Design Rationale:
- Smith manages workflows → fires workflow events
- Foundry hosts operations → fires operation events
- Compensation is workflow-level concern → smith fires compensation events
Event Interfaces
1. IWorkflowLifecycleEvents
Workflow-level lifecycle events.
public interface IWorkflowLifecycleEvents
{
event EventHandler<WorkflowStartedEventArgs>? WorkflowStarted;
event EventHandler<WorkflowCompletedEventArgs>? WorkflowCompleted;
event EventHandler<WorkflowFailedEventArgs>? WorkflowFailed;
}
Implemented By: IWorkflowSmith
When Events Fire:
WorkflowStarted: Before first operation executesWorkflowCompleted: After all operations complete successfullyWorkflowFailed: When workflow execution fails
2. IOperationLifecycleEvents
Operation-level lifecycle events.
public interface IOperationLifecycleEvents
{
event EventHandler<OperationStartedEventArgs>? OperationStarted;
event EventHandler<OperationCompletedEventArgs>? OperationCompleted;
event EventHandler<OperationFailedEventArgs>? OperationFailed;
}
Implemented By: IWorkflowFoundry
When Events Fire:
OperationStarted: Before operation executesOperationCompleted: After operation completes successfullyOperationFailed: When operation execution fails
3. ICompensationLifecycleEvents
Compensation (rollback) lifecycle events.
public interface ICompensationLifecycleEvents
{
event EventHandler<CompensationTriggeredEventArgs>? CompensationTriggered;
event EventHandler<CompensationCompletedEventArgs>? CompensationCompleted;
event EventHandler<OperationRestoreStartedEventArgs>? OperationRestoreStarted;
event EventHandler<OperationRestoreCompletedEventArgs>? OperationRestoreCompleted;
event EventHandler<OperationRestoreFailedEventArgs>? OperationRestoreFailed;
}
Implemented By: IWorkflowSmith
When Events Fire:
CompensationTriggered: When workflow failure triggers compensationCompensationCompleted: After all compensations completeOperationRestoreStarted: Before each operation’s RestoreAsync executesOperationRestoreCompleted: After successful operation restorationOperationRestoreFailed: When operation restoration fails
Event Args
All event arguments inherit from BaseWorkflowForgeEventArgs:
public abstract class BaseWorkflowForgeEventArgs : EventArgs
{
public Guid ExecutionId { get; }
public string WorkflowName { get; }
public DateTimeOffset Timestamp { get; }
}
Workflow Event Args
WorkflowStartedEventArgs:
public class WorkflowStartedEventArgs : BaseWorkflowForgeEventArgs
{
public Guid WorkflowId { get; }
public int OperationCount { get; }
}
WorkflowCompletedEventArgs:
public class WorkflowCompletedEventArgs : BaseWorkflowForgeEventArgs
{
public Guid WorkflowId { get; }
public TimeSpan Duration { get; }
public int OperationsExecuted { get; }
}
WorkflowFailedEventArgs:
public class WorkflowFailedEventArgs : BaseWorkflowForgeEventArgs
{
public Guid WorkflowId { get; }
public Exception Exception { get; }
public string? FailedOperationName { get; }
}
Operation Event Args
OperationStartedEventArgs:
public class OperationStartedEventArgs : BaseWorkflowForgeEventArgs
{
public Guid OperationId { get; }
public string OperationName { get; }
public int OperationIndex { get; }
}
OperationCompletedEventArgs:
public class OperationCompletedEventArgs : BaseWorkflowForgeEventArgs
{
public Guid OperationId { get; }
public string OperationName { get; }
public TimeSpan Duration { get; }
public int OperationIndex { get; }
}
OperationFailedEventArgs:
public class OperationFailedEventArgs : BaseWorkflowForgeEventArgs
{
public Guid OperationId { get; }
public string OperationName { get; }
public Exception Exception { get; }
public int OperationIndex { get; }
}
Compensation Event Args
CompensationTriggeredEventArgs:
public class CompensationTriggeredEventArgs : BaseWorkflowForgeEventArgs
{
public Guid WorkflowId { get; }
public int OperationsToCompensate { get; }
public Exception TriggeringException { get; }
}
OperationRestoreStartedEventArgs:
public class OperationRestoreStartedEventArgs : BaseWorkflowForgeEventArgs
{
public Guid OperationId { get; }
public string OperationName { get; }
}
OperationRestoreCompletedEventArgs:
public class OperationRestoreCompletedEventArgs : BaseWorkflowForgeEventArgs
{
public Guid OperationId { get; }
public string OperationName { get; }
public TimeSpan Duration { get; }
}
Subscribing to Events
Basic Subscription
using var smith = WorkflowForge.CreateSmith();
// Subscribe to workflow events
smith.WorkflowStarted += (sender, e) => {
Console.WriteLine($"Workflow {e.WorkflowName} started at {e.Timestamp}");
};
smith.WorkflowCompleted += (sender, e) => {
Console.WriteLine($"Workflow completed in {e.Duration.TotalMilliseconds}ms");
};
smith.WorkflowFailed += (sender, e) => {
Console.WriteLine($"Workflow failed: {e.Exception.Message}");
};
// Execute workflow
await smith.ForgeAsync(workflow);
Monitoring Operations
using var foundry = WorkflowForge.CreateFoundry("MonitoredWorkflow");
// Subscribe to operation events
foundry.OperationStarted += (sender, e) => {
Console.WriteLine($"Operation {e.OperationName} started (#{e.OperationIndex})");
};
foundry.OperationCompleted += (sender, e) => {
Console.WriteLine($"Operation {e.OperationName} completed in {e.Duration.TotalMilliseconds}ms");
};
foundry.OperationFailed += (sender, e) => {
Console.WriteLine($"Operation {e.OperationName} failed: {e.Exception.Message}");
};
// Execute workflow with foundry
using var smith = WorkflowForge.CreateSmith();
await smith.ForgeAsync(workflow, foundry);
Monitoring Compensation
using var smith = WorkflowForge.CreateSmith();
// Subscribe to compensation events
smith.CompensationTriggered += (sender, e) => {
Console.WriteLine($"Compensation triggered: {e.OperationsToCompensate} operations to rollback");
Console.WriteLine($"Cause: {e.TriggeringException.Message}");
};
smith.OperationRestoreStarted += (sender, e) => {
Console.WriteLine($"Rolling back operation: {e.OperationName}");
};
smith.OperationRestoreCompleted += (sender, e) => {
Console.WriteLine($"Operation {e.OperationName} rolled back in {e.Duration.TotalMilliseconds}ms");
};
smith.CompensationCompleted += (sender, e) => {
Console.WriteLine("All compensation completed");
};
await smith.ForgeAsync(workflow);
Event Patterns
Pattern 1: Centralized Event Logger
public class WorkflowEventLogger
{
private readonly ILogger _logger;
public void AttachToSmith(IWorkflowSmith smith)
{
smith.WorkflowStarted += OnWorkflowStarted;
smith.WorkflowCompleted += OnWorkflowCompleted;
smith.WorkflowFailed += OnWorkflowFailed;
smith.CompensationTriggered += OnCompensationTriggered;
smith.CompensationCompleted += OnCompensationCompleted;
}
public void AttachToFoundry(IWorkflowFoundry foundry)
{
foundry.OperationStarted += OnOperationStarted;
foundry.OperationCompleted += OnOperationCompleted;
foundry.OperationFailed += OnOperationFailed;
}
private void OnWorkflowStarted(object? sender, WorkflowStartedEventArgs e)
{
_logger.LogInformation(
"Workflow started: {WorkflowName} ({ExecutionId}) with {OperationCount} operations",
e.WorkflowName, e.ExecutionId, e.OperationCount);
}
// ... other event handlers
}
// Usage
var eventLogger = new WorkflowEventLogger(logger);
var smith = WorkflowForge.CreateSmith();
var foundry = WorkflowForge.CreateFoundry("MyWorkflow");
eventLogger.AttachToSmith(smith);
eventLogger.AttachToFoundry(foundry);
await smith.ForgeAsync(workflow, foundry);
Pattern 2: Performance Metrics Collection
public class WorkflowMetrics
{
private readonly List<(string Name, TimeSpan Duration)> _operationMetrics = new();
private DateTimeOffset _workflowStart;
public void AttachToEvents(IWorkflowSmith smith, IWorkflowFoundry foundry)
{
smith.WorkflowStarted += (s, e) => _workflowStart = e.Timestamp;
smith.WorkflowCompleted += (s, e) => LogWorkflowMetrics(e);
foundry.OperationCompleted += (s, e) => {
_operationMetrics.Add((e.OperationName, e.Duration));
};
}
private void LogWorkflowMetrics(WorkflowCompletedEventArgs e)
{
var totalDuration = e.Timestamp - _workflowStart;
var slowestOp = _operationMetrics.OrderByDescending(m => m.Duration).FirstOrDefault();
Console.WriteLine($"Workflow Metrics:");
Console.WriteLine($" Total Duration: {totalDuration.TotalMilliseconds}ms");
Console.WriteLine($" Operations: {_operationMetrics.Count}");
Console.WriteLine($" Slowest: {slowestOp.Name} ({slowestOp.Duration.TotalMilliseconds}ms)");
}
}
Pattern 3: Audit Trail
public class WorkflowAuditTrail
{
private readonly IAuditRepository _repository;
public void AttachToEvents(IWorkflowSmith smith, IWorkflowFoundry foundry)
{
smith.WorkflowStarted += async (s, e) => {
await _repository.LogAsync(new AuditEntry {
EventType = "WorkflowStarted",
WorkflowName = e.WorkflowName,
ExecutionId = e.ExecutionId,
Timestamp = e.Timestamp
});
};
foundry.OperationCompleted += async (s, e) => {
await _repository.LogAsync(new AuditEntry {
EventType = "OperationCompleted",
OperationName = e.OperationName,
Duration = e.Duration,
Timestamp = e.Timestamp
});
};
smith.WorkflowFailed += async (s, e) => {
await _repository.LogAsync(new AuditEntry {
EventType = "WorkflowFailed",
Error = e.Exception.Message,
FailedOperation = e.FailedOperationName,
Timestamp = e.Timestamp
});
};
}
}
Pattern 4: Error Notification
public class ErrorNotificationHandler
{
private readonly INotificationService _notificationService;
public void AttachToEvents(IWorkflowSmith smith, IWorkflowFoundry foundry)
{
smith.WorkflowFailed += async (s, e) => {
await _notificationService.SendAsync(new Notification {
Title = $"Workflow Failed: {e.WorkflowName}",
Message = $"Error: {e.Exception.Message}",
Severity = NotificationSeverity.Critical
});
};
foundry.OperationFailed += async (s, e) => {
await _notificationService.SendAsync(new Notification {
Title = $"Operation Failed: {e.OperationName}",
Message = $"Error: {e.Exception.Message}",
Severity = NotificationSeverity.High
});
};
smith.CompensationTriggered += async (s, e) => {
await _notificationService.SendAsync(new Notification {
Title = "Compensation Triggered",
Message = $"Rolling back {e.OperationsToCompensate} operations",
Severity = NotificationSeverity.High
});
};
}
}
Best Practices
1. Always Unsubscribe
// Good: Using statement ensures disposal
using var smith = WorkflowForge.CreateSmith();
smith.WorkflowCompleted += OnCompleted;
await smith.ForgeAsync(workflow);
// smith is disposed, events unsubscribed
// Alternative: Manual cleanup
var smith = WorkflowForge.CreateSmith();
try
{
smith.WorkflowCompleted += OnCompleted;
await smith.ForgeAsync(workflow);
}
finally
{
smith.WorkflowCompleted -= OnCompleted;
smith.Dispose();
}
2. Handle Exceptions in Event Handlers
smith.WorkflowCompleted += (sender, e) => {
try
{
// Your event handling logic
UpdateMetrics(e);
}
catch (Exception ex)
{
// Log but don't throw - don't break workflow execution
_logger.LogError(ex, "Error in event handler");
}
};
3. Use Async Event Handlers Carefully
// Events are synchronous - don't await in handlers unless necessary
smith.WorkflowCompleted += (sender, e) => {
// Don't do this - blocks workflow completion
// await LongRunningOperationAsync();
// Do this instead - fire and forget
_ = Task.Run(async () => {
try
{
await LongRunningOperationAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in async event handler");
}
});
};
4. Collect Contextual Information
foundry.OperationCompleted += (sender, e) => {
var context = new {
ExecutionId = e.ExecutionId,
WorkflowName = e.WorkflowName,
OperationName = e.OperationName,
Duration = e.Duration,
Timestamp = e.Timestamp,
// Access foundry properties for additional context
OrderId = foundry.Properties.TryGetValue("OrderId", out var id) ? id : null
};
_metrics.Record(context);
};
5. Create Reusable Event Handlers
public static class WorkflowEventHandlers
{
public static EventHandler<WorkflowStartedEventArgs> CreateStartLogger(ILogger logger)
{
return (sender, e) => logger.LogInformation(
"Workflow {WorkflowName} started with {OperationCount} operations",
e.WorkflowName, e.OperationCount);
}
public static EventHandler<WorkflowFailedEventArgs> CreateErrorLogger(ILogger logger)
{
return (sender, e) => logger.LogError(
e.Exception,
"Workflow {WorkflowName} failed at operation {OperationName}",
e.WorkflowName, e.FailedOperationName);
}
}
// Usage
smith.WorkflowStarted += WorkflowEventHandlers.CreateStartLogger(logger);
smith.WorkflowFailed += WorkflowEventHandlers.CreateErrorLogger(logger);
6. Use Events for Integration
// Integrate with Application Insights
smith.WorkflowCompleted += (s, e) => {
_telemetryClient.TrackEvent("WorkflowCompleted", new Dictionary<string, string> {
["WorkflowName"] = e.WorkflowName,
["Duration"] = e.Duration.TotalMilliseconds.ToString(),
["OperationCount"] = e.OperationsExecuted.ToString()
});
};
// Integrate with Prometheus
foundry.OperationCompleted += (s, e) => {
_operationDurationHistogram
.WithLabels(e.WorkflowName, e.OperationName)
.Observe(e.Duration.TotalSeconds);
};
Related Documentation
- Architecture - Understanding event system design
- Operations - Creating operations that fire events
- Samples Guide - See events in action (Sample 11: Workflow Events)
- Extensions - Audit and Performance extensions use events