FlowOrchestrator.Testing
1.21.0
See the version list below for details.
dotnet add package FlowOrchestrator.Testing --version 1.21.0
NuGet\Install-Package FlowOrchestrator.Testing -Version 1.21.0
<PackageReference Include="FlowOrchestrator.Testing" Version="1.21.0" />
<PackageVersion Include="FlowOrchestrator.Testing" Version="1.21.0" />
<PackageReference Include="FlowOrchestrator.Testing" />
paket add FlowOrchestrator.Testing --version 1.21.0
#r "nuget: FlowOrchestrator.Testing, 1.21.0"
#:package FlowOrchestrator.Testing@1.21.0
#addin nuget:?package=FlowOrchestrator.Testing&version=1.21.0
#tool nuget:?package=FlowOrchestrator.Testing&version=1.21.0
FlowOrchestrator
Code-first DAG orchestration for .NET. Runs on Hangfire β or in-process with zero infrastructure.
π Documentation Β· NuGet Β· GitHub
What's new in v1.19 β
AddFlowOrchestratorHealthChecks()for storage reachability probes, plus two new docs: Versioning Flows in Production and Production Deployment Checklist. v1.18 shippedWaitForSignalfor human-in-loop workflows; v1.17 shippedWhenconditions onRunAfter. Full CHANGELOG.
When to choose FlowOrchestrator
β Choose FlowOrchestrator if:
- You want multi-step DAGs in .NET without standing up a separate workflow server
- Your team writes C# and wants flows defined as plain code, not JSON or a designer
- You need conditional branching (
When), polling, fan-out (ForEach), human-in-loop (WaitForSignal), and cron in one library - You want a built-in dashboard with Timeline, DAG, and Gantt views
- You want flows that are unit-testable in-process (
FlowTestHost) and renderable as Mermaid diagrams in a PR - You already use Hangfire β or you want zero infrastructure at all (in-process runtime works without Hangfire or a database)
β Choose something else if:
- You need multi-language workflows (Python + Go + .NET) β Temporal
- You want replay-based deterministic execution β Temporal
- You're running a service mesh and want workflow as one of several building blocks β Dapr Workflows
- Non-developers need to author workflows in a visual designer β Elsa Workflows
- You only need fire-and-forget background jobs with no DAG β Hangfire alone
FlowOrchestrator is intentionally narrow. It is the DAG layer Hangfire is missing β nothing more, nothing less.
How it compares
| Hangfire | FlowOrchestrator | Elsa v3 | Temporal .NET | Dapr Workflows | |
|---|---|---|---|---|---|
| Background job execution | β | β (via Hangfire) | β | β | β |
Multi-step DAG with runAfter |
Manual | β | β | Implicit (code) | Implicit (code) |
| Polling pattern (no thread block) | Manual | β built-in | β | β durable timers | β durable timers |
| Code-first C# definitions | β | β | β | β | β |
| JSON / YAML workflow files | β | β by design | β | β | β |
| Visual designer | β | β by design | β Studio | β | β |
| Built-in DAG / Gantt / Timeline UI | β | β | β Studio | β Web UI | β |
| Polyglot SDK | .NET only | .NET only | .NET only | Go, Java, TS, Python, .NET | .NET, Python, JS, Java, Go |
| Separate server / sidecar required | β | β | Optional | β Required | β Sidecar |
| Storage you already have | SQL Server, PG, Redis | SQL Server, PG | SQL Server, PG, MongoDB | Cassandra, MySQL, PG | State store of choice |
| Deterministic replay | β | β | β | β | β |
| External signals / human-in-loop | β | β WaitForSignal |
β | β | β |
| Operational complexity | Low | Low | LowβMedium | High | Medium |
| Learning curve (.NET dev) | Low | Low | Medium | MediumβHigh | Medium |
FlowOrchestrator deliberately ships fewer features than Temporal or Dapr Workflows. It does not replay. It does not run a separate server. It is for teams that want DAG orchestration inside their existing ASP.NET Core app β alongside Hangfire if they have it, or fully in-process if they do not.
Comparison verified 2026-04-30 against Elsa v3, Temporal .NET SDK v1, Dapr .NET SDK v1. PRs welcome to keep it current.
Coming from Hangfire?
// Before β recurring job with manual chaining, no DAG, no run history
RecurringJob.AddOrUpdate<NightlyOrdersJob>("nightly-orders",
job => job.RunAsync(), "0 2 * * *");
// Inside RunAsync: call FetchOrders, then SubmitToWms, then NotifySlack.
// Error branching, retry-per-step, run history, Gantt view β all on you.
// After β FlowOrchestrator declarative manifest
public sealed class NightlyOrdersFlow : IFlowDefinition
{
public Guid Id { get; } = new("a1b2c3d4-0000-0000-0000-000000000001");
public FlowManifest Manifest { get; set; } = new()
{
Triggers = { ["cron"] = new() { Type = TriggerType.Cron,
Inputs = { ["cronExpression"] = "0 2 * * *" } } },
Steps = {
["fetch"] = new() { Type = "FetchOrders" },
["submit"] = new() { Type = "SubmitToWms",
RunAfter = { ["fetch"] = [StepStatus.Succeeded] } },
["notify"] = new() { Type = "NotifySlack",
RunAfter = { ["submit"] = [StepStatus.Succeeded] } }
}
};
}
// Dashboard, per-step retry, full run history, DAG view β included.
And yes β your flows are testable. See FlowOrchestrator.Testing for a one-liner test host that runs flows in-process without Hangfire or ASP.NET.
Coming from Temporal or Dapr?
If you don't need replay-based determinism and a separate cluster, here is the simpler model:
// Temporal .NET β deterministic replay; requires Temporal Server cluster
[Workflow]
public class OrderWorkflow
{
[WorkflowRun]
public async Task RunAsync(string orderId)
{
await Workflow.ExecuteActivityAsync(
(Activities a) => a.FetchOrderAsync(orderId),
new() { ScheduleToCloseTimeout = TimeSpan.FromMinutes(5) });
await Workflow.ExecuteActivityAsync(
(Activities a) => a.SubmitToWmsAsync(orderId),
new() { ScheduleToCloseTimeout = TimeSpan.FromMinutes(5) });
}
}
// Requires: Temporal Server (Cassandra / MySQL / PG + Elasticsearch + server cluster)
// FlowOrchestrator β same outcome, runs inside your existing ASP.NET Core app
public sealed class OrderFlow : IFlowDefinition
{
public Guid Id { get; } = new("a1b2c3d4-0000-0000-0000-000000000002");
public FlowManifest Manifest { get; set; } = new()
{
Triggers = { ["manual"] = new() { Type = TriggerType.Manual } },
Steps = {
["fetch"] = new() { Type = "FetchOrder" },
["submit"] = new() { Type = "SubmitToWms",
RunAfter = { ["fetch"] = [StepStatus.Succeeded] } }
}
};
}
// Requires: SQL Server or PostgreSQL you already have, plus Hangfire.
Why FlowOrchestrator?
- Zero new infrastructure β runs inside your existing Hangfire app on SQL Server or PostgreSQL.
- Code-first, always β flows are plain C# classes; no YAML, no JSON files, no designer to learn.
- Built-in dashboard β Timeline, DAG, and Gantt views with retry, cancel, and re-run controls.
- Runtime-agnostic core β swap Hangfire for in-process channels (or any future adapter) without touching flow definitions.
Pick a runtime
FlowOrchestrator separates storage (where flow definitions and run history live) from the runtime adapter (which dispatches and executes steps).
| Hangfire runtime | InMemory runtime | |
|---|---|---|
| Step dispatcher | IBackgroundJobClient |
Channel<T> inside the host process |
| Cron triggers | IRecurringJobManager (multi-instance safe) |
PeriodicTimer (single-instance only) |
| Survives process restart | β (jobs in Hangfire storage) | β (in-memory queue) |
| Multi-instance horizontal scale | β | β |
| Extra infrastructure | Hangfire + SQL Server / PostgreSQL | None |
| Best for | Production workloads | Local dev, integration tests, single-node side projects |
Storage is independent β InMemory storage works only for dev / tests, while SQL Server and PostgreSQL are production-ready under either runtime.
Install
dotnet add package FlowOrchestrator.Core
# Runtime adapter β pick one
dotnet add package FlowOrchestrator.Hangfire # Hangfire-backed (production default)
dotnet add package FlowOrchestrator.InMemory # In-process Channel<T> (dev / testing / single-node)
# Storage backend β pick one
dotnet add package FlowOrchestrator.SqlServer # or FlowOrchestrator.PostgreSQL
# FlowOrchestrator.InMemory ships its own storage too
# Optional
dotnet add package FlowOrchestrator.Dashboard # REST API + SPA dashboard
dotnet add package FlowOrchestrator.Testing # FlowTestHost β in-process integration test helper
Quick Start β SQL Server + Hangfire
// Program.cs
builder.Services.AddHangfire(c => c
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(connectionString));
builder.Services.AddHangfireServer();
builder.Services.AddFlowOrchestrator(options =>
{
options.UseSqlServer(connectionString); // persist + auto-migrate tables
options.UseHangfire(); // Hangfire step dispatcher
options.AddFlow<OrderFulfillmentFlow>();
});
builder.Services.AddStepHandler<FetchOrdersStep>("FetchOrders");
builder.Services.AddStepHandler<SubmitToWmsStep>("SubmitToWms");
builder.Services.AddFlowDashboard(builder.Configuration); // optional
app.UseHangfireDashboard("/hangfire");
app.MapFlowDashboard("/flows");
Define a flow:
public sealed class OrderFulfillmentFlow : IFlowDefinition
{
// Always use a fixed GUID literal β never Guid.NewGuid()
public Guid Id { get; } = new("a1b2c3d4-0000-0000-0000-000000000002");
public FlowManifest Manifest { get; set; } = new()
{
Triggers = {
["manual"] = new() { Type = TriggerType.Manual },
["webhook"] = new() { Type = TriggerType.Webhook,
Inputs = { ["webhookSlug"] = "order-fulfillment" } }
},
Steps = {
["fetch"] = new() { Type = "FetchOrders" },
["submit"] = new() { Type = "SubmitToWms",
RunAfter = { ["fetch"] = [StepStatus.Succeeded] } }
}
};
}
Open http://localhost:5000/flows β trigger the flow, watch steps execute in the DAG view, retry any failure.
Quick Start β InMemory (zero infrastructure)
For local development, prototypes, and single-node side projects β no Hangfire, no database:
// Program.cs
builder.Services.AddFlowOrchestrator(options =>
{
options.UseInMemory(); // storage in-process
options.UseInMemoryRuntime(); // Channel<T> dispatcher + PeriodicTimer cron
options.AddFlow<OrderFulfillmentFlow>();
});
builder.Services.AddStepHandler<FetchOrdersStep>("FetchOrders");
builder.Services.AddStepHandler<SubmitToWmsStep>("SubmitToWms");
builder.Services.AddFlowDashboard(builder.Configuration);
app.MapFlowDashboard("/flows");
All run data is lost on restart β see Storage Backends for the full picture, and Production Checklist for why this combo is unsuitable for production.
For PostgreSQL, see π Getting Started.
Full documentation
| Topic | Link |
|---|---|
| Getting started (all runtimes) | getting-started |
| Core concepts β Flow, Step, RunId | core-concepts |
| Step handlers | step-handlers |
| Trigger types | triggers |
Expression reference (@triggerBody()) |
expressions |
| Polling pattern | polling |
| ForEach / fan-out | foreach |
| Dashboard & REST API | dashboard |
| Storage backends | storage |
| Configuration reference | configuration |
| Architecture | architecture |
| Observability | observability |
| Mermaid export | mermaid-export |
Conditional execution (When) |
conditional-execution |
Human-in-loop (WaitForSignal) |
wait-for-signal |
Testing flows (FlowTestHost) |
testing |
| Versioning flows in production | versioning |
| Production deployment checklist | production-checklist |
Production?
Before changing any deployed flow, read Versioning Flows β it explains which manifest changes are safe and which need a maintenance window. Before go-live, walk through the Production Checklist for storage, multi-instance, monitoring, secrets, capacity, and upgrade guidance. Wire AddFlowOrchestratorHealthChecks() into /health so your load balancer can drop traffic when the flow store is unreachable.
Visualize any flow with one line
flow.ToMermaid() returns a Mermaid flowchart string that renders in GitHub
READMEs, Notion, Confluence, and any modern Markdown surface β no running app
required. Here is the sample OrderFulfillmentFlow:
flowchart TD
classDef trigger fill:#e1f5ff,stroke:#0288d1
classDef entry fill:#c8e6c9,stroke:#388e3c
classDef polling fill:#fff9c4,stroke:#f57f17
classDef loop fill:#f3e5f5,stroke:#7b1fa2
T_manual["β‘ manual<br/>Manual"]:::trigger
T_webhook["β‘ webhook<br/>Webhook /order-fulfillment"]:::trigger
fetch_orders["fetch_orders<br/><i>QueryDatabase</i>"]:::entry
submit_to_wms["submit_to_wms<br/><i>CallExternalApi</i>"]:::polling
save_result["save_result<br/><i>SaveResult</i>"]
T_manual --> fetch_orders
T_webhook --> fetch_orders
fetch_orders -- Succeeded --> submit_to_wms
submit_to_wms -- Succeeded --> save_result
The dashboard ships a Copy Mermaid button on every flow detail page, and the
sample app exposes --export-mermaid <flowId> for CI integrations.
Compatibility
| Package | Target frameworks |
|---|---|
FlowOrchestrator.Core |
net8.0 Β· net9.0 Β· net10.0 |
FlowOrchestrator.Hangfire |
net8.0 Β· net9.0 Β· net10.0 |
FlowOrchestrator.InMemory |
net8.0 Β· net9.0 Β· net10.0 |
FlowOrchestrator.SqlServer |
net8.0 Β· net9.0 Β· net10.0 |
FlowOrchestrator.PostgreSQL |
net8.0 Β· net9.0 Β· net10.0 |
FlowOrchestrator.Dashboard |
net8.0 Β· net9.0 Β· net10.0 |
FlowOrchestrator.Testing |
net8.0 Β· net9.0 Β· net10.0 |
License
MIT β see the LICENSE file.
| 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 is compatible. 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 is compatible. 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. |
-
net10.0
- Cronos (>= 0.13.0)
- FlowOrchestrator.Core (>= 1.21.0)
- FlowOrchestrator.Hangfire (>= 1.21.0)
- FlowOrchestrator.InMemory (>= 1.21.0)
- Hangfire.Core (>= 1.8.23)
- Microsoft.Extensions.DependencyInjection (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.7)
- Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Hosting (>= 10.0.7)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Logging (>= 10.0.7)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.7)
- Newtonsoft.Json (>= 13.0.4)
- OpenTelemetry (>= 1.15.3)
- OpenTelemetry.Api (>= 1.15.3)
-
net8.0
- Cronos (>= 0.13.0)
- FlowOrchestrator.Core (>= 1.21.0)
- FlowOrchestrator.Hangfire (>= 1.21.0)
- FlowOrchestrator.InMemory (>= 1.21.0)
- Hangfire.Core (>= 1.8.23)
- Microsoft.Extensions.DependencyInjection (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.7)
- Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Hosting (>= 10.0.7)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Logging (>= 10.0.7)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.7)
- Newtonsoft.Json (>= 13.0.4)
- OpenTelemetry (>= 1.15.3)
- OpenTelemetry.Api (>= 1.15.3)
- System.Text.Json (>= 10.0.7)
-
net9.0
- Cronos (>= 0.13.0)
- FlowOrchestrator.Core (>= 1.21.0)
- FlowOrchestrator.Hangfire (>= 1.21.0)
- FlowOrchestrator.InMemory (>= 1.21.0)
- Hangfire.Core (>= 1.8.23)
- Microsoft.Extensions.DependencyInjection (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.7)
- Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Hosting (>= 10.0.7)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Logging (>= 10.0.7)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.7)
- Newtonsoft.Json (>= 13.0.4)
- OpenTelemetry (>= 1.15.3)
- OpenTelemetry.Api (>= 1.15.3)
- System.Text.Json (>= 10.0.7)
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 |
|---|---|---|
| 1.27.3 | 44 | 5/27/2026 |
| 1.27.3-preview.152 | 38 | 5/31/2026 |
| 1.27.3-preview.150 | 44 | 5/27/2026 |
| 1.27.2 | 126 | 5/24/2026 |
| 1.27.2-preview.148 | 50 | 5/24/2026 |
| 1.27.1 | 89 | 5/24/2026 |
| 1.27.1-preview.146 | 46 | 5/24/2026 |
| 1.27.0 | 103 | 5/24/2026 |
| 1.27.0-preview.144 | 54 | 5/24/2026 |
| 1.26.3 | 101 | 5/17/2026 |
| 1.26.3-preview.142 | 50 | 5/16/2026 |
| 1.26.3-preview.141 | 45 | 5/16/2026 |
| 1.26.2 | 90 | 5/16/2026 |
| 1.26.2-preview.139 | 47 | 5/16/2026 |
| 1.26.1 | 158 | 5/14/2026 |
| 1.26.1-preview.138 | 45 | 5/15/2026 |
| 1.26.1-preview.136 | 43 | 5/14/2026 |
| 1.26.0 | 107 | 5/10/2026 |
| 1.26.0-preview.134 | 51 | 5/10/2026 |
| 1.21.0 | 88 | 5/2/2026 |