Workflow.Core.Fluent 8.1.1

dotnet add package Workflow.Core.Fluent --version 8.1.1
                    
NuGet\Install-Package Workflow.Core.Fluent -Version 8.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Workflow.Core.Fluent" Version="8.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Workflow.Core.Fluent" Version="8.1.1" />
                    
Directory.Packages.props
<PackageReference Include="Workflow.Core.Fluent" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Workflow.Core.Fluent --version 8.1.1
                    
#r "nuget: Workflow.Core.Fluent, 8.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Workflow.Core.Fluent@8.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Workflow.Core.Fluent&version=8.1.1
                    
Install as a Cake Addin
#tool nuget:?package=Workflow.Core.Fluent&version=8.1.1
                    
Install as a Cake Tool

๐Ÿ”„ WorkflowCore

.NET 8 License: MIT Version

A powerful and flexible workflow orchestration engine for .NET 8+ that provides strongly typed contexts, conditional step execution, sub-workflow composition, and comprehensive error tracking.

โœจ Features

  • ๐Ÿ”€ Conditional Step Execution - Steps execute based on context conditions
  • ๐Ÿงฉ Sub-Workflow Composition - Nest workflows within workflows for complex orchestrations
  • ๐Ÿ“Š Comprehensive Result Tracking - Detailed execution results with StepResult class
  • โ™ป Lifecycle Hooks - OnStart, OnComplete, OnError, OnSkipped events for workflows and steps
  • โšก Async/Await - Built on modern async patterns for high performance
  • ๐ŸŽจ Fluent API - Chainable methods for elegant workflow configuration
  • ๐Ÿ”ง Extensible Architecture - Easy to extend with custom steps and workflows
  • ๐Ÿ“ Rich Logging - Built-in integration with Microsoft.Extensions.Logging
  • โš ๏ธ Error Handling - Comprehensive error tracking with severity levels
  • ๐Ÿ” Workflow Reusability - Use workflows as steps in other workflows

๐Ÿ“ฆ Installation

NuGet Package Manager

Install-Package WorkflowCore

.NET CLI

dotnet add package WorkflowCore

Package Reference

<PackageReference Include="WorkflowCore" Version="8.1.0" />

๐Ÿš€ Quick Start

1. Create a Workflow Context

Define a context that holds your workflow data:

using WorkFlowEngine.Core;

public class DataProcessingContext
{
    public string InputData { get; set; } = string.Empty;
    public string ProcessedData { get; set; } = string.Empty;
    public bool IsDataValid { get; set; }
    public string OutputPath { get; set; } = string.Empty;
}

2. Create Workflow Steps

Implement IWorkflowStep<TContext> to create individual workflow steps:

using WorkFlowEngine.Core;
using WorkFlowEngine.Core.Abstractions;
using Microsoft.Extensions.Logging;

public class ValidateDataStep : IWorkflowStep<DataProcessingContext>
{
    private readonly ILogger<ValidateDataStep> _logger;

    public ValidateDataStep(ILogger<ValidateDataStep> logger)
    {
        _logger = logger;
    }

    public string Name => "ValidateData";

    public Task<bool> ShouldExecuteAsync(DataProcessingContext context)
    {
        return Task.FromResult(true); // Always execute validation
    }

    public async Task<StepResult> ExecuteStepWithResultAsync(
        DataProcessingContext context, 
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Validating data...");

        context.IsDataValid = !string.IsNullOrWhiteSpace(context.InputData);

        if (!context.IsDataValid)
        {
            return StepResult.Failure(
                stepName: Name,
                message: "Input data is empty",
                shouldContinue: false
            );
        }

        return StepResult.Success(Name, "Data validated successfully");
    }

    public Task OnStepStartAsync(DataProcessingContext context)
    {
        _logger.LogInformation("Starting validation step");
        return Task.CompletedTask;
    }

    public Task OnStepCompletedAsync(DataProcessingContext context, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Validation completed");
        return Task.CompletedTask;
    }

    public Task OnStepSkippedAsync(DataProcessingContext context, CancellationToken cancellationToken)
    {
        _logger.LogWarning("Validation skipped");
        return Task.CompletedTask;
    }

    public Task OnStepErrorAsync(DataProcessingContext context, Exception exception)
    {
        _logger.LogError(exception, "Validation error");
        return Task.CompletedTask;
    }
}

3. Create a Workflow

Implement IWorkflow<TContext> and chain your steps together:

using WorkFlowEngine.Core.Abstractions;
using WorkFlowEngine.Core.Extensions;
using Microsoft.Extensions.Logging;

public class DataProcessingWorkflow : IWorkflow<DataProcessingContext>
{
    private readonly ILogger<DataProcessingWorkflow> _logger;
    private readonly IServiceProvider _serviceProvider;

    public string Name => "DataProcessingWorkflow";
    public List<IWorkflowStep<DataProcessingContext>> Steps { get; set; }

    public DataProcessingWorkflow(
        ILogger<DataProcessingWorkflow> logger, 
        IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
        Steps = new List<IWorkflowStep<DataProcessingContext>>();
        ConfigureSteps();
    }

    private void ConfigureSteps()
    {
        // Fluent API for adding steps
        this.AddStep<DataProcessingContext, ValidateDataStep>(_serviceProvider)
            .AddStep<DataProcessingContext, ProcessDataStep>(_serviceProvider)
            .AddStep<DataProcessingContext, SaveDataStep>(_serviceProvider);
    }

    public Task OnWorkflowStartAsync(DataProcessingContext context, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting workflow: {WorkflowName}", Name);
        return Task.CompletedTask;
    }

    public Task OnWorkflowCompletedAsync(DataProcessingContext context, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Workflow completed: {WorkflowName}", Name);
        return Task.CompletedTask;
    }

    public Task OnWorkflowErrorAsync(DataProcessingContext context, Exception exception, CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Workflow error: {WorkflowName}", Name);
        return Task.CompletedTask;
    }
}

4. Register Services

Register workflows, steps, and contexts with dependency injection:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

// Register logging
services.AddLogging(builder => builder.AddConsole());

// Register steps
services.AddTransient<ValidateDataStep>();
services.AddTransient<ProcessDataStep>();
services.AddTransient<SaveDataStep>();

// Register workflow
services.AddTransient<DataProcessingWorkflow>();

// Register context
services.AddTransient<DataProcessingContext>();

var serviceProvider = services.BuildServiceProvider();

5. Execute the Workflow

Create a context and execute your workflow:

// Get the workflow from DI
var workflow = serviceProvider.GetRequiredService<DataProcessingWorkflow>();

// Create context with input data
var context = new DataProcessingContext
{
    InputData = "Sample data to process"
};

// Execute the workflow
await workflow.ExecuteAsync(context);

// Access results
Console.WriteLine($"Valid: {context.IsDataValid}");
Console.WriteLine($"Processed: {context.ProcessedData}");
Console.WriteLine($"Output: {context.OutputPath}");

๐ŸŽฏ Core Concepts

Workflows

A workflow is a collection of steps that execute sequentially on a shared context. Workflows implement IWorkflow<TContext>:

public interface IWorkflow<TContext> where TContext : class
{
    string Name { get; }
    List<IWorkflowStep<TContext>> Steps { get; set; }
    
    Task ExecuteAsync(TContext context, CancellationToken cancellationToken = default);
    Task OnWorkflowStartAsync(TContext context, CancellationToken cancellationToken);
    Task OnWorkflowCompletedAsync(TContext context, CancellationToken cancellationToken);
    Task OnWorkflowErrorAsync(TContext context, Exception exception, CancellationToken cancellationToken);
}

Key Features:

  • โœ… Default ExecuteAsync implementation handles step orchestration
  • โœ… Lifecycle hooks for monitoring and custom logic
  • โœ… Automatic error handling and propagation
  • โœ… Built-in cancellation token support

Steps

Steps are individual units of work. Each step implements IWorkflowStep<TContext>:

public interface IWorkflowStep<TContext> where TContext : class
{
    string Name { get; }
    
    Task<bool> ShouldExecuteAsync(TContext context);
    Task<StepResult> ExecuteStepWithResultAsync(TContext context, CancellationToken cancellationToken);
    
    Task OnStepStartAsync(TContext context);
    Task OnStepCompletedAsync(TContext context, CancellationToken cancellationToken);
    Task OnStepSkippedAsync(TContext context, CancellationToken cancellationToken);
    Task OnStepErrorAsync(TContext context, Exception exception);
}

Key Features:

  • โœ… Conditional execution via ShouldExecuteAsync
  • โœ… Rich result objects with StepResult
  • โœ… Lifecycle hooks for all execution states
  • โœ… Automatic error handling

Step Results

Every step returns a StepResult with detailed execution information:

public class StepResult
{
    public string StepName { get; set; }
    public bool IsSuccess { get; set; }
    public bool ShouldContinue { get; set; }
    public WorkflowError? Error { get; set; }
    public bool WasExecuted { get; set; }
    public string Message { get; set; }
    
    // Factory methods
    public static StepResult Success(string stepName, string message = "Completed successfully");
    public static StepResult Failure(string stepName, WorkflowError? error = null, string message = "Step failed");
    public static StepResult Skipped(string stepName, string reason = "Step skipped");
}

Error Handling

Comprehensive error tracking with severity levels:

public class WorkflowError
{
    public string StepName { get; set; }
    public string Message { get; set; }
    public Exception? Exception { get; set; }
    public DateTime Timestamp { get; set; }
    public WorkflowErrorSeverity Severity { get; set; }
}

public enum WorkflowErrorSeverity
{
    Info,      // Informational message
    Warning,   // Potential issue, can continue
    Error,     // Execution error, step stopped
    Critical   // Severe error, workflow stopped
}

๐Ÿงฉ Advanced Features

Conditional Step Execution

Steps can decide whether to execute based on context state:

public class ProcessDataStep : IWorkflowStep<DataProcessingContext>
{
    public Task<bool> ShouldExecuteAsync(DataProcessingContext context)
    {
        // Only process if validation passed
        return Task.FromResult(context.IsDataValid);
    }

    public async Task<StepResult> ExecuteStepWithResultAsync(
        DataProcessingContext context, 
        CancellationToken cancellationToken)
    {
        context.ProcessedData = context.InputData.ToUpper();
        return StepResult.Success(Name, "Data processed");
    }
    
    // ... lifecycle methods
}

Sub-Workflow Composition

Create complex workflows by composing simpler workflows as steps:

Step 1: Create Base Sub-Workflow Step Classes
// Inherit from BaseSubWorkflowStep
public class DataProcessingSubWorkflowStep 
    : BaseSubWorkflowStep<DataProcessingWorkflow, DataProcessingContext>
{
    private readonly ILogger<DataProcessingSubWorkflowStep> _logger;

    public DataProcessingSubWorkflowStep(
        IServiceProvider serviceProvider,
        ILogger<DataProcessingSubWorkflowStep> logger)
        : base(serviceProvider)
    {
        _logger = logger;
    }

    public override Task<bool> ShouldExecuteAsync(DataProcessingContext context)
    {
        // Always execute, or add conditional logic here
        return Task.FromResult(true);
    }

    public override Task OnStepStartAsync(DataProcessingContext context)
    {
        _logger.LogInformation("Starting sub-workflow");
        return Task.CompletedTask;
    }

    public override Task OnStepCompletedAsync(DataProcessingContext context, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Sub-workflow completed");
        return Task.CompletedTask;
    }

    public override Task OnStepSkippedAsync(DataProcessingContext context, CancellationToken cancellationToken)
    {
        _logger.LogWarning("Sub-workflow skipped");
        return Task.CompletedTask;
    }

    public override Task OnStepErrorAsync(DataProcessingContext context, Exception exception)
    {
        _logger.LogError(exception, "Sub-workflow error");
        return Task.CompletedTask;
    }
}
Step 2: Use in Parent Workflow
public class ParentWorkflow : IWorkflow<DataProcessingContext>
{
    private void ConfigureSteps()
    {
        this.AddStep<DataProcessingContext, ValidateDataStep>(_serviceProvider)
            .AddStep<DataProcessingContext, DataProcessingSubWorkflowStep>(_serviceProvider)  // Sub-workflow!
            .AddStep<DataProcessingContext, SaveDataStep>(_serviceProvider);
    }
}
Step 3: Register All Components
services.AddTransient<ValidateDataStep>();
services.AddTransient<SaveDataStep>();
services.AddTransient<DataProcessingWorkflow>();       // The sub-workflow
services.AddTransient<DataProcessingSubWorkflowStep>(); // The wrapper step
services.AddTransient<ParentWorkflow>();                // The parent workflow

Conditional Sub-Workflows

You can create conditional sub-workflows by implementing ShouldExecuteAsync in your sub-workflow step:

public class ConditionalDataProcessingSubWorkflowStep 
    : BaseSubWorkflowStep<DataProcessingWorkflow, DataProcessingContext>
{
    private readonly ILogger<ConditionalDataProcessingSubWorkflowStep> _logger;

    public ConditionalDataProcessingSubWorkflowStep(
        IServiceProvider serviceProvider,
        ILogger<ConditionalDataProcessingSubWorkflowStep> logger)
        : base(serviceProvider)
    {
        _logger = logger;
    }

    public override Task<bool> ShouldExecuteAsync(DataProcessingContext context)
    {
        // Execute sub-workflow only if data is valid
        return Task.FromResult(context.IsDataValid);
    }

    // Implement lifecycle methods...
}

๐Ÿ“Š Logging Integration

Seamless integration with Microsoft.Extensions.Logging:

public class MyStep : IWorkflowStep<MyContext>
{
    private readonly ILogger<MyStep> _logger;

    public MyStep(ILogger<MyStep> logger)
    {
        _logger = logger;
    }

    public async Task<StepResult> ExecuteStepWithResultAsync(
        MyContext context, 
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Processing step {StepName}", Name);
        
        try
        {
            // Step logic
            var result = await ProcessAsync(context);
            _logger.LogDebug("Step completed with result: {Result}", result);
            
            return StepResult.Success(Name);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Step failed: {StepName}", Name);
            throw;
        }
    }
}

๐Ÿ”ง Dependency Injection Patterns

Register Workflows and Steps

Create an extension method for clean registration:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddLightweightWorkflows(this IServiceCollection services)
    {
        // Register steps with interfaces
        services.AddTransient<IWorkflowStep<DataProcessingContext>, ValidateDataStep>();
        services.AddTransient<IWorkflowStep<DataProcessingContext>, ProcessDataStep>();
        services.AddTransient<IWorkflowStep<DataProcessingContext>, SaveDataStep>();

        // Also register concrete types
        services.AddTransient<ValidateDataStep>();
        services.AddTransient<ProcessDataStep>();
        services.AddTransient<SaveDataStep>();

        // Register sub-workflow steps
        services.AddTransient<DataProcessingSubWorkflowStep>();
        services.AddTransient<ConditionalDataProcessingSubWorkflowStep>();

        // Register workflows
        services.AddTransient<IWorkflow<DataProcessingContext>, DataProcessingWorkflow>();
        services.AddTransient<DataProcessingWorkflow>();
        services.AddTransient<ParentWorkflow>();
        services.AddTransient<AdvancedCompositeWorkflow>();

        // Register context
        services.AddTransient<DataProcessingContext>();

        return services;
    }
}

Use in ASP.NET Core

var builder = WebApplication.CreateBuilder(args);

// Add workflow engine
builder.Services.AddLightweightWorkflows();
builder.Services.AddControllers();

var app = builder.Build();

// Execute workflow in API endpoint
app.MapPost("/process", async (
    [FromServices] DataProcessingWorkflow workflow,
    [FromBody] ProcessRequest request) =>
{
    var context = new DataProcessingContext 
    { 
        InputData = request.Data 
    };
    
    await workflow.ExecuteAsync(context);
    
    return Results.Ok(new
    {
        Success = context.IsDataValid,
        ProcessedData = context.ProcessedData,
        OutputPath = context.OutputPath
    });
});

app.Run();

Use in Console Applications

using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        services.AddLightweightWorkflows();
        services.AddLogging(builder => builder.AddConsole());
    })
    .Build();

var workflow = host.Services.GetRequiredService<DataProcessingWorkflow>();
var context = new DataProcessingContext { InputData = "Sample data" };

await workflow.ExecuteAsync(context);

๐Ÿ“š Complete Examples

Example 1: Data Processing Pipeline

// Context
public class DataContext
{
    public string RawData { get; set; } = string.Empty;
    public string CleanedData { get; set; } = string.Empty;
    public string TransformedData { get; set; } = string.Empty;
    public bool IsValid { get; set; }
    public Dictionary<string, object> Metadata { get; set; } = new();
}

// Step 1: Validate
public class ValidateStep : IWorkflowStep<DataContext>
{
    public string Name => "Validate";
    
    public Task<bool> ShouldExecuteAsync(DataContext context) 
        => Task.FromResult(!string.IsNullOrEmpty(context.RawData));

    public async Task<StepResult> ExecuteStepWithResultAsync(
        DataContext context, 
        CancellationToken cancellationToken)
    {
        context.IsValid = context.RawData.Length > 10;
        
        return context.IsValid
            ? StepResult.Success(Name, "Validation passed")
            : StepResult.Failure(Name, message: "Data too short", shouldContinue: false);
    }
    
    // ... lifecycle methods
}

// Step 2: Clean
public class CleanStep : IWorkflowStep<DataContext>
{
    public string Name => "Clean";
    
    public Task<bool> ShouldExecuteAsync(DataContext context) 
        => Task.FromResult(context.IsValid);

    public async Task<StepResult> ExecuteStepWithResultAsync(
        DataContext context, 
        CancellationToken cancellationToken)
    {
        context.CleanedData = context.RawData.Trim().ToLower();
        context.Metadata["cleaned_at"] = DateTime.UtcNow;
        
        return StepResult.Success(Name, "Data cleaned");
    }
    
    // ... lifecycle methods
}

// Step 3: Transform
public class TransformStep : IWorkflowStep<DataContext>
{
    public string Name => "Transform";
    
    public Task<bool> ShouldExecuteAsync(DataContext context) 
        => Task.FromResult(!string.IsNullOrEmpty(context.CleanedData));

    public async Task<StepResult> ExecuteStepWithResultAsync(
        DataContext context, 
        CancellationToken cancellationToken)
    {
        context.TransformedData = $"[PROCESSED] {context.CleanedData.ToUpper()}";
        context.Metadata["transformed_at"] = DateTime.UtcNow;
        
        return StepResult.Success(Name, "Data transformed");
    }
    
    // ... lifecycle methods
}

// Workflow
public class DataPipeline : IWorkflow<DataContext>
{
    public string Name => "DataPipeline";
    public List<IWorkflowStep<DataContext>> Steps { get; set; }

    public DataPipeline(IServiceProvider serviceProvider)
    {
        Steps = new List<IWorkflowStep<DataContext>>();
        
        this.AddStep<DataContext, ValidateStep>(serviceProvider)
            .AddStep<DataContext, CleanStep>(serviceProvider)
            .AddStep<DataContext, TransformStep>(serviceProvider);
    }
    
    // ... lifecycle methods
}

// Usage
var context = new DataContext { RawData = "  Sample Input Data  " };
await pipeline.ExecuteAsync(context);

Console.WriteLine($"Valid: {context.IsValid}");
Console.WriteLine($"Cleaned: {context.CleanedData}");
Console.WriteLine($"Transformed: {context.TransformedData}");

Example 2: Order Processing with Error Handling

public class OrderProcessingStep : IWorkflowStep<OrderContext>
{
    public async Task<StepResult> ExecuteStepWithResultAsync(
        OrderContext context, 
        CancellationToken cancellationToken)
    {
        try
        {
            // Process order
            await ProcessOrderAsync(context);
            return StepResult.Success(Name, "Order processed");
        }
        catch (ValidationException ex)
        {
            var error = new WorkflowError
            {
                StepName = Name,
                Message = "Validation failed",
                Exception = ex,
                Severity = WorkflowErrorSeverity.Warning
            };
            
            // Continue workflow with warning
            return StepResult.Failure(Name, error, shouldContinue: true);
        }
        catch (Exception ex)
        {
            var error = new WorkflowError
            {
                StepName = Name,
                Message = "Critical error",
                Exception = ex,
                Severity = WorkflowErrorSeverity.Critical
            };
            
            // Stop workflow
            return StepResult.Failure(Name, error, shouldContinue: false);
        }
    }
    
    // ... lifecycle methods
}

๐Ÿงช Testing

Unit Testing Steps

using Xunit;
using Moq;

public class ValidateDataStepTests
{
    [Fact]
    public async Task ValidData_ReturnsSuccess()
    {
        // Arrange
        var logger = Mock.Of<ILogger<ValidateDataStep>>();
        var step = new ValidateDataStep(logger);
        var context = new DataProcessingContext { InputData = "Valid data" };

        // Act
        var result = await step.ExecuteStepWithResultAsync(
            context, 
            CancellationToken.None
        );

        // Assert
        Assert.True(result.IsSuccess);
        Assert.True(context.IsDataValid);
        Assert.Equal("ValidateData", result.StepName);
    }

    [Fact]
    public async Task EmptyData_ReturnsFailure()
    {
        // Arrange
        var logger = Mock.Of<ILogger<ValidateDataStep>>();
        var step = new ValidateDataStep(logger);
        var context = new DataProcessingContext { InputData = "" };

        // Act
        var result = await step.ExecuteStepWithResultAsync(
            context, 
            CancellationToken.None
        );

        // Assert
        Assert.False(result.IsSuccess);
        Assert.False(context.IsDataValid);
        Assert.False(result.ShouldContinue);
    }

    [Fact]
    public async Task InvalidData_SkipsProcessing()
    {
        // Arrange
        var logger = Mock.Of<ILogger<ProcessDataStep>>();
        var step = new ProcessDataStep(logger);
        var context = new DataProcessingContext { IsDataValid = false };

        // Act
        var shouldExecute = await step.ShouldExecuteAsync(context);

        // Assert
        Assert.False(shouldExecute);
    }
}

Integration Testing Workflows

using Xunit;
using Microsoft.Extensions.DependencyInjection;

public class DataProcessingWorkflowTests
{
    [Fact]
    public async Task CompleteWorkflow_ProcessesDataSuccessfully()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddTransient<ValidateDataStep>();
        services.AddTransient<ProcessDataStep>();
        services.AddTransient<SaveDataStep>();
        services.AddTransient<DataProcessingWorkflow>();
        
        var serviceProvider = services.BuildServiceProvider();
        var workflow = serviceProvider.GetRequiredService<DataProcessingWorkflow>();
        var context = new DataProcessingContext { InputData = "Test data" };

        // Act
        await workflow.ExecuteAsync(context);

        // Assert
        Assert.True(context.IsDataValid);
        Assert.NotEmpty(context.ProcessedData);
        Assert.NotEmpty(context.OutputPath);
        Assert.Contains("TEST DATA", context.ProcessedData); // Uppercased
    }

    [Fact]
    public async Task InvalidInput_StopsWorkflow()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddTransient<ValidateDataStep>();
        services.AddTransient<ProcessDataStep>();
        services.AddTransient<SaveDataStep>();
        services.AddTransient<DataProcessingWorkflow>();
        
        var serviceProvider = services.BuildServiceProvider();
        var workflow = serviceProvider.GetRequiredService<DataProcessingWorkflow>();
        var context = new DataProcessingContext { InputData = "" };

        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(
            async () => await workflow.ExecuteAsync(context)
        );
    }
}

๐ŸŽจ Best Practices

1. Keep Steps Focused (Single Responsibility)

โœ… GOOD - Separate concerns:

- ValidateDataStep      // Only validates
- CleanDataStep         // Only cleans
- TransformDataStep     // Only transforms
- SaveDataStep          // Only saves

โŒ BAD - One mega step:

- ProcessEverythingStep // Does validation, cleaning, transforming, saving

2. Use Strongly Typed Contexts

โœ… GOOD - Clear, strongly typed:

public class OrderContext
{
    public Order Order { get; set; }
    public Customer Customer { get; set; }
    public PaymentInfo Payment { get; set; }
    public List<ValidationResult> ValidationResults { get; set; }
}

โŒ BAD - Untyped dictionary:

public class Context
{
    public Dictionary<string, object> Data { get; set; }
}

3. Handle Errors Gracefully

โœ… GOOD - Comprehensive error information:

return StepResult.Failure(
    Name,
    new WorkflowError
    {
        StepName = Name,
        Message = $"Validation failed: {string.Join(", ", errors)}",
        Exception = ex,
        Severity = WorkflowErrorSeverity.Error,
        Timestamp = DateTime.UtcNow
    },
    shouldContinue: false
);

โŒ BAD - Generic exception:

throw new Exception("Error");

4. Use Dependency Injection

โœ… GOOD - Constructor injection:

public class MyStep : IWorkflowStep<MyContext>
{
    private readonly ILogger _logger;
    private readonly IDataService _dataService;
    private readonly IValidator _validator;

    public MyStep(
        ILogger<MyStep> logger, 
        IDataService dataService,
        IValidator validator)
    {
        _logger = logger;
        _dataService = dataService;
        _validator = validator;
    }
}

โŒ BAD - Hard-coded dependencies:

public class MyStep : IWorkflowStep<MyContext>
{
    private readonly ILogger _logger = new ConsoleLogger();
    private readonly IDataService _dataService = new DataService();
}

5. Implement All Lifecycle Hooks

โœ… GOOD - Comprehensive logging and monitoring:

public async Task OnStepStartAsync(MyContext context)
{
    _logger.LogInformation("Starting step {StepName} for {EntityId}", 
        Name, context.EntityId);
    _metrics.IncrementStepStarted(Name);
    context.Metadata[$"{Name}_StartTime"] = DateTime.UtcNow;
}

public async Task OnStepCompletedAsync(MyContext context, CancellationToken cancellationToken)
{
    var startTime = (DateTime)context.Metadata[$"{Name}_StartTime"];
    var duration = DateTime.UtcNow - startTime;
    
    _logger.LogInformation("{StepName} completed in {Duration}ms", 
        Name, duration.TotalMilliseconds);
    _metrics.RecordStepDuration(Name, duration);
}

public async Task OnStepErrorAsync(MyContext context, Exception exception)
{
    _logger.LogError(exception, "{StepName} failed for {EntityId}: {Message}", 
        Name, context.EntityId, exception.Message);
    _metrics.IncrementStepError(Name);
    _alerting.SendAlert($"Step {Name} failed", exception);
}

6. Use Conditional Execution Wisely

โœ… GOOD - Clear conditions:

public Task<bool> ShouldExecuteAsync(OrderContext context)
{
    var shouldExecute = context.IsValid 
        && context.Order.TotalAmount > 100 
        && context.Customer.IsPremium;
        
    _logger.LogDebug("Should execute {StepName}: {ShouldExecute}", 
        Name, shouldExecute);
        
    return Task.FromResult(shouldExecute);
}

โŒ BAD - Complex nested logic:

public Task<bool> ShouldExecuteAsync(OrderContext context)
{
    return Task.FromResult(
        context.IsValid ? (context.Order != null ? 
            (context.Order.TotalAmount > 100 ? 
                (context.Customer?.IsPremium ?? false) : false) : false) : false
    );
}

7. Document Your Workflows

โœ… GOOD - Clear documentation:

/// <summary>
/// Order processing workflow that validates, processes payment, and fulfills orders.
/// 
/// Steps:
/// 1. ValidateOrderStep - Validates order data and inventory
/// 2. ProcessPaymentStep - Processes payment (skipped if payment method is invoice)
/// 3. FulfillOrderStep - Creates fulfillment tasks
/// 4. NotifyCustomerStep - Sends confirmation email
/// 
/// Expected Context:
/// - Order with line items
/// - Customer information
/// - Payment details
/// 
/// Outputs:
/// - Order confirmation number
/// - Fulfillment tracking information
/// </summary>
public class OrderProcessingWorkflow : IWorkflow<OrderContext>
{
    // ...
}

๐Ÿ” Troubleshooting

Common Issues

1. Step Not Executing

Problem: Step is being skipped unexpectedly

Solution: Check ShouldExecuteAsync implementation and log why:

public Task<bool> ShouldExecuteAsync(MyContext context)
{
    var shouldExecute = context.IsValid && !context.IsComplete;
    
    if (!shouldExecute)
    {
        _logger.LogWarning(
            "Step {StepName} skipped. IsValid: {IsValid}, IsComplete: {IsComplete}",
            Name, context.IsValid, context.IsComplete
        );
    }
    
    return Task.FromResult(shouldExecute);
}
2. Dependency Injection Resolution Errors

Problem: Cannot resolve workflow or step from DI container

Solution: Ensure all types are registered:

// Register the concrete type
services.AddTransient<MyStep>();

// AND register with interface if needed
services.AddTransient<IWorkflowStep<MyContext>, MyStep>();

// AND register all dependencies
services.AddTransient<IDataService, DataService>();
services.AddTransient<IValidator, Validator>();
3. Sub-Workflow Registration Issues

Problem: "Cannot instantiate implementation type" error

Solution: Register concrete implementations, not abstract base classes:

โŒ BAD - Trying to register abstract class:
services.AddTransient(typeof(BaseSubWorkflowStep<,>));

โœ… GOOD - Register concrete implementation:
services.AddTransient<DataProcessingSubWorkflowStep>();
4. Context Not Shared Between Steps

Problem: Changes in one step not visible in next step

Solution: Ensure context is a reference type (class) and same instance is used:

โœ… GOOD:
public class MyContext  // Reference type
{
    public string Data { get; set; }
}

โŒ BAD:
public struct MyContext  // Value type - will be copied!
{
    public string Data { get; set; }
}
5. Async/Await Issues

Problem: Deadlocks or async methods not awaited

Solution: Always use async/await properly:

โœ… GOOD:
public async Task<StepResult> ExecuteStepWithResultAsync(
    MyContext context, 
    CancellationToken cancellationToken)
{
    var result = await _service.ProcessAsync(context.Data);
    return StepResult.Success(Name);
}

โŒ BAD:
public async Task<StepResult> ExecuteStepWithResultAsync(
    MyContext context, 
    CancellationToken cancellationToken)
{
    var result = _service.ProcessAsync(context.Data).Result; // Deadlock risk!
    return StepResult.Success(Name);
}

๐Ÿ“– API Reference

Core Interfaces

Interface Description
IWorkflow<TContext> Defines a workflow that executes steps on a context
IWorkflowStep<TContext> Defines an individual workflow step

Core Classes

Class Description
StepResult Result of step execution with success/failure info
WorkflowError Error information with severity levels
BaseSubWorkflowStep<TWorkflow, TContext> Base class for using workflows as steps

Extension Methods

Method Description
AddStep<TContext, TStep>(IServiceProvider) Add step to workflow with DI resolution

Enums

Enum Values
WorkflowErrorSeverity Info, Warning, Error, Critical

๐Ÿค Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

Development Guidelines

  • Follow C# coding conventions
  • Add XML documentation comments
  • Include unit tests for new features
  • Update README with new features
  • Ensure all tests pass before submitting PR

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.


๐Ÿ‘ค Author

Slim Ben Belgacem

๐Ÿ“Š Project Information

Property Value
Version 8.1.0
Target Framework .NET 8.0
License MIT
Language C# 12
Status โœ… Active Development


๐ŸŽ“ Learning Resources

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
8.1.1 533 3/23/2026
8.1.0 1,388 10/6/2025

Initial release of WorkflowCore - A comprehensive workflow orchestration engine for .NET 8+