Skip to main content

Main Ideas

Decoupling the Code

Each state of the state machine is an atomic execution context. In real life each state is not aware of any other state. However they can share data layer, for example, to react on any changes in the "outside world" of the state.

Hence, in this framework there is no strong bundling between states. Each state can only trigger a transition to another state by ID. States are loosely bound.

// defining states, each state needs an ID - given as an argument in the constructor.
// each ID need to be unique within state machine scope
State initializing = new State("initializing");

// define transitions, on what event what state to shift to
// each event is a string, as well as next state is targeted by ID
// if no state with this ID found, the exception is thrown
initializing
.WithTransition("INITIALIZATION_DONE", "waitingForUser")
.WithTransition("INITIALIZATION_ERROR", "reportingError");

State Work Flow

state run flow

Each state has so called services to invoke, services affects state machine flow, service can cause state machine to switch state. Each state can have one or multiple services, that are executed in parallel, and raice for the first service to trigger event or finalized, causing all other services to be cancelled.

Service can be also another state machine - this helps to make complicated code cleaner.

As side effect, each state also can invoke actions on enter and actions on exit, as well as activity - a long running background operation that can't affect state machine. (see next chapters for details)

While state is active it executes or can execute three different invocations:

InvocationPurpose Description
ServicesExecutes an async operation and eventually affects the state machine current state. If state has multiple services defined, all of them are executed in parallel.
ActivitiesExecutes an async operation but can't affect state machine state. Is running only while state defining it is active.
ActionsFire-and-forget actions that are running on state enter or on state exit.

Services

Service can affect or will affect the state machine current state. The main purpose of the service is to eventually stop, that will cause the state machine to switch the state.

The XStateNet supports three types of services:

Async with Callback

You must call await callback("MY_EVENT") to make state machine to go to next state based on emitted event.

State myState = new State("myState")
.WithInvoke(async (callback) => {
// do here some job, once done, emit event
await callback("MY_EVENT");
}, () => {
// optional cleanup method
// cleanup here all resources
})
.WithTransition("MY_EVENT", "nextStateId");

Notice, the second argument of this .WithInvoke(...) overloading is a clean up action. Optional action to clean up code once callback is called and the state machine exiting the state.

Async Task with onDone and onError Target Transitions

In this oveloading of .WithInvoke(...) you can start long running task and define to what state to move on this task done or in case of exception.

It can be useful, for example, when you need to have a linear execution chain, download some large files, upload logs to database, etc.

State myState = new State("myState")
.WithInvoke(async (cancel) => {
// upload logs to the database
await uploadLogs();
// second and third arguments define where to move in
// case of success or failure
}, "continueWaitingUser", "retryLogsUploading");

Error target state ID is optional, if it is set to null, the exception will be thrown and no transition would be executed.

Invoking Another State Machine

In complex systems you may have multiple state machines and parent state machine may execute child state machine to make code implementation cleaner.

Service can invoke any state machine, wait for it's finalization or react on exception, in the same way as previously defined service type.

StateMachine adminFlowStateMachine = new StateMachine("adminFlow",
"this is admin sequence flow machine", "adminSignedIn");

// ... define here states for child state machine

// root state machine state definitions
State adminSignedInState = new State("myState")
.WithInvoke(adminFlowStateMachine, "continueWaitingUser", "errorInAdminFlow");

StateMachine rootStateMachine = new StateMachine("root", "root parent machine", "myState");
rootStateMachine.States = new [] {
adminSignedInState
// other states....
};
Interpreter interpreter = new Interpreter(rootStateMachine);
await interpreter.StartStateMachine();

Parallel Services

State can have multiple services, they are all executed in parallel and first service causing the state switch cancels the other services.

State myState = new State("myState")
.WithInvoke(async (callback) => {
// do here some job, once done, emit event
await callback("MY_EVENT");
}, () => {
// optional cleanup method
// cleanup here all resources
})
.WithInvoke(async (callback) => {
// do another task in parallel
await callback("ADMIN_USER_ACCESS_REQUESTED");
})
.WithTransition("MY_EVENT", "nextStateId")
.WithTransition("ADMIN_USER_ACCESS_REQUESTED", "initiatingAdminUserFlow");

Decoupling the services helps to build cleaner state machine code and not to put all code into one method.

Actions

Fire-and-forget actions to execute on enter and on exit. Each state can have many onEnter and onExit actions.

You need to be sure that onEnter and onExit actions are light and fast. Avoid heavy and long running work there. For background and long running tasks you need to use either Service or Activity.

On Enter Actions

These actions are executed before services are started.

State state1 = new State("myState")
.WithActionOnEnter(() => {
// do some work on enter
})
.WithActionOnEnter(() => {
// do another work on enter
});

On Exit Actions

These actions are executed before services are exited. At this point all services are stopped.

State state1 = new State("myState")
.WithActionOnExit(() => {
// do some work before state exits
})
.WithActionOnExit(() => {
// do another work before state exits
});

Activities

Asynchronous long running operations that can be considered as side effect of service. Activities are running in parallel with services but they can't affect state machine state.

You can use activities to execute any not so necessary operation that is not affecting the application flow. For example UI update loop.

State can run multiple activities at the same time. Notice, the second argument of WithActivity method has a cleanup action. The cleanup action is executed when state has exited and before new state has started.

State state1 = new State("myState")
.WithActivity(async () => {
// do some long running operation in async mode
while(!isReady)
{
await updateUI();
}
}, () => {
// cleanup code, here you can clean all activity resources.
isReady = true;
});