FluentFlow 2.7.3

dotnet add package FluentFlow --version 2.7.3
                    
NuGet\Install-Package FluentFlow -Version 2.7.3
                    
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="FluentFlow" Version="2.7.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FluentFlow" Version="2.7.3" />
                    
Directory.Packages.props
<PackageReference Include="FluentFlow" />
                    
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 FluentFlow --version 2.7.3
                    
#r "nuget: FluentFlow, 2.7.3"
                    
#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 FluentFlow@2.7.3
                    
#: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=FluentFlow&version=2.7.3
                    
Install as a Cake Addin
#tool nuget:?package=FluentFlow&version=2.7.3
                    
Install as a Cake Tool

FluentFlow

NuGet License .NET

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?

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 before OnExecute()
  • 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 DataContext class) 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 DataContext class) 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 StepDataContext in your own project that adds an application-specific error-state property, and then derive all your step contexts from that base. This keeps FluentFlow itself free of any application-specific concerns.

StopFlow vs AbortFlow

  • AbortFlow() — treats the flow as failed; triggers OnFailed / 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 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. 
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
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
Loading failed