Open source workflow engine Workflow Core studies and tutorials

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, RecurIt 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.

1

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.

UseDefaultErrorBehaviorI 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 Sequenceachieved 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.

CompensateWithThe internal implementation CompensateWith, is a CompensateWithpackage.

        /// 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.

1565439224(1)

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

Guess you like

Origin www.cnblogs.com/whuanle/p/11332892.html