Sahner.AutomatedStateMachine 1.0.1

dotnet add package Sahner.AutomatedStateMachine --version 1.0.1                
NuGet\Install-Package Sahner.AutomatedStateMachine -Version 1.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="Sahner.AutomatedStateMachine" Version="1.0.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Sahner.AutomatedStateMachine --version 1.0.1                
#r "nuget: Sahner.AutomatedStateMachine, 1.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.
// Install Sahner.AutomatedStateMachine as a Cake Addin
#addin nuget:?package=Sahner.AutomatedStateMachine&version=1.0.1

// Install Sahner.AutomatedStateMachine as a Cake Tool
#tool nuget:?package=Sahner.AutomatedStateMachine&version=1.0.1                

AutomatedStateMachine

This repository provides an implementation of an asynchronous, deterministic state machine with support for transient states, automatic behavior and custom exception handling. It allows separating the control flow logic from the applications business logic. The state machine sequentially processes input symbols, transitions between states, and invokes automation functions of transient states.

Key Concept

*   The Statemachine processes string symbols like a deterministic finite automaton.
*   A transient state is a special state which has an automation function attached to it.
*   When entering a transient state, its automation function is executed.
*   The automation functions result is then read as input symbol.
*   The returned symbol has priority and is read before any other may be.

Key Features

*   Transient States: Allow automatic behavior and complex workflows.
*   Thread-Safe Execution: Calls to state transitions are serialized, ensuring that only one transition happens at a time.
*   Advanced Exception Handling: The TransientStateException allows controlled state changes in response to errors.
*   Async-Await Pattern: The task-based asynchronous pattern allows non-blocking operation.

Getting Started

Installation via NuGet:

dotnet add package Sahner.AutomatedStateMachine

Basic Usage

Constructing state machines:
StateMachineBuilder builder = new();

//Non- transient states can easily be added as bulk
builder.AddStates("initial", "failure", "final")
    //Add a transient one
    .AddState("busy", async parameters =>
    {
        //Perform some work
        bool result = await Work();
        return result ? "success" : "error";
    })
//Add transitions
.AddTransitions(
    ("initial", "begin", "busy"),
    ("busy", "success", "final"),
    ("busy", "error", "failure"),
    ("failure", "retry", "busy")
);

//Build the state machine, given its initial state
AsyncStateMachine stateMachine = builder.BuildAsyncStateMachine("initial");
Reading Symbols and Performing Transitions

Perform an asynchronous read operation:

await stateMachine.ReadSymbolAsync("begin");
//Now in state final or failure

Or a synchronous one if you need:

stateMachine.ReadSymbolAsync("begin").Wait()
//Now in state final or failure
Basic Exception Handling
try
{
    //Read a symbol
    await stateMachine.ReadSymbolAsync("invalidSymbol");
}
catch (NoTransitionForSymbolException)
{
    //Usually no need to check the state machines current state.
    //Thrown if it can not find a transition for the given symbol
}
catch (AggregateException ex) { 
    //If more than one exception occurred, they are wrapped in an AggregateException
}
Defining Transient States

An automation function can be attached to a state during the build process. Either inline like before or as an explicit function: Lets say there is a class with the following function:

private async Task<string> AutomationFunction(object[]? parameters)
{
    var result = await Work();
    return result ? "success" : "error";
}

We can then use it as follows:

StateMachineBuilder builder = new();

builder.AddState("busy", AutomationFunction);
Passing Parameters

It is possible to pass parameters to the automation function:

private async Task<string> AutomationFunction(object[]? parameters)
{
    var parameter0 = (string)parameters![0];
    var parameter1 = (string)parameters![1];
    var result = await Work();
    return result ? "success" : "error";
}

private async void ReadWithParameter(){
    //Read a symbol
    await stateMachine.ReadSymbolAsync("begin", "parameter content 0", "parameter content 1");
}
Exceptions in Transient States

When an uncaught exception occurs within the automation function of a transient state, the state machine does not get a symbol to continue and gets stuck at a transient state, unable to read further symbols. To still allow proper exception handling, throw a TransientStateException:

throw new TransientStateException("symbol");
//Or:
throw new TransientStateException("symbol", "message");
//Or:
throw new TransientStateException("symbol", "message", innerExeption);

It could look like this:

private async Task<string> AutomationFunction(object[]? parameters)
{
    try
    {
        await Work();
        return "success";
    }
    catch (Exception innerEx)
    {
        throw new TransientStateException("error", "message", innerEx);
    }
}

Alternatively set the DefaultErrorSymbol property. Every time an exception not of type TransientStateException is thrown, this predefined symbol is read:

StateMachineBuilder builder = new();
//Add states and transitions...
AsyncStateMachine stateMachine = builder.BuildAsyncStateMachine("initial", "default error symbol");

Responding to State Changes

There are three basic events to perform certain actions if a specific state was entered or left. Those events are suited e.g. for logging or to perform changes on the UI, if existent, like enabling or disabling control elements.

stateMachine.OnStateChanged += (AsyncStateMachine sender, StateChangedEventArgs e) =>
{
    Console.WriteLine($"State change from: {e.FromState} to {e.ToState} by symbol {e.Symbol}.");
};

stateMachine.States["initial"].Entered += (State sender, StateEnteredEventArgs e) =>
{
    Console.WriteLine($"The initial state was entered from {e.FromState} by symbol {e.Symbol}");
};

stateMachine.States["initial"].Leave += (State sender, StateLeaveEventArgs e) =>
{
    Console.WriteLine($"The initial state was left to {e.ToState} by symbol {e.Symbol}");
};

Larger Abstract Example

This larger example retries some action a given number of times before throwing an exception. It makes use of the possibility to concatenate transient states. More complex use cases would probably include more different transient states than this example.

public class RetryExample
{
    private int retries;
    private readonly int maxRetries;
    private readonly AsyncStateMachine stateMachine;
    public RetryExample(int maxRetries) { 
        retries = maxRetries;
        this.maxRetries = maxRetries;

        //Construct state machine

        StateMachineBuilder builder = new();

        builder
            .AddStates("initial", "failure", "finish")
            .AddState("trying", Trying)
            .AddTransitions(
                ("initial", "try", "trying"),
                ("trying", "success", "finish"),
                ("trying", "error", "failure"),
                ("trying", "retry", "trying")
                );


        stateMachine = builder.BuildAsyncStateMachine("initial");
    }

    private async Task<string> Trying(object[]? parameters)
    {
        try
        {
            //Perform something that could fail instead
            await Task.Delay(1000);
            //Success
            return "success";
        }
        catch (Exception ex)
        {
            //Failed, reduce remaining retries
            retries--;
            if (retries > 0)
            {
                //Ignore the exception and retry
                return "retry";
            }
            else
            {
                //No more retries
                throw new TransientStateException("error", $"Failed to perform the action {maxRetries} times.", ex);
            }
        }
    }

    public async Task BeginTrying()
    {
        try
        {
            await stateMachine.ReadSymbolAsync("try");
        }
        catch (NoTransitionForSymbolException)
        {
            //Already done, current state is failure or finish
        }
        catch (Exception ex)
        {
            //The final exception if failed.
            //Catch and process here or just propagate
        }
    }
}

License

This repository is licensed under the MIT License. See the LICENSE file for more details.

Feel free to open issues or submit pull requests to contribute to this project!

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. 
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
1.0.1 80 10/24/2024
1.0.0 72 10/24/2024

Now including the api documentation.