danielgerlag / workflow-core

Lightweight workflow engine for .NET Standard
MIT License
5.35k stars 1.19k forks source link

Attach - End sub flow #325

Open terry-delph opened 5 years ago

terry-delph commented 5 years ago

Hi, I have been experimenting with your library. I have found it really useful so far, specially for maintaining state and conditional flows.

I am trying to create a scenario of an approval process, where if the document is not approved it should return back to a particular step and begin the review process again.

I am using the Attach method when I want to go back to a particular step. What I am finding is that because it attaches to review, it executes the "Goodbye" stage every time it is not approved, after it is approved. I need it to stop the sub flow where it attaches. How does the "End" method work? I am stumped on what Step (IStepBody) it should reference for my scenario. I also noticed that when using "When" the step names in the database are missing for the contents of the "Do". Is this normal?

I have tried the following:

builder
                .StartWith<PrintMessage>(x => x.Name("start"))
                    .Input(step => step.Message, data => "Starting")
                    .Id("Review")
                .Then<PrintMessage>()
                    .Input(step => step.Message, data => "Reviewing")
                .Then<Review>()
                    .Output(data => data.Approved, step => step.Approved)
                .When(x => true, "Approved")
                    .Do(then => then.StartWith<PrintMessage>()
                        .Input(step => step.Message, data => "Approved")
                    )
                .When(x => false, "NotApproved")
                .Do(then => then.StartWith<PrintMessage>()
                    .Id("NotApproved")
                    .Input(step => step.Message, data => "Not Approved")
                    .Attach("Review")
                    //.End<SayGoodbye>("Terminating Not Approved Flow")
                )
                .Then<SayGoodbye>()
                    .Id("Goodbye"); //Only want this to execute once approved, not when attaching after not approved flow.

The execution proceeds like this when it is not approved once, then approved the second pass. These are the step names:

  1. start
  2. PrintMessage ("Starting")
  3. Review
  4. PrintMessage ("Reviewing")
  5. null - Should be PrintMessage ("Not Approved") but Step name is missing in database (it is null)
  6. start
  7. PrintMessage ("Starting")
  8. Review
  9. PrintMessage ("Reviewing")
  10. null - Should be PrintMessage ("Approved") but Step name is missing in database (it is null)
  11. SayGoodbye
  12. SayGoodbye (I only want this to execute once, no matter how many times the document is not approved)

Many thanks

danielgerlag commented 5 years ago

Have you tried changing

.When(x => true, "Approved")

to

.When(x => data.Approved, "Approved")

and

.When(x => false, "NotApproved")

to

.When(x => !data.Approved, "NotApproved")
terry-delph commented 5 years ago

I had that originally but I decided I want to return true or false from the Review step using return OutcomeResult() rather than keep state in the global model. So my When nodes are using the outcome result rather than the global model state. Is the line:

.Output(data => data.Approved, step => step.Approved)

interfering in some way?

public class Review : StepBody
{
        private ILogger<Review> Logger { get; set; }

        public bool Approved { get; set; }

        public Review(ILogger<Review> logger)
        {
            Logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public override ExecutionResult Run(IStepExecutionContext context)
        {
            Logger.LogInformation("Review");

            bool? result = true; //Temporary used as a breakpoint to change to false at runtime
            return OutcomeResult(result);
        }
}
terry-delph commented 5 years ago

How would I break the chain of steps after using Attach? Do the pointers that come after need to be deleted? I guess how I am trying to use it is similar to a workflow/state machine hybrid.

zrkcode commented 5 years ago

I have the same problem.

@danielgerlag

jinbo2015 commented 5 years ago

Put the .Then<SayGoodbye>().Id("Goodbye") after .Input(step => step.Message, data => "Approved"). Note that the functionality of "Attach" is similar to "goto" statement in C programming language, .Then<SayGoodbye>() will definitely be executed after the When switch step finishes, and the When switch step will never finish until the execution path in the Do reaches the end. After you called Attach in the Do block, the execution path of the Do block will never reach the end until the nested execution path reach the end, and this case is only possible when the When switch finally chooses the “true" path and executes the .Then<SayGoodbye>().Id("Goodbye"). So, after the .Attach("Review") is executed once, the SayGoodbye step will be executed once more, in your code.

xiaolele1314 commented 3 years ago

I had that originally but I decided I want to return true or false from the Review step using return OutcomeResult() rather than keep state in the global model. So my When nodes are using the outcome result rather than the global model state. Is the line:

.Output(data => data.Approved, step => step.Approved)

interfering in some way?

public class Review : StepBody
{
        private ILogger<Review> Logger { get; set; }

        public bool Approved { get; set; }

        public Review(ILogger<Review> logger)
        {
            Logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public override ExecutionResult Run(IStepExecutionContext context)
        {
            Logger.LogInformation("Review");

            bool? result = true; //Temporary used as a breakpoint to change to false at runtime
            return OutcomeResult(result);
        }
}

I think it's probably due to constructors I tried adding a parameterless constructor and it worked perfectly like this:


public class Review : StepBody
{
private ILogger<Review> Logger { get; set; }
    public bool Approved { get; set; }

    public Review()
    {

    }

    public Review(ILogger<Review> logger)
    {
        Logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public override ExecutionResult Run(IStepExecutionContext context)
    {
        bool? result = true; //Temporary used as a breakpoint to change to false at runtime
        return OutcomeResult(result);
    }
}