Operation flow of stateless in. Net

Time:2022-5-8

introduce

What are state machines and state patterns

State machine is a tool for object modeling. It is a directed graph, which is composed of a group of nodes and a group of corresponding transfer functions. The state machine “runs” in response to a series of events. Each event is within the control range of the transfer function belonging to the “current” node, where the range of the function is a subset of the node. Function returns the “next” (perhaps the same) node. At least one of these nodes must be the final state. When the final state is reached, the state machine stops.

State mode is mainly used to solve the complex situation of object state transformation. It transfers the logical judgment of state to different classes, which can simplify the complex logic.

Elements of state machine

The state machine has four elements: current state, condition, action and secondary state. Among them, the present state and condition are “cause”, and the action and secondary state are “result”.

  1. Current status – refers to the status of the current object
  2. Condition – when a condition is met, the current object triggers an action
  3. Action – the action to be performed after the condition is met
  4. Secondary state – the new state of the current object after the condition is met. The secondary state is relative to the current state. Once the secondary state is triggered, it becomes the current state

Stateless

StatelessIt is based on Net open source state machine library, the latest version 4.2.1, you can easily use it in Net to create state machine and lightweight workflow based on state machine.

Because the whole project is based on Net standard, so in Net framework and Net core project.

Project source codehttps://github.com/dotnet-state-machine/stateless

The following is a call process written using stateless


var phoneCall = new StateMachine<State, Trigger>(State.OffHook);

phoneCall.Configure(State.OffHook)
    .Permit(Trigger.CallDialled, State.Ringing);
	
phoneCall.Configure(State.Ringing)
    .Permit(Trigger.CallConnected, State.Connected);
 
phoneCall.Configure(State.Connected)
    .OnEntry(() => StartCallTimer())
    .OnExit(() => StopCallTimer())
    .Permit(Trigger.LeftMessage, State.OffHook)
    .Permit(Trigger.PlacedOnHold, State.OnHold);

// ...

phoneCall.Fire(Trigger.CallDialled);
Assert.AreEqual(State.Ringing, phoneCall.State);

Code interpretation

At present, a state machine is initialized to describe the state of the point phone. Here, the initial state of the phone is off hook
When the phone is on hook, if the called event is triggered, the status of the phone will change to ringing
When the phone is in the ringing state, if the through connection event is triggered, the status of the phone will change to connected
When the phone is in the connected state, the system will start timing, and when the connected state changes to other states, the system will end timing
When the phone is in the connected state, if a message event is triggered, the state of the phone will change to off hook
When the phone is connected, if a pending event is triggered, the status of the phone will change to the pending state (onhold)
Fire is a function that triggers an event. Here, a call event is triggered

After the call event is triggered, the state of the phone changes to the ring state, soAssert.AreEqual(State.Ringing, phoneCall.State)Your assertion is correct.

Stateless supported features

  • For any Net type of state and trigger universal support
  • Layered state
  • State entry and exit events
  • Protect clauses to support conditional conversions
  • introspection

At the same time, some useful extensions are provided:

  • Support external state storage (e.g. attributes tracked by ORM)
  • Parametric trigger
  • Reentrant state
  • Support dot format map export

Layered state

In the following example,OnHoldStatus isConnectedSub state of the state. This means that when the phone hangs, it is still connected.


phoneCall.Configure(State.OnHold)
    .SubstateOf(State.Connected)
    .Permit(Trigger.TakenOffHold, State.Connected)
    .Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);

State entry and exit events

In the previous example,StartCallTimer()Method will be executed when the call is connected,StopCallTimer()The method is executed at the end of the call (or when the phone hangs up, or when the phone is thrown on the wall and destroyed).

It is not triggered when the status of the phone changes from connected to onholdStartCallTimer()Methods andStopCallTimer()Method, this is becauseOnHoldyesConnectedSubstatus of.

External state storage

Sometimes, the state of the current object needs to come from an ORM object, or the state of the current object needs to be saved to an ORM object. To support this external state storage,StateMachineClass’s constructor supports reading and writing status values.


var stateMachine = new StateMachine<State, Trigger>(
    () => myState.Value,
    s => myState.Value = s);

introspection

The state machine canStateMachine.PermittedTriggersProperty to provide a list of triggers that can be triggered in the current object state. And provides a methodStateMachine.GetInfo()To get configuration information about the status.

Protection clause

The state machine will choose between multiple transitions according to the protection clause.


phoneCall.Configure(State.OffHook)
    .PermitIf(Trigger.CallDialled, State.Ringing, () => IsValidNumber)
    .PermitIf(Trigger.CallDialled, State.Beeping, () => !IsValidNumber);

be careful:

The protection clauses in the configuration must be mutually exclusive. The child state can override the state transition through reassignment, but the child state cannot override the state transition allowed by the parent state.

Parametric trigger

Specifying strongly typed parameters to triggers is supported in stateless.


var assignTrigger = stateMachine.SetTriggerParameters<string>(Trigger.Assign);

stateMachine.Configure(State.Assigned)
    .OnEntryFrom(assignTrigger, email => OnAssigned(email));

stateMachine.Fire(assignTrigger, "[email protected]");

Export dot diagram

Stateless also provides a function to generate dot graph code at runtime. Using the generated dot graph code, we can generate visual state machine graph.

Here we can useUmlDotGraph.Format()Method to generate dot graph code.


phoneCall.Configure(State.OffHook)
    .PermitIf(Trigger.CallDialled, State.Ringing, IsValidNumber);
    
string graph = UmlDotGraph.Format(phoneCall.GetInfo());

Generated dot diagram code example


digraph {
	compound=true;
	node [shape=Mrecord]
	rankdir="LR"

	subgraph clusterOpen
    {
        label = "Open"
		Assigned [label="Assigned|exit / Function"];
	}
	Deferred [label="Deferred|entry / Function"];
	Closed [label="Closed"];

	Open -> Assigned [style="solid", label="Assign / Function"];
	Assigned -> Assigned [style="solid", label="Assign"];
	Assigned -> Closed [style="solid", label="Close"];
	Assigned -> Deferred [style="solid", label="Defer"];
	Deferred -> Assigned [style="solid", label="Assign / Function"];
}

Graphical dot diagram example

An example of bugtracker

After reading so many introductions, let’s practice and write a bug state machine.

Suppose that in the current bugtracker system, bugs have four states: open, assigned, deferred and closed. From this, we can create an enumeration classState


public enum State
    {
        Open,
        Assigned,
        Deferred,
        Closed
    }

If you want to change the state of a bug, there are three actions: assign, defer, and close.


public enum Trigger
    {
        Assign,
        Defer,
        Close
    }

Let’s list the possible state changes of the bug object.

  • The initial state of each bug is open
  • If the status of the current bug is open, the action assign is triggered, and the status of the bug will change to assigned
  • If the status of the current bug is assigned, the action defer will be triggered and the status of the bug will change to deferred
  • If the status of the current bug is assigned, the action close will be triggered and the status of the bug will change to closed
  • If the status of the current bug is assigned, the action assign is triggered, and the status of the bug will remain assigned (the scene of changing the bug modifier)

If the status of the current bug is deferred, the action assign is triggered, and the status of the bug will change to assignedFrom this we can write bug classes


public class Bug
    {
        State _state = State.Open;
        StateMachine<State, Trigger> _machine;
        StateMachine<State, Trigger>.TriggerWithParameters<string> _assignTrigger;

        string _title;
        string _assignee;

        public Bug(string title)
        {
            _title = title;

            _machine = new StateMachine<State, Trigger>(() => _state, s => _state = s);

            _assignTrigger = _machine.SetTriggerParameters<string>(Trigger.Assign);

            _machine.Configure(State.Open).Permit(Trigger.Assign, State.Assigned);
            _machine.Configure(State.Assigned)
                .OnEntryFrom(_assignTrigger, assignee => _assignee = assignee)
                .SubstateOf(State.Open)
                .PermitReentry(Trigger.Assign)
                .Permit(Trigger.Close, State.Closed)
                .Permit(Trigger.Defer, State.Deferred);

            _machine.Configure(State.Deferred)
                .OnEntry(() => _assignee = null)
                .Permit(Trigger.Assign, State.Assigned);
        }
        
        public string CurrentState
        {
            get
            {
                return _machine.State.ToString();
            }
        }
        
        public string Title
        {
            get
            {
                return _title;
            }
        }

        public string Assignee
        {
            get
            {
                if (string.IsNullOrWhiteSpace(_assignee))
                {
                    return "Not Assigned";
                }

                return _assignee;
            }
        }

        public void Assign(string assignee)
        {
            _machine.Fire(_assignTrigger, assignee);
        }

        public void Defer()
        {
            _machine.Fire(Trigger.Defer);
        }

        public void Close()
        {
            _machine.Fire(Trigger.Close);
        }
    }

Code interpretation:

  • Each bug should have an assignor and title, so I added an assignee and title attribute here
  • When assigning a bug, you need to specify an assignor, so I use a parameterized trigger for the assign action
  • When the bug object enters the assigned state, I assign the currently specified assignor to_assigneeField.Final effect

Here we first show a normal operation process.


class Program
    {
        static void Main(string[] args)
        {
            Bug bug = new Bug("Hello World!");

            Console.WriteLine($"Current State: {bug.CurrentState}");

            bug.Assign("Lamond Lu");

            Console.WriteLine($"Current State: {bug.CurrentState}");
            Console.WriteLine($"Current Assignee: {bug.Assignee}");

            bug.Defer();

            Console.WriteLine($"Current State: {bug.CurrentState}");
            Console.WriteLine($"Current Assignee: {bug.Assignee}");

            bug.Assign("Lu Nan");

            Console.WriteLine($"Current State: {bug.CurrentState}");
            Console.WriteLine($"Current Assignee: {bug.Assignee}");

            bug.Close();

            Console.WriteLine($"Current State: {bug.CurrentState}");
        }
    }

Operation results

Next, we modify the code. After creating a bug, we immediately try to close it


class Program
    {
        static void Main(string[] args)
        {
            Bug bug = new Bug("Hello World!");
            bug.Close();
        }
    }

After rerunning the program, the program throws the following exception.

Unhandled Exception: System.InvalidOperationException: No valid leaving transitions are permitted from state ‘Open’ for trigger ‘Close’. Consider ignoring the trigger.

When the bug is in the open state, the close action is triggered. Since there is no secondary state definition, an exception is thrown, which is consistent with the logic defined above. If you want the program to support the state change of open – > closed, we need to modify the configuration of the open state to allow the open state to change to the closed state through the close action.


_machine.Configure(State.Open)
	.Permit(Trigger.Assign, State.Assigned)
	.Permit(Trigger.Close, State.Closed);

It can be seen that we can define a simple workflow according to the needs of our own project, and stateless will automatically help us verify the wrong process operation.

summary

Today I share with you Net, with which we can easily define the state machine needed by our business or the workflow based on state machine. Most of the content of this paper comes from the official GitHub. Interested students can have an in-depth study.

This is about Net state machine library stateless operation process of the article introduced here, more related Net state machine stateless content, please search the previous articles of developeppaer or continue to browse the relevant articles below. I hope you can support developeppaer in the future!