SiLA2.StateMachine 10.2.6

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

SiLA2.StateMachine

A state machine module for orchestrating multi-step SiLA2 lab automation workflows. Built on the Stateless library, it provides a fluent API for defining workflows with automatic retry policies, OpenTelemetry tracing, and dependency injection support.

NuGet Package SiLA2.StateMachine
Repository https://gitlab.com/SiLA2/sila_csharp
SiLA Standard https://sila-standard.com
License MIT

Features

  • Fluent Builder API: Define states, transitions, and actions with method chaining
  • Async Entry/Exit Actions: Execute SiLA2 service calls on state transitions
  • Workflow Context: Thread-safe ConcurrentDictionary-backed data sharing between workflow steps
  • Retry Policies: Configurable retry with constant or exponential backoff (with capped max delay)
  • Auto-Advance: Automatic state progression via completion triggers with infinite-loop protection
  • Progress Events: Subscribe to workflow state changes, retries, and errors via EventHandler<WorkflowProgress<TState>>
  • OpenTelemetry: Built-in distributed tracing via System.Diagnostics.ActivitySource
  • DOT Graph Export: Visualize state machines with Graphviz (UML DOT format)
  • Dependency Injection: First-class ASP.NET Core IServiceCollection integration
  • Cancellation Support: All async operations respect CancellationToken propagation
  • Thread Safety: Workflow execution is protected with SemaphoreSlim locking

Installation

dotnet add package SiLA2.StateMachine

Platform Compatibility

Target Framework Supported
.NET 10.0+ Yes (full feature support)
.NET Standard 2.0 Yes
.NET Framework 4.6.1+ Yes (via netstandard2.0)
.NET Core 2.0+ Yes (via netstandard2.0)

This package multi-targets net10.0 and netstandard2.0, allowing it to be used from both modern .NET and legacy .NET Framework projects. PolySharp is used to backport modern C# language features (e.g. required, init) to netstandard2.0.

Dependencies

Package Purpose
Stateless 5.20.1 Underlying finite state machine engine
Microsoft.Extensions.Logging.Abstractions ILogger integration (no concrete logger required)
PolySharp Polyfills for C# 11+ features on older targets

Quick Start

using SiLA2.StateMachine;
using SiLA2.StateMachine.Models;

// Define states and triggers
enum State { Idle, Processing, Done, Error }
enum Trigger { Start, Complete, Fault }

// Build workflow
var workflow = new SilaWorkflowBuilder<State, Trigger>()
    .StartIn(State.Idle)
    .Permit(State.Idle, Trigger.Start, State.Processing)
    .Permit(State.Processing, Trigger.Complete, State.Done)
    .Permit(State.Processing, Trigger.Fault, State.Error)
    .InState(State.Processing)
        .OnEntry(async (ctx, sp, ct) =>
        {
            // Call SiLA2 services here
            ctx.Set("result", 42);
        })
        .OnCompleted(Trigger.Complete)
        .OnError(Trigger.Fault, maxRetries: 3, retryDelayMs: 1000)
    .Build();

// Run workflow
await workflow.FireAsync(Trigger.Start);

Console.WriteLine($"State: {workflow.CurrentState}");  // Done
Console.WriteLine($"Result: {workflow.Context.Get<int>("result")}");  // 42

Architecture

Key Types

Type Description
SilaWorkflowBuilder<TState, TTrigger> Fluent builder — separates workflow definition from execution
ISilaWorkflow<TState, TTrigger> Runtime workflow interface — fire triggers, query state, subscribe to progress
SilaWorkflow<TState, TTrigger> Internal engine wrapping Stateless with retry logic and tracing
IWorkflowContext Thread-safe key-value data bag flowing through the workflow
WorkflowContext Default ConcurrentDictionary-backed implementation
WorkflowErrorPolicy Retry configuration: NoRetry, SimpleRetry, or ExponentialBackoff
WorkflowProgress<TState> Event payload with FromState, ToState, Exception, RetryAttempt, and Timestamp
WorkflowStatus Enum: CreatedRunningCompleted / Faulted / Aborted
ServiceCollectionExtensions AddSilaStateMachine() and AddSilaWorkflow<>() DI helpers
WorkflowActivitySource Static ActivitySource named "SiLA2.StateMachine" for OpenTelemetry

Execution Flow

When RunAsync() or FireAsync() is called, the engine follows this sequence:

  1. Transition — The Stateless state machine validates and executes the trigger
  2. Entry Action — The configured OnEntry action runs (typically a SiLA2 gRPC call)
  3. Retry Loop — If the entry action throws, the WorkflowErrorPolicy determines whether to retry or escalate
  4. Auto-Advance — If OnCompleted(trigger) is configured, that trigger fires automatically, returning to step 1 for the next state
  5. Completion — When no more triggers are permitted, Status transitions to Completed

Example: Plate Assay Workflow

The following diagram shows a complete lab automation workflow orchestrating multiple SiLA2 services — from network discovery through liquid handling, incubation, measurement, and reporting.

State Diagram

stateDiagram-v2
    [*] --> Idle

    Idle --> Discovering : Start

    Discovering --> Aspirating : ServersFound
    Discovering --> Error : Fault

    Aspirating --> Dispensing : AspirateComplete
    Aspirating --> Error : Fault

    Dispensing --> Incubating : DispenseComplete
    Dispensing --> Error : Fault

    Incubating --> Measuring : IncubationComplete
    Incubating --> Error : Fault

    Measuring --> Reporting : MeasurementComplete
    Measuring --> Error : Fault

    Reporting --> Completed : ReportComplete

    Error --> Idle : Retry
    Error --> Idle : Reset
    Completed --> Idle : Reset

    Completed --> [*]

State Descriptions

State SiLA2 Service Action
Discovering Discover SiLA2 servers on the network via mDNS; store host endpoints in context
Aspirating LiquidHandler Aspirate sample from source plate; store aspiratedVolume in context
Dispensing LiquidHandler Dispense sample into target plate wells (A1–H12)
Incubating TemperatureController Set target temperature (37 °C) and wait for incubation to complete
Measuring PlateReader Read absorbance across all wells; store absorbanceResults array in context
Reporting Compute min/max/average and generate the assay report
Error Halt — awaiting Retry or Reset trigger to recover

Full Example Code

The complete working example is at src/Examples/WorkflowOrchestration/:

dotnet run --project src/Examples/WorkflowOrchestration/SiLA2.StateMachine.Example.csproj

<details> <summary>Defining the workflow (click to expand)</summary>

var workflow = new SilaWorkflowBuilder<AssayState, AssayTrigger>()
    .StartIn(AssayState.Idle)

    // Transitions
    .Permit(AssayState.Idle,        AssayTrigger.Start,               AssayState.Discovering)
    .Permit(AssayState.Discovering, AssayTrigger.ServersFound,        AssayState.Aspirating)
    .Permit(AssayState.Discovering, AssayTrigger.Fault,               AssayState.Error)
    .Permit(AssayState.Aspirating,  AssayTrigger.AspirateComplete,    AssayState.Dispensing)
    .Permit(AssayState.Aspirating,  AssayTrigger.Fault,               AssayState.Error)
    // ... (remaining transitions)

    // State configuration with SiLA2 service calls
    .InState(AssayState.Aspirating)
        .WithDescription("Aspirate sample from source plate using LiquidHandler")
        .OnEntry(async (ctx, sp, ct) =>
        {
            var liquidHandler = sp.GetRequiredService<ILiquidHandlerClient>();
            var host = ctx.Get<string>("liquidHandler.host");
            await liquidHandler.AspirateAsync(100.0, ct);
            ctx.Set("aspiratedVolume", 100.0);
        })
        .OnCompleted(AssayTrigger.AspirateComplete)
        .OnError(AssayTrigger.Fault, maxRetries: 2, retryDelayMs: 1000)

    .Build(services, logger);

</details>

Core Concepts

States and Triggers

States and triggers are defined as enums (or any struct). The workflow transitions between states when triggers are fired.

enum AssayState { Idle, Aspirating, Dispensing, Incubating, Measuring, Done, Error }
enum AssayTrigger { Start, AspirateComplete, DispenseComplete, IncubateComplete, MeasureComplete, Fault }

Workflow Builder

The SilaWorkflowBuilder<TState, TTrigger> separates workflow definition from execution. This means you can register a builder as a singleton in DI and create many workflow instances from it, each with their own context and state.

var workflow = new SilaWorkflowBuilder<AssayState, AssayTrigger>()
    .StartIn(AssayState.Idle)

    // Define transitions
    .Permit(AssayState.Idle, AssayTrigger.Start, AssayState.Aspirating)
    .Permit(AssayState.Aspirating, AssayTrigger.AspirateComplete, AssayState.Dispensing)

    // Configure state behavior
    .InState(AssayState.Aspirating)
        .WithDescription("Aspirate sample from source plate")
        .OnEntry(async (ctx, sp, ct) =>
        {
            var liquidHandler = sp.GetRequiredService<ILiquidHandlerClient>();
            await liquidHandler.AspirateAsync(100.0, ct);
            ctx.Set("aspiratedVolume", 100.0);
        })
        .OnCompleted(AssayTrigger.AspirateComplete)
        .OnError(AssayTrigger.Fault, maxRetries: 2, retryDelayMs: 1000)

    .Build(services, logger);

Note: InState() sets the "current" state being configured — all subsequent OnEntry, OnExit, OnCompleted, and OnError calls apply to that state until InState() is called again.

Workflow Context

The IWorkflowContext is a thread-safe data bag (backed by ConcurrentDictionary<string, object> with case-insensitive keys) for passing values between states:

// In state A — store results
ctx.Set("temperature", 37.5);
ctx.Set("sampleId", "SAMPLE-001");

// In state B — retrieve results
var temp = ctx.Get<double>("temperature");       // throws KeyNotFoundException if missing
if (ctx.TryGet<string>("sampleId", out var id))  // safe retrieval
{
    Console.WriteLine($"Processing {id}");
}

// Other operations
ctx.ContainsKey("temperature");  // true
ctx.Keys;                        // ["temperature", "sampleId"]
ctx.Remove("temperature");
ctx.Clear();

Entry and Exit Actions

Entry actions receive three parameters: the workflow context, a service provider for resolving dependencies, and a cancellation token:

// Full signature with CancellationToken
.OnEntry(async (IWorkflowContext ctx, IServiceProvider sp, CancellationToken ct) =>
{
    var client = sp.GetRequiredService<ILiquidHandlerClient>();
    await client.AspirateAsync(100.0, ct);
})

// Convenience overload without CancellationToken
.OnEntry(async (ctx, sp) =>
{
    await Task.Delay(1000);
})

// Synchronous overload
.OnEntry((ctx, sp) =>
{
    ctx.Set("ready", true);
})

Exit actions run during cleanup when leaving a state (no retry support):

.OnExit(async (ctx, sp, ct) =>
{
    // Release resources, close channels, etc.
})

Error Policies

Configure retry behavior for transient hardware or network failures:

// Exponential backoff — delays: 1s, 2s, 4s, 8s, 16s (capped at MaxRetryDelay)
.InState(State.Measuring)
    .OnEntry(async (ctx, sp, ct) => { /* may fail */ })
    .OnError(Trigger.Fault, WorkflowErrorPolicy.ExponentialBackoff(
        maxRetries: 5,
        initialDelay: TimeSpan.FromSeconds(1),
        multiplier: 2.0))
Factory Method Behavior
NoRetry Fail immediately — fire error trigger on first exception (default)
SimpleRetry(n, delay) Retry n times with constant delay between attempts
ExponentialBackoff(n, delay, mult) Retry n times with delay × mult^attempt (capped at MaxRetryDelay, default 1 min)

When all retries are exhausted, the configured ErrorTrigger fires (if permitted), and Status becomes Faulted.

Auto-Advance

When OnCompleted(trigger) is set on a state, that trigger fires automatically after the entry action succeeds. This enables fully automatic "step-through" workflows:

Idle --Start--> Discovering --ServersFound--> Aspirating --AspirateComplete--> ...
                   (auto)                        (auto)

A safety guard limits auto-advance to 100 consecutive steps to prevent infinite loops. Exceeding this sets Status = Faulted.

Progress Events

Subscribe to real-time workflow progress including retries and errors:

workflow.ProgressChanged += (sender, progress) =>
{
    Console.WriteLine($"[{progress.Timestamp:HH:mm:ss}] {progress.FromState} → {progress.ToState}");

    if (progress.Exception != null)
        Console.WriteLine($"  Error (attempt {progress.RetryAttempt}): {progress.Exception.Message}");

    if (progress.Message != null)
        Console.WriteLine($"  {progress.Message}");
};

The WorkflowProgress<TState> payload contains:

Property Type Description
FromState TState Origin state of the transition
ToState TState Destination state
Timestamp DateTimeOffset UTC timestamp of the event
Message string? Human-readable description (e.g. "Auto-advanced via 'Complete'")
Exception Exception? The exception that caused a retry or fault
RetryAttempt int Current retry attempt number (0 if no retries)

Workflow Status Lifecycle

Status Description
Created Workflow built via Build() but not yet started
Running RunAsync() or FireAsync() has been called
Paused Reserved for future use
Completed No more permitted triggers — workflow finished successfully
Aborted AbortAsync() was called
Faulted Unrecoverable error — retries exhausted or auto-advance loop detected

ASP.NET Core Integration

Register the state machine infrastructure and pre-configured workflow builders via dependency injection:

// Program.cs
using SiLA2.StateMachine.Extensions;

// Option 1: Register infrastructure only (create builders manually)
builder.Services.AddSilaStateMachine();

// Option 2: Register a pre-configured workflow builder as a singleton
builder.Services.AddSilaWorkflow<AssayState, AssayTrigger>(builder =>
{
    builder
        .StartIn(AssayState.Idle)
        .Permit(AssayState.Idle, AssayTrigger.Start, AssayState.Processing)
        // ... configure workflow
});
// In a controller or service — inject the builder and create workflow instances
public class AssayController(
    SilaWorkflowBuilder<AssayState, AssayTrigger> builder,
    IServiceProvider services,
    ILogger<AssayController> logger)
{
    public async Task<IActionResult> RunAssay()
    {
        // Each call creates a fresh workflow instance with its own context
        var workflow = builder.Build(services, logger);
        await workflow.FireAsync(AssayTrigger.Start);
        return Ok(new { workflow.Status, State = workflow.CurrentState });
    }
}

AddSilaStateMachine() registers IWorkflowContext as scoped, so each HTTP request (or scope) gets its own context. AddSilaWorkflow<>() registers the builder as a singleton factory.

OpenTelemetry Integration

The library emits distributed traces via System.Diagnostics.ActivitySource under the source name SiLA2.StateMachine:

// Subscribe to workflow traces
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddSource("SiLA2.StateMachine");  // or WorkflowActivitySource.Name
    });

Traced Activities

Activity Name Scope Tags
Workflow.Run Full RunAsync() execution workflow.initial_state
Workflow.Fire Single FireAsync() call workflow.trigger, workflow.from_state
Workflow.StateEntry Entry action execution (per attempt) workflow.state, workflow.attempt

Advanced Patterns

Hierarchical (Sub) States

.SubstateOf(State.Processing, State.Running)
// Processing is a substate of Running — triggers permitted on Running
// are also permitted on Processing

Conditional Transitions (Guard Clauses)

.PermitIf(State.Idle, Trigger.Start, State.FastPath,
    () => _config.UseFastPath, "Fast path enabled")
.PermitIf(State.Idle, Trigger.Start, State.SlowPath,
    () => !_config.UseFastPath, "Slow path enabled")

Reentrant Transitions

.PermitReentry(State.Polling, Trigger.Poll)
// The state transitions to itself — entry/exit actions re-execute

Ignoring Triggers

.Ignore(State.Running, Trigger.Start)
// The Start trigger is silently ignored in the Running state (no exception)

Raw Stateless Access

For advanced Stateless features not exposed by the fluent API:

.ConfigureStateMachine(sm =>
{
    sm.Configure(State.Active)
        .InternalTransition(Trigger.Heartbeat, () => { /* no state change */ });
})

DOT Graph Export

Generate a Graphviz-compatible DOT graph for documentation or debugging:

var dotGraph = workflow.ToDotGraph();
File.WriteAllText("workflow.dot", dotGraph);
// Render: dot -Tpng workflow.dot -o workflow.png
// Or paste into https://dreampuf.github.io/GraphvizOnline/

Pre-populated Context

Pass initial data into the workflow at build time:

var context = new WorkflowContext();
context.Set("batchId", "BATCH-2025-001");
context.Set("targetTemperature", 37.0);

var workflow = builder.Build(services, logger, context);

Manual Step-Through

Omit OnCompleted() to pause the workflow at each state, allowing external control:

.InState(State.WaitingForApproval)
    .OnEntry(async (ctx, sp, ct) =>
    {
        // Notify approver, then workflow pauses here
    })
    // No .OnCompleted() — workflow waits for manual FireAsync()
// Later, after approval is received:
if (workflow.CanFire(Trigger.Approved))
    await workflow.FireAsync(Trigger.Approved);

Builder API Reference

Method Description
StartIn(state) Set the initial state (required before Build())
InState(state) Begin configuring a specific state
WithDescription(text) Add a human-readable description to the current state
OnEntry(action) Async action executed when entering the state (3 overloads)
OnExit(action) Async cleanup action when leaving the state
OnCompleted(trigger) Auto-fire this trigger after successful entry (enables auto-advance)
OnError(trigger, policy) Error trigger + retry policy when entry action fails
OnError(trigger, maxRetries, retryDelayMs) Convenience overload creating a SimpleRetry policy
Permit(from, trigger, to) Define a permitted transition
PermitIf(from, trigger, to, guard, desc?) Conditional transition with a guard function
PermitReentry(state, trigger) Self-transition (re-executes entry/exit actions)
Ignore(state, trigger) Silently ignore a trigger in a state
SubstateOf(sub, super) Declare a hierarchical state relationship
ConfigureStateMachine(action) Raw access to the underlying StateMachine<TState, TTrigger>
Build(services?, logger?, context?) Create an ISilaWorkflow<TState, TTrigger> instance

Workflow Instance API

Member Description
CurrentState The current state of the workflow
Status High-level status: Created, Running, Completed, Faulted, Aborted
PermittedTriggers Triggers currently valid from the current state
Context The IWorkflowContext data bag
RunAsync(ct) Start the workflow (executes initial state's entry action + auto-advance)
FireAsync(trigger, ct) Manually fire a trigger to cause a state transition
CanFire(trigger) Check if a trigger is permitted without firing
AbortAsync(reason?) Abort the workflow (sets Status = Aborted)
ToDotGraph() Export the state machine as a UML DOT graph string
ProgressChanged Event raised on every transition, retry, and error

Troubleshooting

Error Cause Solution
Initial state not set Missing StartIn() call Call StartIn(state) before Build()
No state selected Missing InState() call Call InState(state) before OnEntry() / OnError() / etc.
Trigger 'X' is not permitted in state 'Y' Invalid transition Check CanFire() first, or add a Permit() for that state/trigger pair
Max auto-advance depth exceeded Infinite auto-advance loop Ensure the workflow reaches a terminal state (no OnCompleted)
KeyNotFoundException in context Missing ctx.Set() in prior state Use ctx.TryGet() for defensive access, or verify data flow between states
No IServiceProvider services available Build() called without services Pass your DI IServiceProvider to Build(services, logger)

License

MIT License — See LICENSE file for details.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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 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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
10.2.6 98 7/5/2026
10.2.5 94 7/4/2026
10.2.4 151 3/13/2026
10.2.3 117 3/10/2026