Actions, IWorkflowActionProvider and CodeActions

To put it simply, Action is a method called when a process is being executed. Action allows you to call any code, business logic methods which your application already has, external services, side libraries' methods, send mail, etc. Though extremely simple, Actions are a powerful tool of integration into an existing application.

There are two ways of creating Actions in your application: inherit the IWorkflowActionProvider interface and create Actions in this class, or create Actions right in the scheme in the CodeActions section. These two approaches may be combined.

IWorkflowActionProvider

Let's start with a code example, which you may copy and paste for a faster kick-off.

public class ActionProvider : OptimaJet.Workflow.Core.Runtime.IWorkflowActionProvider
{
    private readonly Dictionary<string, Action<ProcessInstance, WorkflowRuntime, string>> _actions = new Dictionary<string, Action<ProcessInstance, WorkflowRuntime, string>>();

    private readonly Dictionary<string, Func<ProcessInstance, WorkflowRuntime, string, CancellationToken, Task>> _asyncActions = new Dictionary<string, Func<ProcessInstance, WorkflowRuntime, string, CancellationToken, Task>>();

    private readonly Dictionary<string, Func<ProcessInstance, WorkflowRuntime, string, bool>> _conditions = new Dictionary<string, Func<ProcessInstance, WorkflowRuntime, string, bool>>();

    private readonly Dictionary<string, Func<ProcessInstance, WorkflowRuntime, string, CancellationToken, Task<bool>>> _asyncConditions = new Dictionary<string, Func<ProcessInstance, WorkflowRuntime, string, CancellationToken, Task<bool>>>();

    public ActionProvider()
    {
        //Register your actions in _actions and _asyncActions dictionaries
        _actions.Add("MyAction", MyAction); //sync
        _asyncActions.Add("MyAsyncAction", MyAsyncAction); //async

        //Register your conditions in _conditions and _asyncConditions dictionaries
        _conditions.Add("MyCondition", MyCondition); //sync
        _asyncConditions.Add("MyAsyncCondition", MyAsyncCondition); //async
    }

    private void MyAction(ProcessInstance processInstance, WorkflowRuntime runtime,
        string actionParameter)
    {
        //Execute your synchronous code here
    }

    private async Task MyAsyncAction(ProcessInstance processInstance, WorkflowRuntime runtime, string actionParameter, CancellationToken token)
    {
        //Execute your asynchronous code here. You can use await in your code.
     }

    private bool MyCondition(ProcessInstance processInstance, WorkflowRuntime runtime, string actionParameter)
    {
        //Execute your code here
        return false;
    }

    private async Task<bool> MyAsyncCondition(ProcessInstance processInstance, WorkflowRuntime runtime, string actionParameter, CancellationToken token)
    {
         //Execute your asynchronous code here. You can use await in your code.
         return false;
    }

    #region Implementation of IWorkflowActionProvider

    public void ExecuteAction(string name, ProcessInstance processInstance, WorkflowRuntime runtime,
        string actionParameter)
    {
        if (_actions.ContainsKey(name))
            _actions[name].Invoke(processInstance, runtime, actionParameter);
        else
            throw new NotImplementedException($"Action with name {name} isn't implemented");
    }

    public async Task ExecuteActionAsync(string name, ProcessInstance processInstance, WorkflowRuntime runtime, string actionParameter, CancellationToken token)
    {
        //token.ThrowIfCancellationRequested(); // You can use the transferred token at your discretion
        if (_asyncActions.ContainsKey(name))
            await _asyncActions[name].Invoke(processInstance, runtime, actionParameter, token);
        else
            throw new NotImplementedException($"Async Action with name {name} isn't implemented");
     }

    public bool ExecuteCondition(string name, ProcessInstance processInstance, WorkflowRuntime runtime,
        string actionParameter)
    {
        if (_conditions.ContainsKey(name))
            return _conditions[name].Invoke(processInstance, runtime, actionParameter);

        throw new NotImplementedException($"Condition with name {name} isn't implemented");
    }

    public async Task<bool> ExecuteConditionAsync(string name, ProcessInstance processInstance, WorkflowRuntime runtime, string actionParameter, CancellationToken token)
    {
        //token.ThrowIfCancellationRequested(); // You can use the transferred token at your discretion
        if (_asyncConditions.ContainsKey(name))
            return await _asyncConditions[name].Invoke(processInstance, runtime, actionParameter, token);

        throw new NotImplementedException($"Async Condition with name {name} isn't implemented");
    }

    public bool IsActionAsync(string name)
    {
        return _asyncActions.ContainsKey(name);
    }

    public bool IsConditionAsync(string name)
    {
        return _asyncConditions.ContainsKey(name);

    public List<string> GetActions()
    {
        return _actions.Keys.Union(_asyncActions.Keys).ToList();
    }

    public List<string> GetConditions()
    {
        return _conditions.Keys.Union(_asyncConditions.Keys).ToList();
    }

    #endregion
}

Let's see how it works. Workflow Engine identifies Actions and Conditions by unique names. In order to specify Actions in the Implementation section of an Activity, their names should be returned by the GetActions method. The same is true for Conditions in Transitions; the only difference is that you have to use the GetConditions method. When WorkflowRuntime calls an Action, it calls the ExecuteAction method and conveys the name of the Action to it from the scheme. Process object ProcessInstance, a link to WorkflowRuntime that called this Action, and Action Parameter from the scheme are also conveyed to this method. Then, you need to call your code in this method. This is also true for Conditions; the only difference is that the ExecuteCondition method with the Condition's name from the scheme is called; the called method should return true or false.

If you need to call asynchronous methods from Actions (and Conditions) you should add their handlers _asyncActions and _asyncConditions dictionaries, rather than _actions and _conditions. In this case the IsActionAsync and IsConditionAsync methods will return true and the engine will call the ExecuteActionAsync and ExecuteConditionAsync methods. You can use the keyword await in these methods and call asynchronous methods. If you are using asynchronous Actions, then you should use asynchronous versions of methods of the WorkflowRuntime object so that your application enjoys better performance and higher efficiency. For instance, use ExecuteCommandAsync instead of ExecuteCommand and SetStateAsync instead of SetState. Pay attention to the fact, that when calling WorkflowInit.Runtime.ExecuteCommandAsyncyou can pass a CancellationToken object to it. Cancellation token will be passed to the ExecuteActionAsync and ExecuteConditionAsync methods without changes and its handling is solely up to you. You can use the token to cancel long-running operations or to set up timeouts.

In the abovementioned example, the IWorkflowActionProvider implementation code won't change upon adding new Actions or Conditions, and this is good. The class contains four dictionaries that use the name of the Action (or Condition) as a key, and a delegate containing a link to your methods as a value. That's why upon adding a new Action (or Condition), you simply need to register this method in the dictionary under the name you would like to see in Designer. This code does not contain reflection or any other components that may slow things down.

In order for WorkflowRuntime to learn about the provider you created, the provider's instance should be conveyed to WorkflowRuntime upon configuring.

runtime.WithActionProvider(new ActionProvider())

Creation of Action in the scheme

In order to enable the creation of Actions in designer, you should configure WorkflowRuntime in the following way (the configuration example already has this line):

runtime.EnableCodeActions()

Designer

It is quite easy to create Actions (or Conditions) in Designer. Open the CodeActions window by clicking a respective button in the toolbar. Then, create a new line, and specify the name and type - Action or Condition. Open the code editor by clicking Edit Code. Here you can write your code in ะก#; use the Compile button to check whether the code is compiled correctly. You may also edit the list of included namespaces in the Usings field. Usually, CodeActions are stored right in the scheme, and compiled once upon its first loading. However, if attribute IsGlobal is set, CodeAction will be saved in the WorkflowGlobalParameter table (or object), and it will be possible to utilize it in all schemes. Such CodeActions are compiled immediately upon executing of WorkflowRuntime.

If you want to create an asynchronous Action (or Condition) in the scheme, simply tick the checkbox Async beside its name in the CodeActions window in designer. Afterwards, you will be able to use the await keyword in the code. No further actions need to be undertaken in this case.

If you are going to utilize methods from your assemblies in your code, you should notify WorkflowRuntime about them upon configuration:

runtime.RegisterAssemblyForCodeActions(Assembly.GetAssembly(typeof(SomeTypeFromMyAssembly)))

Besides, it is possible to set breakpoints in CodeActions for debugging purposes. To do this, you should first enable debugging upon initialization of WorkflowRuntime:

runtime.CodeActionsDebugOn()

Then, you can set a breakpoint right in the code of CodeAction by writing /break/ (commentary brackets do matter). It is rather convenient because if debugging is disabled, a breakpoint will transform into an ordinary comment. At the same time, if debugging is enabled, a breakpoint will turn into the following code:

if (System.Diagnostics.Debugger.IsAttached) {
    System.Diagnostics.Debugger.Break();
}

A combined approach

You may combine Actions created in IWorkflowProvider with those created in Designer. For instance, you may set complex conditions in IWorkflowProvider, and then call and combine them in CodeActions. To do this, you should be able to call Actions from other Actions.

If you have a link to WorkflowRuntime, you can always get access to IWorkflowActionProvider:

var actionProvider = runtime.ActionProvider;

In order to call Actions created in the scheme, use the following method:

string parameter = string.Empty;
processInstance.ExecuteCodeAction("Action name",runtime,parameter);

In order to call Conditions created in the scheme, use the following method:

string parameter = string.Empty;
bool result = processInstance.ExecuteConditionFromCodeActions("Condition name",runtime,parameter);

Connection with the workflow designer

The list of Actions that you see in the workflow designer (in the Implementation section in an Activity) is formed as a combination of string lists returned by the IWorkflowActionProvider.GetActions method and received directly from the scheme. Elements with type Action are selected from the CodeActions section.

The list of Conditions (Actions that return a bool) that you see in the workflow designer (in the Condition section in a Transition) is formed as a combination of string lists returned by the IWorkflowActionProvider.GetConditions method and received directly from the scheme. Elements with type Condition are selected from the CodeActions section.

Top