WorkflowForge Configuration
Options bind from JSON or code. Extensions read nested sections under WorkflowForge:Extensions.
Table of Contents
- Overview
- Core Settings
- Configuration Methods
- Configuration Patterns
- Extension Configuration
- Environment Strategies
- Configuration Guidelines
Overview
Config can come from JSON, code, or both.
- appsettings.json: Typed options (typical in production)
- Programmatic: Set
WorkflowForgeOptionsand extension types in code - Service provider: Resolve options through DI
- Extensions: Optional sections under
WorkflowForge:Extensions
Configuration philosophy
- Prefer settings you can search and diff in source control
- Keep hot paths typed whether values come from config or constructors
- Defaults should be enough for small demos without a file
- Prefer options types and middleware over forking the library
Core Settings
Bind WorkflowForgeOptions from appsettings.json or set properties in code.
It derives from WorkflowForgeOptionsBase (Enabled, SectionName, Validate(), Clone()).
WorkflowForgeOptions Properties
| Property | Type | Default | Description |
|---|---|---|---|
Enabled |
bool | true | Enables or disables the core WorkflowForge feature set |
SectionName |
string | “WorkflowForge” | Configuration section name used for binding |
MaxConcurrentWorkflows |
int | 0 (unlimited) | Maximum concurrent workflows (0-10000, 0 = unlimited) |
ContinueOnError |
bool | false | Continue execution and throw AggregateException at end |
FailFastCompensation |
bool | false | Stop compensation on first restore failure |
ThrowOnCompensationError |
bool | false | Throw AggregateException when compensation fails |
EnableOutputChaining |
bool | true | Pass operation output as next operation input |
Configuration Section: "WorkflowForge" in appsettings.json
Configuration Methods
Method 1: appsettings.json (Recommended)
Best for: Production applications, ASP.NET Core, containerized deployments
Step 1: Create appsettings.json
{
"WorkflowForge": {
"Enabled": true,
"MaxConcurrentWorkflows": 10,
"ContinueOnError": false,
"FailFastCompensation": false,
"ThrowOnCompensationError": false,
"EnableOutputChaining": true,
"Extensions": {
"Polly": {
"Enabled": true,
"EnableDetailedLogging": true,
"Retry": {
"IsEnabled": true,
"MaxRetryAttempts": 3,
"BaseDelay": "00:00:01",
"BackoffType": "Exponential",
"UseJitter": true
},
"CircuitBreaker": {
"IsEnabled": false,
"FailureThreshold": 5,
"BreakDuration": "00:00:30"
},
"Timeout": {
"IsEnabled": true,
"DefaultTimeout": "00:00:30"
}
},
"Validation": {
"Enabled": true,
"ThrowOnValidationError": true,
"LogValidationErrors": true
},
"Audit": {
"Enabled": true,
"DetailLevel": "Standard",
"LogDataPayloads": false
},
"Persistence": {
"Enabled": false,
"PersistOnOperationComplete": true,
"PersistOnWorkflowComplete": true
}
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"WorkflowForge": "Information"
}
}
}
Step 2: Register Configuration in Startup
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using WorkflowForge.Options;
using WorkflowForge.Extensions.Resilience.Polly;
using WorkflowForge.Extensions.Resilience.Polly.Options;
using WorkflowForge.Extensions.Validation;
using WorkflowForge.Extensions.Audit;
using WorkflowForge.Extensions.Persistence;
// Build configuration
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment}.json", optional: true)
.AddEnvironmentVariables()
.Build();
// Register configuration with Options pattern
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
// Core WorkflowForge settings
services.Configure<WorkflowForgeOptions>(
configuration.GetSection(WorkflowForgeOptions.DefaultSectionName));
// Extension settings using extension methods
services.AddWorkflowForgePolly(configuration);
services.AddValidationConfiguration(configuration);
services.AddAuditConfiguration(configuration);
services.AddPersistenceConfiguration(configuration);
var serviceProvider = services.BuildServiceProvider();
Step 3: Use Configuration in Application
using Microsoft.Extensions.Options;
using WorkflowForge.Options;
using WorkflowForge.Extensions.Resilience.Polly.Options;
public class OrderWorkflowService
{
private readonly IOptions<WorkflowForgeOptions> _workflowOptions;
private readonly IOptions<PollyMiddlewareOptions> _pollyOptions;
public OrderWorkflowService(
IOptions<WorkflowForgeOptions> workflowOptions,
IOptions<PollyMiddlewareOptions> pollyOptions)
{
_workflowOptions = workflowOptions;
_pollyOptions = pollyOptions;
}
public async Task ProcessOrderAsync(Order order)
{
// Configuration is automatically loaded from appsettings.json
var settings = _workflowOptions.Value;
Console.WriteLine($"MaxConcurrentWorkflows: {settings.MaxConcurrentWorkflows}");
Console.WriteLine($"ContinueOnError: {settings.ContinueOnError}");
// Create foundry and apply configuration
using var foundry = WorkflowForge.CreateFoundry($"Order-{order.Id}");
// Apply Polly configuration if enabled
if (_pollyOptions.Value.Enabled)
{
foundry.UsePollyFromSettings(_pollyOptions.Value);
}
foundry.SetProperty("Order", order);
var workflow = BuildOrderWorkflow();
using var smith = WorkflowForge.CreateSmith();
await smith.ForgeAsync(workflow, foundry);
}
}
Why appsettings.json
- Swap
appsettings.{Environment}.jsonwithout rebuilding - Override keys from environment variables in containers
- Bind to POCOs the compiler understands
- Call
Validate()where you register options - Reload on change when the host supports it
- One place for core flags and extension toggles
Method 2: Programmatic Configuration
Best for: Dynamic scenarios, tests, and small console apps.
WorkflowForgeOptions in code
Execution behavior comes from WorkflowForgeOptions:
public sealed class WorkflowForgeOptions : WorkflowForgeOptionsBase
{
public bool Enabled { get; set; } = true;
public string SectionName { get; }
public int MaxConcurrentWorkflows { get; set; } = 0;
public bool ContinueOnError { get; set; } = false;
public bool FailFastCompensation { get; set; } = false;
public bool ThrowOnCompensationError { get; set; } = false;
public bool EnableOutputChaining { get; set; } = true;
public override IList<string> Validate();
public override object Clone();
}
Default behavior
If you do not set options, you get:
- Automatic compensation: On failure,
WorkflowSmithtriggersRestoreAsyncon all completed operations (no-op base class default handles non-restorable operations) - Stop-on-first-error: Execution stops at the first failed operation
- Best-effort compensation: Compensation continues even if a restore fails
- No concurrency limits: Limited only by system resources
- Minimal logging: Via injected
IWorkflowForgeLogger
Behavior Switches
ContinueOnError = true: Continue execution, then throwAggregateExceptionFailFastCompensation = true: Stop compensation on the first restore failureThrowOnCompensationError = true: Surface restore failures viaAggregateException
When to Use Each Switch
ContinueOnError: Batch-style runs where you finish the list, then inspect anAggregateException.FailFastCompensation: Stop restore after the first failed rollback when going further would make things worse.ThrowOnCompensationError: Bubble restore failures so monitors and callers can react.
Configuration Patterns
Pattern 1: Basic Configuration
using WorkflowForge;
// Simple workflow with defaults
var workflow = WorkflowForge.CreateWorkflow("SimpleWorkflow")
.AddOperation(new CalculateTotal())
.AddOperation(new SendNotification())
.Build();
using var smith = WorkflowForge.CreateSmith();
await smith.ForgeAsync(workflow);
Pattern 2: Foundry with Properties
// Configure via foundry properties
using var foundry = WorkflowForge.CreateFoundry("ConfiguredWorkflow");
// Set execution context
foundry.SetProperty("OrderId", orderId);
foundry.SetProperty("CustomerId", customerId);
foundry.SetProperty("Environment", "Production");
// Execute workflow
using var smith = WorkflowForge.CreateSmith();
await smith.ForgeAsync(workflow, foundry);
// Read results
var total = foundry.GetPropertyOrDefault<decimal>("Total");
Pattern 3: Service Provider Integration
// ASP.NET Core / DI integration
public class OrderWorkflowService
{
private readonly IServiceProvider _serviceProvider;
private readonly IWorkflowForgeLogger _logger;
public OrderWorkflowService(
IServiceProvider serviceProvider,
IWorkflowForgeLogger logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task ProcessOrderAsync(Order order)
{
// Create smith with service provider (foundry inherits it)
using var smith = WorkflowForge.CreateSmith(_logger, _serviceProvider);
using var foundry = smith.CreateFoundry();
foundry.SetProperty("Order", order);
var workflow = BuildOrderWorkflow();
await smith.ForgeAsync(workflow, foundry);
}
private IWorkflow BuildOrderWorkflow()
{
return WorkflowForge.CreateWorkflow("OrderProcessing")
.AddOperation(new ValidateOrderOperation())
.AddOperation(new ChargePaymentOperation())
.AddOperation(new ReserveInventoryOperation())
.AddOperation(new CreateShipmentOperation())
.Build();
}
}
Pattern 4: Event-Based Configuration
// Subscribe to events for monitoring
using var smith = WorkflowForge.CreateSmith();
using var foundry = WorkflowForge.CreateFoundry("MonitoredWorkflow");
// Configure workflow events
smith.WorkflowStarted += (s, e) =>
_logger.LogInformation("Workflow started: {Name}", e.Foundry.CurrentWorkflow?.Name);
smith.WorkflowCompleted += (s, e) =>
_logger.LogInformation("Workflow completed in {Duration}ms",
e.Duration.TotalMilliseconds);
smith.WorkflowFailed += (s, e) =>
_logger.LogError(e.Exception, "Workflow failed: {Name}", e.Foundry.CurrentWorkflow?.Name);
// Configure operation events
foundry.OperationStarted += (s, e) =>
_logger.LogDebug("Operation started: {Name}", e.Operation.Name);
foundry.OperationCompleted += (s, e) =>
_metrics.RecordOperationDuration(e.Operation.Name, e.Duration);
await smith.ForgeAsync(workflow, foundry);
Pattern 5: Middleware Configuration
// Configure operation middleware
using var foundry = WorkflowForge.CreateFoundry("MiddlewareExample");
// Add timing middleware (built-in)
foundry.UseTiming();
// Add error handling middleware (built-in)
foundry.UseErrorHandling(rethrowExceptions: true);
// Add validation middleware (from Validation extension)
foundry.UseValidation(f => f.GetPropertyOrDefault<Order>("Order"));
await smith.ForgeAsync(workflow, foundry);
Extension Configuration
Serilog Extension
using WorkflowForge.Extensions.Logging.Serilog;
// Create a WorkflowForge logger configured via options
var logger = SerilogLoggerFactory.CreateLogger(new SerilogLoggerOptions
{
MinimumLevel = "Information",
EnableConsoleSink = true
});
using var foundry = WorkflowForge.CreateFoundry("MyWorkflow");
Bundling: ILRepack ships Serilog inside the extension assembly; Microsoft/System stay external.
Resilience Extension
using WorkflowForge.Extensions.Resilience;
using WorkflowForge.Extensions.Resilience.Strategies;
// Wrap operations with retry logic
var resilientOperation = RetryWorkflowOperation.WithExponentialBackoff(
operation: myOperation,
baseDelay: TimeSpan.FromMilliseconds(100),
maxDelay: TimeSpan.FromSeconds(30),
maxAttempts: 3);
// Or use specific strategies
var strategy = new ExponentialBackoffStrategy(
baseDelay: TimeSpan.FromSeconds(1),
maxDelay: TimeSpan.FromSeconds(60),
maxAttempts: 5,
logger: logger);
var resilientOp = new RetryWorkflowOperation(myOperation, strategy);
// Add to workflow
var workflow = WorkflowForge.CreateWorkflow("ResilientProcess")
.AddOperation("ProcessWithRetry", resilientOp)
.Build();
Strategies Available:
ExponentialBackoffStrategy- Best for external servicesFixedIntervalStrategy- Best for databasesRandomIntervalStrategy- Prevents thundering herd
Zero Dependencies: Pure WorkflowForge extension with no external dependencies.
Configuration: Programmatic only (via code). For appsettings.json support, use WorkflowForge.Extensions.Resilience.Polly.
Polly Extension
using WorkflowForge.Extensions.Resilience.Polly;
using Polly;
// Configure retry policy
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (exception, timeSpan, retryCount, context) =>
{
logger.LogWarning("Retry {RetryCount} after {Delay}ms",
retryCount, timeSpan.TotalMilliseconds);
});
// Wrap operation with Polly retry
var resilientOp = PollyRetryOperation.WithRetryPolicy(
new CallExternalApiOperation(),
maxRetryAttempts: 3,
baseDelay: TimeSpan.FromSeconds(1));
var workflow = WorkflowForge.CreateWorkflow("ResilientWorkflow")
.AddOperation(resilientOp)
.Build();
Bundling: ILRepack embeds Polly; Microsoft/System stay external.
OpenTelemetry Extension
using WorkflowForge.Extensions.Observability.OpenTelemetry;
using OpenTelemetry;
using OpenTelemetry.Trace;
// Configure OpenTelemetry
var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("WorkflowForge")
.AddConsoleExporter()
.AddJaegerExporter(options =>
{
options.AgentHost = "localhost";
options.AgentPort = 6831;
})
.Build();
// Create foundry with tracing
using var foundry = WorkflowForge.CreateFoundry("TracedWorkflow");
var tracer = tracerProvider.GetTracer("WorkflowForge");
// Operations will create spans
await smith.ForgeAsync(workflow, foundry);
Bundling: ILRepack embeds OpenTelemetry; Microsoft/System stay external.
Validation Extension
using System.ComponentModel.DataAnnotations;
using WorkflowForge.Extensions.Validation;
// Define model with DataAnnotations
public class Order
{
[Required]
public string CustomerId { get; set; } = string.Empty;
[Range(0.01, double.MaxValue)]
public decimal Amount { get; set; }
[MinLength(1)]
public List<OrderItem> Items { get; set; } = new();
}
// Configure validation
using var foundry = WorkflowForge.CreateFoundry("ValidatedWorkflow");
foundry.SetProperty("Order", order);
foundry.UseValidation(
f => f.GetPropertyOrDefault<Order>("Order"),
new ValidationMiddlewareOptions { ThrowOnValidationError = true });
// Validation runs before every operation
await smith.ForgeAsync(workflow, foundry);
Dependencies: DataAnnotations only; no extra third-party packages.
Audit Extension
using WorkflowForge.Extensions.Audit;
// Implement audit provider
public class FileAuditProvider : IAuditProvider
{
private readonly string _logPath;
private readonly SemaphoreSlim _writeLock = new(1, 1);
public FileAuditProvider(string logPath)
{
_logPath = logPath;
}
public async Task WriteAuditEntryAsync(
AuditEntry entry,
CancellationToken cancellationToken = default)
{
await _writeLock.WaitAsync(cancellationToken);
try
{
var json = JsonSerializer.Serialize(entry);
await File.AppendAllTextAsync(_logPath, json + Environment.NewLine, cancellationToken);
}
finally
{
_writeLock.Release();
}
}
public Task FlushAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
// Configure audit logging
var auditProvider = new FileAuditProvider("audit.log");
var auditLogger = new AuditLogger(
auditProvider,
userId: "user@example.com",
sessionId: Guid.NewGuid().ToString(),
timeProvider: new SystemTimeProvider());
// Subscribe to events
smith.WorkflowStarted += async (s, e) =>
await auditLogger.LogWorkflowStartedAsync(e);
smith.WorkflowCompleted += async (s, e) =>
await auditLogger.LogWorkflowCompletedAsync(e);
foundry.OperationCompleted += async (s, e) =>
await auditLogger.LogOperationCompletedAsync(e);
await smith.ForgeAsync(workflow, foundry);
Zero Dependencies: Pure WorkflowForge extension. Implement IAuditProvider for your storage.
Persistence Extension
using WorkflowForge.Extensions.Persistence;
using WorkflowForge.Extensions.Persistence.Abstractions;
// Implement IWorkflowPersistenceProvider for your storage
public sealed class MyPersistenceProvider : IWorkflowPersistenceProvider
{
public Task SaveAsync(WorkflowExecutionSnapshot snapshot, CancellationToken ct = default)
{
// Save to your database, file system, etc.
return Task.CompletedTask;
}
public Task<WorkflowExecutionSnapshot?> TryLoadAsync(
Guid foundryExecutionId, Guid workflowId, CancellationToken ct = default)
{
// Load from your storage
return Task.FromResult<WorkflowExecutionSnapshot?>(null);
}
public Task DeleteAsync(Guid foundryExecutionId, Guid workflowId, CancellationToken ct = default)
{
// Delete from your storage
return Task.CompletedTask;
}
}
// Enable persistence via middleware
var provider = new MyPersistenceProvider();
using var foundry = WorkflowForge.CreateFoundry("OrderProcessing");
foundry.UsePersistence(provider);
Zero Dependencies: Bring-your-own-storage pattern with no external dependencies.
Environment Strategies
Development Environment
public static class DevelopmentConfiguration
{
public static IWorkflowFoundry CreateFoundry(string name)
{
var logger = SerilogLoggerFactory.CreateLogger(new SerilogLoggerOptions
{
MinimumLevel = "Debug",
EnableConsoleSink = true
});
var foundry = WorkflowForge.CreateFoundry(name, logger);
foundry.UseTiming();
return foundry;
}
}
Production Environment
public static class ProductionConfiguration
{
public static IWorkflowFoundry CreateFoundry(string name)
{
var logger = SerilogLoggerFactory.CreateLogger(new SerilogLoggerOptions
{
MinimumLevel = "Information",
EnableConsoleSink = true
});
return WorkflowForge.CreateFoundry(name, logger);
}
}
Configuration Guidelines
1. Use Service Provider for Dependencies
// Good: DI-based
public class OrderOperation : WorkflowOperationBase
{
protected override async Task<object?> ForgeAsyncCore(
object? inputData,
IWorkflowFoundry foundry,
CancellationToken cancellationToken)
{
// Get service from foundry's service provider
var orderService = foundry.ServiceProvider
.GetRequiredService<IOrderService>();
var order = foundry.GetPropertyOrDefault<Order>("Order");
await orderService.ProcessAsync(order, cancellationToken);
return null;
}
}
// Bad: Static dependencies
public class OrderOperation : WorkflowOperationBase
{
private static readonly HttpClient _httpClient = new(); // Don't do this
// ...
}
2. Configure Middleware in Order
// Correct order: Validation → Timing → Error Handling → Business Logic
foundry.UseValidation(f => f.GetPropertyOrDefault<Order>("Order")); // First
foundry.UseTiming(); // Second
foundry.UseErrorHandling(rethrowExceptions: true); // Third
// Operations execute last
3. Use Properties for Context, Not Input Data
// Good: Context in properties
foundry.SetProperty("OrderId", orderId);
foundry.SetProperty("CustomerId", customerId);
foundry.SetProperty("CorrelationId", correlationId);
var workflow = WorkflowForge.CreateWorkflow("OrderWorkflow")
.AddOperation(new FetchOrderOperation()) // Reads OrderId from properties
.AddOperation(new ProcessPaymentOperation()) // Reads order data from properties
.Build();
// Bad: Passing data as input (only use for generic operations)
var data = new { OrderId = orderId, CustomerId = customerId };
await operation.ForgeAsync(data, foundry, cancellationToken); // Avoid unless IWorkflowOperation<TInput, TOutput>
4. Dispose Resources Properly
// Good: Using statement
using var foundry = WorkflowForge.CreateFoundry("MyWorkflow");
using var smith = WorkflowForge.CreateSmith();
await smith.ForgeAsync(workflow, foundry);
// Automatic disposal
// Also good: Try-finally
var foundry = WorkflowForge.CreateFoundry("MyWorkflow");
try
{
await smith.ForgeAsync(workflow, foundry);
}
finally
{
foundry.Dispose();
}
5. Inject ISystemTimeProvider for Testability
// Good: Inject time provider
public class TimeSensitiveOperation : WorkflowOperationBase
{
private readonly ISystemTimeProvider _timeProvider;
public TimeSensitiveOperation(ISystemTimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
protected override async Task<object?> ForgeAsyncCore(
object? inputData,
IWorkflowFoundry foundry,
CancellationToken cancellationToken)
{
var now = _timeProvider.UtcNow; // Testable!
// ...
return inputData;
}
}
// Bad: Direct DateTime usage
var now = DateTime.UtcNow; // Can't mock in tests
6. Use Events for Observability, Not Control Flow
// Good: Events for monitoring
smith.WorkflowCompleted += (s, e) => _metrics.RecordDuration(e.Duration);
// Bad: Events for control flow
smith.WorkflowCompleted += (s, e) =>
{
// Don't do complex business logic in event handlers
StartAnotherWorkflow(); // Use operations instead
};
7. Version Your Workflows
// Good: Include version in workflow name
var workflow = WorkflowForge.CreateWorkflow("OrderProcessing_v2")
.AddOperation(new ValidateOrderV2())
.AddOperation(new ProcessPaymentV2())
.Build();
// Track version in properties
foundry.SetProperty("WorkflowVersion", "2.0");
8. Test with Mock Implementations
// Create testable time provider
public class MockTimeProvider : ISystemTimeProvider
{
private DateTimeOffset _currentTime;
public DateTimeOffset UtcNow => _currentTime;
public void SetTime(DateTimeOffset time) => _currentTime = time;
public void Advance(TimeSpan duration) => _currentTime += duration;
}
// Use in tests
var mockTime = new MockTimeProvider();
mockTime.SetTime(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var operation = new TimeSensitiveOperation(mockTime);
// Test with controlled time