Parallel approval without branches

Parallel approval without branches

Parallel approval without branches

photo by Grethel Quintero

Introduction

Sometimes we need to send the document for approval to several people at one stage. And people should agree it in parallel. You can use the parallel branches. But often the set of people who have right to agree the document is not fixed at every stage. In this case it is necessary to make the generation of the scheme that can complicate your project.

However, there is a fairly simple solution to uniformly describe this logic. In this case, parallel branches are not used and you do not need to use the generation of the scheme. The solution is based on the ability of the engine to save any objects in the variables (parameters) of the process and pass parameters to code actions. You can modify it and use it in your own solutions.

The Project

Please pay attention that the another project called Business was added in solution. It contains just two classes.
  • Approvers - this class used to save information about users who can approve the document and who has already approved. Class is designed so as to be serializable into JSON. The code of this class is trivial and does not require the notes.

    public class Approvers
    {
    	//Must be а public property to serialize to JSON
            public Dictionary<string, bool> ApproversDictionary { get; set; }
          
            public Approvers(List<string> ids)
            {
               if (ids == null) //Required for correct deserialization from JSON
               {
                   ApproversDictionary = new Dictionary<string, bool>();
               }
               else
               {
                   ApproversDictionary = ids.ToDictionary(id => id, id => false);
               }
            }
    
            [JsonIgnore]
            public bool IsApproved
            {
                get { return ApproversDictionary.Values.All(v => v); }
            }
    
            public void Approve(string id)
            {
                ApproversDictionary[id] = true;
            }
    
         ...
    
            public List GetAvailiableApprovers()
            {
                return ApproversDictionary.Where(s => !s.Value).Select(s=>s.Key).ToList();
            }
    }
    
  • ApproversProvider - this static class contains only one static method. GetApprovers - method designed to place in it a logic of determination of the approvers at each stage. In this sample it takes the string parameter, depending on which we define the list of identifiers of the approvers. But it is not a rule but just a an example. Your application could contain a completely different logic.

    public static class ApproversProvider
    {
        public static Approvers GetApprovers(ProcessInstance processInstance, string name)
        {
            switch (name)
            {
                case "Stage1": 
                    return new Approvers(new List{"user1","user2"});
                case "Stage2":
                    return new Approvers(new List {"user3","user4","user5" });
                default:
                    return new Approvers(new List());
            }
        }
    }
    

The Scheme

Let's consider the scheme focusing on the more important details.

Please pay attention that parameter Approvers (type Business.Approveres) was added  in the parameters of the scheme. We need it to save information about approvers in times between the execution of commands.

Further we describe Actions and Conditions which we need to organize our logic. The scheme contains two Actions, one Condition and one Rule.

  • FillApprovers - the Action which gets Approvers object from ApproversProvider.GetApprovers method and save it into the process variables (parameters).This method called in the beginning of each stage.

    void FillApprovers (ProcessInstance processInstance, WorkflowRuntime runtime, string parameter)
    {
    	processInstance.SetParameter ("Approvers",ApproversProvider.GetApprovers(processInstance,parameter));
    }
    
  • Approve - the Action which registers a fact that a user who executes a command approved a document.

    void Approve (ProcessInstance processInstance, WorkflowRuntime runtime, string parameter)
    {
    	if (string.IsNullOrEmpty(processInstance.CurrentCommand) ||
    	    processInstance.CurrentCommand.Equals("start",StringComparison.InvariantCultureIgnoreCase))
    	        return;
    
    	var approvers = processInstance.GetParameter<Approvers>("Approvers");
    	approvers.Approve(processInstance.IdentityId);
    }
    
  • IsApproveComplete - the Condition which checks the all users approved a document

    bool IsApproveComplete (ProcessInstance processInstance, WorkflowRuntime runtime, string parameter)
    {
    	var approvers = processInstance.GetParameter<Approvers>("Approvers");
    	return approvers.IsApproved;
    }
    
  • Approver - the Rule which checks that the identityId is registerd in Approvers object and this user not approved this document before.

    IEnumerable<string> Approver (ProcessInstance processInstance, string parameter)
    {
    	var approvers = processInstance.GetParameter<Approvers>("Approvers");
    	return approvers.GetAvailiableApprovers();
    }
    

The process represented by scheme consists of four states. Draft and Final - is ordinary states represented by one Activity. Stage1 and Stage2 consists of two Activities each. Let's look at how the approval works on the example of Stage1. Stage2 works similar to Stage1.

First, the process enters in the  Stage1Init Activity. And executes FillApprovers action. After that Approvers object will be saved in the variables of the process. Than process enters in the Stage1  Activity. This Activity have two outgoing transition, one triggered with approve command and one auto transition. Immediately after set the Statge1 as current Activity, engine will try to execute all outgoing auto transitions. It will check IsApproveComplete condition, but because nobody approved the document, condition will return false. And the engine will stoped the transitional process.

Before returning a list of available commands engine will check the Approver Rule. If a user with the specified id must approve on the document at this stage and not yet approved it, the engine will return the approve command as available for execution.

As we can see the command transition which triggered by the approve command is cycled. After execution of the approve command engine will execute the Approve action which will modify Approvers object in the variables of the process. It will write an information that the user which executed the command have approved the document.

Than the engine will check IsApproveComplete condition  and if there are no more users who not approved the document it will execute transition to Stage2Init. If any users have to approve of the document, the transition process will be stopped, waiting for the next commands.

Let's proceed to the demo.

The Demo

First, execute the start command.

...
The process is not created.
CreateInstance - OK.
ProcessId = 'b6f1b7d4-...'. CurrentState: Draft, CurrentActivity: Draft
Current user is undefined.
Enter code of operation:3
Available commands:
- start (LocalizedName:start, Classifier:Direct)
Enter command:start
ExecuteCommand - OK.

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage1, CurrentActivity: Stage1

Then, if you get the list of available commands you will see only deny command. You need to set the user id for command execution. You can do it using 0-Set current user operation.

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage1, CurrentActivity: Stage1
Current user is undefined.
Enter code of operation:3
Available commands:
- deny (LocalizedName:deny, Classifier:Reverse)
Enter command:

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage1, CurrentActivity: Stage1
Current user is undefined.
Enter code of operation:0
Enter user id:user1

After that you can get and execute the approve command. Execute the command and get the list of available commands. You will see only deny command again. You need to change user to user2 and execute the approve command again.

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage1, CurrentActivity: Stage1
Current user = user1.
...
Enter command:approve
ExecuteCommand - OK.

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage1, CurrentActivity: Stage1
Current user = user1.
Enter code of operation:3
Available commands:
- deny (LocalizedName:deny, Classifier:Reverse)
Enter command:

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage1, CurrentActivity: Stage1
Current user = user1.
Enter code of operation:0
Enter user id:user2

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage1, CurrentActivity: Stage1
Current user = user2.
Enter code of operation:3
Available commands:
- approve (LocalizedName:approve, Classifier:Direct)
- deny (LocalizedName:deny, Classifier:Reverse)
Enter command:approve
ExecuteCommand - OK.

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage2, CurrentActivity: Stage2

Now we see, that the process changed it's state to Stage2. First state was completed. Then you need to execute the approve command for user4, user5 and user6 to completely approve the document.

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage2, CurrentActivity: Stage2
Current user = user2.
Enter code of operation:0
Enter user id:user3

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage2, CurrentActivity: Stage2
Current user = user3.
Enter code of operation:3
...
Enter command:approve
ExecuteCommand - OK.

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage2, CurrentActivity: Stage2
Current user = user3.
Enter code of operation:0
Enter user id:user4

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage2, CurrentActivity: Stage2
Current user = user4.
...
Enter command:approve
ExecuteCommand - OK.

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage2, CurrentActivity: Stage2
Current user = user4.
Enter code of operation:0
Enter user id:user5

ProcessId = 'b6f1b7d4-...'. CurrentState: Stage2, CurrentActivity: Stage2
Current user = user5.
...
Enter command:approve
ExecuteCommand - OK.

ProcessId = 'b6f1b7d4-...'. CurrentState: Final, CurrentActivity: Final

Thank you for the attention!

  • By Dmitry Melnikov
  • 5/9/2015
  • Parallel branches, sub, parallel process, subprocess, fork, parallel fork