Features (docs)
- Lightweight & Fast – Optimized for high-performance applications.
- Asynchronous & Thread-Safe – Ensures smooth execution in concurrent environments.
- Declarative API – Define state transitions with a clean and fluent syntax.
- State Persistence – Supports JSON-based storage, in-memory storage.
- Event Hooks – Capture and log every state change for debugging and auditing (check state, check entry, delete entry).
- Cycle Prevention – Detects and prevents cyclic transitions.
- Error Handling – Graceful fallback mechanisms for invalid transitions.
- Final States – Define terminal states where transitions are restricted.
- Flexible Configuration – Supports builder pattern for easy transition setup.
- Parallel & Sequential State Execution – Suitable for multi-threaded workflows.
- Custom Transition Conditions – Add business logic to control transitions.
- Dependency Injection Support – Seamless integration into DI containers.
- Storage-Agnostic – Plug in your own storage strategy.
- Diagram Export – Export transitions as Mermaid.js or Graphviz DOT.
For FlowLite.Diag
(docs):
- Scans
.cs
files and locates allFlowTransitionBuilder<...>
instances. - Generates Mermaid.js or Graphviz DOT diagrams from C# code.
- Automatically separates transitions by builder instance.
- Displays file path and per-builder FSM diagrams in the output.
For FlowLite.Testing
(docs):
- Fluent Assertions — expressive
.Should().Allow(...)
,.BeIn(...)
,.Logs()
, etc. - Trigger Validation —
.Assert().Allow(...)
or.NotAllow(...)
. - Entity Validation — validate entity changes or deletions via
.HaveEntity(...)
and.DeleteEntity()
. - Log Inspection — assert warnings, errors, or specific messages.
- History Validation — verify transition path, length, or contents.
- Minimal & Fast — no dependencies on other testing libraries or frameworks.
For FlowLite.Diagnostics
(docs):
- Console logging of all FSM events.
- Telemetry metrics via System.Diagnostics.Metrics (OpenTelemetry-compatible).
- DiagnosticListener events (StateChanged, EntityChanged, EntityDeleted).
- Integration with ILogger (Microsoft.Extensions.Logging).
- Global diagnostics: auto-attach listeners to all FSMs via FlowLite global hook.
- Extensibility via custom IDiagnosticsFlowLiteListener.
- FlowLite is a high-performance, thread-safe, and developer-friendly finite state machine (FSM) library for .NET. It helps you manage entity state transitions, workflow automation, and domain-driven state logic in a structured and testable way.
- FlowLite.Diag is a cross-platform .NET CLI tool for visualizing finite state machine (FSM) state transitions based on
FlowTransitionBuilder
code from theFlowLite
library. - FlowLite.Testing is a minimalistic and fluent testing library designed for writing expressive and structured unit tests for your
FlowLite
workflows. - FlowLite.Diagnostics is an extension for
FlowLite
that adds logging, telemetry, observability, and global FSM diagnostics. It allows you to monitor the full lifecycle of your finite state machine, including transitions, errors, entity changes, and deletions.
- Order Processing Systems – Manage payment, shipping, and delivery statuses.
- Workflow Automation – Control document approval, user onboarding, or task execution.
- Game Development – Manage player states, AI behavior, and event triggers.
- IoT Device State Tracking – Handle device power states, connectivity, and error handling.
- Business Processes – Automate transitions in CRM, ERP, BPM applications.
- Designing workflows and business processes using FlowLite
- Auto-generating documentation from source.
- Visual debugging of state transitions.
- To write unit tests for your FlowLite-based FSMs.
- To verify that a specific trigger is allowed or disallowed.
- To check logs, such as errors, warnings, or final state messages.
- To assert transition history, including order and contents.
- To validate that an entity was updated or deleted during a transition.
- Building test-driven FSM logic with clear, maintainable assertions.
- To test FSM behavior independently of storage or infrastructure.
- Track state transitions, entity changes, and lifecycle events using logs, metrics, and diagnostic events.
- Gain insights into FSM internals when developing complex transition logic. Easily catch unexpected transitions or silent errors.
- Detect and analyze transition failures, entity deletions, and state rollbacks using structured logs or metrics.
- Collect metrics with OpenTelemetry or System.Diagnostics.Metrics to monitor FSM health, latency, and usage patterns.
- Enable DiagnosticListener integration to inspect FSM activity in debuggers, profilers, or telemetry dashboards.
- Log all state and entity changes for compliance or historical tracking purposes (e.g., in financial systems or healthcare).
- Automatically attach FSM listeners to track performance and detect abnormal behavior in live systems.
To add the Nuget package for the project type.
Package | Description |
---|---|
FlowLite | A high-performance, thread-safe, and developer-friendly finite state machine (FSM) library for .NET. It helps you manage entity state transitions, workflow automation, and domain-driven state logic in a structured and testable way. Features: fluent API for state and trigger configuration, async support for transition logic, export as Mermaid.js or DOT graph, built-in JSON and in-memory storage. |
FlowLite.Diag | A cross-platform .NET CLI tool for visualizing finite state machine (FSM) state transitions based on FlowTransitionBuilder code from the FlowLite library. |
FlowLite.Testing | A minimalistic and fluent testing library designed for writing expressive and structured unit tests for your FlowLite FSM workflows. |
FlowLite.Diagnostics | An extension for FlowLite that adds logging, telemetry, observability, and global FSM diagnostics. It allows you to monitor the full lifecycle of your finite state machine, including transitions, errors, entity changes, and deletions. |
To install the latest version of the FlowLite:
Install-Package FlowLite -Version 8.0.0
dotnet add package FlowLite.Abstractions --version 8.0.0
dotnet add package FlowLite --version 8.0.0
Installation (as tool)
dotnet tool install -g flowlite-diag --add-source ./nupkg
Install-Package FlowLite.Testing -Version 8.0.0
dotnet add package FlowLite.Abstractions --version 8.0.0
dotnet add package FlowLite.Testing --version 8.0.0
Install-Package FlowLite.Diagnostics -Version 8.0.0
dotnet add package FlowLite.Abstractions --version 8.0.0
dotnet add package FlowLite.Diagnostics --version 8.0.0
- .NET 8 or higher.
using FlowLite.Core.Abstractions;
using FlowLite.Configuration;
using FlowLite.Core;
using FlowLite.Extensions;
using FlowLite.Storage;
using FlowLite.Storage.Abstractions;
public enum OrderState { Created, Paid, Shipped, Delivered, Canceled }
public enum OrderTrigger { Pay, Ship, Deliver, Cancel }
public class Order
{
public int Id { get; set; }
public string? Status { get; set; }
public string? TransitionHistory { get; set; }
public bool PaymentConfirmed { get; set; }
}
services.AddFlowLiteStorage<OrderState, int, Order>(
StorageType.Json,
"C:\FlowLite_Storage\"
);
JSON
storage is used for learning or review state processing
services.AddFlowLiteStorage<OrderState, int, Order>(
StorageType.Memory,
"C:\FlowLite_Storage\"
);
In-Memory
storage is used for production environment.
var stateMachine = new StateFlowMachine<OrderState, OrderTrigger, int, Order>(
initialState: OrderState.Created,
stateStorage: storage,
entityKey: order.Id,
entity: order
);
stateMachine
.AddTransition(OrderState.Created, OrderTrigger.Pay, OrderState.Paid, async (_, ctx) => {
ctx.Order.Status = "Paid";
await Task.CompletedTask;
})
.AddTransition(OrderState.Paid, OrderTrigger.Ship, OrderState.Shipped, async (_, ctx) => {
ctx.Order.Status = "Shipped";
await Task.CompletedTask;
}).AsFinal();
Each transition can include custom actions executed when the state changes:
var flowBuilder = new FlowTransitionBuilder<OrderState, OrderTrigger, Order>()
.AddTransition(OrderState.Created, OrderTrigger.Pay, OrderState.Paid, async (_, ctx) => {
ctx.Entity!.Status = "Paid";
await Task.CompletedTask;
})
.AddTransition(OrderState.Paid, OrderTrigger.Ship, OrderState.Shipped, async (_, ctx) => {
ctx.Entity!.Status = "Shipped";
await Task.CompletedTask;
})
.AsFinal()
.Build();
Apply the configuration with builder
stateMachine.ConfigureTransitions(flowBuilder);
You can enforce custom conditions before a transition is allowed:
stateMachine.AddTransition(OrderState.Created, OrderTrigger.Pay, OrderState.Paid,
async (moveTo, ctx) => {
if (!ctx.Entity.PaymentConfirmed)
{
// Move to transition with state `Canceled` and trigger `Cancel` (entity should be saved)
await moveTo(OrderState.Canceled, OrderTrigger.Cancel, true);
}
}
);
Sample
stateMachine.AddTransition(OrderState.Created, OrderTrigger.Pay, OrderState.Paid,
async (moveTo, ctx) => {
if (!ctx.Entity.PaymentConfirmed)
await moveTo(OrderState.Canceled, true);
}
);
Move to transition with state Canceled
and entity will be saved
Sample
stateMachine.AddTransition(OrderState.Created, OrderTrigger.Pay, OrderState.Paid,
async (moveTo, ctx) => {
if (!ctx.Entity.PaymentConfirmed) {
ctx.Entity.Status = OrderState.Canceled.ToString();
await moveTo(OrderState.Canceled, true);
}
}
);
The entity (order) will be changed the status to Canceled
. After that the flow is moving to transition with state Canceled
, entity will be saved.
Sample
stateMachine.AddTransition(OrderState.Created, OrderTrigger.Pay, OrderState.Paid,
async (_, _) => {
await Task.CompletedTask;
}
);
There is a default implementation of empty transition.
await stateMachine.FireAsync(OrderTrigger.Pay);
await stateMachine.FireAsync(OrderTrigger.Ship);
await stateMachine.FireAsync(OrderTrigger.Deliver);
or use the alternative with configuration
stateMachine.ConfigureTransitions(stateFlowConfig);
await stateMachine.FireAsync(OrderTrigger.Pay);
await stateMachine.FireAsync(OrderTrigger.Ship);
await stateMachine.FireAsync(OrderTrigger.Deliver);
Using Result pattern:
Result<bool> result = await fsm.TryFireAsync(OrderTrigger.Pay);
var history = stateMachine.GetTransitionHistory();
var logs = stateMachine.GetLogs();
OnStateChanged
event is triggered whenever the state of an entity changes. It allows you to execute custom logic when a state transition occurs.
stateMachine.OnStateChanged += async (state, trigger) => {
Console.WriteLine($"State changed to {state} ({trigger})");
await Task.CompletedTask;
};
Use Cases:
-
Logging state transitions.
-
Sending notifications when a state changes.
-
Updating UI or external services based on state changes.
-
OnEntityChanged
event is fired when an entity is modified during a transition. It helps track updates to entity data as the workflow progresses.
stateMachine.OnEntityChanged += async (entity) => {
Console.WriteLine($"Entity {entity.Id} updated: {entity}");
await Task.CompletedTask;
};
Use Cases:
-
Persisting entity changes after a transition.
-
Auditing modifications to entity properties.
-
Triggering additional actions based on entity updates.
-
OnEntityDeleted
event is fired when an entity is deleted during a transition. It helps track deletes entity data during workflow progress.
stateMachine.OnEntityDeleted += async (id) => {
Console.WriteLine($"Entity {id.ToString()} was deleted");
await Task.CompletedTask;
};
OnTransitionFailed
event is fired when an unhandled exception occurs during transition execution in FireAsync.
fsm.OnTransitionFailed += async (state, trigger, ex) =>
{
Console.WriteLine($"Transition failed: [{state}] + [{trigger}] => {ex.Message}");
await Task.CompletedTask;
};
var diagramMermaid = stateMachine.Export(ExportType.Mermaid);
var diagramDot = stateMachine.Export(ExportType.Dot);
flowlite-diag --source "C:\Orders\" --format "mermaid"
Argument | Description | Example |
---|---|---|
--source, -s | Specifies the path to the project directory. | "C:\Orders" |
--format, -f | Specifies the export format of the output. Supported values: dot or mermaid . |
"mermaid" |
stateDiagram-v2
Pending --> Created : Create
Created --> Shipped : Ship
Shipped --> Canceled : Cancel
Failed --> Deleted : Delete
using FlowLite.Testing;
using FlowLite.Testing.Assertions;
fsm.Should().Allow(OrderTrigger.Pay);
fsm.Should().NotAllow(OrderTrigger.Cancel);
fsm.Should().BeIn(OrderState.Paid);
fsm.Should().NotNullEntity();
fsm.Should().HaveEntity(order =>
{
Assert.Equal("Shipped", order?.Status);
});
fsm.Should().Logs().Log(LogLevel.Warning, "invalid transition");
fsm.Should().Logs().BeFinalState();
fsm.Should().Logs().ContainFinalStateLog();
fsm.Should().Logs().MatchSnapshot(new[] { "FSM initialized", "Transitioned to Paid" });
fsm.Should().History()
.StartWith(OrderState.Created)
.HaveInHistory(OrderState.Created, OrderState.Paid, OrderState.Shipped)
.Contains(OrderTrigger.Pay, OrderState.Paid)
.HaveLength(3)
.ContainsTrigger(OrderTrigger.Ship);
You can use .Assert() style for concise checks:
fsm.Assert().Allow(OrderTrigger.Pay);
fsm.Assert().NotAllow(OrderTrigger.Cancel);
Minimal sample
fsm.AddTransition(OrderState.Created, OrderTrigger.Pay, OrderState.Paid, async (_, ctx) =>
{
ctx.Entity!.Status = "Paid";
await Task.CompletedTask;
});
await fsm.FireAsync(OrderTrigger.Pay);
fsm.Should()
.BeIn(OrderState.Paid)
.NotNullEntity()
.Logs().Log(LogLevel.Info, "Paid");
services.AddFlowLiteDiagnostics<[State], [Trigger], [Key], [Entity]>(opt =>
{
opt.EnableGlobalDiagnostics = true;
opt.Telemetry.Enabled = true;
opt.Logging.Enabled = true;
opt.Logging.UseConsole = true;
opt.Logging.UseLogger = true;
opt.Logging.LoggerFactory = services.BuildServiceProvider().GetRequiredService<ILoggerFactory>();
opt.DiagnosticObserver.Enabled = true;
});
After registration, all state machines created will automatically get hooked into diagnostics. Sample:
services.AddFlowLiteDiagnostics<OrderState, OrderTrigger, int, Order>(opt =>
{
opt.EnableGlobalDiagnostics = true;
opt.Telemetry.Enabled = true;
opt.Logging.Enabled = true;
opt.Logging.UseConsole = true;
opt.Logging.UseLogger = true;
opt.Logging.LoggerFactory = services.BuildServiceProvider().GetRequiredService<ILoggerFactory>();
opt.DiagnosticObserver.Enabled = true;
});
fsm_transitions_total
- number of FSM transitions.fsm_failures_total
- number of failed FSM transitions.fsm_state_changed_total
- number of state changes.fsm_entity_changed_total
- number of entity updates.fsm_entity_deleted_total
- number of entity deletions.fsm_transition_failed_total
- number of failed transitions.
fsm.UseTelemetry(new TelemetryOptions()
{
Enabled = true,
Source = "FlowLite.FSM"
});
or
fsm.UseTelemetry();
fsm.UseLogging(new LoggingOptions()
{
Enabled = true,
UseConsole = true,
UseLogger = false
});
or
fsm.UseConsoleLogging();
fsm.UseLogging(new LoggingOptions()
{
Enabled = true,
UseConsole = true,
UseLogger = true,
LoggerFactory = services.GetRequiredService<ILoggerFactory>()
});
fsm.UseDiagnosticObserver(new DiagnosticObserverOptions()
{
Enabled = true,
Source = "FlowLite.Diagnostics"
});
or
fsm.UseDiagnosticObserver();
services.AddFlowLiteDiagnostics<OrderState, OrderTrigger, int, Order>(opt =>
{
opt.EnableGlobalDiagnostics = true;
opt.Telemetry.Enabled = true;
opt.Logging.Enabled = true;
opt.Logging.UseConsole = true;
opt.Logging.UseLogger = true;
opt.Logging.LoggerFactory = services.BuildServiceProvider().GetRequiredService<ILoggerFactory>();
opt.DiagnosticObserver.Enabled = true;
opt.CustomListeners.Add(new MyCustomFlowDiagnosticsListener());
});
Use CustomListeners to add a custom listener (based on IDiagnosticsFlowLiteListener)
This project is licensed under the MIT License.
FlowLite — a lightweight way to manage state in .NET without the pain