Skip to main content

State Machine

Invoking another state machine as a service gives you a lot of power to build complex and complicated systems without writing too much code in one class.

State machine invocation is implemented with the same semantics as async services. Once executing state machine is done, the parent state machine moved to the next state by ID given in "onDoneTargetStateId" argument.

Let's create a child state machine, the machine that is triggered when an administrator signs in to the system.

// create the states for child state machine that must be executed
State checkingAdminAccessRights = new State("checkingAdminAccessRights")
.WithInvoke(async (callback) => {
// checking rights
bool hasAccess = await checkAccessAsync();
await callback(hasAccess ? "ACCESS_GRANTED" : "ACCESS_DENIED");
})
.WithTransition("ACCESS_GRANTED", "unlockingDoorForAdmin")
.WithTransition("ACCESS_DENIED", "exit");

State unlockingDoorForAdmin = new State("unlockingDoorForAdmin")
.WithInvoke(async (cancel) => {
// unlocking the door, once done, go to exit
await unlockDoor();
}, "exit", null);

State exit = new State("exit")
// important! to make machine to exit, mark final state as final
// if you don't mark state as final, the state machine will never exits
.AsFinalState()
.WithInvoke(async (cancel) => {
// notify some third party system about admin to sign in, exit
await notifySystemAboutAdminSignIn();
// as this state is a final state, no need to move to any other state
// set both onDone and onError target state IDs as null
}, null, null);

// create state machine, providing ID, Name, Initia state ID and list of states.
StateMachine adminSignsInMachine = new StateMachine("adminSignsInMachine",
"Machine is executed on admin signs in", "checkingAdminAccessRights",
checkingAdminAccessRights, unlockingDoorForAdmin, exit);

Next step, create a root state machine to execute the previously defined adminSignsInMachine. Let's use the example from the chapter with callback service and extend it. Once admin requests the maintenance mode, the state machine moves to "startingMaintenanceMode". In this state the previously composed state machine is executed as a service.

Once done, the state machine triggers another state, which is defined, in this case as the state with ID "waitingForUser".

// imagine we have a vending machine that awaits for
// user to come or an administrator to request a maintenance
State waitingForUser = new State("waitingForUser")
.WithInvoke(async (callback) => {
// imagine we have an MQTT message to arrive when user signs in to a vending machine
// we need to wait fot that
mqttClient.Subscribe("user/unlockedDoor");
// as well administrator can come and request maintenance mode
// we wait for it in parallel
mqttClient.Subscribe("adminUser/MaintenanceRequested");
mqttClient.OnMessage((topic, message) => {
switch(topic)
{
// based on the arrived message, we trigger event
case "user/unlockedDoor":
await callback("USER_UNLOCKED_DOOR");
break;
case "adminUser/MaintenanceRequested":
await callback("ADMINISTRATOR_MAINTENANCE_ACCESS_REQUESTED");
break;
}
});
}, () => {
// as we are leaving the state, to make sure those messages are
// not arriving then we are not waiting for them, unsubscribe on cleanup code
mqttClient.Unsubscribe("user/unlockedDoor");
mqttClient.Unsubscribe("adminUser/MaintenanceRequested");
})
.WithTransition("USER_UNLOCKED_DOOR", "chekingUserBalance")
.WithTransition("ADMINISTRATOR_MAINTENANCE_ACCESS_REQUESTED", "startingMaintenanceMode");

// crete the state to trigger previously composed state machine as a service
State startingMaintenanceMode = new State("startingMaintenanceMode")
// once the state machine is done, start again the "waitingForUser" state
// and throw exception in case of error (null argument indicates that go nowhere in case of error)
.WithInvoke(adminSignsInMachine, "waitingForUser", null);