prasannavl / LiquidState

Efficient asynchronous and synchronous state machines for .NET
Apache License 2.0
240 stars 29 forks source link
async c-sharp state-machine

LiquidState

Efficient state machines for .NET with both synchronous and asynchronous support. Heavily inspired by the excellent state machine library Stateless by Nicholas Blumhardt.

NuGet badge

Installation

NuGet:

Install-Package LiquidState

Supported Platforms:

.NETPlatform 1.0 (Formerly PCL259 profile - Supports .NETCore, .NETDesktop, Xamarin and Mono)

Highlights

Release Notes

How To Use

You only ever create machines with the StateMachineFactory static class. This is the factory for both configurations and the machines. The different types of machines given above are automatically chosen based on the parameters specified from the factory.

Step 1: Create a configuration:

var config = StateMachineFactory.CreateConfiguration<State, Trigger>();

or for awaitable, or async machine:

var config = StateMachineFactory.CreateAwaitableConfiguration<State, Trigger>();

Step 2: Setup the machine configurations using the fluent API.

    config.ForState(State.Off)
        .OnEntry(() => Console.WriteLine("OnEntry of Off"))
        .OnExit(() => Console.WriteLine("OnExit of Off"))
        .PermitReentry(Trigger.TurnOn)
        .Permit(Trigger.Ring, State.Ringing,
                () => { Console.WriteLine("Attempting to ring"); })
        .Permit(Trigger.Connect, State.Connected,
                () => { Console.WriteLine("Connecting"); });

    var connectTriggerWithParameter =
                config.SetTriggerParameter<string>(Trigger.Connect);

    config.ForState(State.Ringing)
        .OnEntry(() => Console.WriteLine("OnEntry of Ringing"))
        .OnExit(() => Console.WriteLine("OnExit of Ringing"))
        .Permit(connectTriggerWithParameter, State.Connected,
                name => { 
                 Console.WriteLine("Attempting to connect to {0}", name);
                })
        .Permit(Trigger.Talk, State.Talking,
                () => { Console.WriteLine("Attempting to talk"); });

Step 3: Create the machine with the configuration:

var machine = StateMachineFactory.Create(State.Ringing, config);

Step 4: Use them!

machine.Fire(Trigger.On);

or

await machine.FireAsync(Trigger.On);
machine.MoveToState(State.Ringing);

or its async variant.

Examples

A synchronous machine example:

    var config = StateMachineFactory.CreateConfiguration<State, Trigger>();

    config.ForState(State.Off)
        .OnEntry(() => Console.WriteLine("OnEntry of Off"))
        .OnExit(() => Console.WriteLine("OnExit of Off"))
        .PermitReentry(Trigger.TurnOn)
        .Permit(Trigger.Ring, State.Ringing,
                () => { Console.WriteLine("Attempting to ring"); })
        .Permit(Trigger.Connect, State.Connected,
                () => { Console.WriteLine("Connecting"); });

    var connectTriggerWithParameter =
                config.SetTriggerParameter<string>(Trigger.Connect);

    config.ForState(State.Ringing)
        .OnEntry(() => Console.WriteLine("OnEntry of Ringing"))
        .OnExit(() => Console.WriteLine("OnExit of Ringing"))
        .Permit(connectTriggerWithParameter, State.Connected,
                name => { Console.WriteLine("Attempting to connect to {0}", name); })
        .Permit(Trigger.Talk, State.Talking,
                () => { Console.WriteLine("Attempting to talk"); });

    config.ForState(State.Connected)
        .OnEntry(() => Console.WriteLine("AOnEntry of Connected"))
        .OnExit(() => Console.WriteLine("AOnExit of Connected"))
        .PermitReentry(Trigger.Connect)
        .Permit(Trigger.Talk, State.Talking,
            () => { Console.WriteLine("Attempting to talk"); })
        .Permit(Trigger.TurnOn, State.Off,
            () => { Console.WriteLine("Turning off"); });

    config.ForState(State.Talking)
        .OnEntry(() => Console.WriteLine("OnEntry of Talking"))
        .OnExit(() => Console.WriteLine("OnExit of Talking"))
        .Permit(Trigger.TurnOn, State.Off,
            () => { Console.WriteLine("Turning off"); })
        .Permit(Trigger.Ring, State.Ringing,
            () => { Console.WriteLine("Attempting to ring"); });

    var machine = StateMachineFactory.Create(State.Ringing, config);

    machine.Fire(Trigger.Talk);
    machine.Fire(Trigger.Ring);
    machine.Fire(connectTriggerWithParameter, "John Doe");

Now, let's take the same dumb, and terrible example, but now do it asynchronously! (Mix and match synchronous code when you don't need asynchrony to avoid the costs.)

    // Note the "CreateAwaitableConfiguration"
    var config = StateMachineFactory.CreateAwaitableConfiguration<State, Trigger>();

    config.ForState(State.Off)
        .OnEntry(async () => Console.WriteLine("OnEntry of Off"))
        .OnExit(async () => Console.WriteLine("OnExit of Off"))
        .PermitReentry(Trigger.TurnOn)
        .Permit(Trigger.Ring, State.Ringing,
            async () => { Console.WriteLine("Attempting to ring"); })
        .Permit(Trigger.Connect, State.Connected,
            async () => { Console.WriteLine("Connecting"); });

    var connectTriggerWithParameter =
                config.SetTriggerParameter<string>(Trigger.Connect);

    config.ForState(State.Ringing)
        .OnEntry(() => Console.WriteLine("OnEntry of Ringing"))
        .OnExit(() => Console.WriteLine("OnExit of Ringing"))
        .Permit(connectTriggerWithParameter, State.Connected,
                name => { Console.WriteLine("Attempting to connect to {0}", name); })
        .Permit(Trigger.Talk, State.Talking,
                () => { Console.WriteLine("Attempting to talk"); });

    config.ForState(State.Connected)
        .OnEntry(async () => Console.WriteLine("AOnEntry of Connected"))
        .OnExit(async () => Console.WriteLine("AOnExit of Connected"))
        .PermitReentry(Trigger.Connect)
        .Permit(Trigger.Talk, State.Talking,
            async () => { Console.WriteLine("Attempting to talk"); })
        .Permit(Trigger.TurnOn, State.Off,
            async () => { Console.WriteLine("Turning off"); });

    config.ForState(State.Talking)
        .OnEntry(() => Console.WriteLine("OnEntry of Talking"))
        .OnExit(() => Console.WriteLine("OnExit of Talking"))
        .Permit(Trigger.TurnOn, State.Off,
            () => { Console.WriteLine("Turning off"); })
        .Permit(Trigger.Ring, State.Ringing,
            () => { Console.WriteLine("Attempting to ring"); });

    var machine = StateMachineFactory.Create(State.Ringing, config);

    await machine.FireAsync(Trigger.Talk);
    await machine.FireAsync(Trigger.Ring);
    await machine.FireAsync(connectTriggerWithParameter, "John Doe");

Core APIs

IStateMachineCore:

public interface IStateMachineCore<TState, TTrigger>
{
    TState CurrentState { get; }
    bool IsEnabled { get; }
    void Pause();
    void Resume();

    event Action<TriggerStateEventArgs<TState, TTrigger>> UnhandledTrigger;
    event Action<TransitionEventArgs<TState, TTrigger>> InvalidState;
    event Action<TransitionEventArgs<TState, TTrigger>> TransitionStarted;
    event Action<TransitionExecutedEventArgs<TState, TTrigger>>
                                                       TransitionExecuted;
}

Synchronous:

public interface IStateMachine<TState, TTrigger> 
        : IStateMachineCore<TState, TTrigger>
{
    IStateMachineDiagnostics<TState, TTrigger> Diagnostics { get; }

    void Fire<TArgument>(
            ParameterizedTrigger<TTrigger, TArgument> parameterizedTrigger, 
            TArgument argument);
    void Fire(TTrigger trigger);
    void MoveToState(TState state, 
            StateTransitionOption option = StateTransitionOption.Default);
}

Awaitable:

public interface IAwaitableStateMachine<TState, TTrigger> 
        : IStateMachineCore<TState, TTrigger>
{
    IAwaitableStateMachineDiagnostics<TState, TTrigger> Diagnostics { get; }

    Task FireAsync<TArgument>(
            ParameterizedTrigger<TTrigger, TArgument> parameterizedTrigger,
            TArgument argument);
    Task FireAsync(TTrigger trigger);
    Task MoveToStateAsync(TState state, 
            StateTransitionOption option = StateTransitionOption.Default);
}

In-built Machines

Notes:

Dynamic Triggers

A simple implementation of the dynamic trigger is a part of the sample. For more information or if you want to understand in detail the choices leading up to the design, please have a look at: https://github.com/prasannavl/LiquidState/pull/20, and https://github.com/prasannavl/LiquidState/pull/7

Support

Please use the GitHub issue tracker here if you'd like to report problems or discuss features. As always, do a preliminary search in the issue tracker before opening new ones - (Tip: include pull requests, closed, and open issues: Exhaustive search ).

Credits

Thanks to JetBrains for the OSS license of Resharper Ultimate.

Proudly developed using:

Resharper logo

License

This project is licensed under either of the following, at your choice:

Code of Conduct

Contribution to the LiquidState project is organized under the terms of the Contributor Covenant, and as such the maintainer @prasannavl promises to intervene to uphold that code of conduct.