Optima.Net.StateMachines
3.0.1
Prefix Reserved
dotnet add package Optima.Net.StateMachines --version 3.0.1
NuGet\Install-Package Optima.Net.StateMachines -Version 3.0.1
<PackageReference Include="Optima.Net.StateMachines" Version="3.0.1" />
<PackageVersion Include="Optima.Net.StateMachines" Version="3.0.1" />
<PackageReference Include="Optima.Net.StateMachines" />
paket add Optima.Net.StateMachines --version 3.0.1
#r "nuget: Optima.Net.StateMachines, 3.0.1"
#:package Optima.Net.StateMachines@3.0.1
#addin nuget:?package=Optima.Net.StateMachines&version=3.0.1
#tool nuget:?package=Optima.Net.StateMachines&version=3.0.1
Optima.Net.StateMachines
Optima.Net.StateMachines is a lightweight, declarative, and idempotent state machine framework for .NET 8 and later.
It models event-driven lifecycles in a passive and deterministic way:
- transitions are declared as data
- undefined transitions are ignored
- applying the same event multiple times yields the same state
This package is domain-agnostic and contains no persistence, infrastructure, or orchestration code.
Overview
Core principles:
- Declarative configuration (event → from → to)
- Passive semantics (undefined transitions are ignored)
- Idempotent operations
- Deterministic evaluation
- Stateless by default
- Extensible token model (no enums)
- Explicit, type-safe aggregation (major version change)
IMPORTANT: Aggregation Model (Major Version Change)
Aggregation was redesigned in a major release.
Aggregation no longer operates on raw StateToken values.
Instead, aggregation is explicit and type-safe:
- Machines must explicitly opt in to aggregation
- State access is decoupled from state ownership
- Aggregators work with stateless and stateful machines
- Aggregation eligibility is enforced by the type system
Key abstractions:
- IAggregatableStateMachine � permission to participate in aggregation
- IStateProvider � access to a state value
- IStateAggregator � authority that derives aggregate meaning
STATELESS STATE MACHINES (CALCULATOR MODEL)
Stateless state machines do not track internal state. They calculate a next state when given a transition and a current state.
This model is ideal when:
- state is persisted externally
- workflows are event-sourced
- replay and rehydration are required
- machines must remain pure and side-effect free
STATEFUL STATE MACHINE INSTANCES (NEW IN 3.0.0)
In addition to stateless machines, Optima.Net.StateMachines now supports stateful state machine instances.
A stateful machine instance:
- owns its current state
- applies transitions directly
- exposes its state via IStateProvider
- can participate in aggregation without adapters
- Define State and Transition Tokens
State Tokens
public sealed class PaymentState : StateToken
{
private PaymentState(string code) : base(code) { }
public static readonly PaymentState New = new("New");
public static readonly PaymentState Initiated = new("Initiated");
public static readonly PaymentState Processing = new("Processing");
public static readonly PaymentState Settled = new("Settled");
public static readonly PaymentState Failed = new("Failed");
}
Transition Tokens
public sealed class PaymentTransition : TransitionToken
{
private PaymentTransition(string code) : base(code) { }
public static readonly PaymentTransition Initiate = new("Initiate");
public static readonly PaymentTransition Process = new("Process");
public static readonly PaymentTransition Settle = new("Settle");
public static readonly PaymentTransition Fail = new("Fail");
}
- Define a Stateful Machine Instance
public sealed class PaymentStateMachine
: StatefulStateMachine<PaymentState, PaymentTransition>
{
protected override PaymentState New => PaymentState.New;
public PaymentStateMachine()
{
var transitions = TransitionMapBuilder<PaymentState, PaymentTransition>.Create()
.When(PaymentTransition.Initiate).From(PaymentState.New).To(PaymentState.Initiated)
.When(PaymentTransition.Process).From(PaymentState.Initiated).To(PaymentState.Processing)
.When(PaymentTransition.Settle).From(PaymentState.Processing).To(PaymentState.Settled)
.When(PaymentTransition.Fail).From(PaymentState.Processing).To(PaymentState.Failed)
.Build();
MapRange(transitions);
}
}
- Using a Stateful Machine in Application Code
var paymentMachine = new PaymentStateMachine();
Console.WriteLine(paymentMachine.CurrentState);
paymentMachine.Apply(PaymentTransition.Initiate);
Console.WriteLine(paymentMachine.CurrentState);
paymentMachine.Apply(PaymentTransition.Process);
Console.WriteLine(paymentMachine.CurrentState);
paymentMachine.Apply(PaymentTransition.Settle);
Console.WriteLine(paymentMachine.CurrentState);
- Create 2 Statefull machines for Aggregating Stateful Machine Instances
Create 2 Statemachines. Ther first represent a claim lifecycle, and the second models the payment lifecycle. Both machines implement IAggregatableStateMachine and IStateProvider, allowing them to be evaluated by an aggregator without adapters.
public sealed class ClaimState : StateToken
{
private ClaimState(string code) : base(code) { }
public static readonly ClaimState New = new("New");
public static readonly ClaimState Submitted = new("Submitted");
public static readonly ClaimState Assessing = new("Assessing");
public static readonly ClaimState Approved = new("Approved");
public static readonly ClaimState Rejected = new("Rejected");
public static readonly ClaimState Settled = new("Settled");
}
public sealed class ClaimTransition : TransitionToken
{
private ClaimTransition(string code) : base(code) { }
public static readonly ClaimTransition Submit = new("Submit");
public static readonly ClaimTransition Assess = new("Assess");
public static readonly ClaimTransition Approve = new("Approve");
public static readonly ClaimTransition Reject = new("Reject");
public static readonly ClaimTransition Settle = new("Settle");
}
public ClaimStateMachine()
{
var transitions = TransitionMapBuilder<ClaimState, ClaimTransition>.Create()
.When(ClaimTransition.Submit).From(ClaimState.New).To(ClaimState.Submitted)
.When(ClaimTransition.Assess).From(ClaimState.Submitted).To(ClaimState.Assessing)
.When(ClaimTransition.Approve).From(ClaimState.Assessing).To(ClaimState.Approved)
.When(ClaimTransition.Reject).From(ClaimState.Assessing).To(ClaimState.Rejected)
.When(ClaimTransition.Settle).From(ClaimState.Approved).To(ClaimState.Settled)
.Build();
MapRange(transitions);
}
public sealed class PaymentState : StateToken
{
private PaymentState(string code) : base(code) { }
public static readonly PaymentState New = new("New");
public static readonly PaymentState Initiated = new("Initiated");
public static readonly PaymentState Processing = new("Processing");
public static readonly PaymentState Settled = new("Settled");
public static readonly PaymentState Failed = new("Failed");
}
public sealed class PaymentTransition : TransitionToken
{
private PaymentTransition(string code) : base(code) { }
public static readonly PaymentTransition Initiate = new("Initiate");
public static readonly PaymentTransition Process = new("Process");
public static readonly PaymentTransition Settle = new("Settle");
public static readonly PaymentTransition Fail = new("Fail");
}
public sealed class PaymentStateMachine
: StatefulStateMachine<PaymentState, PaymentTransition>
{
protected override PaymentState New => PaymentState.New;
public PaymentStateMachine()
{
var transitions = TransitionMapBuilder<PaymentState, PaymentTransition>.Create()
.When(PaymentTransition.Initiate).From(PaymentState.New).To(PaymentState.Initiated)
.When(PaymentTransition.Process).From(PaymentState.Initiated).To(PaymentState.Processing)
.When(PaymentTransition.Settle).From(PaymentState.Processing).To(PaymentState.Settled)
.When(PaymentTransition.Fail).From(PaymentState.Processing).To(PaymentState.Failed)
.Build();
MapRange(transitions);
}
}
- Creating an Aggregator for Stateful Machines
protected override AggregateStatus EvaluateStates(params StateToken[] subStates)
{
if (subStates.Length == 0)
return AggregateStatuses.Unknown;
if (subStates.Any(s =>
s.Equals(ClaimState.Rejected) ||
s.Equals(PaymentState.Failed)))
return AggregateStatuses.Failed;
if (subStates.All(s =>
s.Equals(ClaimState.Settled) ||
s.Equals(PaymentState.Settled)))
return AggregateStatuses.Completed;
if (subStates.Any(s =>
s.Equals(ClaimState.Submitted) ||
s.Equals(ClaimState.Assessing) ||
s.Equals(ClaimState.Approved) ||
s.Equals(PaymentState.Initiated) ||
s.Equals(PaymentState.Processing)))
return AggregateStatuses.Active;
return AggregateStatuses.Pending;
}
- Aggregating Stateful Machine Instances
For aggregation, we created an InsuranceAggregator that derives an aggregate status based on the current states of both machines. We call this The Claim Snapshot in the example below, which is a collection of the machines and their state providers. We then apply a series of transitions to both machines and evaluate the aggregate status at each step.
var claimMachine = new ClaimStateMachine();
var claimState = ClaimState.New;
var paymentState = PaymentState.New;
var paymentMachine = new PaymentStateMachine();
var insuranceAggregator = new InsuranceAggregator();
IEnumerable<(IAggregatableStateMachine, IStateProvider)> ClaimSnapshot() =>
[
(claimMachine, claimMachine),
(paymentMachine, paymentMachine)
];
Console.WriteLine("=== INSURANCE WORKFLOW USING STATEFULL STATEMACHINE ===");
Console.WriteLine($"Initial states: Claim={claimMachine.CurrentState}, Payment={paymentMachine.CurrentState}");
Console.WriteLine($"Aggregate Status: {insuranceAggregator.Evaluate(ClaimSnapshot()).Code}\n");
claimMachine.Apply(ClaimTransition.Submit);
Console.WriteLine($"After Claim submission: Claim={claimMachine.CurrentState}, Payment={paymentMachine.CurrentState}");
Console.WriteLine($"Aggregate Status: {insuranceAggregator.Evaluate(ClaimSnapshot()).Code}\n");
claimMachine.Apply(ClaimTransition.Assess);
Console.WriteLine($"After Claim assessment: Claim={claimMachine.CurrentState}, Payment={paymentMachine.CurrentState}");
Console.WriteLine($"Aggregate Status: {insuranceAggregator.Evaluate(ClaimSnapshot()).Code}\n");
claimMachine.Apply(ClaimTransition.Approve);
Console.WriteLine($"After Claim approval: Claim={claimMachine.CurrentState}, Payment={paymentMachine.CurrentState}");
Console.WriteLine($"Aggregate Status: {insuranceAggregator.Evaluate(ClaimSnapshot()).Code}\n");
paymentMachine.Apply(PaymentTransition.Initiate);
Console.WriteLine($"After Payment initiation: Claim={claimMachine.CurrentState}, Payment={paymentMachine.CurrentState}");
Console.WriteLine($"Aggregate Status: {insuranceAggregator.Evaluate(ClaimSnapshot()).Code}\n");
paymentMachine.Apply(PaymentTransition.Process);
Console.WriteLine($"After Payment processing: Claim={claimMachine.CurrentState}, Payment={paymentMachine.CurrentState}");
Console.WriteLine($"Aggregate Status: {insuranceAggregator.Evaluate(ClaimSnapshot()).Code}\n");
paymentMachine.Apply(PaymentTransition.Settle);
claimMachine.Apply(ClaimTransition.Settle);
Console.WriteLine($"After Calim and Payment Settlement: Claim={claimMachine.CurrentState}, Payment={paymentMachine.CurrentState}");
Console.WriteLine($"Aggregate Status: {insuranceAggregator.Evaluate(ClaimSnapshot()).Code}\n");
- Building a Stateless Stateful Machine Instances
Stateless machines are used in an imperative way by maintaining an external state value and applying transitions to it.
First we create the Transitions and States for a BillingTracker StateMachine
public class BillingState : StateToken
{
private BillingState(string code) : base(code) { }
public static readonly BillingState New = new("New");
public static readonly BillingState Pending = new("Pending");
public static readonly BillingState Paid = new("Paid");
public static readonly BillingState Refunded = new("Refunded");
}
Next we create the TransitionTokens for the BillingTracker StateMachine
public class BillingTransition : TransitionToken
{
private BillingTransition(string code) : base(code) { }
public static readonly BillingTransition Create = new("Create");
public static readonly BillingTransition Pay = new("Pay");
public static readonly BillingTransition Refund = new("Refund");
}
Then we create the Stateless State Machine Instance for the BillingTracker
public class BillingStateMachine : StateMachine<BillingState, BillingTransition>
{
public BillingStateMachine()
{
var transitions = TransitionMapBuilder<BillingState, BillingTransition>.Create()
.When(BillingTransition.Create).From(BillingState.New).To(BillingState.Pending)
.When(BillingTransition.Pay).From(BillingState.Pending).To(BillingState.Paid)
.When(BillingTransition.Refund).From(BillingState.Paid).To(BillingState.Refunded)
.Build();
MapRange(transitions);
}
}
- This is how we can use the stateless machine in an imperative way by maintaining an external state value and applying transitions to it.
var billingMachine = new BillingStateMachine();
// this is the external state value that we maintain for the stateless machine
//The externak state value is typically persisted in a database or other storage mechanism,
//and is loaded into memory when needed. The stateless machine then operates on this external state
//value to calculate the next state based on the applied transition.
var billingState = BillingState.New;
Console.WriteLine($"Initial states: Order={orderState}";
billingState = billingMachine.Apply(BillingTransition.Authorize, billingState);
Console.WriteLine($"After authorization: Order={billingState}");
- Stateless machines can also be aggregated.
var orderMachine = new OrderStateMachine();
var billingMachine = new BillingTrackerStateMachine();
var shippingMachine = new ShippingTrackerStateMachine();
var aggregator = new OrderAggregator();
var orderState = OrderState.New;
var billingState = BillingState.New;
var shippingState = ShippingState.New;
// initialize the aggregate statuses
_ = AggregateStatuses.Unknown;
// helper to build aggregation inputs
IEnumerable<(IAggregatableStateMachine, IStateProvider)> Snapshot() =>
new (IAggregatableStateMachine, IStateProvider)[]
{
(orderMachine, new SnapshotStateProvider(orderState)),
(billingMachine, new SnapshotStateProvider(billingState)),
(shippingMachine, new SnapshotStateProvider(shippingState))
};
Console.WriteLine("=== ORDER WORKFLOW ===");
Console.WriteLine($"Initial states: Order={orderState}, Billing={billingState}, Shipping={shippingState}");
Console.WriteLine($"Aggregate Status: {aggregator.Evaluate(Snapshot()).Code}\n");
// --- Order Creation / Authorization / Prepare Shipping ---
orderState = orderMachine.Apply(OrderTransition.Create, orderState);
billingState = billingMachine.Apply(BillingTransition.Authorize, billingState);
shippingState = shippingMachine.Apply(ShippingTransition.PrepareShipping, shippingState);
Console.WriteLine("After creation:");
Console.WriteLine($"Order={orderState}, Billing={billingState}, Shipping={shippingState}");
Console.WriteLine($"Aggregate Status: {aggregator.Evaluate(Snapshot()).Code}\n");
// --- Mark Ready for Dispatch ---
shippingState = shippingMachine.Apply(ShippingTransition.MarkReady, shippingState);
Console.WriteLine("After marking ready for dispatch:");
Console.WriteLine($"Shipping={shippingState}");
Console.WriteLine($"Aggregate Status: {aggregator.Evaluate(Snapshot()).Code}\n");
// --- Payment & Capture ---
orderState = orderMachine.Apply(OrderTransition.Pay, orderState);
billingState = billingMachine.Apply(BillingTransition.Capture, billingState);
Console.WriteLine("After payment:");
Console.WriteLine($"Order={orderState}, Billing={billingState}");
Console.WriteLine($"Aggregate Status: {aggregator.Evaluate(Snapshot()).Code}\n");
// --- Dispatch & Transit ---
shippingState = shippingMachine.Apply(ShippingTransition.Dispatch, shippingState);
orderState = orderMachine.Apply(OrderTransition.Ship, orderState);
Console.WriteLine("After dispatch:");
Console.WriteLine($"Order={orderState}, Shipping={shippingState}");
Console.WriteLine($"Aggregate Status: {aggregator.Evaluate(Snapshot()).Code}\n");
// --- Out for Delivery ---
shippingState = shippingMachine.Apply(ShippingTransition.OutForDelivery, shippingState);
Console.WriteLine("Out for delivery:");
Console.WriteLine($"Shipping={shippingState}");
Console.WriteLine($"Aggregate Status: {aggregator.Evaluate(Snapshot()).Code}\n");
// --- Delivered ---
shippingState = shippingMachine.Apply(ShippingTransition.Deliver, shippingState);
orderState = orderMachine.Apply(OrderTransition.Deliver, orderState);
Console.WriteLine("After delivery:");
Console.WriteLine($"Order={orderState}, Shipping={shippingState}");
Console.WriteLine($"Aggregate Status: {aggregator.Evaluate(Snapshot()).Code}\n");
Choosing Between Stateless and Stateful Models
Stateless machines:
- simpler
- easier to persist
- ideal for event sourcing
Stateful machines:
- encapsulate state
- easier imperative usage
- integrate directly with aggregation
Public API Summary
StateToken � symbolic extensible value for states and events TransitionToken � symbolic extensible value for transitions StateMachine<TState, TTransition> � passive stateless machine IAggregatableStateMachine � opt-in marker for aggregation IStateProvider � exposes state value IStateAggregator � aggregation authority StateAggregator � base class for aggregators AggregateStatus � aggregate-level outcome
License
MIT License (c) 2026 Optima Software
| 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 was computed. 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 was computed. 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. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
RELEASENOTES.md