How to encapsulate a stateless .NET state machine

I have a project that has an almost linear workflow. I am trying to use the .NET Stateless library to act as a processing engine / state machine. The number of examples there is limited, but I have compiled the following code:

private StateMachine<WorkflowStateType, WorkflowStateTrigger> stateMachine; private StateMachine<WorkflowStateType, WorkflowStateTrigger>.TriggerWithParameters<Guid, DateTime> registrationTrigger; private Patient patient; public Patient RegisterPatient(DateTime dateOfBirth) { configureStateMachine(WorkflowState.Unregistered); stateMachine.Fire<DateTime>(registrationTrigger, dateOfBirth); logger.Info("State changed to: " + stateMachine.State); return patient; } private void configureStateMachine(WorkflowState state) { stateMachine = new StateMachine<WorkflowState, WorkflowTrigger>(state); registrationTrigger = stateMachine.SetTriggerParameters<DateTime>(WorkflowTrigger.Register); stateMachine.Configure(WorkflowState.Unregistered) .Permit(WorkflowTrigger.Register, WorkflowStateType.Registered); stateMachine.Configure(WorkflowState.Registered) .Permit(WorkflowTrigger.ScheduleSampling, WorkflowState.SamplingScheduled) .OnEntryFrom(registrationTrigger, (dateOfBirth) => registerPatient(dateOfBirth)); } private void registerPatient(DateTime dateOfBirth) { //Registration code } 

As you can see, I use Stateless Fire () overload, which allows me to pass the trigger. This means that I can have the business logic of the state machine process, in this case, a code for registering a new patient.

It all works, but now I would like to move all the code of the destination computer to another class in order to encapsulate it, and I am having problems with this. The problems that I had with this are the following:

  • Creating an instance of a StateMachine object requires a state, and State requires the readonly property, which can only be set when an instance is created.
  • my registrationTrigger must be created during state machine configuration, and must also be accessible to the calling class.

How can I overcome these elements and encapsulate the state machine code?

+7
c # stateless-state-machine
source share
2 answers

There is an article by Scott Hanselman with an example and an introduction to the library. There are also several examples available on their GitHub, including the Error Implementation Example mentioned in Scott's article, which encapsulates a state machine.

The following is an example of how state can be derived from behavior:

 public class PatientRegistrationState { private StateMachine<WorkflowState, WorkflowTrigger> stateMachine; private StateMachine<WorkflowState, WorkflowStateTrigger>.TriggerWithParameters<DateTime> registrationTrigger; public PatientRegistrationState(State initialState = default(State)) { stateMachine = new StateMachine<WorkflowState, WorkflowTrigger>(initialState); stateMachine.Configure(WorkflowState.Unregistered) .Permit(WorkflowTrigger.Register, WorkflowStateType.Registered); stateMachine.Configure(WorkflowState.Registered) .Permit(WorkflowTrigger.ScheduleSampling, WorkflowState.SamplingScheduled) .OnEntryFrom(registrationTrigger, (date) => OnPatientRegistered(date)); } public WorkflowState State => stateMachine.State; public Action<DateTime> OnPatientRegistered {get; set;} = (date) => { }; // For state changes that do not require parameters. public void ChangeTo(WorkflowTrigger trigger) { stateMachine.Fire<DateTime>(trigger); } // For state changes that require parameters. public void ChangeToRegistered(DateTime dateOfBirth) { stateMachine.Fire<DateTime>(registrationTrigger, dateOfBirth); } // Change to other states that require parameters... } public class PatientRegistration { private PatientRegistrationState registrationState; private Patient patient; public PatientRegistration() { registrationState = PatientRegistrationState(WorkflowState.Unregistered) { OnPatientRegistered = RegisterPatient; } } public Patient RegisterPatient(DateTime dateOfBirth) { registrationState.ChangeToRegistered(dateOfBirth); logger.Info("State changed to: " + registrationState.State); return patient; } private void RegisterPatient(DateTime dateOfBirth) { // Registration code } } 
+5
source share

Here is how I did it in my project.

Shared workflow logic for class separation. I had several workflows based on one of the flags present in the request object; below is one of the workflow classes:

 public class NationalWorkflow : BaseWorkflow { public NationalWorkflow(SwiftRequest request) : this(request, Objects.RBDb) { } public NationalWorkflow(SwiftRequest request, RBDbContext dbContext) { this.request = request; this.dbContext = dbContext; this.ConfigureWorkflow(); } protected override void ConfigureWorkflow() { workflow = new StateMachine<SwiftRequestStatus, SwiftRequestTriggers>( () => request.SwiftRequestStatus, state => request.SwiftRequestStatus = state); workflow.OnTransitioned(Transitioned); workflow.Configure(SwiftRequestStatus.New) .OnEntry(NotifyRequestCreation) .Permit(SwiftRequestTriggers.ProcessRequest, SwiftRequestStatus.InProgress); workflow.Configure(SwiftRequestStatus.InProgress) .OnEntry(ValidateRequestEligibility) .Permit(SwiftRequestTriggers.AutoApprove, SwiftRequestStatus.Approved) .Permit(SwiftRequestTriggers.AdvancedServicesReview, SwiftRequestStatus.PendingAdvancedServices); ..................... } 

which is launched from the controller / any other level:

 private static void UpdateRequest(SwiftRequestDTO dtoRequest) { var workflow = WorkflowFactory.Get(request); workflow.UpdateRequest(); } 

As mentioned above, I had different workflow rules based on the conditions in the request object, and therefore used the factory template WorkflowFactory.Get(request) ; you can create an instance of your workflow / enter it as desired

And inside the workflow class (BaseWorkflow class in my case), I discovered actions:

  public void UpdateRequest() { using (var trans = this.dbContext.Database.BeginTransaction()) { this.actionComments = "Updating the request"; this.TryFire(SwiftRequestTriggers.Update); SaveChanges(); trans.Commit(); } } protected void TryFire(SwiftRequestTriggers trigger) { if (!workflow.CanFire(trigger)) { throw new Exception("Cannot fire " + trigger.ToString() + " from state- " + workflow.State); } workflow.Fire(trigger); } 
+2
source share

All Articles