table of Contents
Open source workflow engine Workflow Core studies and tutorials
A front workflow objects and use
To avoid ambiguity, agreed in advance.
Workflow There are many nodes, a node point becomes step (Step).
1,IWorkflow / IWorkflowBuilder
Workflow Core used to construct a workflow class inheritance IWorkflow
, has the task on behalf of a workflow rules, workflow tasks can indicate the beginning or the Do () method, or other method of obtaining the work flow branches.
IWorkflow the same name has two interfaces:
public interface IWorkflow<TData>
where TData : new()
{
string Id { get; }
int Version { get; }
void Build(IWorkflowBuilder<TData> builder);
}
public interface IWorkflow : IWorkflow<object>
{
}
Id: unique identifier for this workflow;
Version: version of this workflow.
void Build
: Construction of the workflow in this method.
Operation process workflow, data can be transferred. Two delivery methods: using the generic, when the incoming flow will run from the work; using simple object types, produced by a separate step and passed to the next node.
IWorkflowBuilder workflow objects, building the workflow having a logic rules. Can build complex, having a circulating, determines the workflow rules, or in parallel or asynchronous processing workflow tasks.
A simple workflow rules:
public class DeferSampleWorkflow : IWorkflow
{
public string Id => "DeferSampleWorkflow";
public int Version => 1;
public void Build(IWorkflowBuilder<object> builder)
{
builder
.StartWith(context =>
{
// 开始工作流任务
Console.WriteLine("Workflow started");
return ExecutionResult.Next();
})
.Then<SleepStep>()
.Input(step => step.Period, data => TimeSpan.FromSeconds(20))
.Then(context =>
{
Console.WriteLine("workflow complete");
return ExecutionResult.Next();
});
}
}
2,EndWorkflow
This object represents the current workflow task has been completed, you can indicate the completion of the main branch of the workflow or workflow tasks.
/// Ends the workflow and marks it as complete
IStepBuilder<TData, TStepBody> EndWorkflow();
Because the workflow is work independently, each branch has its life cycle may occur, each branch of the workflow.
3, container
ForEach
, While
, If
, When
, Schedule
, Recur
It is the step of the container . ReturnIContainerStepBuilder<TData, Schedule, TStepBody>
Parallel, Saga a container step , return IStepBuilder<TData, Sequence>
.
ForEach, While, If, When, Schedule, Recur return types of interfaces:
public interface IContainerStepBuilder<TData, TStepBody, TReturnStep>
where TStepBody : IStepBody
where TReturnStep : IStepBody
{
/// The block of steps to execute
IStepBuilder<TData, TReturnStep> Do(Action<IWorkflowBuilder<TData>> builder);
Parallel、Saga :
/// Execute multiple blocks of steps in parallel
IParallelStepBuilder<TData, Sequence> Parallel();
/// Execute a sequence of steps in a container
IStepBuilder<TData, Sequence> Saga(Action<IWorkflowBuilder<TData>> builder);
In other words, ForEach, While, If, When, Schedule, Recur is a real container.
According to my understanding, it inherited IContainerStepBuilder
, is a vessel, a step in a process / container; author named because Workflow Core Interface obviously expressed This a container
.
Because it contains a set of operations, which can be a step comprising a process, the process consists of a series of operations composition, which is linear, sequential . Inside is a workflow (Workflow).
And Parllel, Saga, points corresponding to the container step.
More intuitive understanding that the circuit is inherited IContainerStepBuilder series equipment container , is sequential;
Parllel is a container parallel circuit / device , both as a switch, so that a plurality of circuits and circuit into the flow, but also contains the electrical circuits. Which can generate a plurality of workflows, multi-branched, unsynchronized and independent.
The interface from the implementation point of view, the ForEach, the While, the If, the When, Schedule, Recur, Parllel have achieved Do()
method, and Saga did not materialize.
About Saga, described later.
4, the step of the workflow point
Implement the interface as follows:
IStepBuilder<TData, TStep> StartWith<TStep>(Action<IStepBuilder<TData, TStep>> stepSetup = null) where TStep : IStepBody;
IStepBuilder<TData, InlineStepBody> StartWith(Func<IStepExecutionContext, ExecutionResult> body);
IStepBuilder<TData, ActionStepBody> StartWith(Action<IStepExecutionContext> body);
IEnumerable<WorkflowStep> GetUpstreamSteps(int id);
IWorkflowBuilder<TData> UseDefaultErrorBehavior(WorkflowErrorHandling behavior, TimeSpan? retryInterval = null);
Method name | Explanation |
---|---|
startsWith | Start a task, you must call this method |
GetUpstreamSteps | Obtaining a step (StepBody) of the ID |
UseDefaultErrorBehavior | Unknown |
StepBody is a node, IStepBuilder build a node, only through StartWith, to start a workflow, a branch of asynchronous tasks.
UseDefaultErrorBehavior
I did not use to, not nonsense. Seemingly associated with the transaction, when abnormality occurs in a step point may be terminated, retries and the like.
Two, IStepBuilder node
IStepBuilder represents a node, or a container, which may contain other operations, such as a parallel, asynchronous, circulation.
1, setting properties
Name: Set the name of this step point;
ID: a unique identifier of the step points.
/// Specifies a display name for the step
IStepBuilder<TData, TStepBody> Name(string name);
/// Specifies a custom Id to reference this step
IStepBuilder<TData, TStepBody> Id(string id);
2, setting data
As mentioned earlier, each step of the workflow transfer point data in two ways.
TData (generics) is a workflow, with the spread of the data, the object will survive throughout the workflow.
For example Mydata
class RecurSampleWorkflow : IWorkflow<MyData>
{
public string Id => "recur-sample";
public int Version => 1;
public void Build(IWorkflowBuilder<MyData> builder)
{
...
}
}
public class MyData
{
public int Counter { get; set; }
}
3,Input / Output
The step of setting data to the current point (StepBody), may also be provided TData data.
Two types of data: Each step can have many points fields, properties and methods; workflow transfer TData.
Input, Output data of these specific settings.
IStepBuilder<TData, TStepBody> Input<TInput>(Expression<Func<TStepBody, TInput>> stepProperty, Expression<Func<TData, TInput>> value);
IStepBuilder<TData, TStepBody> Input<TInput>(Expression<Func<TStepBody, TInput>> stepProperty, Expression<Func<TData, IStepExecutionContext, TInput>> value);
IStepBuilder<TData, TStepBody> Input(Action<TStepBody, TData> action);
IStepBuilder<TData, TStepBody> Output<TOutput>(Expression<Func<TData, TOutput>> dataProperty, Expression<Func<TStepBody, object>> value);
Third, the workflow node and logical operations
Container operations
1,Saga
For performing a series of operations in the container.
/// Execute a sequence of steps in a container
IStepBuilder<TData, Sequence> Saga(Action<IWorkflowBuilder<TData>> builder);
Although the explanatory notes "used to perform a series of operations in the container," but in fact it's not really a "container."
Because it does not inherit IContainerStepBuilder
, nor achieved Do()
.
But it returns Sequence
achieved ContainerStepBody
.
If the container is equivalent to a real lake a long rivers (can accommodate and storage), while Saga might just be a name for a river, rather than specific lakes.
Or that static void Main(string[] args)
there are too many of the code, a new method thereof, the part of the code into it. You can not put all the code written in a way in, right? Then create a class, the code into a plurality of portions, into different methods, to enhance readability. Nature is not changed.
Saga transaction processing can be used, like for retry or rollback operation. Described later.
Ordinary node
1,Then
Used to create the next node, create a common node. It may be a node of the main workflow (outermost layer), or as a loop, conditional node in the node, a node in the node.
IStepBuilder<TData, TStep> Then<TStep>(Action<IStepBuilder<TData, TStep>> stepSetup = null) where TStep : IStepBody;
IStepBuilder<TData, TStep> Then<TStep>(IStepBuilder<TData, TStep> newStep) where TStep : IStepBody;
IStepBuilder<TData, InlineStepBody> Then(Func<IStepExecutionContext, ExecutionResult> body);
IStepBuilder<TData, ActionStepBody> Then(Action<IStepExecutionContext> body);
2,Attach
Then as an ordinary node, performed sequentially. The type of the operation object is, StepBody.
Attach also a common node, no special significance, specified by id to be executed StepBody. It can be used as jump control process.
The equivalent of the goto statement.
/// Specify the next step in the workflow by Id
IStepBuilder<TData, TStepBody> Attach(string id);
event
1,WaitFor
Used to define the event, the current node as the event node, and then hang in the background, then the workflow to the next node. Before work flow stops, you can specify the identifier (Id) trigger events. In a workflow, the identifier for each event is unique.
IStepBuilder<TData, WaitFor> WaitFor(string eventName, Expression<Func<TData, string>> eventKey, Expression<Func<TData, DateTime>> effectiveDate = null, Expression<Func<TData, bool>> cancelCondition = null);
IStepBuilder<TData, WaitFor> WaitFor(string eventName, Expression<Func<TData, IStepExecutionContext, string>> eventKey, Expression<Func<TData, DateTime>> effectiveDate = null, Expression<Func<TData, bool>> cancelCondition = null);
Conditional body and loop
1,End
Meaning it should be the end of the run a node.
If you use the When, the equivalent break.
IStepBuilder<TData, TStep> End<TStep>(string name) where TStep : IStepBody;
Examples of use
builder
.StartWith<RandomOutput>(x => x.Name("Random Step"))
.When(0)
.Then<TaskA>()
.Then<TaskB>()
.End<RandomOutput>("Random Step")
.When(1)
.Then<TaskC>()
.Then<TaskD>()
.End<RandomOutput>("Random Step");
2,CancelCondition
Under a condition prematurely cancels the execution of this step.
It should be equivalent to contiune.
/// Prematurely cancel the execution of this step on a condition
IStepBuilder<TData, TStepBody> CancelCondition(Expression<Func<TData, bool>> cancelCondition, bool proceedAfterCancel = false);
Or multi-threaded asynchronous node
1,Delay
Delayed execution, so that the current node to delay execution. Not blocking the current running workflow. Delay followed node that this node extended run. Can be understood as an asynchronous, the workflow is finished without waiting for this node will execute the next node / step directly.
/// Wait for a specified period
IStepBuilder<TData, Delay> Delay(Expression<Func<TData, TimeSpan>> period);
2,Schedule
Scheduled execution. The current node a time, will be performed after a period of time. Schedule does not block workflow.
Schedule is non-blocking, the workflow does not wait Schedule finished, perform a node directly under / step.
/// Schedule a block of steps to execute in parallel sometime in the future
IContainerStepBuilder<TData, Schedule, TStepBody> Schedule(Expression<Func<TData, TimeSpan>> time);
example
builder
.StartWith(context => Console.WriteLine("Hello"))
.Schedule(data => TimeSpan.FromSeconds(5)).Do(schedule => schedule
.StartWith(context => Console.WriteLine("Doing scheduled tasks"))
)
.Then(context => Console.WriteLine("Doing normal tasks"));
3,Recur
Repeat for a node until the condition does not match.
Recur is non-blocking, not wait Rezur workflow is finished, it performs the next node / step directly.
/// Schedule a block of steps to execute in parallel sometime in the future at a recurring interval
IContainerStepBuilder<TData, Recur, TStepBody> Recur(Expression<Func<TData, TimeSpan>> interval, Expression<Func<TData, bool>> until);
Operation for the transaction
It corresponds to the transaction in the database, when an exception occurs during the process some of the steps to perform certain operations.
E.g:
builder
.StartWith(context => Console.WriteLine("Begin"))
.Saga(saga => saga
.StartWith<Task1>()
.CompensateWith<UndoTask1>()
.Then<Task2>()
.CompensateWith<UndoTask2>()
.Then<Task3>()
.CompensateWith<UndoTask3>()
)
.OnError(Models.WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(5))
.Then(context => Console.WriteLine("End"));
1,CompensateWith
If this step unhandled exception, revoking step; If an exception occurs, it is performed.
Plan B can be used as nodes. When a node to perform tasks without problems, CompensateWith will not run; if a node error occurs, it will perform according to certain requirements CompensateWith.
/// Undo step if unhandled exception is thrown by this step
IStepBuilder<TData, TStepBody> CompensateWith<TStep>(Action<IStepBuilder<TData, TStep>> stepSetup = null) where TStep : IStepBody;
IStepBuilder<TData, TStepBody> CompensateWith(Func<IStepExecutionContext, ExecutionResult> body);
IStepBuilder<TData, TStepBody> CompensateWith(Action<IStepExecutionContext> body);
2,CompensateWithSequence
If this step unhandled exception, revoking step; If an exception occurs, it is performed. The difference between CompensateWith is that the former is the parameter passed Func, which is the Action.
CompensateWith
The internal implementation CompensateWith
, is a CompensateWith
package.
/// Undo step if unhandled exception is thrown by this step
IStepBuilder<TData, TStepBody> CompensateWithSequence(Action<IWorkflowBuilder<TData>> builder);
3,OnError
For transactional operations, it said in a rollback if an error occurs, set the time and so on. Generally used with Saga.
OnError is blocked.
/// Configure the behavior when this step throws an unhandled exception
IStepBuilder<TData, TStepBody> OnError(WorkflowErrorHandling behavior, TimeSpan? retryInterval = null);
OnError can be captured in a container, a node is abnormal, and the rollback operation. If instead of directly on the container nodes rollback can occur, and then the next node. If the role of the container, it can let the container be re-run, a series of operations.
You can use the OnError When, While peers containers together, but with their own circulatory function, the code will use logical transaction becomes strange.
Saga is not conditional, no cycle is itself a simple bag is a container of nodes. Thus use as a container Saga transaction operations, very suitable, rollback, and a series of retry operations.
Fourth, conditions or switch
Iteration
1,ForEach
Iteration, it can be said that cycle. Internal use IEnumerable to achieve.
The difference is in C # Foreach, C # is used for iterative data;
ForEach workflow can be used to determine the number of elements, the number of loops to be identified.
ForEach is blocked.
/// Execute a block of steps, once for each item in a collection in a parallel foreach
IContainerStepBuilder<TData, Foreach, Foreach> ForEach(Expression<Func<TData, IEnumerable>> collection);
Examples
builder
.StartWith<SayHello>()
.ForEach(data => new List<int>() { 1, 2, 3, 4 })
.Do(x => x
.StartWith<DisplayContext>()
.Input(step => step.Item, (data, context) => context.Item)
.Then<DoSomething>())
.Then<SayGoodbye>();
The final will cycle five times.
Conditional
1,When
Conditional, conditions are true.
When it is blocked.
When a node can capture the data transfer (non TData).
/// Configure an outcome for this step, then wire it to another step
[Obsolete]
IStepOutcomeBuilder<TData> When(object outcomeValue, string label = null);
/// Configure an outcome for this step, then wire it to a sequence
IContainerStepBuilder<TData, When, OutcomeSwitch> When(Expression<Func<TData, object>> outcomeValue, string label = null);
Previous methods e.g.
When(0)
, Captures return ExecutionResult.Outcome(value);
the value, it is determined whether or not equal. However, this approach is outdated.
You need to use expressions to judge. E.g
.When(data => 1)
.When(data => data.value==1)
2,While
Conditional, conditions are true. And When there are differences, When you can capture ExecutionResult.Outcome(value);
.
While it is blocked.
/// Repeat a block of steps until a condition becomes true
IContainerStepBuilder<TData, While, While> While(Expression<Func<TData, bool>> condition);
Examples
builder
.StartWith<SayHello>()
.While(data => data.Counter < 3)
.Do(x => x
.StartWith<DoSomething>()
.Then<IncrementStep>()
.Input(step => step.Value1, data => data.Counter)
.Output(data => data.Counter, step => step.Value2))
.Then<SayGoodbye>();
3,If
Conditions judge eligibility.
If it is blocked.
/// Execute a block of steps if a condition is true
IContainerStepBuilder<TData, If, If> If(Expression<Func<TData, bool>> condition);
Difference When, While, If is, When, While whether the condition is true, If an expression is true.
In essence, it is the difference in language, regardless of the code logic.
With genuine When / While, conditional expression is determined by If.
Concurrent node
1,Parallel
Parallel tasks. As a container, you can set up multiple sets of tasks on the inside, these tasks will at the same time, to run concurrently.
Parallel is blocked.
/// Execute multiple blocks of steps in parallel
IParallelStepBuilder<TData, Sequence> Parallel();
Example:
.StartWith<SayHello>()
.Parallel()
.Do(then =>
then.StartWith<PrintMessage>()
.Input(step => step.Message, data => "Item 1.1")
.Then<PrintMessage>()
.Input(step => step.Message, data => "Item 1.2"))
.Do(then =>
then.StartWith<PrintMessage>()
.Input(step => step.Message, data => "Item 2.1")
.Then<PrintMessage>()
.Input(step => step.Message, data => "Item 2.2")
.Then<PrintMessage>()
.Input(step => step.Message, data => "Item 2.3"))
.Do(then =>
then.StartWith<PrintMessage>()
.Input(step => step.Message, data => "Item 3.1")
.Then<PrintMessage>()
.Input(step => step.Message, data => "Item 3.2"))
.Join()
.Then<SayGoodbye>();
Three Do, on behalf of three parallel tasks. Do three parallel, code within the Do, will be executed sequentially.
Paeallel of Do:
public interface IParallelStepBuilder<TData, TStepBody>
where TStepBody : IStepBody
{
IParallelStepBuilder<TData, TStepBody> Do(Action<IWorkflowBuilder<TData>> builder);
IStepBuilder<TData, Sequence> Join();
}
Compared ForEach, When, While, If, in addition to Do, as well as Join methods.
For other types, nodes, Do building node directly.
Parallel to it, Do collection tasks, Join eventually need to build and run the task node.
V. Other
Written long does not look good, other content to compress.
And transmitting data dependency injection
Workflow Core support point for each step dependency injection.
Support for data persistence
Workflow Core support workflow storage will be built into the database for later recall again.
支持 Sql Server、Mysql、SQLite、PostgreSQL、Redis、MongoDB、AWS、Azure、
Elasticsearch、RabbitMQ... ....
Support dynamic invocation and dynamically generated workflow
You can build workflow through C # code, or build workflow by Json, Yaml dynamic.
Visual designer can use, and the task generation logic configuration files, and then transfer dynamically create the workflow using the Workflow Core Dynamics.
Limited space, not repeat them.
Interested please pay attention Core Workflow: https://github.com/danielgerlag/workflow-core