A Simple State Machine in .NET

Introduction

This is another post that should be tagged lazy-weekend!

Let's imagine a simple state machine for tracking the state of tickets. It consists of the following states:

  • Created: the ticket has been created
  • Ready: the ticket is ready to be picked up for implementation
  • In Progress: the ticket is being implemented
  • Blocked: the ticket is currently blocked
  • In Review: the ticket was sent for review
  • Closed: the ticket has been closed

The following diagram illustrates these states and the possible transitions:

A simple state machine

Don't worry too much about the actual states, this is meant to be an example. An explanation for these transitions is in order:

  • The first state for a ticket is Created
  • From Created, the ticket can transition to Closed, or to Ready
  • From Ready, the ticket can transition to In Progress, Blocked, or Closed 
  • From In Progress, the ticket can transition to Blocked, In Review, Ready, or Closed
  • From In Review, a ticket can transition to In Progress, or to Closed
  • From Blocked, a ticket can transition to In Progress, or to Closed
  • Closed is the final state for a ticket

Now, let's implement a very simple library, SimpleStateMachine, to help us implement these features in .NET. What we will need is:

  • Check if a type or an enumeration value belongs to a state machine defined by an enumeration
  • Check if a state is the initial or the final one (has no transitions defined for it)
  • Check if it is possible to transition from one state to another
  • Get the possible transitions from one state
  • Get the initial state

As you can see, this is meant to be very simple, nothing fancy here, essentially just some checks.

This library uses attributes on enumeration values. Let's have a look at them now.

State Machine Attributes

As I said, we will be leveraging .NET enumerations for the states, and a couple attributes can be applied to the enumeration values:

  • [InitialState]: marks an enumeration value as the initial state; it is mandatory
  • [Transitions<T>]: defines the possible transitions for an enumeration value; we can specify any number of states, of the supplied generic parameter type. This is a generic attribute

Now, the state machine operations.

State Machine Operations

We will need the following operations:

  • IsStateMachine: checks if an enumeration, or an enumeration value, is a state machine; this will be true if it has one and only one [InitialState] attribute
  • IsInitialState: checks if if an enumeration value is marked with the [InitialState] attribute
  • IsFinalState: checks if an enumeration value does not have any transitions defined for it
  • CanTransitionTo: checks if an enumeration value can transition to another given state
  • GetTransitions: gets the list of transitions from a given state
  • GetInitialState: returns the initial state

To implement them, we will create some extension methods over the enumeration Type and/or an enumeration value. We will see now how works.

Putting it All Together

Lets first add the attributes to a TicketState enumeration type:

public enum TicketState
{
    [InitialState]
    [Transitions<TicketState>(TicketState.Ready, TicketState.Closed)]
    Created,
    [Transitions<TicketState>(TicketState.InProgress, TicketState.Blocked, TicketState.Closed)]
    Ready,
    [Transitions<TicketState>(TicketState.Blocked, TicketState.InReview, TicketState.Closed, TicketState.Ready)]
    InProgress,
    [Transitions<TicketState>(TicketState.InProgress, TicketState.Closed)]
    Blocked,
    [Transitions<TicketState>(TicketState.InProgress, TicketState.Closed)]
    InReview,
    Closed
}

And now, we can call some operations:

var state = TicketState.Created;
var isStateMachine = state.IsStateMachine();                              //true
var isInitialState = state.IsInitialState();                              //true
var isFinalState = state.IsFinalState();                                  //false
var canTransitionToClosed = state.CanTransitionTo(TicketState.Closed);    //true
var canTransitionToBlocked = state.CanTransitionTo(TicketState.Blocked);  //false
var transitions = state.GetTransitions();                                 //Ready, Closed
var initialState = StateMachine.GetInitialState<TicketState>();           //Created

Conclusion

I hope you find this useful, or you think if I forgot something, please let me know. You can find the code in GitHub and a Nuget package exists too:

GitHub: http://github.com/rjperes/SimpleStateMachine

Nuget: https://nuget.org/packages/SimpleStateMachine

Comments

Popular posts from this blog

OpenTelemetry with ASP.NET Core

ASP.NET Core Middleware

.NET Cancellation Tokens