13

How do people structure their code when using the c# stateless library?

https://github.com/nblumhardt/stateless

I'm particularly interested in how this ties in with injected dependencies, and a correct approach of responsibilities and layering correctly.

My current structure involves the following:

public class AccountWf
{
    private readonly AspNetUser aspNetUser;

    private enum State { Unverified, VerificationRequestSent, Verfied, Registered }
    private enum Trigger { VerificationRequest, VerificationComplete, RegistrationComplete }

    private readonly StateMachine<State, Trigger> machine;

    public AccountWf(AspNetUser aspNetUser, AccountWfService userAccountWfService)
    {
        this.aspNetUser = aspNetUser;

        if (aspNetUser.WorkflowState == null)
        {
            aspNetUser.WorkflowState = State.Unverified.ToString();
        }

        machine = new StateMachine<State, Trigger>(
        () => (State)Enum.Parse(typeof(State), aspNetUser.WorkflowState),
        s => aspNetUser.WorkflowState = s.ToString()
        );

        machine.Configure(State.Unverified)
        .Permit(Trigger.VerificationRequest, State.VerificationRequestSent);

        machine.Configure(State.VerificationRequestSent)
        .OnEntry(() => userAccountWfService.SendVerificationRequest(aspNetUser))
        .PermitReentry(Trigger.VerificationRequest)
        .Permit(Trigger.VerificationComplete, State.Verfied);

        machine.Configure(State.Verfied)
        .Permit(Trigger.RegistrationComplete, State.Registered);

    }

    public void VerificationRequest()
    {
        machine.Fire(Trigger.VerificationRequest);
    }

    public void VerificationComplete()
    {
        machine.Fire(Trigger.VerificationComplete);
    }

    public void RegistrationComplete()
    {
        machine.Fire(Trigger.RegistrationComplete);
    }

}

Should we implement all processes (call to services) within the OnEntry hook, or implement the processes on the outside after the state transition has been verified that it is allowed to take place? I'm wonder how to do the transaction management if so.

I guess what I'm after is some best guidance from those who have already implemented something using stateless and how to approach the code structure.

granadaCoder
  • 26,328
  • 10
  • 113
  • 146
dandcg
  • 442
  • 4
  • 16
  • Looking at this some more I'm leaning towards using a factory injected into the domain services to construct the workflow object and this can pass in the services required by the workflow object. – dandcg Oct 09 '14 at 15:44
  • Still looking at some guidance on best approach of using the state machine. Say I need to call a method on a send email service which exists for the lifetime of the web request. Should this call go within the OnEntry or within the public method. If its in the OnEntry what happens if there is an issue during the transition? Some guidance from people who have implement code using stateless and where they have placed the actual doing code would be greatly appreciated. – dandcg Oct 12 '14 at 15:55

1 Answers1

12

Before addressing the structure itself a couple remarks:

  • OnEntry actions are only executed if the trigger has been successfully fired.

  • Triggers fired that are not allowed in the current state will throw an InvalidOperationException. Consider overriding OnUnhandledTrigger if you're not expecting an exception (I've found that logging unhandled triggers is a good approach to finding the flaws in the logic).

My rule of thumb for the OnEntry/OnExit structuring is that any creation and logic will be placed OnEntry and any required clean-up is done OnExit.

So in your case, given that the you're using injected dependencies (and assuming you're not taking ownership of those, i.e, someone else will manage their lifecycle) you can place all your logic OnEntry.

With that in mind, the way that your state machine is currently structured is perfectly fine.

One last note, keep in mind that firing triggers from within the same thread that's advancing the state machine and doing the state machine logic can and will lead to stackoverflow exceptions (see here on how to solve the auto advance issue).

Community
  • 1
  • 1
jpmnteiro
  • 747
  • 1
  • 13
  • 23
  • Hi Omni, thanks for this. what would then happen if error then occurred during the OnEntry implementation - would the state still change? Also would you typically use a factory to create the workflow instance? This would deal with new-ing up the wf instance with the starting state and passing in the dependencies required by the implementation? – dandcg Oct 15 '14 at 10:12
  • Hi @dandcg. The state transition happens before the `OnEntry` is processed, so by the time the exception is thrown, the state is already changed. You then have to decide where to handle the exception. Either inside the `OnEntry` or in `machine.Fire(...)` that transitioned into the state that threw the exception. There's not much in using a factory to create the `AccountWf` I'd say. A factory would be useful if depending on the parameters, you had different types of machines/configurations. – jpmnteiro Oct 15 '14 at 10:33
  • The reason for the factory being that i don't want to inject the workflow instance itself? But I suppose this is ok. How did you play it? – dandcg Oct 15 '14 at 10:36
  • if you're talking about the exception handling, in my use cases (usually tcp connections) i usually log the exception and transition to a more appropriate state where a decision can be made (reconnect or stop depending on configuration). – jpmnteiro Oct 15 '14 at 10:44