Skip to main content

Getting Started

.NET nuget package version

Nuget Package

Find the Nuget package here:

https://www.nuget.org/packages/XStateNet

The library is targeted for netstandard2.0 and for net5.0, feel free to comment or open an issue in case any other compatibility is needed. Those two target frameworks should cover most of the development needs.

Main Classes

State

The main state class, compose the class with chained methods to fill it with services, actions or activities.

  • Services are methods that are executed and their goal is to change the state machine state;
  • Actions are "fire and forget" methods that are running on state is entered and/or on state is exited. This is a good place to do some initialization and cleanup work.
  • Activities are long running methods that can do any long running parallel work together with services but they can't affect state machine state.
// create state. Each state has a unique ID
State myState = new State("myState")
.WithInvoke(...) // it has one or many services to invoke
.WithActionOnEnter(...) // one or many actions to run on state starts
.WithActionOnExit(...) // one or many actions to run on state exits
.WithActivity(...) // one or many async methods to run while state runs services
.AsFinalState() // if state machine need to exit after this state, mark it as final
// transient state, does not do any services, only actions, moves to next state right away
.AsTransientState("targetStateId")
.WithTimeout(10000, "targetState"); // timeout service, where to go if nothing happens for given miliseconds

StateMachine

The state machine class, that has a collection of states and initial state to start execution from.

// create state. Each state has a unique ID
State myState1 = new State("myState1");
State myState2 = new State("myState2");
State myState3 = new State("myState3");
State myState4 = new State("myState4");

// create state machine
// provide ID, name, and initial state ID
StateMachine machine = new StateMachine("myMachine1", "My Machine", "myState1");
// fill up states
machine.States = new []{
myState1, myState2, myState3, myState4
};

Interpreter

The state machine interpreter or the state machine runner, the main goal of this class instance is to start state machine execution.

It can await until state machine is done, or subscribe for event in case the state machine is started in the background thread without using awaitable start method.

// create state machine
// provide ID, name, and initial state ID
StateMachine machine = new StateMachine("myMachine1", "My Machine", "myState1");
// fill up states
machine.States = new []{
myState1, myState2, myState3, myState4
};

Interpreter interpreter = new Interpreter(machine);
interpreter.StartStateMachine();
// you can subscribe for state changed event
interpreter.OnStateChanged += (sender, args) => {
Console.WriteLine("Current state: " + args.State.Id);
Console.WriteLine("Previous state: " + args.PreviousState.Id);
};
// you can subscribe for machine done event
interpreter.OnStateMachineDone += (sender, args) => {
Console.WriteLine("State machine finalized!");
};

Interpreter can be awaited, in case you need to know explicitly, without event, when state machine is done.

// create state machine
// provide ID, name, and initial state ID
StateMachine machine = new StateMachine("myMachine1", "My Machine", "myState1");
// fill up states
machine.States = new []{
myState1, myState2, myState3, myState4
};

Interpreter interpreter = new Interpreter(machine);
// you can subscribe for state changed event
interpreter.OnStateChanged += (sender, args) => {
Console.WriteLine("Current state: " + args.State.Id);
Console.WriteLine("Previous state: " + args.PreviousState.Id);
};
await interpreter.StartStateMachineAsync();

Examples

Traffic Light

Simple example that uses callback services and no side effects.

First, add namespace using directives:

using XStateNet;

Then you can use classes from the library.

// create all needed states for traffic light
State redLight = new State("redLight");
State amberLight = new State("amberLight");
State greenLight = new State("greenLight");
State errorBlinkingAmber = new State("error");
bool isError = false;
int fixAttempt = 1;

// define services with callbacks.
// callback can trigger a certain event, and according to the event
// and registered transition, state machine is then switched to another state
redLight.WithInvoke(async (callback) => {
Console.WriteLine("RED LIGHT!!! STOP!");
await Task.Delay(1000); // emulate traffic light delay
// check error
if(isError)
{
// if traffic light has error, emit ERROR event
await callback("ERROR");
}
else
{
// is not, emit success event
await callback("RED_LIGHT_FINISHED");
}
})
// add transition, in case of ERROR event, move to error state,
// in case of RED_LIGHT_FINISHED move to amber light
// if no transition is registered, no exception is thrown, as we can reuse same services in different scenarios.
.WithTransition("ERROR", "error")
.WithTransition("RED_LIGHT_FINISHED", "amberLight");

// fill up amber state.
// here we use different service type: async action with cancellation token
amberLight.WithInvoke(async (cancel) => {
Console.WriteLine("Amber light! Attention!");

// as state can run multiple services at parallel, if another service switches the state of the state machine
// the cancellation token is then canceled, and you need to check it in your
// buiseness logic.
await Task.Delay(3000, cancel);

// when async task is done, it moves to the "greenLight" state, in case of error to the third
// argument state, which is in our case is "error"
// both of them can be null if you don't need to swich state, for instance in Final state case.
// if onErrorTargetStateId argument is null, then the error is thrown if it happens.
}, "greenLight", "error");

// fill up green state, easy timeout, no services but only an indicator
// where to move the state machine after certain time
greenLight.WithTimeout(5000, "redLight")
.WithActionOnEnter(() => {
Console.WriteLine("Green light! Go go go!");
})
.WithActionOnExit(() => {
// each time we exit green light, emulate error in the traffic light
// 50% chance for demo only
isError = new Random().NextDouble() > 0.5;
});

// error state
errorBlinkingAmber.WithInvoke(async (callback) => {
Console.WriteLine("Error in the traffic light!");
Console.WriteLine($"Trying to fix... Attempt #{fixAttempt}");
await Task.Delay(1000);
isError = new Random().NextDouble() > 0.5;
if(isError)
{
Interlocked.Increment(ref fixAttempt);
await callback("COULD_NOT_FIX");
}
else
{
Interlocked.Exchange(ref fixAttempt, 1);
await callback("ERROR_FIXED");
}
})
// register transitions, if we could not fix, repeat the same state
// if we fixed, move to red light
.WithTransition("COULD_NOT_FIX", "error")
.WithTransition("ERROR_FIXED", "redLight");

// once all states are defined, create state machine and interpreter
// define ID, name and initial state ID for the machine
StateMachine machine = new StateMachine("myTrafficLight", "My traffic light machine", redLight.Id);
// set states, order does not matter
machine.States = new []
{
redLight, amberLight, greenLight, errorBlinkingAmber
};
// create interpreter and start the state machine
Interpreter interpreter = new Interpreter(machine);
interpreter.StartStateMachine();
// also can be awaitable if you need to run it in background thread
// await interpreter.StartStateMachineAsync();

The console output is as expected:

RED LIGHT!!! STOP!
Amber light! Attention!
Green light! Go go go!
RED LIGHT!!! STOP!
Amber light! Attention!
Green light! Go go go!
RED LIGHT!!! STOP!
Amber light! Attention!
Green light! Go go go!
RED LIGHT!!! STOP!
Amber light! Attention!
Green light! Go go go!
RED LIGHT!!! STOP!
Error in the traffic light!
Trying to fix... Attempt #1
RED LIGHT!!! STOP!
Amber light! Attention!
Green light! Go go go!

Notice, that there was an error state switch and one attempt to fix error. State machine works as expected.

More examples: look at the documentation or in the "demo-console-app" folder.

Good practice

It would be better not to use inline methods if the state machine is complex. You can use State as a base class, inherit your own class from it, and use the construct chain methods in the class constructor, for example, to build a class.

Example:

class ShowingRedLight : State
{
public ShowingRedLight() : base("showingRedLight")
{
// wait for the red light timer
this.WithInvoke(WaitForRedLightTimer);
// show the red light picture
this.WithActionOnEnter(ShowRedLight);
}

/// <summary>
/// Turns on the red light.
/// </summary>
private void ShowRedLight()
{
Helper.DrawTrafficLight(Helper.Mode.Red);
}

/// <summary>
/// Waits for the timer to switch to next state.
/// </summary>
/// <param name="state"></param>
/// <param name="callback"></param>
private async Task WaitForRedLightTimer(CallbackAction callback)
{
await Task.Delay(1000);
await callback("RED_LIGHT_DONE");
}
}

Manual Build

In case you don't want to use Nuget package, you can manually build the library for Debug or Release.

Navigate to the folder XStateNet, run

dotnet build

or for Release build

dotnet build -c Release

The only library you need to reference to your project is XStateNet.dll. Depending on your project target, reference the correct target framework built dll.

the build result

Test

The the root project folder, run

dotnet test

or for verbose results (normal level of console output)

dotnet test -v n