FlowSmith Core - Lightweight workflow engine with retry and compensation. Contains engine, contracts, and abstractions.
$ dotnet add package FlowSmith.CoreLightweight workflow engine for .NET with SQL Server persistence.
FlowSmith v0.1 is a minimal workflow execution engine that supports:
FlowSmith.Core - Shared contracts and domain models
Workflow, IFlowStep, IFlowBuilder)IInstanceStore, ILeaseStore, IStepExecutionStore)IWorkflowCommandService, IWorkflowQueryService)FlowSmith.Runtime - Execution runtime and persistence
AddFlowSmithRuntime(connectionString) - register all servicesMapFlowSmithRuntimeApi() - register HTTP API endpointsFlowSmith.Worker - Background worker library
AddFlowSmithWorker(connectionString) - register all servicesRegisterWorkflow<T>(name) - register workflow typesFlowSmith.UI - Monitoring interface (Blazor Server)
Located in examples/ folder:
FlowSmith.Examples - Sample workflows and worker host
AddFlowSmithWorker()FlowSmith.Api - REST API for commands and queries
AddFlowSmithRuntime()FlowSmith.AppHost - .NET Aspire orchestration
FlowSmith.ServiceDefaults - Aspire service defaults
UPDLOCK + READPAST + ROWLOCKIServiceProviderInstall Aspire workload:
dotnet workload install aspire
Ensure Docker Desktop is running (required for SQL Server)
Run AppHost:
dotnet run --project examples/FlowSmith.AppHost/FlowSmith.AppHost.csproj
Aspire will automatically:
Create database:
CREATE DATABASE FlowSmith;
Run schema script:
sqlcmd -S localhost -d FlowSmith -i database/schema.sql
Update connection strings in:
examples/FlowSmith.Examples/appsettings.json (Worker)examples/FlowSmith.Api/appsettings.json (API + UI)# Build solution
dotnet build FlowSmith.slnx
# Run tests
dotnet test FlowSmith.slnx
# Run API + UI (in one terminal)
cd examples/FlowSmith.Api
dotnet run
# Run worker (in separate terminal)
cd examples/FlowSmith.Examples
dotnet run
public class ReserveInventoryStep : IFlowStep
{
private readonly ILogger<ReserveInventoryStep> _logger;
public ReserveInventoryStep(ILogger<ReserveInventoryStep> logger)
{
_logger = logger;
}
public Task Execute(StepContext context)
{
var orderId = context.Data["orderId"];
var items = context.Data["items"];
_logger.LogInformation("Reserving inventory for order {OrderId}", orderId);
// Business logic here
var reservationId = Guid.NewGuid();
context.Data["reservationId"] = reservationId;
return Task.CompletedTask;
}
public Task Compensate(StepContext context)
{
var reservationId = context.Data.GetValueOrDefault("reservationId");
_logger.LogWarning("Compensation: Releasing inventory reservation {ReservationId}", reservationId);
// Reverse the reservation
return Task.CompletedTask;
}
}
public class OrderProcessingWorkflow : Workflow
{
public override void Build(IFlowBuilder builder)
{
builder
.AddStep<ValidateOrderStep>("ValidateOrder")
.WithRetry(maxAttempts: 3, backoffSeconds: 2)
.AddStep<ReserveInventoryStep>("ReserveInventory")
.WithRetry(maxAttempts: 3, backoffSeconds: 5)
.AddStep<ChargePaymentStep>("ChargePayment")
.WithRetry(maxAttempts: 2, backoffSeconds: 10)
.AddStep<SendConfirmationEmailStep>("SendConfirmation")
.WithRetry(maxAttempts: 2, backoffSeconds: 3);
}
}
In Worker Program.cs:
using FlowSmith.Worker.Extensions;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Register Worker
var connectionString = builder.Configuration.GetConnectionString("FlowSmithDb");
builder.Services.AddFlowSmithWorker(connectionString);
var host = builder.Build();
// Register workflow(s)
host.RegisterWorkflow<OrderProcessingWorkflow>("OrderProcessing");
// Or register multiple workflows:
host.RegisterWorkflows(registry =>
{
registry.Register<OrderProcessingWorkflow>("OrderProcessing");
registry.Register<UserRegistrationWorkflow>("UserRegistration");
});
host.Run();
Option A: Via REST API (recommended)
The FlowSmith.Api project demonstrates how to set up the API in just a few lines:
using FlowSmith.Runtime.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Register FlowSmith Runtime
var connectionString = builder.Configuration.GetConnectionString("FlowSmithDb");
builder.Services.AddFlowSmithRuntime(connectionString);
var app = builder.Build();
// Register API endpoints (default prefix: /api/workflows)
app.MapFlowSmithRuntimeApi();
app.Run();
Start workflow via HTTP:
curl -X POST "http://localhost:5000/api/workflows/start?workflowName=OrderProcessing" \
-H "Content-Type: application/json" \
-d '{"orderId": 12345, "customerEmail": "user@example.com", "items": ["item1", "item2"]}'
Option B: Via code
using FlowSmith.Runtime.Extensions;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddFlowSmithRuntime(connectionString);
var serviceProvider = services.BuildServiceProvider();
var commandService = serviceProvider.GetRequiredService<IWorkflowCommandService>();
var instanceId = await commandService.StartAsync(
"OrderProcessing",
new Dictionary<string, object>
{
["orderId"] = 12345,
["customerEmail"] = "user@example.com",
["items"] = new[] { "item1", "item2" }
});
Pending → Running → SucceededRunning → Compensating → CompensatedCompensating → CompensationFailed (terminal)Pending → Running → SucceededRunning → Failed (with retry)Succeeded → Compensated (during compensation)Use NSubstitute for mocking (never Moq):
[Fact]
public async Task StartAsync_CreatesInstance()
{
// Arrange
var store = Substitute.For<IInstanceStore>();
var service = new WorkflowCommandService(store, logger);
// Act
var instanceId = await service.StartAsync("TestWorkflow");
// Assert
await store.Received(1).CreateAsync(
Arg.Is<WorkflowInstance>(i => i.WorkflowName == "TestWorkflow"),
Arg.Any<CancellationToken>());
}
Update in appsettings.json:
{
"ConnectionStrings": {
"FlowSmithDb": "Server=localhost;Database=FlowSmith;Integrated Security=true;TrustServerCertificate=true;"
}
}
v0.1 (Current)
Future
See LICENSE file.