Generate schema of workflow

Generate schema of workflow

Generate schema of workflow

Introduction

Workflow scheme generation is the creation of schemes on the basis of a specific set of input data, such as the list of status types and the standard rules for transitions between states.

The use of the generating scheme for workflow is feasible in the following cases:

  • The route consists of standard elements
  • It is necessary to give the user a simple tool for define a route

Options for scheme generation in WFE:

  • Code Generation
  • Generation through IWorkflowGenerator
  • EasyWorkflow (alpha)

The options for scheme generation are universal and can be used not only with WorkflowEngine.NET but with other solutions as well. Let us consider each of these methods.

Code Generation

The workflow scheme includes the following sections: Activities, Transitions, Actors, Commands, Parameters, and Localization. You can create the XML in the code. You can use XElement from the standard library of the .NET Framework for xml generation. However, we recommend you use the ProcessDefinition class.

We shall work on the example from our previous article.

static void Main(string[] args)
{
    ProcessDefinition pd = new ProcessDefinition();
	
	//commands
    pd.Commands.AddRange(
        new CommandDefinition[]{
			CommandDefinition.Create("StartToRoute"),
			CommandDefinition.Create("Agree"),
			CommandDefinition.Create("Reject")});

	//activities
    pd.Activities.AddRange(
        new ActivityDefinition[]{
			ActivityDefinition.Create("Draft", "Draft", true, false, true, true),
			ActivityDefinition.Create("State1", "State1", false, false, true, true),
			ActivityDefinition.Create("State2", "State2", false, false, true, true),
			ActivityDefinition.Create("Final", "Final", false, true, true, true)});

    //Start transition
    var tcStart = TriggerDefinition.Create("Command");
    tcStart.Command = pd.Commands[0];

    pd.Transitions.Add(
        TransitionDefinition.Create("Draft_State1", TransitionClassifier.Direct,
            pd.Activities[0], pd.Activities[1],
            tcStart, ConditionDefinition.Always));

    //Agree transition
    var tcAgree = TriggerDefinition.Create("Command");
    tcAgree.Command = pd.Commands[1];

    for (int i = 1; i < pd.Activities.Count - 1; i++)
    {
        int j = i + 1;
        string tName = string.Format("{0}_{1}", pd.Activities[i].Name, pd.Activities[j].Name);
        pd.Transitions.Add(
            TransitionDefinition.Create(tName, TransitionClassifier.Direct,
                pd.Activities[i], pd.Activities[j],
                tcAgree, ConditionDefinition.Always));
    }

    //Reject transition to Draft
    var tcReject = TriggerDefinition.Create("Command");
    tcReject.Command = pd.Commands[2];

    for (int i = pd.Activities.Count - 2; i > 0; i--)
    {
        int j = 0;
        string tName = string.Format("{0}_{1}", pd.Activities[i].Name, pd.Activities[j].Name);
        pd.Transitions.Add(
            TransitionDefinition.Create(tName, TransitionClassifier.Reverse,
                pd.Activities[i], pd.Activities[j],
                tcAgree, ConditionDefinition.Always));
    }

	//serialize to xml
    string scheme = pd.Serialize();    
    Console.WriteLine(scheme);
}

Output:

Workflow Code Generation Output

We received a scheme that is identical to the one created through the designer.

IWorkflowGenerator

IWorkflowGenerator is the interface of the schemes generator. It is specified at the initialization of WorkflowBuilder.

public interface IWorkflowGenerator < out TSchemeMedium > where TSchemeMedium: class
{
    TSchemeMedium Generate (string SchemeCode, Guid schemeId, IDictionary < string, object > parameters);
}

This interface is available in each provider. In the IWorkflowBuilder provider, the scheme for document flow is received from the database and converted into XElement. Additional parameters aren't supported.

T4 Text Templates

The Core includes a generator for T4 (TT) templates, TTXmlWorkflowGenerator. To use it, you need to define the mapping between the code and the scheme by the AddMapping method:

public void AddMapping (string SchemeCode, object generatorSource)

The parameters for the templates are to be transmitted through CreateInstance:

public void CreateInstance (string schemeCode, Guid processId, IDictionary < string, object > parameters)

TTXmlWorkflowGenerator is included in the Core, but I have published its implementation with open access: T4 Text Templates for workflow generate.

EasyWorkflow (alpha)

Our last project included the requirement to enable end users to create routes. The route was set in the directory.

Routes dictionary

Each entry outlined a set of stages. For each stage, there was a list of users who can approve this document.

We implemented the project by creating standard blocks. This development has alpha status. Later, this functionality will be available in the Core and Designer.

So, let us get started.

We will take WorkflowApp from the last article as a basis. Connect OptimaJet.EasyWorkflow.Core to it.

Let us introduce some changes to Program.cs:

Let us add a class that will store the current sequence of stages.

public class Routes
{
    private static Dictionary<string, object> _parameters = new Dictionary<string, object>();

    public static void InitParams(string[] stages)
    {
        _parameters = new Dictionary<string, object>();
        _parameters.Add("Stages", stages);
    }

    public static Dictionary<string, object> GetParams()
    {
        return _parameters;
    }
}

Then we add the ChangeRoute method, which is used to enter the stages of the route.

private static void ChangeRoute()
{
    string[] states;
    do
    {
        Console.Write("Please enter States (separator:','): ", processId);
        states = Console.ReadLine().Split(',').Select(c => c.Trim()).ToArray();

        if (states.Length < 2)
            Console.WriteLine("Error: min 2 stages. Please, try retry.", processId);

    } while (states.Length < 2);
            
    Routes.InitParams(states);

    Console.WriteLine("ChangeRoute - OK.", processId);
            
    WorkflowInit.Runtime.SetSchemeIsObsolete(schemeCode);
    if(processId.HasValue)
        WorkflowInit.Runtime.UpdateSchemeIfObsolete(processId.Value, Routes.GetParams());
}

*We use UpdateSchemeIfObsolete call, because the option of nested call for the basic methods is not available in the free version.

We add the ChangeRoute method call to the CreateInstanse method, unless the route has been assigned and the parameters are used when calling CreateInstance.

private static void CreateInstance()
{
    if (Routes.GetParams().Count == 0)
        ChangeRoute();

    processId = Guid.NewGuid();

    try
    {
        WorkflowApp.WorkflowInit.Runtime.CreateInstance(schemeCode, processId.Value, Routes.GetParams());
        Console.WriteLine("CreateInstance - OK.", processId);
    }
    catch (Exception ex) {
        Console.WriteLine("CreateInstance - Exception: {0}", ex.Message);
        processId = null;
    }
}

We will add a ChangeRoute call by pressing the "6" to the Main function.

static void Main(string[] args)
{
    //...
    Console.WriteLine("5 - DeleteProcess");
    Console.WriteLine("6 - ChangeRoute");
    Console.WriteLine("9 - Exit");

    //...
                
    do
    {
        //...        
        switch(operation)
        {
            ...
            case '6':
                ChangeRoute();
                break;
            ...
        }

        Console.WriteLine();
    } while (true);
}

SimpleBlock

Each stage has the following parameters:

  • Name
  • Transitions according to commands
public class SimpleCommand
{
    public string Name { get; set; }
    public TransitionClassifier Classifier { get; set; }
    public WorkflowBlock ToBlock { get; set; }
}

Let us create a SimpleBlock class based on WorkflowBlock class. We need to implement two methods: Register and RegisterFinal. In these methods, the elements of the scheme are registered in ProcessDefinition. Using the first one, we register Activity; and using the second one, Command and Transition.</>

public class SimpleBlock : WorkflowBlock
{
    public SimpleBlock(string name)
        : base(name, "SimpleBlock", new Dictionary<string, object>())
    {
    }

    public SimpleCommand GetCommandByName(string commandName)
    {
        return this["Commands"] == null ? null :
            (this["Commands"] as List<SimpleCommand>).Where(c => c.Name == commandName).FirstOrDefault();
    }

    public override void Register(ProcessDefinition pd, List<WorkflowBlock> blocks)
    {
        base.Register(pd, blocks);

        bool isFinal = this["Commands"] == null 
            ? true : 
            (this["Commands"] as List<SimpleCommand>).Count == 0;
            
        //activity
        var c = ActivityDefinition.Create(Name, Name, TryCast<bool>("Initial", false), isFinal, true, true);
        pd.Activities.Add(c);
        this["_currentActivity"] = c;
    }

    public override void RegisterFinal(ProcessDefinition pd, List<WorkflowBlock> blocks)
    {
        base.RegisterFinal(pd, blocks);

        //transitions
        var commands = this["Commands"] as List<SimpleCommand>;
        if(commands != null)
        {
            foreach (var c in commands)
            {
                var pdCommand = pd.Commands.Where(pdc => pdc.Name == c.Name).FirstOrDefault();
                if (pdCommand == null)
                {
                    pdCommand = CommandDefinition.Create(c.Name);
                    pd.Commands.Add(pdCommand);
                }

                pd.Transitions.Add(new TransitionDefinition()
                {
                    Name = string.Format("{0}_{1}_{2}", Name, c.ToBlock.Name, c.Name),
                    Classifier = c.Classifier,
                    From = (ActivityDefinition)this["_currentActivity"],
                    To = (ActivityDefinition)c.ToBlock["_currentActivity"],
                    Trigger = new TriggerDefinition(TriggerType.Command)
                    {
                        Command = pdCommand
                    },
                    Condition = ConditionDefinition.Always
                });
            }
        }
    }
}

SimpleSchemeGenerator

Let us create the interface implementation of IWorkflowGenerator, which will create standard block types based on the list of stages.

public class SimpleSchemeGenerator : IWorkflowGenerator<XElement>
{
    public XElement Generate(string processName, Guid schemeId, IDictionary<string, object> parameters)
    {
        if (!parameters.ContainsKey("Stages"))
            throw new NotSupportedException("You must specify Stages in parameters");

        string[] stages = (string[])parameters["Stages"];

        var blocks = new List<WorkflowBlock>();

        //create SimpleBlocks
        SimpleBlock draftBlock = null;
        for (int i = 0; i < stages.Length; i++)
        {
            var block = new SimpleBlock(stages[i]);
            block["Initial"] = (i == 0);
            if (i == 0)
            {
                draftBlock = block;
            }

            blocks.Add(block);
        }

        //set Commands property
        for (int i = 0; i < blocks.Count; i++)
        {
            var block = blocks[i];
            if ((bool)block["Initial"] == true)
            {
                block["Commands"] = new List<SimpleCommand>() { 
                    new SimpleCommand(){ Name = "StartToRoute", ToBlock = blocks[i+1], Classifier = TransitionClassifier.Direct}
                };
            }
            else if (i < blocks.Count - 1)
            {
                block["Commands"] = new List<SimpleCommand>() { 
                    new SimpleCommand(){ Name = "Agree", ToBlock = blocks[i+1], Classifier = TransitionClassifier.Direct},
                    new SimpleCommand(){ Name = "Reject", ToBlock = draftBlock, Classifier = TransitionClassifier.Reverse}
                };
            }
        }

        return Converter.ToXElement(processName, blocks);
    }
}

I showed the transfer of the list of steps through the parameters variable for convenience. For more complex routes, only route id should be shown.

Then we use OptimaJet.EasyWorkflow.Core.Converter to convert a set of SimpeBlocks into a schema of process.

To connect SimpleSchemeGenerator to WorkflowRuntime, we have to specify SimpleSchemeGenerator as the first parameter at the creation of the WorkflowBuilder.

//...
var builder = new WorkflowBuilder(
    new SimpleSchemeGenerator(),
    new OptimaJet.Workflow.Core.Parser.XmlWorkflowParser(),
    new OptimaJet.Workflow.DbPersistence.DbSchemePersistenceProvider(connectionString)
    ).WithDefaultCache();
//...

As the sets of steps can vary greatly, it is necessary to add a mechanism for status correction. If the new scheme does not have this stage, the initial status has to be set.

private static void _runtime_OnSchemaWasChanged(object sender, SchemaWasChangedEventArgs e)
{
    var processId = e.ProcessId;
    var currentActivity = Runtime.GetCurrentActivityName(processId);
    var currentState = Runtime.GetCurrentStateName(processId);
    var newScheme = Runtime.GetProcessScheme(processId);


    ActivityDefinition oldActivity = null;
    try
    {
        oldActivity = newScheme.FindActivity(currentActivity);
    }
    catch (ActivityNotFoundException)
    {
    }

    //If new scheme have current Activity then do nothing 
    if (oldActivity != null)
        return;

    //If new scheme have current State then set this State
    if (
        newScheme.Activities.Any(
            a =>
                !string.IsNullOrEmpty(a.State) &&
                a.State.Equals(currentState, StringComparison.InvariantCultureIgnoreCase)))
    {
        _runtime.SetState(processId, string.Empty, string.Empty, currentState, new Dictionary<string, object>());
        return;
    }

    //Set to Initial state
    if (!string.IsNullOrEmpty(newScheme.InitialActivity.State))
    {
        Runtime.SetState(processId, string.Empty, string.Empty, newScheme.InitialActivity.State, new Dictionary<string, object>());
        return;
    }
}

To do this, add OnSchemaWasChanged, an event handler.

//...
 _runtime.OnSchemaWasChanged += _runtime_OnSchemaWasChanged;
//...

Test

Run the project and introduce the stages of a route:

Operation:
0 - CreateInstance
1 - GetAvailableCommands
2 - ExecuteCommand
3 - GetAvailableState
4 - SetState
5 - DeleteProcess
6 - ChangeRoute
9 - Exit
The process is not created.
Please enter Stages (separator:','): draft, state1, state2, state3, final
ChangeRoute - OK.
CreateInstance - OK.
ProcessId = '863724c0-a013-492a-b5be-84206957220a'. CurrentState: draft, CurrentActivity: draft
Enter code of operation:2
Available commands:
- StartToRoute (LocalizedName:StartToRoute, Classifier:Direct)
Enter command:starttoroute
ExecuteCommand - OK.

ProcessId = '863724c0-a013-492a-b5be-84206957220a'. CurrentState: state1, CurrentActivity: state1
Enter code of operation:2
Available commands:
- Agree (LocalizedName:Agree, Classifier:Direct)
- Reject (LocalizedName:Reject, Classifier:Reverse)
Enter command:agree
ExecuteCommand - OK.

ProcessId = '863724c0-a013-492a-b5be-84206957220a'. CurrentState: state2, CurrentActivity: state2
Enter code of operation:4
Available state to set:
- draft
- state1
- state2
- state3
- final
Enter state:state1
SetState - OK.

ProcessId = '863724c0-a013-492a-b5be-84206957220a'. CurrentState: state1, CurrentActivity: state1
Enter code of operation:9

About the Product

WorkflowEngine.NET - component that adds workflow in your application. It can be fully integrated into your application, or be in the form of a specific service (such as a web service).

The benefits of using WorkflowEngine.NET:

  • Designer of process scheme in web-browser (HTML5)
  • High performance
  • Quick adding to your project workflow
  • Autogenerate incoming list
  • Change the scheme process in real-time
  • Database providers: MS SQL Server, MongoDB, RavenDB, Oracle, MySQL, PostgreSQL

WorkflowEngine.NET is perfect for:

  • Adding workflow in your application with minimal changes to your code
  • The process of challenging and \ or frequently changing
  • Some modules or some applications need to change the status of the documents

  • By Dmitry Melnikov
  • 2/3/2015
  • workflow, scheme generate, generation, easyworkflow, IWorkflowGenerator