WorkflowForge Extension System

WorkflowForge follows an extension-first architecture where the core library provides minimal functionality, and rich features are delivered through a comprehensive extension ecosystem.

Table of Contents


Dependency-Free Core and Dependency Isolation

WorkflowForge core is zero-dependency. Extensions isolate third-party libraries where it makes sense, while keeping Microsoft/System dependencies external to avoid runtime conflicts.

  • Internalized with ILRepack: Serilog, Polly, OpenTelemetry
  • Always external: Microsoft/System assemblies (runtime unification)
  • Validation: DataAnnotations (no third-party dependency)

How It Works

Extensions that depend on non-BCL packages use ILRepack to internalize those libraries into the extension assembly. This keeps the public API clean (only WorkflowForge or BCL types) while avoiding version conflicts.

Microsoft/System assemblies are never embedded; those are resolved by the runtime using the application’s dependency graph.

Extension Architecture

Core Principles

  1. Dependency-Free Core - The core library has zero dependencies
  2. Isolated Extensions - Third-party dependencies are internalized where appropriate
  3. Optional Extensions - Add only what you need
  4. Composable Features - Extensions work together seamlessly
  5. Configuration-Driven - Extensions are configured, not hard-coded
  6. Production-Ready - Professional features for production use

Extension Categories

WorkflowForge Packages
├── Testing
│   └── FakeWorkflowFoundry (unit testing)
├── Logging
│   └── Serilog Integration
├── Resilience
│   ├── Basic Patterns
│   └── Polly Integration
├── Persistence (BYO storage)
│   └── Recovery Extensions
├── Validation
│   └── DataAnnotations Integration
├── Audit
│   └── Compliance & Operational Monitoring
└── Observability
    ├── Performance Monitoring
    ├── Health Checks
    └── OpenTelemetry Integration

Available Packages

Testing Package

WorkflowForge.Testing

Unit testing utilities for WorkflowForge operations and workflows.

Installation:

dotnet add package WorkflowForge.Testing

Features:

  • FakeWorkflowFoundry - Lightweight fake for unit testing operations in isolation
  • Execution tracking - Assert which operations executed
  • Property management - Test data flow between operations
  • Event verification - Subscribe to operation events in tests
  • Reset capability - Reuse foundry between tests

Quick Start:

using WorkflowForge.Testing;
using Xunit;

public class MyOperationTests
{
    [Fact]
    public async Task Operation_Should_SetProperty()
    {
        // Arrange
        var foundry = new FakeWorkflowFoundry();
        var operation = new MyCustomOperation();
        
        // Act
        await operation.ForgeAsync("input", foundry, CancellationToken.None);
        
        // Assert
        Assert.True(foundry.Properties.ContainsKey("expectedKey"));
    }
    
    [Fact]
    public async Task Workflow_Should_ExecuteAllOperations()
    {
        // Arrange
        var foundry = new FakeWorkflowFoundry();
        foundry.AddOperation(new OpA());
        foundry.AddOperation(new OpB());
        
        // Act
        await foundry.ForgeAsync();
        
        // Assert
        Assert.Equal(2, foundry.ExecutedOperations.Count);
    }
}

API Reference: | Property/Method | Description | |—————–|————-| | ExecutionId | Unique ID (auto-generated, settable) | | Properties | Thread-safe property storage | | Operations | Added operations | | ExecutedOperations | Operations executed during ForgeAsync | | ForgeAsync() | Execute all operations sequentially | | Reset() | Clear all state for test reuse | | TrackExecution(op) | Manually track operation as executed |


DependencyInjection Extension

WorkflowForge.Extensions.DependencyInjection

Microsoft.Extensions.DependencyInjection integration for ASP.NET Core and hosted applications.

Installation:

dotnet add package WorkflowForge.Extensions.DependencyInjection

Features:

  • IServiceCollection extension methods for registration
  • IOptions pattern support with automatic validation
  • Seamless integration with ASP.NET Core DI container
  • Scoped and singleton lifetime management

Usage:

using WorkflowForge.Extensions.DependencyInjection;

// In Program.cs or Startup.cs
services.AddWorkflowForge(options =>
{
    options.ContinueOnError = false;
    options.FailFastCompensation = false;
    options.ThrowOnCompensationError = true;
});

// Inject IWorkflowSmith in your services
public class OrderService
{
    private readonly IWorkflowSmith _smith;

    public OrderService(IWorkflowSmith smith)
    {
        _smith = smith;
    }

    public async Task ProcessOrderAsync(Order order)
    {
        var foundry = WorkflowForge.CreateFoundry("ProcessOrder");
        foundry.SetProperty("Order", order);

        var workflow = WorkflowForge.CreateWorkflow()
            .WithName("OrderWorkflow")
            .AddOperation(new ValidateOrderOperation())
            .AddOperation(new ProcessPaymentOperation())
            .Build();

        await _smith.ForgeAsync(workflow, foundry);
    }
}

Configuration via appsettings.json:

{
  "WorkflowForge": {
    "ContinueOnError": false,
    "FailFastCompensation": false,
    "ThrowOnCompensationError": true
  }
}
// Bind from configuration
services.AddWorkflowForge(configuration.GetSection("WorkflowForge"));

Logging Extensions

WorkflowForge.Extensions.Logging.Serilog

Professional structured logging with Serilog integration.

Installation:

dotnet add package WorkflowForge.Extensions.Logging.Serilog

Features:

  • Structured logging with rich context
  • Correlation ID tracking
  • Property enrichment
  • Scope management
  • Multiple output targets (Console, File, Database, etc.)

Usage:

using WorkflowForge.Extensions.Logging.Serilog;

var logger = SerilogLoggerFactory.CreateLogger(new SerilogLoggerOptions
{
    MinimumLevel = "Information",
    EnableConsoleSink = true
});

var options = new WorkflowForgeOptions
{
    ContinueOnError = false,
    FailFastCompensation = false,
    ThrowOnCompensationError = true
};
var foundry = WorkflowForge.CreateFoundry("ProcessOrder", logger, options: options);

Configuration:

{
  "Serilog": {
    "MinimumLevel": "Information",
    "WriteTo": [
      { "Name": "Console" },
      { 
        "Name": "File", 
        "Args": { 
          "path": "logs/workflow-.txt",
          "rollingInterval": "Day" 
        } 
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName"]
  }
}

Resilience Extensions

WorkflowForge.Extensions.Resilience

Basic resilience patterns and retry middleware.

Installation:

dotnet add package WorkflowForge.Extensions.Resilience

Features:

  • Retry middleware with configurable policies
  • Circuit breaker patterns
  • Timeout management
  • Basic rate limiting

Usage:

// See package README for current API; base resilience is covered by Polly extension.

WorkflowForge.Extensions.Resilience.Polly

Advanced resilience patterns using the Polly library.

Installation:

dotnet add package WorkflowForge.Extensions.Resilience.Polly

Features:

  • Comprehensive retry strategies (exponential backoff, jitter)
  • Circuit breakers with failure thresholds
  • Rate limiting and throttling
  • Timeout policies
  • Policy combination and chaining
  • Environment-specific configurations

Usage:

using WorkflowForge;
using WorkflowForge.Extensions.Resilience.Polly;

var foundry = WorkflowForge.CreateFoundry("ProcessOrder");

// Custom policies
foundry
    .UsePollyRetry(maxRetryAttempts: 5, baseDelay: TimeSpan.FromSeconds(1))
    .UsePollyCircuitBreaker(failureThreshold: 3, durationOfBreak: TimeSpan.FromMinutes(1))
    .UsePollyTimeout(TimeSpan.FromSeconds(30));

Configuration:

{
  "WorkflowForge": {
    "Polly": {
      "Retry": {
        "MaxAttempts": 3,
        "BaseDelay": "00:00:01",
        "UseExponentialBackoff": true,
        "UseJitter": true
      },
      "CircuitBreaker": {
        "FailureThreshold": 5,
        "BreakDuration": "00:01:00",
        "MinimumThroughput": 10
      },
      "RateLimit": {
        "PermitLimit": 100,
        "Window": "00:00:01"
      }
    }
  }
}

Persistence Extensions

WorkflowForge.Extensions.Persistence

Core workflow state persistence abstraction with bring-your-own-storage pattern.

Installation:

dotnet add package WorkflowForge.Extensions.Persistence

Features:

  • Abstract persistence layer
  • State snapshot and restoration
  • Pluggable storage providers
  • Metadata management
  • Operation state tracking

Usage:

using WorkflowForge.Extensions.Persistence;

// Implement custom storage provider
public class MyStorageProvider : IWorkflowStateStore
{
    public async Task SaveStateAsync(WorkflowState state, CancellationToken ct)
    {
        // Save to your database, file system, etc.
    }
    
    public async Task<WorkflowState?> LoadStateAsync(string workflowId, CancellationToken ct)
    {
        // Load from your storage
    }
}

// Use in workflow
var stateStore = new MyStorageProvider();
var foundry = WorkflowForge.CreateFoundry("PersistentWorkflow");
foundry.AddStateStore(stateStore);

WorkflowForge.Extensions.Persistence.Recovery

Resume interrupted workflows from saved state.

Installation:

dotnet add package WorkflowForge.Extensions.Persistence.Recovery

Features:

  • Automatic workflow resumption
  • Skip completed operations
  • State validation and integrity checks
  • Recovery point management
  • Failure recovery strategies

Usage:

using WorkflowForge.Extensions.Persistence;
using WorkflowForge.Extensions.Persistence.Recovery;

var stateStore = new MyStorageProvider();
var recoveryService = new WorkflowRecoveryService(stateStore);

// Attempt to recover workflow
var state = await recoveryService.LoadWorkflowStateAsync("workflow-123");
if (state != null && state.CanRecover)
{
    var foundry = WorkflowForge.CreateFoundry("RecoveredWorkflow");
    foundry.AddStateStore(stateStore);
    foundry.EnableRecovery(state);
    
    // Resume from last checkpoint
    await smith.ForgeAsync(workflow, foundry);
}

See Also:

  • Sample 18: Persistence (BYO Storage)
  • Sample 21: Recovery Only
  • Sample 22: Recovery + Resilience

Observability Extensions

WorkflowForge.Extensions.Observability.Performance

Comprehensive performance monitoring and metrics collection.

Installation:

dotnet add package WorkflowForge.Extensions.Observability.Performance

Features:

  • Operation timing and throughput metrics
  • Memory allocation tracking
  • Success/failure rate monitoring
  • Performance baseline establishment
  • Real-time statistics collection

Usage:

using WorkflowForge.Extensions.Observability.Performance;

var foundry = WorkflowForge.CreateFoundry("ProcessOrder");
foundry.EnablePerformanceMonitoring();

// Execute workflow
foundry.SetProperty("Order", order);
await smith.ForgeAsync(workflow, foundry);

// Analyze performance
var stats = foundry.GetPerformanceStatistics();
Console.WriteLine($"Total operations: {stats.TotalOperations}");
Console.WriteLine($"Success rate: {stats.SuccessRate:P2}");
Console.WriteLine($"Average duration: {stats.AverageDuration.TotalMilliseconds:F2}ms");
Console.WriteLine($"Operations/sec: {stats.OperationsPerSecond:F2}");

// Per-operation statistics
foreach (var opStats in stats.GetAllOperationStatistics())
{
    Console.WriteLine($"{opStats.OperationName}: {opStats.AverageDuration.TotalMilliseconds:F2}ms average");
}

Persistence (Bring Your Own Storage)

Enable resumable workflows without adding dependencies by providing your own storage implementation. Package: WorkflowForge.Extensions.Persistence

1) Implement the provider interface and plug it in via middleware:

using WorkflowForge.Extensions; // UsePersistence
using WorkflowForge.Extensions.Persistence.Abstractions; // IWorkflowPersistenceProvider
using WorkflowForge.Extensions.Persistence.Abstractions; // WorkflowExecutionSnapshot

public sealed class MyPersistenceProvider : IWorkflowPersistenceProvider
{
    public Task SaveAsync(WorkflowExecutionSnapshot snapshot, CancellationToken ct = default) => Task.CompletedTask;
    public Task<WorkflowExecutionSnapshot?> TryLoadAsync(Guid foundryExecutionId, Guid workflowId, CancellationToken ct = default) => Task.FromResult<WorkflowExecutionSnapshot?>(null);
    public Task DeleteAsync(Guid foundryExecutionId, Guid workflowId, CancellationToken ct = default) => Task.CompletedTask;
}

// Enable persistence
var provider = new MyPersistenceProvider();
using var foundry = WorkflowForge.CreateFoundry("OrderProcessing");
foundry.UsePersistence(provider);

This middleware checkpoints after each operation and attempts to resume by skipping already-completed operations. Snapshot data includes:

  • FoundryExecutionId, WorkflowId, WorkflowName
  • NextOperationIndex (next op to run)
  • Properties captured from the foundry

Note: You control storage and serialization. The core remains zero-dependency.

Important:

  • Use a shared provider (e.g., DB/file/queue-backed) for real resume across processes. The in-memory provider in samples is for demonstration only and does not persist across app restarts.
  • Keep snapshots minimal. Only store the properties required to resume safely.

2) Cross-process resume (stable keys):

using WorkflowForge.Extensions;
using WorkflowForge.Extensions.Persistence;

var options = new PersistenceOptions
{
  InstanceId = "order-service-west-1", // maps to a deterministic foundry key
  WorkflowKey = "ProcessOrder-v1"      // maps to a deterministic workflow key
};

using var foundry = WorkflowForge.CreateFoundry("OrderProcessing");
foundry.UsePersistence(provider, options);

Notes:

  • With stable keys and a shared provider, a new process can resume from the last successful step.
  • Properties saved in the snapshot are restored on resume before determining which step to execute next.

Recovery (Resume + Retry)

Package: WorkflowForge.Extensions.Persistence.Recovery

Adds simple recovery orchestration that first attempts to resume from a snapshot and then runs a fresh execution with configurable retries/backoff. Best used with stable InstanceId/WorkflowKey and a shared provider (DB, file share, cache) for cross-process/host resume.

using WorkflowForge.Extensions.Persistence.Recovery;

var foundryKey = DeterministicGuid(options.InstanceId!);
var workflowKey = DeterministicGuid(options.WorkflowKey!);

await smith.ForgeWithRecoveryAsync(
    workflow,
    foundry,
    provider,
    foundryKey,
    workflowKey,
    new RecoveryPolicy { MaxAttempts = 5, BaseDelay = TimeSpan.FromMilliseconds(50), UseExponentialBackoff = true },
    cancellationToken);

Key points:

  • Resume attempts restore foundry properties and skip completed steps.
  • After resume, a fresh execution is attempted with retries (policy).
  • If resume or execution ultimately fails, the last exception is surfaced to the caller.
Using Resilience With Recovery (Unified Experience)

You can combine base Resilience retry middleware with Recovery for a unified experience. The retry middleware handles transient failures during a single run; Recovery resumes from the last checkpoint across runs.

using WorkflowForge.Extensions.Resilience;
using WorkflowForge.Extensions.Persistence;
using WorkflowForge.Extensions.Persistence.Recovery;

var provider = new FilePersistenceProvider(checkpointPath);
var options = new PersistenceOptions { InstanceId = "svc-west-1", WorkflowKey = "Order-v1" };
var foundryKey = DeterministicGuid(options.InstanceId!);
var workflowKey = DeterministicGuid(options.WorkflowKey!);

using var foundry = WorkflowForge.CreateFoundry("OrderProcessing");
foundry.UsePersistence(provider, options);
foundry.AddMiddleware(
    RetryMiddleware.WithExponentialBackoff(foundry.Logger,
        initialDelay: TimeSpan.FromMilliseconds(50),
        maxDelay: TimeSpan.FromMilliseconds(500),
        maxAttempts: 2));

await smith.ForgeWithRecoveryAsync(
    workflow,
    foundry,
    provider,
    foundryKey,
    workflowKey,
    new RecoveryPolicy { MaxAttempts = 3, BaseDelay = TimeSpan.FromMilliseconds(100), UseExponentialBackoff = true },
    cancellationToken);

See interactive sample: Recovery + Resilience (menu 22) in src/samples/WorkflowForge.Samples.BasicConsole.

Catalog-driven recovery (multiple workflows): implement IRecoveryCatalog to enumerate pending snapshots, then use ResumeAllAsync:

public sealed class MyRecoveryCatalog : IRecoveryCatalog
{
    public Task<IReadOnlyList<WorkflowExecutionSnapshot>> ListPendingAsync(CancellationToken ct = default)
    {
        // Query your store for snapshots that need recovery
        return Task.FromResult<IReadOnlyList<WorkflowExecutionSnapshot>>(pendingList);
    }
}

var catalog = new MyRecoveryCatalog();
var coordinator = new RecoveryCoordinator(provider, new RecoveryPolicy { MaxAttempts = 3 });
int recovered = await coordinator.ResumeAllAsync(
    () => WorkflowForge.CreateFoundry("Service"),
    () => BuildWorkflow(),
    catalog,
    cancellationToken);

WorkflowForge.Extensions.Observability.HealthChecks

System health monitoring and diagnostics.

Installation:

dotnet add package WorkflowForge.Extensions.Observability.HealthChecks

Features:

  • Built-in health checks (memory, GC, thread pool)
  • Custom health check support
  • Health status aggregation
  • Integration with monitoring systems
  • Performance impact assessment

Usage:

using WorkflowForge.Extensions.Observability.HealthChecks;

var foundry = WorkflowForge.CreateFoundry("ProcessOrder");
var healthService = foundry.CreateHealthCheckService();
var result = await healthService.CheckHealthAsync();

Console.WriteLine($"Overall Status: {result.Status}");
Console.WriteLine($"Memory Usage: {result.Results["Memory"].Description}");
Console.WriteLine($"GC Health: {result.Results["GarbageCollector"].Description}");
Console.WriteLine($"Thread Pool: {result.Results["ThreadPool"].Description}");

// Custom health checks
foundry.AddHealthCheck("Database", async () =>
{
    var isHealthy = await CheckDatabaseConnectionAsync();
    return isHealthy ? HealthCheckResult.Healthy("Database connection OK") 
                     : HealthCheckResult.Unhealthy("Database connection failed");
});

WorkflowForge.Extensions.Observability.OpenTelemetry

Distributed tracing and telemetry using OpenTelemetry.

Installation:

dotnet add package WorkflowForge.Extensions.Observability.OpenTelemetry

Features:

  • Distributed tracing with span creation
  • Activity source integration
  • Custom metrics and events
  • Integration with observability backends (Jaeger, Zipkin, etc.)
  • Correlation context propagation

Usage:

using WorkflowForge.Extensions.Observability.OpenTelemetry;

var foundry = WorkflowForge.CreateFoundry("ProcessOrder");
foundry.EnableOpenTelemetry(new WorkflowForgeOpenTelemetryOptions
{
    ServiceName = "OrderService",
    ServiceVersion = "1.0.0"
});

// Create custom spans
using var activity = foundry.StartActivity("ProcessOrder")
    .SetTag("order.id", order.Id)
    .SetTag("customer.id", order.CustomerId);

// Execute with tracing
foundry.SetProperty("Order", order);
await smith.ForgeAsync(workflow, foundry);

// Add custom events
foundry.AddEvent("PaymentProcessed", new { 
    Amount = order.Amount, 
    PaymentMethod = order.PaymentMethod 
});

Extension Configuration Patterns

Environment-Specific Configuration

Prefer explicit configuration per environment using appsettings.{Environment}.json or programmatic options. This keeps extension behavior predictable and avoids preset helper methods that hide configuration details.

Configuration-Driven Setup

// appsettings.json
{
  "WorkflowForge": {
    "Extensions": {
      "Logging": {
        "Provider": "Serilog",
        "Configuration": { /* Serilog config */ }
      },
      "Resilience": {
        "Provider": "Polly",
        "Configuration": { /* Polly config */ }
      },
      "Observability": {
        "Performance": { "Enabled": true },
        "HealthChecks": { "Enabled": true },
        "OpenTelemetry": { 
          "Enabled": true,
          "ServiceName": "MyService",
          "ServiceVersion": "1.0.0"
        }
      }
    }
  }
}

// Configuration loading
var foundryConfig = configuration.GetSection("WorkflowForge");
var foundry = WorkflowForge.CreateFoundry("ProcessOrder")
    .ConfigureFromSection(foundryConfig);

Validation Extension

WorkflowForge.Extensions.Validation

Input validation and business rule enforcement using DataAnnotations.

Installation:

dotnet add package WorkflowForge.Extensions.Validation

Features:

  • DataAnnotations validation
  • Automatic middleware-based validation
  • Manual validation support
  • Comprehensive error reporting
  • Property-level error details
  • Validation result caching

Usage:

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; }
}

// Automatic validation via middleware (DataAnnotations)
var foundry = WorkflowForge.CreateFoundry("OrderProcessing");
foundry.UseValidation(
    f => f.GetPropertyOrDefault<Order>("Order"));

// Manual validation
var order = new Order { CustomerId = "CUST-123", Amount = 99.99m };
var result = await foundry.ValidateAsync(order);
if (!result.IsValid)
{
    foreach (var error in result.Errors)
    {
        Console.WriteLine($"{error.PropertyName}: {error.ErrorMessage}");
    }
}

Middleware Integration:

// Validation happens automatically before operation execution
var workflow = WorkflowForge.CreateWorkflow()
    .WithName("ValidatedWorkflow")
    .AddOperation(new ProcessOrderOperation())
    .Build();

// Validation errors are stored in foundry properties
var status = foundry.GetPropertyOrDefault<string>("Validation.ProcessOrder.Status");
var errors = foundry.GetPropertyOrDefault<IReadOnlyList<ValidationError>>(
    "Validation.ProcessOrder.Errors");

Configuration:

{
  "WorkflowForge": {
    "Validation": {
      "ThrowOnFailure": true,
      "CacheResults": false,
      "DetailedErrors": true
    }
  }
}

See Also:

  • Sample: src/samples/WorkflowForge.Samples.BasicConsole/Samples/ValidationSample.cs
  • Tests: tests/WorkflowForge.Extensions.Validation.Tests/
  • README: src/extensions/WorkflowForge.Extensions.Validation/README.md

Audit Extension

WorkflowForge.Extensions.Audit

Comprehensive audit logging for compliance and operational monitoring.

Installation:

dotnet add package WorkflowForge.Extensions.Audit

Features:

  • Automatic operation auditing
  • Pluggable storage providers (bring your own)
  • In-memory provider for testing
  • Immutable audit entries
  • Performance timing capture
  • Metadata enrichment
  • User context tracking

Usage:

using WorkflowForge.Extensions.Audit;

// Create audit provider (in-memory for demo)
var auditProvider = new InMemoryAuditProvider();

// Enable audit logging
var foundry = WorkflowForge.CreateFoundry("OrderProcessing");
foundry.EnableAudit(
    auditProvider,
    initiatedBy: "admin@company.com",
    includeMetadata: true);

// Workflow operations are automatically audited
var workflow = WorkflowForge.CreateWorkflow()
    .WithName("ProcessOrder")
    .AddOperation(new ValidateOrderOperation())
    .AddOperation(new ProcessPaymentOperation())
    .Build();

using var smith = WorkflowForge.CreateSmith();
await smith.ForgeAsync(workflow, foundry);

// Query audit entries
foreach (var entry in auditProvider.Entries)
{
    Console.WriteLine($"[{entry.Timestamp:HH:mm:ss.fff}] {entry.EventType} - " +
                     $"{entry.OperationName}: {entry.Status} ({entry.DurationMs}ms)");
}

// Custom audit entries
await foundry.WriteCustomAuditAsync(
    auditProvider,
    "ManualApproval",
    AuditEventType.Custom,
    "Approved",
    initiatedBy: "manager@company.com");

Custom Audit Provider:

public class DatabaseAuditProvider : IAuditProvider
{
    private readonly DbContext _context;

    public async Task WriteAuditEntryAsync(AuditEntry entry)
    {
        _context.AuditLog.Add(entry);
        await _context.SaveChangesAsync();
    }

    public async Task FlushAsync()
    {
        await _context.SaveChangesAsync();
    }
}

// Use custom provider
var auditProvider = new DatabaseAuditProvider(dbContext);
foundry.EnableAudit(auditProvider);

Audit Entry Structure:

public class AuditEntry
{
    public Guid Id { get; init; }
    public DateTimeOffset Timestamp { get; init; }
    public string WorkflowName { get; init; }
    public string OperationName { get; init; }
    public AuditEventType EventType { get; init; }
    public string Status { get; init; }
    public long? DurationMs { get; init; }
    public string InitiatedBy { get; init; }
    public IReadOnlyDictionary<string, string> Metadata { get; init; }
}

Event Types:

public enum AuditEventType
{
    OperationStarted,
    OperationCompleted,
    OperationFailed,
    Custom
}

Configuration:

{
  "WorkflowForge": {
    "Audit": {
      "Enabled": true,
      "IncludeMetadata": true,
      "DefaultInitiatedBy": "system"
    }
  }
}

See Also:

  • Sample: src/samples/WorkflowForge.Samples.BasicConsole/Samples/AuditSample.cs
  • Tests: tests/WorkflowForge.Extensions.Audit.Tests/
  • README: src/extensions/WorkflowForge.Extensions.Audit/README.md

Creating Custom Extensions

Extension Development Pattern

// 1. Define extension interface
public interface ICustomExtension
{
    Task<string> ProcessAsync(string input);
}

// 2. Create extension implementation
public class CustomExtensionImplementation : ICustomExtension
{
    private readonly CustomExtensionSettings _settings;

    public CustomExtensionImplementation(IOptions<CustomExtensionSettings> settings)
    {
        _settings = settings.Value;
    }

    public async Task<string> ProcessAsync(string input)
    {
        // Extension logic
        return await ProcessWithCustomLogicAsync(input);
    }
}

// 3. Create foundry extension method
public static class CustomExtensions
{
    public static IWorkflowFoundry UseCustomExtension(
        this IWorkflowFoundry foundry, 
        Action<CustomExtensionSettings>? configureOptions = null)
    {
        // Register services
        var services = foundry.ServiceProvider ?? new ServiceCollection().BuildServiceProvider();
        services.AddSingleton<ICustomExtension, CustomExtensionImplementation>();
        
        // Configure options
        if (configureOptions != null)
        {
            services.Configure(configureOptions);
        }

        // Return configured foundry
        return foundry.WithServiceProvider(services);
    }
}

// 4. Usage
var foundry = WorkflowForge.CreateFoundry("ProcessOrder")
    .UseCustomExtension(options =>
    {
        options.CustomSetting = "value";
        options.Timeout = TimeSpan.FromSeconds(30);
    });

Middleware-Based Extensions

public class CustomMiddleware : IWorkflowOperationMiddleware
{
    private readonly ICustomService _customService;

    public CustomMiddleware(ICustomService customService)
    {
        _customService = customService;
    }

    public async Task<object?> ExecuteAsync(
        IWorkflowOperation operation,
        IWorkflowFoundry foundry,
        object? inputData,
        Func<CancellationToken, Task<object?>> next,
        CancellationToken cancellationToken)
    {
        // Pre-execution logic
        await _customService.PreProcessAsync(operation, inputData);

        try
        {
            var result = await next(cancellationToken);
            
            // Post-execution logic
            await _customService.PostProcessAsync(operation, result);
            
            return result;
        }
        catch (Exception ex)
        {
            // Error handling
            await _customService.HandleErrorAsync(operation, ex);
            throw;
        }
    }
}

// Extension method
public static class CustomMiddlewareExtensions
{
    public static IWorkflowFoundry UseCustomMiddleware(this IWorkflowFoundry foundry)
    {
        return foundry.UseMiddleware<CustomMiddleware>();
    }
}

Extension Best Practices

1. Configuration Management

// Use strongly-typed configuration
public class ExtensionSettings
{
    public const string SectionName = "WorkflowForge:CustomExtension";
    
    public bool Enabled { get; set; } = true;
    public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
    public string ConnectionString { get; set; } = string.Empty;
}

// Register with DI container
services.Configure<ExtensionSettings>(
    configuration.GetSection(ExtensionSettings.SectionName));

2. Logging Integration

public class CustomExtension
{
    private readonly IWorkflowForgeLogger _logger;

    public CustomExtension(IWorkflowForgeLogger logger)
    {
        _logger = logger;
    }

    public async Task ProcessAsync()
    {
        _logger.LogInformation("Starting custom processing");
        
        try
        {
            // Processing logic
            _logger.LogInformation("Custom processing completed successfully");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Custom processing failed");
            throw;
        }
    }
}

3. Resource Management

public class CustomExtension : IDisposable
{
    private readonly IDisposableResource _resource;
    private bool _disposed = false;

    public CustomExtension()
    {
        _resource = CreateResource();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed && disposing)
        {
            _resource?.Dispose();
            _disposed = true;
        }
    }
}

4. Performance Considerations

public class PerformantExtension
{
    private static readonly ObjectPool<StringBuilder> StringBuilderPool = 
        new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());

    public async Task<string> ProcessAsync(string input)
    {
        var sb = StringBuilderPool.Get();
        try
        {
            // Use pooled StringBuilder
            sb.Append(input);
            // ... processing
            return sb.ToString();
        }
        finally
        {
            StringBuilderPool.Return(sb);
        }
    }
}

Extension Testing

Unit Testing Extensions

public class CustomExtensionTests
{
    [Fact]
    public async Task Should_Process_Input_Successfully()
    {
        // Arrange
        var mockLogger = new Mock<IWorkflowForgeLogger>();
        var settings = Options.Create(new CustomExtensionSettings());
        var extension = new CustomExtensionImplementation(settings);

        // Act
        var result = await extension.ProcessAsync("test input");

        // Assert
        Assert.NotNull(result);
        Assert.Contains("processed", result);
    }
}

Integration Testing

public class ExtensionIntegrationTests
{
    [Fact]
    public async Task Should_Integrate_With_Foundry_Successfully()
    {
        // Arrange
        var foundry = WorkflowForge.CreateFoundry("Test")
            .UseCustomExtension();

        var workflow = WorkflowForge.CreateWorkflow()
            .WithName("TestWorkflow")
            .AddOperation(new ActionWorkflowOperation(
                "TestOp",
                async (input, foundry, ct) =>
                {
                    var extension = foundry.ServiceProvider
                        ?.GetRequiredService<ICustomExtension>()
                        ?? throw new InvalidOperationException("Service provider is required.");
                    var value = foundry.GetPropertyOrDefault<string>("Input");
                    var result = await extension.ProcessAsync(value);
                    foundry.SetProperty("Result", result);
                }
            ))
            .Build();

        var smith = WorkflowForge.CreateSmith();

        // Act
        foundry.SetProperty("Input", "test");
        await smith.ForgeAsync(workflow, foundry);

        // Assert
        var result = foundry.GetPropertyOrDefault<string>("Result");
        Assert.NotNull(result);
    }
}