FluentFlow 2.7.3
dotnet add package FluentFlow --version 2.7.3
NuGet\Install-Package FluentFlow -Version 2.7.3
<PackageReference Include="FluentFlow" Version="2.7.3" />
<PackageVersion Include="FluentFlow" Version="2.7.3" />
<PackageReference Include="FluentFlow" />
paket add FluentFlow --version 2.7.3
#r "nuget: FluentFlow, 2.7.3"
#:package FluentFlow@2.7.3
#addin nuget:?package=FluentFlow&version=2.7.3
#tool nuget:?package=FluentFlow&version=2.7.3
FluentFlow
FluentFlow is an open-source .NET library that provides a structured, registry-based approach to decomposing handler logic into sequential, ordered steps.
Rather than embedding business logic directly into handlers, FluentFlow enables handlers to orchestrate a defined flow of discrete, testable steps that execute in sequence — each with clearly bounded input, runtime, and output parameters.
Table of Contents
- Why FluentFlow?
- Prerequisites
- Installation
- Core Concepts
- Getting Started
- DI Registration
- Advanced Features
- Complete Example
- Contributing
- License
Why FluentFlow?
| Without FluentFlow | With FluentFlow |
|---|---|
| Business logic embedded in handlers | Handlers are thin orchestration boundaries |
| Difficult to unit test individual concerns | Each step is independently testable |
| Implicit data flow between operations | Explicit input → runtime → output parameter contracts |
| Validation scattered across handler code | Validation centralised per step, runs before execution |
| Inconsistent error handling | Structured AbortFlow() / StopFlow() signalling |
Key benefits:
- Clear separation of concerns — each step has a single, focused responsibility
- Explicit data flow — input, runtime, and output parameters are declared and annotated
- Fail-fast validation — step-level
Validate()runs beforeOnExecute() - Consistent error handling — failures propagate through a defined callback mechanism
- Scales naturally — from a single step to complex multi-step orchestrations
Prerequisites
- .NET 8 or later (targets .NET 10)
Installation
dotnet add package FluentFlow
Core Concepts
Flows
A flow is a class that inherits FlowBase<TContext>. It defines:
- A data context (inner
DataContextclass) containing all parameters used across the flow - The ordered sequence of steps via
DefineSteps(FlowStepsRegistry)
public class MyFlow : FlowBase<MyFlow.DataContext>
{
public class DataContext : FlowContextBase
{
[InputParameter] public MyRequest Request { get; set; }
[RuntimeParameter] public Guid? ResolvedEntityId { get; set; }
[OutputParameter] public string? ResultMessage { get; set; }
}
protected override void DefineSteps(FlowStepsRegistry stepsRegistry) => stepsRegistry
.AddStep<ValidateStep>(cfg => cfg.OnStepDataContextBound = ctx =>
{
ctx.Access<ValidateStep.DataContext>()!.EntityId = base.Context.Request.EntityId;
})
.AddStep<ProcessStep>()
.AddStep<PersistStep>();
}
Steps
A step is a class that inherits FlowStepBase<TStepContext>. It defines:
- A local data context (inner
DataContextclass) containing only the parameters this step uses - An optional
Validate()method that runs before execution - An
OnExecute()method containing the business logic
public class ValidateStep(IRepository<Entity, MyDbContext> repo)
: FlowStepBase<ValidateStep.DataContext>
{
public class DataContext : StepDataContext
{
[InputParameter] public Guid? EntityId { get; set; }
[RuntimeParameter] public Guid? ResolvedEntityId { get; set; }
[RuntimeParameter] public string? ErrorMessage { get; set; }
public override void Validate()
{
if (EntityId == null || EntityId == Guid.Empty)
AbortFlow("EntityId is required",
msg => ErrorMessage = msg);
}
}
protected override async Task OnExecute(CancellationToken cancellationToken = default)
{
var entity = await repo.GetById(Context.EntityId!.Value, cancellationToken: cancellationToken);
if (entity == null)
AbortFlow("Entity not found", msg => Context.ErrorMessage = msg);
Context.ResolvedEntityId = entity.Id;
}
}
Parameter Types
Parameters in both flow and step contexts are annotated to declare their role:
| Attribute | Role | Direction |
|---|---|---|
[InputParameter] |
Request data passed into the flow | Handler → Flow → Step |
[RuntimeParameter] |
Shared state between steps | Step → Step |
[OutputParameter] |
Data projected to the response | Step → Handler |
Rules:
- Input parameters are set by the caller when configuring the flow context.
- Runtime parameters are written by earlier steps and read by later steps.
- Output parameters are written by steps and read by the handler's success callback.
- Steps can only read runtime/output values populated by earlier steps.
- Parameter names and types must match exactly between the flow context and each step context that references them — this ensures correct automatic mapping.
Flow Control
FlowBase, FlowStepBase, and StepDataContext all inherit from FlowControl, which exposes three control methods:
| Method | Behaviour |
|---|---|
AbortFlow(message?, onAbort?) |
Stops the flow and triggers OnFailed / OnFailedAsync |
StopFlow(message?, onStop?) |
Gracefully stops the flow without triggering failure callbacks |
AbortStep(message?, onAbort?) |
Stops only the current step without aborting the flow |
Error Handling
FluentFlow does not prescribe a specific error-response contract — how you surface errors to the caller is entirely up to your application. The library provides the signalling mechanism; you own the error model.
Signalling a failure from a step
Use AbortFlow() to halt execution and route the flow to the OnFailed / OnFailedAsync callback. The optional onAbort callback runs before the exception is thrown, giving you a chance to write error details back into the step context (which is then mapped back to the flow context automatically):
// In StepDataContext.Validate() or FlowStepBase.OnExecute()
AbortFlow("Wallet not found", msg => Context.ErrorMessage = msg);
Alternatively, set the context property before calling AbortFlow():
Context.ErrorMessage = "Wallet not found";
AbortFlow();
Exposing error details to the caller
Declare a [RuntimeParameter] on both the step context and the flow context to carry error information back to the OnFailed callback:
// Flow DataContext
[RuntimeParameter] public string? ErrorMessage { get; set; }
// Step DataContext
[RuntimeParameter] public string? ErrorMessage { get; set; }
Because runtime parameters are mapped back to the flow context after each step, the value is available in the failure callback:
.OnFailedAsync(async ctx =>
{
var message = ctx.ErrorMessage ?? "An unexpected error occurred.";
// use message to populate your error response
})
Extending the error model: Real-world applications often need richer error information (e.g. an error code or HTTP status equivalent). A common approach is to introduce a thin base class for
StepDataContextin your own project that adds an application-specific error-state property, and then derive all your step contexts from that base. This keepsFluentFlowitself free of any application-specific concerns.
StopFlow vs AbortFlow
AbortFlow()— treats the flow as failed; triggersOnFailed/OnFailedAsync.StopFlow()— treats the flow as gracefully stopped (e.g. a no-op short-circuit); does not trigger failure callbacks.
Getting Started
1. Define a Flow
using FluentFlow.Core.Configuration;
using FluentFlow.Core.Flow;
using FluentFlow.Core.Flow.Context;
using FluentFlow.Core.Flow.Context.Attributes;
public class CreateOrderFlow : FlowBase<CreateOrderFlow.DataContext>
{
public class DataContext : FlowContextBase
{
[InputParameter] public CreateOrderRequest Request { get; set; }
[RuntimeParameter] public Guid? ResolvedCustomerId { get; set; }
[RuntimeParameter] public string? ErrorMessage { get; set; }
[OutputParameter] public Guid? CreatedOrderId { get; set; }
[OutputParameter] public string? ResultMessage { get; set; }
}
protected override void DefineSteps(FlowStepsRegistry stepsRegistry) => stepsRegistry
.AddStep<ValidateCustomerStep>(cfg => cfg.OnStepDataContextBound = ctx =>
{
var c = ctx.Access<ValidateCustomerStep.DataContext>()!;
c.CustomerId = base.Context.Request.CustomerId;
})
.AddStep<CreateOrderStep>(cfg => cfg.OnStepDataContextBound = ctx =>
{
var c = ctx.Access<CreateOrderStep.DataContext>()!;
c.ProductId = base.Context.Request.ProductId;
c.Quantity = base.Context.Request.Quantity;
});
}
2. Implement Steps
using FluentFlow.Core.Flow.Context.Attributes;
using FluentFlow.Core.Step;
using FluentFlow.Core.Step.Context;
public class ValidateCustomerStep(ICustomerRepository customerRepo)
: FlowStepBase<ValidateCustomerStep.DataContext>
{
public class DataContext : StepDataContext
{
[InputParameter] public Guid? CustomerId { get; set; }
[RuntimeParameter] public Guid? ResolvedCustomerId { get; set; }
[RuntimeParameter] public string? ErrorMessage { get; set; }
public override void Validate()
{
if (CustomerId == null || CustomerId == Guid.Empty)
AbortFlow("CustomerId is required",
msg => ErrorMessage = msg);
}
}
protected override async Task OnExecute(CancellationToken cancellationToken = default)
{
var customer = await customerRepo.GetById(Context.CustomerId!.Value, cancellationToken);
if (customer == null || !customer.IsActive)
AbortFlow("Customer not found or inactive", msg => Context.ErrorMessage = msg);
Context.ResolvedCustomerId = customer!.Id;
}
}
public class CreateOrderStep(IOrderRepository orderRepo)
: FlowStepBase<CreateOrderStep.DataContext>
{
public class DataContext : StepDataContext
{
[InputParameter] public Guid? ProductId { get; set; }
[InputParameter] public int? Quantity { get; set; }
[RuntimeParameter] public Guid? ResolvedCustomerId { get; set; }
[RuntimeParameter] public string? ErrorMessage { get; set; }
[OutputParameter] public Guid? CreatedOrderId { get; set; }
[OutputParameter] public string? ResultMessage { get; set; }
public override void Validate()
{
if (Quantity is null or <= 0)
AbortFlow("Quantity must be greater than zero",
msg => ErrorMessage = msg);
}
}
protected override async Task OnExecute(CancellationToken cancellationToken = default)
{
var order = await orderRepo.Create(
Context.ResolvedCustomerId!.Value,
Context.ProductId!.Value,
Context.Quantity!.Value,
cancellationToken);
Context.CreatedOrderId = order.Id;
Context.ResultMessage = "Order created successfully.";
}
}
3. Execute the Flow
await _createOrderFlow.Execute(null, cfg => cfg
.WithContext(() => new CreateOrderFlow.DataContext
{
Request = request,
})
.MustFailSilently()
.OnFailedAsync(async ctx =>
{
var message = ctx.ErrorMessage ?? "An unexpected error occurred.";
// handle error — e.g. return an error response
})
.OnSucceeded(ctx =>
{
response.OrderId = ctx.CreatedOrderId;
response.ResultMessage = ctx.ResultMessage;
response.Success = true;
}));
The FlowExecutionConfiguration<TContext> fluent API:
| Method | Description |
|---|---|
.WithContext(Func<TContext>) |
Supplies the flow's data context |
.MustFailSilently() |
Suppresses exceptions on AbortFlow() — triggers OnFailed instead |
.OnFailed(Action<TContext>) |
Synchronous failure callback |
.OnFailedAsync(Func<TContext, Task>) |
Asynchronous failure callback |
.OnSucceeded(Action<TContext>) |
Synchronous success callback |
.OnSucceededAsync(Func<TContext, Task>) |
Asynchronous success callback |
.OverrideStepDefinition(Action<FlowStepsRegistry>) |
Replaces the step sequence at runtime (useful in tests) |
.MustNotAutomaticallyUnsubscribeFromAllEvents() |
Keeps flow event subscriptions alive after execution |
DI Registration
Register all flows from one or more assemblies using the RegisterFluentFlows extension method:
builder.Services.RegisterFluentFlows(
registerByInterface: true, // also register as IFlow<TContext>
typeof(Program).Assembly // scan this assembly for FlowBase<T> subclasses
);
- All non-abstract classes that implement
IFlow<T>are discovered automatically. - Each flow is registered as Transient — a fresh instance per execution.
- Pass multiple assemblies to scan more than one project.
Advanced Features
Step Groups
Register two steps that share a configuration block with AddStepGroup:
stepsRegistry.AddStepGroup<StepA, StepB>(cfg => { /* shared configuration */ });
Overriding Step Definitions
You can replace the flow's registered steps at the point of execution — particularly useful in integration tests:
await flow.Execute(null, cfg => cfg
.WithContext(() => new MyFlow.DataContext { ... })
.OverrideStepDefinition(registry => registry
.AddStep<MockStep>()));
Flow Events
FlowBase exposes events you can subscribe to for observability or cross-cutting concerns:
flow.BeforeFlowExecuted += f => Console.WriteLine($"Starting {f.GetType().Name}");
flow.AfterFlowExecuted += f => Console.WriteLine($"Finished {f.GetType().Name}");
flow.BeforeStepExecuted += s => Console.WriteLine($"Step: {s.Id}");
flow.AfterStepExecuted += s => Console.WriteLine($"Done: {s.Id}");
flow.FlowError += (ex, f) => Console.WriteLine($"Error: {ex.Message}");
flow.FlowGracefullyStopped += (ex, f) => Console.WriteLine("Flow stopped gracefully.");
By default, FluentFlow unsubscribes from all events after execution. Call .MustNotAutomaticallyUnsubscribeFromAllEvents() to opt out.
Complete Example
For a more detailed, multi-step example demonstrating ID validation, runtime parameter propagation, value derivation, and persistence, see the examples in the repository.
Contributing
Contributions are welcome. Please open an issue to discuss proposed changes before submitting a pull request.
License
This project is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- AutoMapper (>= 16.1.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.8)
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 |
|---|---|---|
| 2.7.3 | 169 | 5/21/2026 |
| 2.7.1 | 102 | 5/21/2026 |
| 2.6.0 | 1,068 | 11/8/2025 |
| 2.5.0 | 1,258 | 6/11/2025 |
| 2.4.1 | 761 | 3/15/2025 |
| 2.4.0 | 156 | 3/15/2025 |
| 2.3.1 | 937 | 12/19/2024 |
| 2.3.0 | 212 | 12/19/2024 |
| 2.2.5 | 205 | 12/18/2024 |
| 2.2.4 | 204 | 12/18/2024 |
| 2.2.3 | 261 | 12/13/2024 |
| 2.2.2 | 203 | 12/11/2024 |
| 2.2.1 | 197 | 12/11/2024 |
| 2.2.0 | 199 | 12/11/2024 |
| 2.1.2 | 203 | 12/11/2024 |
| 2.1.1 | 196 | 12/10/2024 |
| 2.1.0 | 191 | 12/10/2024 |
| 2.0.0 | 1,118 | 9/21/2024 |
| 1.12.1 | 261 | 9/17/2024 |
| 1.12.0 | 336 | 9/16/2024 |