LightWorkFlowManager lightweight work process management library

This article will recommend my team’s open source LightWorkFlowManager lightweight work process management library, which is suitable for any application logic that needs to execute work processes. It can easily piece together multiple work processes, and automatically integrates retry and failure processing, as well as logs and Reporting function

This LightWorkFlowManager lightweight work process management library is an open source library that my team uses the most friendly MIT license on GitHub. Please see https://github.com/dotnet-campus/LightWorkFlowManager

This LightWorkFlowManager lightweight work process management library has been running on my team’s official product application for half a year now, and it is relatively stable. If you have any questions during use, you are welcome to create new Issues on GitHub to provide feedback.

There are many existing work process management systems in the world, so why reinvent one? No reason, I made it myself and it works fine. This LightWorkFlowManager library lacks many functions compared with other work process management libraries, but it is better than being lightweight. And the most important selling point of LightWorkFlowManager is that it is friendly to debugging. It is suitable for use in logical situations with relatively good complexity, and can effectively reduce the additional complexity brought by the introduction of the work process management module itself.

Many existing full-featured workflow management libraries are available on ASP.NET Core. However, my requirement here is to use it on the client application framework, and in most cases it runs on the user’s device. Many service dependencies themselves do not exist. Although many existing fully functional work process management libraries can be configured to run on a single machine, doing so is not the recommended use of the library most of the time and will not be as smooth to use. To this end, my team developed LightWorkFlowManager, a lightweight work process management library. This library can be easily run on client frameworks, such as WPF applications, WinForms applications, and MAUI applications. At the same time, LightWorkFlowManager also takes into account the ASP.NET Core service itself, especially the dependency injection part, which can be connected to the default service dependency injection.

I’ll give you an example to show you where this LightWorkFlowManager library is applicable. Suppose I get a development task, which is to parse PPT and take out the pictures in the PPT file. Breaking down this task will include the following three steps: Step 1, obtain the PPT file. Step 2: Call an existing helper method to obtain the image in the PPT file. Step 3: Process the image files in the obtained PPT file

In step one, there are multiple ways to obtain PPT files. For example, some are downloaded from online CDN servers, some are obtained from disk paths, and some are dragged in by users. These different input acquisition methods come from different entrances of each business

In step three, according to different business situations, it is necessary to upload the image files to the backend server, or some businesses need to display these images on the interface, etc.

As you can see from the above description, what I got is a requirement that can be scattered in various businesses. It seems that there are many different but logically similar requirements. Logical similarity means that steps can be forcibly classified for constraints. Logical duplication lies in the fact that many logics are completely reusable, such as logging and retry mechanisms. The retry mechanism can be used in almost all the above three steps. Whether you are a novice developer or an experienced developer, you know that whether you are downloading content from the Internet or uploading files, you need to go through unstable network transmission. At this time, the network transmission fails. Executing retry logic is a very common processing logic

Assuming that there is no assistance from the LightWorkFlowManager library, a possible implementation design is as follows: design a unified entrance, and pass in parameters through the entrance to determine and execute different step processing logic, as shown below

4803824368939687d04276f153a47330.jpeg

The problem can be easily seen from the above figure. The entire logic complexity will be relatively high. There are multiple original business entrances. Then enter the unified entrance in the PPT parsing task, and then enter the unified entrance in the PPT parsing task. Distribute logic to different processing logic based on parameters. The cyclomatic complexity of the entire logic will be relatively high. At the same time, if more business requirements are added in step one or step three, the entire logic will become more complicated. On the other hand, such complex logic will make it difficult to read and debug the code. For example, it is difficult to know the entire call chain very clearly. The call chain may be too complicated, such as it is difficult to execute repeatedly during debugging.

With the assistance of the LightWorkFlowManager library, you can modify the logic design of the code, that is, no longer distribute unified entrances, but let the respective businesses connect the corresponding logic in series, so that a logic can be straightened out and the PPT parsing task module can be maintained. of simplicity. At the same time, the business side can also have a clearer understanding of the details of the PPT parsing task.

bf98a02da452c79dc5943815691298b0.jpeg

You can see the modified code, which allows every logic to flow smoothly and there are no logical circles. You can reduce the logic complexity of already complex logic. A series of smooth logic will be more friendly to code reading and debugging. And with the help of the LightWorkFlowManager library, repeated code logic can be minimized, such as reducing repeated logging and failure retry logic.

After understanding the applicable design of the LightWorkFlowManager library, I will introduce to you the specific usage method.

According to dotnet’s practice, install the NuGet library first, please see https://www.nuget.org/packages/dotnetCampus.LightWorkFlowManager

Before starting to introduce how to use it, we need to introduce the concepts in LightWorkFlowManager. The MessageWorkerManager type is defined in LightWorkFlowManager and is used as a worker manager. Its purpose is to connect various workers in series and run them. This MessageWorkerManager is the entry type of LightWorkFlowManager.

The basic type of worker defined in LightWorkFlowManager is the MessageWorker type. All workers customized by business developers need to directly or indirectly inherit the MessageWorker type.

After understanding the basic concepts, let’s start writing a simple logic as an example

How to use

1. Create a MessageWorkerManager object. Create a MessageWorkerManager worker manager as a framework to host workers. Please create an independent MessageWorkerManager object for each separate task.

//A TaskId number for each task
            string taskId = Guid.NewGuid().ToString();
            //Tasks of the same type use the same name, for example, this is a task for PPT parsing
            string taskName = "PPT analysis";
            // Provide container
            IServiceScope serviceScope = serviceProvider.CreateScope();

            var workerManager = new MessageWorkerManager(taskId, taskName, serviceScope);

2. Define the Worker worker. The following example code defines a FooWorker worker that simulates business code. The business code for execution can be written in the DoInnerAsync method overridden by FooWorker. The InputType and OutputType of the following code are the input and output types of the worker respectively. The output object of each worker will be automatically added to the process context cache of MessageWorkerManager.

record InputType();

recordOutputType();

class FooWorker : MessageWorker<InputType, OutputType>
{
    protected override ValueTask<WorkerResult<OutputType>> DoInnerAsync(InputType input)
    {
        ...
    }
}

3. Execute Worker. After completing the definition of the type, let’s demonstrate how to run FooWorker through MessageWorkerManager

var result = await workerManager
                .GetWorker<FooWorker>()
                .RunAsync();

The above code can also be abbreviated as the following code

var result = await workerManager.RunWorker<FooWorker>();

The premise for the above code to run is to first inject the InputType type object required by FooWorker. The following will tell more details about the parameter section of the injection worker.

Mechanisms and functions

Worker parameters

Worker parameters are read through the MessageWorkerManager worker-managed IWorkerContext context. The returnable value type of each worker is automatically set into the IWorkerContext context. In this way, the output of the previous Worker can be automatically used as the input of the next Worker.

In each worker, context information can be set through SetContext

When starting to execute the worker, you can also manually set the input parameters, as in the following example

//Example 1: Get the worker first, and then assign it to the worker’s execution method

            await Manager
                .GetWorker<FooWorker>()
                .RunAsync(new InputType());

 //Example 2: Set parameters through SetContext and then execute the worker
            await Manager
                .SetContext(new InputType())
                .RunWorker<FooWorker>();

If the input and output parameters between some workers need to be converted, you can also pass in the conversion delegate to SetContext for parameter conversion.

//The following example converts the Foo1Type type in the current context into the Foo2Type parameter required by FooWorker
           await Manager
                .SetContext((Foo1Type foo1) => ConvertFoo1ToFoo2(foo1))
                .RunWorker<FooWorker>();

Abnormal interruption and retry

Each Worker can return a return value of type WorkerResult, which can tell the framework layer whether the current Worker is executed successfully. When execution fails, an error code can be assigned to facilitate positioning, debugging or output. When execution fails, you can return to inform the framework layer whether it needs to retry. By default, the framework will automatically retry.

There are two ways to interrupt subsequent worker executions:

Method 1: By returning a WorkerResult with a status of failure. Once the status of the work manager is IsFail, all workers that do not mark CanRunWhenFail as true will be blocked from executing. In other words, except for the Worker workers that need to be executed continuously in a successful or failed state, other workers will not be executed, including the delegation transformation in SetContext.

Method 2: By throwing exceptions, the subsequent logic can be blown up and no longer executed through exceptions in dotnet.

The above two methods are recommended by the framework. The framework design preference is to use method 1 to interrupt execution if performance is important. If there is complex business logic and a large amount of business logic is interspersed outside the work process, you can easily interrupt it through method 2.

Execute other Worker workers in Worker

In a Worker, other Workers can be executed, so that branch logic can be implemented more freely. Matryoshka decides to execute the worker.

Examples are as follows:

Assume there is a Worker2 worker defined as follows:

class Worker2 : MessageWorker<InputType, OutputType>
    {
        protected override ValueTask<WorkerResult<OutputType>> DoInnerAsync(InputType input)
        {
            return SuccessTask(new OutputType());
        }
    }

    recordInputType();

    record OutputType();

There is another Worker1 worker, and Worker2 worker can be executed inside Worker1:

class Worker1 : MessageWorkerBase
    {
        public override async ValueTask<WorkerResult> Do(IWorkerContext context)
        {
            await Manager
                .GetWorker<Worker2>()
                .RunAsync(new InputType());

            return WorkerResult.Success();
        }
    }

Delegated Worker

There is some very small and light logic that I want to add to the work process, but I don’t want to define a separate worker for this. You can try a delegate worker, such as the following code example

var delegateMessageWorker = new DelegateMessageWorker(_ =>
            {
                //Write the business content of the delegate worker here
            });

            var result = await messageWorkerManager.RunWorker(delegateMessageWorker);

If you don’t even want to create a DelegateMessageWorker, you can also directly use the RunWorker method of MessageWorkerManager to pass in the delegate, as shown in the following code example

await messageWorkerManager.RunWorker((IWorkerContext context) =>
            {
                //Write the business content of the delegate worker here
            });