Skip to main content

Getting Started with Stateless : A Lightweight Workflow Library Alternative for .NET

Image Credit: https://www.pioneerrx.com

A year ago, I was looking for a simple workflow manager for a project I was working. Its a medium sized application that involves tracking the state of assets in the system. Back in 2008, Microsoft (MS) introduced new technologies along with the release of Visual Studio 2008: Windows Presentation Foundation (WPF), Windows Communication Foundation (WCF), and Windows Workflow Foundation(WF). Having worked in a company utilizing mostly MS products for development, my first option was to go with WF. After doing some time reading and studying the library, I paused and decided it was too complex for my requirement. Using WF would be an overkill and the fact that it has, a rather, steep learning curve, there has to be another option. My mind toyed with the idea of developing a simple workflow library myself. It would be a learning experience but it might end up consuming a lot of time.

Why reinvent the wheel? So I started querying the internet for a better solution. I stumbled upon Stateless, an open source project. A library that lets developer create state machines and lightweight state machine-based workflow directly inside .NET code. Sweet, just what I need.

The projects GitHub page is informative, comprising the necessary details to kickstart a developer. It has sample projects included to see the actual code in action. You can clone the repository and include the project in your solution, or, a simpler and better way, used the NuGet package. Instructions are also included on how to add it to your solution using Visual Studio’s Package  Console Manager or the .Net CLI.

To get started, we will look into a simple workflow and implement a solution using the traditional ‘if-then-else’ construct followed by the Stateless approach.

The Light Switch Scenario

Light Switch Scenario

A straightforward scenario with an added conditional criteria. In this example, we assume a time controlled switch is used. During the day, say from 06:00AM to 06:00PM, lights would remain in off state to conserve electricity.

The traditional solution is to use the ‘if-then-else’ approach. In this case, I have to admit, this approach is the best option. No additional library needed. For an analogy, we will implement also the solution using Stateless.

Traditional Approach

The code is straightforward. No explanation needed basic C# coding. Just in case you are wondering, the variable _enforceTimeConstraint enables/disables the time controlled feature.
if (CurrentState == State.ON)
{
    CurrentState = State.OFF;
}
else
{
    if(_enforceTimeConstraint)
    {
        if (IsLightNeeded()) CurrentState = State.ON;
    }
    else
    {
        CurrentState = State.ON;
    }
}
Stateless Approach
First, add the Stateless package to your project. Enter the following on Visual Studio's package console manager.
Install-Package Stateless -Version 3.1.0
If you are using VS Code or VS 2017 for Mac, you might need to use the CLI for this. The instruction can be seen on this site. The latest version is at 4.0.0, version 3.1.0 was used in my example.
Reference the package in your code.
using Stateless;
Declare a  new Trigger of type enum. This is a list of trigger that causes a change of state. Then declare a StateMachine of type State and Trigger. State is an enum comprising the list of states possible in our state machine.
enum Trigger { TOGGLE };
enum State { ON, OFF };
StateMachine<State, Trigger> _machine;
Configure your state machine inside your code. In this example, I did the initialization on the class constructor.
_machine = new StateMachine<State, Trigger>(() => CurrentState, s => CurrentState = s);

_machine.Configure(State.ON)
        .Permit(Trigger.TOGGLE, State.OFF);

_machine.Configure(State.OFF)
        .PermitIf(Trigger.TOGGLE, State.ON, () => IsLightNeeded(), "Toggle allowed")
        .PermitReentryIf(Trigger.TOGGLE, () => !IsLightNeeded(), "Toggle not allowed");
Some points worth explaining :
  • CurrentState is a variable of type State, it stores the current state. 
  • Configure is a method of our state machine. It takes the State that we want to set up. In our first Configure statement, we are setting up State.ON.
  • Permit is a method that lets us specify a valid state transition based on a trigger. When we configure State.ON, we are permitting it to transition to State.OFF when the trigger, Trigger.TOGGLE, is fired.
  • PermitIf method is similar to Permit and an additional condition that must be met to permit the transition. In the code above, notice the method IsLightNeeded(). This method act as a guard method to either allow or prevent the transition. Translating the PermitIf call to lay mans sentence: Permit the transition from OFF to ON when  TOGGLE is fired if, and only if,  light is needed.
  • Reentry to the state is also allowed. In this case, reentry is allowed if a certain condition is met. Thus, the PermitReentryIf method was used. The condition is, reentry is only allowed if IsLightNeeded() method returns false.
To start any transition, a trigger must be fired. This is done by calling the Fire method.
_machine.Fire(Trigger.TOGGLE);
Whew! That was a lot to go through for implementing a light switch. Again, as I have mentioned a while back, this is a simple example to get your feet wet with Stateless.  You can go ahead and do some coding to try or you can try cloning the code examples in GitHub. The solution contains two projects, the one discussed above is the Lightswitch project.

The Asset Workflow Scenario

We won't be using Stateless for projects as simple as Lightswitch. When the traditional approach of using the 'if-then-else' or even the 'switch-case' construct is too confusing to use, enter Stateless. Consider the following state diagram below.

Asset Flow Scenario
I'm not doing 'if-then-else' with that. I need a more elegant way of implementing and managing the flow. Stateless can be used in this scenario, simple and readable. Codes integrated inside your code, no external tool to design the workflow.

The Implementation

Let us begin by describing our workflow in terms of States and Triggers.
public enum State { New, Available, Allocated, UnderMaintenance,
                    Unavailable, Decommissioned };
public enum Trigger { Tested, Assigned, Released, RequestRepair, 
                    RequestUpdate,Transferred, Repaired, Lost, 
                    Discarded, Found };
Now we initialize the State Machine. I included the whole method, well go through each unfamiliar lines.
private void InitializeStateMachine()
{
    _state = State.New;
    _machine = new StateMachine<State, Trigger>(() => AssetState, s => AssetState = s);

    _assignTrigger = _machine.SetTriggerParameters<Person>(Trigger.Assigned);
    _transferTrigger = _machine.SetTriggerParameters<Person>(Trigger.Transferred);

    _machine.Configure(State.New)
            .Permit(Trigger.Tested, State.Available)
            .OnEntry(()=>OnEntry())
            .OnActivate(() => OnActivate())
            .Permit(Trigger.Lost, State.Unavailable)
            .OnDeactivate(()=>OnDeactivate())
            .OnExit(() => OnExit());

    _machine.Configure(State.Available)
            .OnEntry(() => OnEntry())
            .OnActivate(() => OnActivate())
            .Permit(Trigger.Assigned, State.Allocated)
            .Permit(Trigger.Lost, State.Unavailable)
            .OnExit(() => OnExit())
            .OnEntryFrom(Trigger.Found,()=> ProcessFound())
            .OnEntryFrom(Trigger.Released,() => ProcessDecommission())
            .OnDeactivate(() => OnDeactivate());

    _machine.Configure(State.Allocated)
            .OnEntry(()=>OnEntry())
            .OnEntryFrom(_assignTrigger, owner => SetOwner(owner))
            .OnEntryFrom(_transferTrigger, owner => SetOwner(owner))
            .OnActivate(()=> OnActivate())
            .OnExit(()=>OnExit())
            .OnDeactivate(()=>OnDeactivate())
            .PermitReentry(Trigger.Transferred)
            .Permit(Trigger.Released, State.Available)
            .Permit(Trigger.RequestRepair, State.UnderMaintenance)
            .Permit(Trigger.RequestUpdate, State.UnderMaintenance)
            .Permit(Trigger.Lost, State.Unavailable);

    _machine.Configure(State.UnderMaintenance)
            .OnEntry(() => OnEntry())
            .OnActivate(() => OnActivate())
            .OnExit(() => OnExit())
            .OnDeactivate(() => OnDeactivate())
            .Permit(Trigger.Repaired, State.Allocated)
            .Permit(Trigger.Lost,State.Unavailable)
            .Permit(Trigger.Discarded, State.Decommissioned);

    _machine.Configure(State.Unavailable)
            .OnEntry(() => OnEntry())
            .OnActivate(() => OnActivate())
            .OnExit(() => OnExit())
            .OnDeactivate(() => OnDeactivate())
            .PermitIf(Trigger.Found, State.Available,()=>(_previousState != State.New))
            .PermitIf(Trigger.Found,State.New,()=>(_previousState == State.New));

    _machine.Configure(State.Decommissioned)
            .OnEntry(() => ProcessDecommission())
            .OnActivate(() => OnActivate())
            .OnExit(() => OnExit())
            .OnDeactivate(() => OnDeactivate());

}
The usual stuff.  We instantiated a new state machine with the triggers and states we created. Then we configured each state. Oh wait! Our configuration is now more complex with new methods coming out of nowhere. Here we go again. We have been introduced to Configure, Permit, PermitIf, and PermitReentryIf so far.
  • The OnEntry method allows the state to call a method during state entry.  
  • The OnActivate method is almost similar to OnEntry, it allows the state to call the specified method OnActivate. You can play around with the codes to see the difference.
  • The OnExit and OnDeactivate are almost similar methods too, they enable calling of a method before the state transitions to a new one.
  • The OnEntryFrom method enables a state to perform a specific method upon entry based on the trigger the initiated the transition. Take this call,  OnEntryFrom(Trigger.Found,()=> ProcessFound()), the method ProcessFound() will only be called upon entry it was initiated by the Trigger.Found.
A trigger can also be fired with a parameter. You can pass an object with the trigger. To do that, we need to declare a Trigger with parameter of type object. Then we assign it to the specific Trigger enum value fro our code. The code below creates a trigger of type Person, then it is assigned to the specific Trigger value.
protected StateMachine<State, Trigger>.TriggerWithParameters<Person> _assignTrigger;
protected StateMachine<State, Trigger>.TriggerWithParameters<Person> _transferTrigger;

_assignTrigger = _machine.SetTriggerParameters<Person>(Trigger.Assigned);
_transferTrigger = _machine.SetTriggerParameters<Person>(Trigger.Transferred);
Note that you don't have to do something special when configuring a state using these triggers, just reference the trigger value ( e.g  Trigger.Assigned, Trigger.Transferred ). To call these triggers, however, we call the overload of Fire method accepting an object as a parameter.
_machine.Fire(_assignTrigger, owner);
So there, the building blocks of using Stateless. Below is a sample code how the trigger is called to initiate state transition on an asset.
Asset asset = new Asset(assInfo);
asset.Assign(GetOwner()); //with parameter
asset.RequestRepair();
asset.Release();
The samples GitHub repository include an interactive CLI to simulate asset movement using Stateless. Go ahead and download it here.

As an added information, the code created is unit testable. I have used Xunit in this example. NUnit was my initial choice but I was having issues setting it up on VS2017 for Mac. Here is a simple Fact testing the transition of asset state from New to Available by firing  Trigger.Finished.
[Fact]
public void FinishedTestingTriggerTest()
{
        LoadTestData();

        Asset toTest = _assets[0];

        toTest.AssetState = Asset.State.New;
        toTest.FinishedTesting();

        Assert.Equal(Asset.State.Available, toTest.AssetState);
}
And here's a sample Theory testing for some invalid state transitions. Note that the test asserts if there was no state transition, which, in the case of State.New, can only happen if the trigger fired is not supported by the current state.
[Theory]
[InlineData(Asset.State.New, Asset.Trigger.Released)]
[InlineData(Asset.State.New, Asset.Trigger.Discarded)]
[InlineData(Asset.State.New, Asset.Trigger.Found)]
[InlineData(Asset.State.New, Asset.Trigger.Repaired)]
[InlineData(Asset.State.New, Asset.Trigger.RequestRepair)]
[InlineData(Asset.State.New, Asset.Trigger.RequestUpdate)]
[InlineData(Asset.State.New, Asset.Trigger.Transferred)]
public void InvalidTriggerTest(Asset.State initialState, Asset.Trigger trigger)
{
        LoadTestData();

        Asset toTest = _assets[0];
        toTest.AssetState = initialState;
        toTest.Fire(trigger);

        Assert.True(toTest.AssetState == initialState);
}

Whats Next?

Stateless provides a simple way of implementing state machines and state machine based workflow in an elegant and readable manner. This adds to the flexibility and maintainability of the workflow in the code. Currently, I am using Stateless in implementing an Asset Workflow web API using .NET Core. I plan to extend the library to make the configuration task much easier, perhaps by creating a Designer UI to generate the initialization code.

The discussion on this post is just an introduction on what Stateless can bring as a library. Their GitHub page also contains additional examples for you to further your reading.  If you think the Stateless library deserves a star, you can do it here. While you are at it, you may as well extend some love to the example on this blog, if it deserves one.

For comments, questions, or suggestions to improve this post, hit the comment area.

Comments

Popular posts from this blog

Hiding Unwanted Python Folders and Files in Visual Studio Code

Visual Studio Code is a universal editor and pretty good at it. However, the explorer view maybe cluttered with the automatically generated folders and files confusing developers. Python is no different. Below are example files and folders generated by Python. The __pycache__ folder and *.pyc files  are totally unnecessary to the developer. To hide these files from the explorer view, we need to edit the settings.json for VSCode. Add the folder and the files as shown below: Copy and paste the lines below : "**/*.pyc" : { "when" : "$(basename).py" }, "**/__pycache__" : true

Cyber-bullying : The "good", the bad and the ugly

Image courtesy of http://www.digitalesq.com/ Cyber-bullying is defined as  the willful and repeated use of cell phones, computers, and other electronic communication devices to harass and threaten others. With the advent of social media, the incidents has increased in numbers and the victims does not even know what is hitting them. For the past years, we have heard of  depressions and deaths because of this. Yet, there has never been a strong drive to increase public awareness and promote support groups to help victims outside of the schools.  Campaigns and programs has never gained mainstream presence enough to make an impact.