Optima.Net.StateMachines 2.0.0

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package Optima.Net.StateMachines --version 2.0.0
                    
NuGet\Install-Package Optima.Net.StateMachines -Version 2.0.0
                    
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="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Optima.Net.StateMachines" Version="2.0.0" />
                    
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 2.0.0
                    
#r "nuget: Optima.Net.StateMachines, 2.0.0"
                    
#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@2.0.0
                    
#: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=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Optima.Net.StateMachines&version=2.0.0
                    
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 is designed to model event-driven lifecycles in a passive and deterministic way. Undefined transitions are ignored, transitions are defined as data, and the same event applied multiple times will always yield the same state.


Overview

Core Principles:

  • Declarative configuration: transitions are defined as data (event → from → to).
  • Passive semantics: undefined transitions are ignored, not thrown.
  • Idempotent operations: reapplying an event results in the same state.
  • Deterministic evaluation: same input produces the same output.
  • Domain-agnostic: contains no domain logic, persistence, or infrastructure code.
  • Extensible: uses open class-based tokens instead of enums.

Building a Complete Example

The following walkthrough builds a complete working scenario involving:

  1. Order state machine
  2. Billing tracker state machine
  3. Shipping tracker state machine
  4. Aggregator that combines all of the above
  5. Console program demonstrating the workflow

Note: Although this scenario uses real-world concepts (orders, billing, shipping), the logic is drastically simplified to illustrate the mechanics of the framework only.


  1. Define State and Event Tokens

Each machine begins in a default state of New. The machine is constructed in this state automatically. Once it leaves New, it can never return.

    public class OrderState : StateToken
    {
        private OrderState(string code) : base(code) { }

        public static readonly OrderState New = new("New");
        public static readonly OrderState Created = new("Created");
        public static readonly OrderState Paid = new("Paid");
        public static readonly OrderState Shipped = new("Shipped");
        public static readonly OrderState Completed = new("Completed");
        public static readonly OrderState Cancelled = new("Cancelled");
    }

    public class OrderTransition : TransitionToken
    {
        private OrderTransition(string code) : base(code) { }

        public static readonly OrderTransition Create = new("Create");
        public static readonly OrderTransition Pay = new("Pay");
        public static readonly OrderTransition Ship = new("Ship");
        public static readonly OrderTransition Deliver = new("Deliver");
        public static readonly OrderTransition Cancel = new("Cancel");
    }

Billing:

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

    public sealed class BillingTransition : TransitionToken
    {
        private BillingTransition(string code) : base(code) { }

        public static readonly BillingTransition Authorize = new("Authorize");
        public static readonly BillingTransition Capture = new("Capture");
        public static readonly BillingTransition Refund = new("Refund");
    }

Shipping:

    public class ShippingState : StateToken
    {
        private ShippingState(string code) : base(code) { }

        // Initial state before any action
        public static readonly ShippingState New = new("New");

        // Preparing  order for shipment
        public static readonly ShippingState Preparing = new("Preparing");

        // Order is packed and waiting for carrier pickup
        public static readonly ShippingState Pending = new("Pending");

        // Package has been shipped and is in transit
        public static readonly ShippingState InTransit = new("InTransit");

        // Package is with the delivery agent and out for final delivery
        public static readonly ShippingState OutForDelivery = new("OutForDelivery");

        // Package delivered successfully to customer
        public static readonly ShippingState Delivered = new("Delivered");
    }

    public class ShippingTransition : TransitionToken
    {
        private ShippingTransition(string code) : base(code) { }

        // Pre-dispatch step: warehouse preparation
        public static readonly ShippingTransition PrepareShipping = new("PrepareShipping");

        // Warehouse marks package as ready for carrier pickup
        public static readonly ShippingTransition MarkReady = new("MarkReady");       

        // Carrier pickup and transit
        public static readonly ShippingTransition Dispatch = new("Dispatch");

        // Final delivery leg
        public static readonly ShippingTransition OutForDelivery = new("OutForDelivery");

        // Completion
        public static readonly ShippingTransition Deliver = new("Deliver");
    }

  1. Build State Machines

Order machine:

    public OrderStateMachine()
    {
        var transitions = TransitionMapBuilder<OrderState, OrderTransition>.Create()
            .When(OrderTransition.Create).From(OrderState.New).To(OrderState.Created)
            .When(OrderTransition.Pay).From(OrderState.Created).To(OrderState.Paid)
            .When(OrderTransition.Ship).From(OrderState.Paid).To(OrderState.Shipped)
            .When(OrderTransition.Deliver).From(OrderState.Shipped).To(OrderState.Completed)
            .When(OrderTransition.Cancel).From(OrderState.Created).To(OrderState.Cancelled)
            .Build();

        MapRange(transitions);
    }

Billing machine:

    public BillingTrackerStateMachine()
    {
        var transitions = TransitionMapBuilder<BillingState, BillingTransition>.Create()
            .When(BillingTransition.Authorize).From(BillingState.New).To(BillingState.Pending)
            .When(BillingTransition.Capture).From(BillingState.Pending).To(BillingState.Paid)
            .When(BillingTransition.Refund).From(BillingState.Paid).To(BillingState.Refunded)
            .Build();

        MapRange(transitions);
    }

Shipping machine:

    public ShippingTrackerStateMachine()
    {
        Map(ShippingTransition.PrepareShipping, ShippingState.New, ShippingState.Preparing);
        Map(ShippingTransition.MarkReady, ShippingState.Preparing, ShippingState.Pending);
        Map(ShippingTransition.Dispatch, ShippingState.Pending, ShippingState.InTransit);
        Map(ShippingTransition.OutForDelivery, ShippingState.InTransit, ShippingState.OutForDelivery);
        Map(ShippingTransition.Deliver, ShippingState.OutForDelivery, ShippingState.Delivered);
    }

  1. Define AggregateStatus Extension (Domain Side)

    public static class AggregateStatuses
    {
        public static readonly AggregateStatus Unknown = new AppAggregateStatus("Unknown");
        public static readonly AggregateStatus Pending = new AppAggregateStatus("Pending");
        public static readonly AggregateStatus Active = new AppAggregateStatus("Active");
        public static readonly AggregateStatus Completed = new AppAggregateStatus("Completed");
        public static readonly AggregateStatus Failed = new AppAggregateStatus("Failed");
    }

    /// <summary>
    /// Concrete subclass of AggregateStatus for the application.
    /// </summary>
    internal sealed class AppAggregateStatus : AggregateStatus
    {
        public AppAggregateStatus(string code) : base(code) { }
    }

  1. Build an Aggregator

    public class OrderAggregator : StateAggregator<AggregateStatus>
    {
        public override AggregateStatus Evaluate(params StateToken[] subStates)
        {
            // Defensive default
            if (subStates == null || subStates.Length == 0)
                return AggregateStatus.Get("Pending");

            // Any failure across subsystems
            if (subStates.Any(s =>
                s.Equals(OrderState.Cancelled) ||
                s.Equals(BillingState.Refunded)))
                return AggregateStatus.Get("Failed");

            // All subsystems complete/delivered/paid ? Completed
            if (subStates.All(s =>
                s.Equals(OrderState.Completed) ||
                s.Equals(ShippingState.Delivered) ||
                s.Equals(BillingState.Paid)))
                return AggregateStatus.Get("Completed");

            // At least one subsystem is "active" (order created, payment or shipping started)
            if (subStates.Any(s =>
                s.Equals(OrderState.Created) ||
                s.Equals(OrderState.Paid) ||
                s.Equals(OrderState.Shipped) ||
                s.Equals(ShippingState.InTransit) ||
                s.Equals(ShippingState.OutForDelivery) ||
                s.Equals(BillingState.Pending)))
                return AggregateStatus.Get("Active");

            // Otherwise, still pending (hasn't even been created)
            return AggregateStatus.Get("Pending");
        }
    }

  1. Console Example (Demonstrating Full Workflow)


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;

Console.WriteLine("=== ORDER WORKFLOW ===");
Console.WriteLine($"Initial states: Order={orderState}, Billing={billingState}, Shipping={shippingState}");
Console.WriteLine($"Aggregate Status: {aggregator.Evaluate(orderState, billingState, shippingState).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(orderState, billingState, shippingState).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(orderState, billingState, shippingState).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(orderState, billingState, shippingState).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(orderState, billingState, shippingState).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(orderState, billingState, shippingState).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(orderState, billingState, shippingState).Code}\n");

Example console output:

=== ORDER WORKFLOW ===
Initial states: Order=New, Billing=New, Shipping=New
Aggregate Status: Pending

After creation:
Order=Created, Billing=Pending, Shipping=Preparing
Aggregate Status: Active

After marking ready for dispatch:
Shipping=Pending
Aggregate Status: Active

After payment:
Order=Paid, Billing=Paid
Aggregate Status: Active

After dispatch:
Order=Shipped, Shipping=InTransit
Aggregate Status: Active

Out for delivery:
Shipping=OutForDelivery
Aggregate Status: Active

After delivery:
Order=Completed, Shipping=Delivered
Aggregate Status: Completed


Public API Summary

StateToken Represents an extensible symbolic value for states or events.

Transition<TState, TEvent> Represents a transition rule (Event, From, To).

TransitionMapBuilder<TState, TEvent> Fluent builder for defining transitions.

IStateMachine<TState, TEvent> Defines the passive state machine interface.

  • Apply(TEvent event, TState current)
  • GetNextStates(TState from)

StateMachine<TState, TEvent> Abstract implementation using internal transition map.

IStateAggregator<TAggregateState> Defines an aggregator interface for computing meta-states.

StateAggregator<TAggregateState> Abstract implementation base class for aggregators.

AggregateStatus (partial) Extensible type for representing aggregate outcomes such as Pending, Active, Completed, Failed.


License

MIT License (c) 2026 Optima Engineering

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