Freigeben über


Parameterised Triggers and Re-Entrant States in Stateless v2

Since working on a clinical outcomes review system a couple of years ago, I’ve been aware of the gap between simple hand-coded workflows and the full-blown workflow tools.

Stateless embodies the idea that a state machine can use closures to implement workflow without taking on persistence responsibilities. So long as all of the data used to drive the state machine is external to it, a regular ORM or other persistence mechanism can deal with identity, persistence and transactional issues. This substantially reduces the technical complexity of the framework.

As a ‘fun’ project, I haven’t done much other than maintain the site since the first version was finished. The second version extends the paradigm just a little, but in doing so makes a wider range of scenarios cleanly approachable.

To illustrate the new features I’ll draw on Scott Allen’s nifty bug tracker example for Windows Workflow.

Parameterised Triggers

The triggers that drive transitions in a state machine are approximately analogous to events or commands. Like events and commands, triggers can be associated with parameters.

  • When a bug is assigned, the parameter may be the assignee;
  • When an order is cancelled, the parameter may be the reason;
  • etc.

Modelling these details in Stateless, I adopted the some design goals and constraints:

  1. Retain the existing simple syntax for non-parameterised triggers;
  2. Execute parameter-driven logic during the transition, i.e. after the exit events for the last state have fired;
  3. Make it clear to the user exactly when parameter-driven logic will execute;
  4. Control interference between parameter-driven logic and guard conditions;
  5. Ensure parameter type-safety at compile time.

New APIs

To associate parameters with a trigger, StateMachine provides the SetTriggerParameters() generic methods:

image

SetTriggerParameters() accepts one generic parameter for each of the trigger’s parameters, so in this case the Assign trigger is being associated with a single parameter of type string.

This method is called once, when the state machine is being configured.

The return value in this case is a TriggerWithParameters<string> object that lets us invoke the Fire() method in a strongly-typed way:

imageThe C# compiler will use inference to determine that the parameter to the trigger is of type string. Attempting to fire assignTrigger with any other parameters will be rejected at compile-time.

Now, all of this would be fairly pointless if Fire() was the only place that the parameters needed checking. However, the firing of the trigger is only half of the story.

When the machine transitions into the new state, logic based on the trigger parameters will need to run:

imageBecause assignTrigger is provided to the OnEntryFrom() method, the compiler knows that the provided entry action accepts a single parameter of type string.

Putting everything together behind the facade of a domain object leads to a natural, understandable design:

image A more complete example is in the Stateless Mercurial repository.

Alternatives

One alternative I considered but later discarded was to parameterise the states rather than the triggers, something that seems to be more established in traditional state machine models. After a good deal of unsuccessful experimentation I concluded that state parameterisation isn’t as useful as the trigger-based version.

Re-Entrant States

Stateless encourages you to attach program logic to entry and exit events for the states in your state machine.

One of the edge cases that the bug tracker example reveals is re-initialisation of an already active state. When the bug is assigned, an email might be sent to the assignee. If Assign() is called again, to reassign the bug to someone else, it makes sense to re-initialise the Assigned state by executing the exit actions then executing the entry actions again.

To enable this behaviour for a state, the PermitReentry() method is supplied:

image

The previous version of Stateless considered a self-transition to be the same as an ignored trigger. Version 2 requires that self-transitions are either explicitly ignored, or configured as re-entrant.

Next?

These small additions were actually quite a challenge, but Stateless is still a very small library. One idea I’m toyed with is to use it as a ‘kernel’ for higher-level state machine functionality, e.g. an XML or DSL-driven framework. If you have ideas or requests, feel free to visit the UserVoice forum!

Comments

  • Anonymous
    September 09, 2009
    I really like Stateless and am using it in production. Thank you very much!

  • Anonymous
    October 13, 2009
    I have used XML for configuring the state machine.  It is very rudimentary to set up triggers for states, so my thought is that an example of using XML to configure may be better than adding the functionality to the library.  I guess what I am saying is that NOT having the configuration capability now did NOT stop me from using Stateless.   That said, I did not program the configuration to configure the guard clauses and had wondered what type of factory could be employed to allow me to assign / configure the guard clauses at runtime. Dude, I'm happy that you are returning home but am BUMMED OUT that you won't be at Microsoft.  I really love your code and had hoped that your thinking would drift into the other products.  Windows Workflow as it is now is an overly complex beast.  Stateless fit my requirements and forced to simplify.  Good stuff.  Safe travels and good luck.

  • Anonymous
    June 28, 2011
    Are there any code examples for using PermitDynamic()?

  • Anonymous
    April 15, 2012
    Hi! before the parameterized trigger implementation the configuration of the state maschine was made only through Configure, _machine.Configure(State.Assigned)                .SubstateOf(State.Open)                .OnEntryFrom(_assignTrigger, assignee => OnAssigned(assignee)) ... but after you have implemented Parameterized triggers the design of the stateless seems to be distrupted due to encapsulation violation, _assignTrigger = _machine.SetTriggerParameters<string>(Trigger.Assign); SetTriggerParameters seems to violate the encapsulation. Why didn't you just add another overload of an OnEntryFrom method with parameters like, public StateConfiguration OnEntryFrom<TArg0,...>(TTrigger trigger,... Regards, Evren