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

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

  1. 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");
}

  1. 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);
    }
}

  1. 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);


  1. 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);
    }
}

  1. 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;
        }


  1. 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");


  1. 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);
    }
}

  1. 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}");


  1. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
3.0.1 111 3/12/2026
3.0.0 115 1/17/2026
2.2.0 115 1/17/2026
2.1.0 119 1/12/2026
2.0.0 114 1/11/2026
1.0.0 122 1/9/2026

RELEASENOTES.md