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.
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.
Some points worth explaining :
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.
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.
Now we initialize the State Machine. I included the whole method, well go through each unfamiliar lines.
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.
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.
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.
Reference the package in your code.
using Stateless;
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");
- 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.
_machine.Fire(Trigger.TOGGLE);
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 |
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 };
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 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.
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);
_machine.Fire(_assignTrigger, owner);
Asset asset = new Asset(assInfo); asset.Assign(GetOwner()); //with parameter asset.RequestRepair(); asset.Release();
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); }
[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.
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