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.

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()

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:

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:

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:

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.