SWS.StateMachine 1.2.0

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

SWS State Machine

badge badge

Orchestrate complex workflows in a declarative and standardized style with decoupled logging, retrying and error handling strategies.

Contents

The features presented are complete and usable, but some aspects of this documentation are a work in progress.

Introduction

This implementation is based on the concept of Finite State Machines and is heavily influenced by the AWS Step Functions API. The idea is that complex workflows should be broken into smaller execution steps and be orchestrated by a separated, dedicated layer, which should use standardized and declarative syntax. This approach allows for the following advantages:

  • Standardization. Using a standardized approach helps different teams understand and maintain the application flow logic across applications.
  • Decoupled orchestration logic. Removing workflow logic such as error handling, retry handling and logging from the application main steps allows for cleaner, more maintainable code that will be easier to test.

Basic Usage

The following instructions explain how to start using the State Machine and it's state types.

Instantiate the State Machine

Instantiate the State Machine object wich will provide the factory methods to create the states.

var stateMachine = new StateMachineV2();

Create a Task State

The task is the basic, core state. It's where the individual program tasks will be will be referenced and executed. The types within the diamond operators represent it's input and output types.

var task = stateMachine.Create.Task<int, int>();

Set a Resource to the Task State

The Resource represents the main action of the task. The input and output types of the function it contains must follow the ones declared in the task's instantiation. Only one resource per task is allowed. Additional declarations will override previous ones.

var task = stateMachine.Create.Task<int, int>();

task
    .Resource(input =>
    {
        Console.WriteLine(input);
        return 2;
    }
    
task.Execute(1);

Outputs:

1

Create a Parallel State

The Parallel State is one one the available orchestration states. It will accept one or more states to be designed as it’s branch states. The branches will be executed in parallel, each in its own separate thread, as soon as the Parallel State execution is requested. The branches can be single states or complex state chains including any other state types. Upon calculating it’s output, the parallel state will take every output of the branches’ “leave” states and compile them to a list of the given output type. “Leave” states are those in the branch chains that don’t have a NextState attached, in other words, that aren’t chained to any other. Make sure these output the parallel state’s expected type to comply to it’s output signature. A parallel state will always output a list of the configured type.

var parallel = stateMachine.Create.Parallel<int, int>();

Add Branches to a Parallel State

Branches are states or state chains that will be run when the parallel state´s execution is requested. The branches are executed in separate threads in parallel, so their execution order is not guaranteed.

var parallel = stateMachine.Create.Parallel<string, string>();
var task1 = stateMachine.Create.Task<string, string>();
var task2 = stateMachine.Create.Task<string, string>();

parallel
    .AddBranchState(task1)
    .AddBranchState(task2);

task1
    .Resource(input =>
    {
        Console.WriteLine(input + " from task1.");
        return input;
    };
    
task2
    .Resource(input =>
    {
        Console.WriteLine(input + " from task2.");
        return input;
    };
    
parallel.Execute("Hello");

Outputs:

Hello from task2.
Hello from task1.

Create a Map State

The Map State is one of the possible orchestration states of the StateMachine. It will summon it's worker state once for every member of the given input list. This can be accomplished sequentially or in parallel, by defining a concurrency parameter. Every Map State has specific input and output types defined upon it's construction. The worker state referenced in it must respect this signature. The Map State will accept as an input a list of the given input type, and will output a list of the given output type.

var map = stateMachine.Create.Map<int, int>();

Set a Worker to a Map State

Workers are states or state chains that will be run when the map state´s execution is requested to process the input.

var map = stateMachine.Create.Map<string, string>();
var task = stateMachine.Create.Task<string, string>();
var input = new List<string> { "Input1", "Input2" };

map
    .WorkerState(task);

task
    .Resource(input =>
    {
        Console.WriteLine(input + " from task.");
        return input;
    };


map.Execute(input);

Outputs:

Input1 from task.
Input2 from task.

Create a ConsumerProducer State

The ConsumerProducer State is one of the possible orchestration states of the StateMachine. This particular state utilizes a different mechanic when compared to the Map and Parallel states. They provide an internal queue and will invoke their associated worker states passing batches made of lists of the configured input type. ConsumerProducers run in the background and can be used to "impedance match" services that have different throughput capability. They don´t provide retry, error catching or conditional routing capabilities and can only be chained to other ConsumerProducer states. Assigned worker states must accept as their input/output a list of the ConsumerProducer declared input/output type. To instantiate a ConsumerProducer state, proceed as follows:

var consProd = stateMachine.Create.ConsumerProducer<int, int>();

Set a Worker to a ConsumerProducer State

Worker states or state chains will be executed in background once inputs are enqueued int the ConsumerProducer state:

var consProd = stateMachine.Create.ConsumerProducer<string, string>();
var task = stateMachine.Create.Task<List<string>, List<string>>();
var input = new List<string> { "Input1", "Input2" };

consProd
    .WorkerState(task);

task
    .Resource(input =>
    {
        var inputAsString = string.Join(", ", input);
        Console.WriteLine(inputAsString);
        return input;
    };

consProd.Equeue(input);
Thread.Sleep(1000);

Outputs:

Input1, Input2

General State Configurations

All state types can be configured with the following.

Set a state name

Naming a state can be useful for logging purposes. The state's name will not interfere nor be used in the state's execution logic. If you don't specify any name, a default one will be provided.

anyState.Name("ThisStatesName");

Set a state description

Describing a state can be useful for logging purposes. The state's description will not interfere nor be used in the state's execution logic.

anyState.Description("ThisStatesDescription");

Chain States (Next State)

Chaining states allows them to be executed in sequence. The output of the first state is passed to the input of the second, so their types must match. Any number of states can be chained. State executions themselves don't return any values.

var task1 = stateMachine.Create.Task<int, int>();
var task2 = stateMachine.Create.Task<int, string>();

task1
    .Resource(input =>
    {
        Console.WriteLine(input);
        return input + 1;
    }
    .NextState(task2);
    
task2
    .Resource(input =>
    {
        Console.WriteLine(input);
        return "Finished";
    };
    
task1.Execute(1);

Outputs:

1
2

Add Conditional Next States

Conditional Next States allows the chained state to be altered or defined conditionally. If any specified condition matches, the state will ignore the configured next state (if any) and route the execution accordingly. Conditional Next States are evaluated in the order they are declared. AddConditionalNextState method receives the following parameters:

  • IState<TStateInput> next. Sets the state that should be triggered if the set condition is satisfied. The state's input type has to match the input type of the state the policy is being configured on.
  • Func<TNextState, bool> condition. Condition evaluated upon the state's execution result. If it's positive, the configured state will be executed instead of the "NextState".
task1
    .AddConditionalNextState(someConditionalState, result => result == 1)
    .AddConditionalNextState(someOtherConditionalState, result => result == 2)
    .Resource(input =>
    {
        var output = input + 1;
        Console.WriteLine(output);
        return output;
    };
    
task1.Execute(1);

Outputs:

2
("someOtherConditionalState" will be executed after task1 completion.)

Ignore Exceptions

Ignore Exceptions will set the state to ignore any exceptions occurred inside it's scope. This will be evaluated after retrying and catching policies. Use this with caution since it will mask any errors occurring. Any chained states will receive a null input if errors are thrown.

task1
    .IgnoreExceptions();
    .Resource(input =>
    {
        Console.WriteLine(input);
        throw new Exception("SomeRandomExceptionMessage");
        return input;
    };
    
task1.Execute(1);

Outputs:

1
(no Exception will be thrown)

Add Retry Policies

Retry policies allow the state to handle any retrying procedures if it's resource, branch or worker throws an exception of the specified type. This strategy decouples the retrying from the main application logic resulting in cleaner and more concise code. It will also allow to retry differently depending on what problem occurred. Use this if you make an architectural decision to delegate the error handling of the application to the orchestration layer, in this case, the state machine. You can add multiple retry policies to your state, assigning each of them to a specific exception type. If you don't specify any exception type, retrying will take effect for all exceptions thrown. Retry policies will be evaluated in the order they are declared. AddRetryPolicy method receives the following parameters:

  • int intervalSeconds. Sets the base waiting interval in seconds between retry attempts for this policy.
  • int maxAttempts. Sets the maximum number of retry attempts.
  • double backoffRate. Sets the backoff rate. The waiting time between each retry attempt will be multiplied by this factor.
  • params Type[] exceptionTypes. Exception types that will trigger this specific retry policy. If none is specified, any will be considered.
task1
    .AddRetryPolicy(1, 2, 2, typeof(NullReferenceException))
    .AddRetryPolicy(1, 2, 1.2, typeof(OutOfMemoryException), typeof(DivideByZeroException))
    .AddRetryPolicy(2, 3, 1.5)
    .Resource(input =>
    {
        Console.WriteLine(input);
        throw new Exception("SomeRandomExceptionMessage");
        return input + 1;
    };
    
task1.Execute(1);

Outputs:

1
1 . . . . . // Application waits for 2 seconds and retries...
1 . . . . . // Application waits for 3 seconds and retries ...
1 . . . . . // Application waits for 4.5 seconds and retries...
(actually throws exception)

Add Catch Policies

Catch policies will configure the state to ignore the configured chained next state (if any was declared) and route the execution to another state if specific or any errors within it's execution scope are caught. They will be evaluated after retry policies. Catch policies themselves will be evaluated in the order they are declared. The state the policy routes to receives the same input as the origin state. AddCatchPolicy method receives the following parameters:

  • IState<TStateInput> next. Sets the state that should be triggered if an exception of the specified type is thrown. The chained next state's input type has to match it's parent's.
  • params Type[] exceptionTypes. Exception types that will trigger this specific catch policy. If none is specified, any will be considered.
task1
    .AddCatchPolicy(someState, typeof(NullReferenceException), typeof(DivideByZeroException))
    .AddCatchPolicy(someOtherState, typeof(OutOfMemoryException))
    .AddCatchPolicy(someFinalState);
    .Resource(input =>
    {
        Console.WriteLine(input);
        throw new Exception("SomeRandomExceptionMessage");
        return input + 1;
    };
    
task1.Execute(1);

Outputs:

1     
("someFinalState" will be executed after task1 completion)

Add Execution Hooks

Execution hooks allow actions to be performed before and after the main purpose of the state. They are ideal for performing auxiliary actions such as logging, monitoring, capturing intermediary state machine results and notifying external application modules. They can also be used to programatically delay the execution of the referenced states. Hooks of the same type will be executed in the order they are added. Hooks don't expect any output from the actions they execute. If the action declared within it's scope throws any exception, the hook will not excecute and the exception will be ignored. Hooks mechanics are available to any state type.

var task = stateMachine.Create.Task<int, int>();

task
    .AddPreHook(info => Console.WriteLine("Executing pre hook 1 ..."))
    .AddPreHook(info => Console.WriteLine("Executing pre hook 2 ..."))
    .AddPostHook(info => Console.WriteLine("Executing post hook 1 ..."))
    .AddPostHook(info => Console.WriteLine("Executing post hook 2 ..."))
    .Resource(input =>
    {
        Console.WriteLine("Executing main action ...");
        return input + 1;
    };
    
task.Execute(1);

Outputs:

Executing pre hook 1 ...
Executing pre hook 2 ...
Executing main action ...
Executing post hook 1 ...
Executing post hook 2 ...

Dependency Injection

It is possible to register states on the dependency injection container. This strategy allows for better memory management and decouples the state dependencies from the orchestration service logic so you can better design your unit tests. States will be injected as transient lifecycle, so each copy of a particular state can be independently used and configured. The mechanic works with the concept of signatures. A signature represents a group of states with a particular input and output type. As long as you register a signature, you can use as many copies of a state following it as necessary in your application. Register the state signatures on your DI container as follows:

private static void ConfigureServices(IServiceCollection services)
{
    // Register your regular application dependencies
    services.AddSingleton<ISomeOrchestrationService, SomeOrchestrationService>();
    services.AddSingleton<ISomeOtherService, SomeOtherService>();
    
    // Register SWS StateMachine state signatures
    services.AddSwsStateMachineSignature()
        .Task<string, string>()
        .Task<List<string>, string>()
        .Map<string, string>();
}

Usage:

public class SomeOrchestrationService : ISomeOrchestrationService
{
    private readonly ISomeOtherService _someOtherService;
    private readonly IMap<string, string> _map1;
    private readonly ITask<string, string> _task1;
    private readonly ITask<List<string>, string> _task2;

    public SomeOrchestrationService(
        ISomeOtherService someOtherService, 
        IMap<string, string> map1, 
        ITask<string, string> task1, 
        ITask<List<string>, string> task2)
    {
        _someOtherService = someOtherService;
        _map1 = map1;
        _task1 = task1;
        _task2 = task2;
        SetupStateMachine();
    }
    
    public void Execute(List<string> input) => _map1.Execute(input);
    
    private void SetupStateMachine()
    {
        _map1
            .WorkerState(_task1)
            .NextState(_task2);
        
        _task1.Resource(input => _someOtherService.SomeAction1(input));
        _task2.Resource(input => _someOtherService.SomeAction4(input));
    }
}
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 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. 
.NET Core netcoreapp3.1 is compatible. 
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
1.2.0 307 6/7/2023
1.1.1 577 6/18/2022
1.1.0 581 5/31/2022