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
<PackageReference Include="Workflow.Core.Fluent" Version="8.1.1" />
<PackageVersion Include="Workflow.Core.Fluent" Version="8.1.1" />
<PackageReference Include="Workflow.Core.Fluent" />
paket add Workflow.Core.Fluent --version 8.1.1
#r "nuget: Workflow.Core.Fluent, 8.1.1"
#:package Workflow.Core.Fluent@8.1.1
#addin nuget:?package=Workflow.Core.Fluent&version=8.1.1
#tool nuget:?package=Workflow.Core.Fluent&version=8.1.1
๐ WorkflowCore
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
StepResultclass - โป 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
ExecuteAsyncimplementation 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:
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - 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
- GitHub: @slimbenbelgacem97
- Repository: Workflow
๐ Project Information
| Property | Value |
|---|---|
| Version | 8.1.0 |
| Target Framework | .NET 8.0 |
| License | MIT |
| Language | C# 12 |
| Status | โ Active Development |
๐ Useful Links
- GitHub Repository
- NuGet Package (coming soon)
- Report Issues
- Discussions
๐ Learning Resources
- Example Projects - Complete working examples
- Unit Tests - Test examples and patterns (coming soon)
| Product | Versions 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. |
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.0)
- Microsoft.Extensions.Hosting (>= 8.0.0)
- Microsoft.Extensions.Logging (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Initial release of WorkflowCore - A comprehensive workflow orchestration engine for .NET 8+