工作流引擎之Elsa入门系列教程之一 初始化项目并创建第一个工作流

引子

工作流(Workflow)是对工作流程及其各操作步骤之间业务规则的抽象、概括描述。
为了实现某个业务目标,需要多方参与、按预定规则提交数据时,就可以用到工作流。
通过流程引擎,我们按照流程图,编排一系列的步骤,让数据可以按照一定的规则,一定的顺序,提交给一定的负责人进行处理,实现带有时间轴的数据协作。

目前dotnet平台主流工作流引擎有两个:

轻量级嵌入式工作流引擎。它支持多种持久化方式和并发提供程序,以允许多节点群集,可以编码或者使用json、xml编排工作流。
这个引擎功能比较简单,但不适合处理长期工作流(定时任务类型的),随着执行的次数越来越多,处理速度会越来越慢。
Workflow slow when the count of the execution point more and more #1028

PersistedWorkflow ExecutionPointers exponentially increase in workflow loop. #1030

而且它是异步的,通过webapi启动流程后不能实时返回此次流程中step返回的数据,官方更新速度也不太理想,所以不选择此工作流引擎。


Elsa Core 是一个工作流库,可在任何 .NET Core 应用程序中执行工作流。可以使用代码和可视化工作流设计器来定义工作流。(功能更加全面,附带可视化流程设计器与流程监控页面)

本系列文章选择使用Elsa作为流程引擎,准备介绍此流程引擎的使用与扩展,如何与Abp框架一起使用,集成swagger,一步一步实现一个Demo。

快速开始

我们用vs2022创建一个空的ASP.NET Core Web应用,作为工作流核心服务,包含仪表盘与流程API。
一步一步添加依赖与配置,并启动。后续慢慢改造。

初始化项目

创建一个名为ElsaCore.Server的新项目

 dotnet new web -n "ElsaCore.Server"

进入项目文件夹中为项目安装包

cd ElsaCore.Server
dotnet add package Elsa
dotnet add package Elsa.Activities.Http
dotnet add package Elsa.Activities.Timers
dotnet add package Elsa.Activities.UserTask
dotnet add package Elsa.Activities.Temporal.Quartz
dotnet add package Elsa.Persistence.EntityFramework.SqlServer
dotnet add package Elsa.Server.Api
dotnet add package Elsa.Designer.Components.Web

dotnet add package Microsoft.EntityFrameworkCore.Tools

添加ef tools用于初始化数据库

Elsa.Activities.Temporal.Quartz可以换成Elsa.Activities.Temporal.Hangfire,后续会讲解集成Hangfire和仪表盘。

上面的Activities是Elsa提供的几个活动实现,Http就是通过webapi接口形式的、Timers提供定时任务功能、UserTask提供了用户审批的功能,后续会详细解释,并且还有好多其他的Activities,我们还可以自己实现一个新的。

修改Program.cs

using Elsa;
using Elsa.Persistence.EntityFramework.Core.Extensions;
using Elsa.Activities.UserTask.Extensions;
using Elsa.Persistence.EntityFramework.SqlServer;

var builder = WebApplication.CreateBuilder(args);

// Elsa services.
var elsaSection = builder.Configuration.GetSection("Elsa");
builder.Services.AddElsa(elsa => elsa
                    .UseEntityFrameworkPersistence(ef => ef.UseSqlServer(builder.Configuration.GetConnectionString("Default"), typeof(Program)))
                    .AddConsoleActivities()
                    .AddJavaScriptActivities()
                    .AddUserTaskActivities()
                    .AddHttpActivities(elsaSection.GetSection("Server").Bind)
                    .AddQuartzTemporalActivities()
                    .AddWorkflowsFrom<Program>()
                )
                // Elsa API endpoints.
                .AddElsaApiEndpoints()

                // For Dashboard.
                .AddRazorPages();
var app = builder.Build();

app.UseStaticFiles()// For Dashboard.
    .UseHttpActivities()
    .UseRouting()
    .UseEndpoints(endpoints =>
    {
        // Elsa API Endpoints are implemented as regular ASP.NET Core API controllers.
        endpoints.MapControllers();
        // For Dashboard
        endpoints.MapFallbackToPage("/_Host");
    });
app.Run();

添加appsettings.json配置

BaseUrl的端口号要和launchSettings.json中的一致

  "ConnectionStrings": {
    "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=ElsaServer;Trusted_Connection=True"
  },
  "Elsa": {
    "Server": {
      "BaseUrl": "//localhost:5001"
    }
  }
  

修改launchSettings.json

把launchSettings中的iis profiles删除,端口号改为5001

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "//localhost:5001",
      "sslPort": 5001
    }
  },
  "profiles": {
    "ElsaCore.Server": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "//localhost:5001",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

初始化数据库

首先生成一次项目,然后执行

dotnet ef migrations add init

会自动创建Migrations目录。

然后更新数据库,执行

dotnet ef database update

此时打开SQL Server对象资源管理器可以看到数据库已经初始化完毕。

创建页面

新建目录Pages,创建在该目录下创建一个_Host.cshtml。

@page "/"
@{
    var serverUrl = $"{Request.Scheme}://{Request.Host}";
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Elsa Workflows</title>
    <link rel="icon" type="image/png" sizes="32x32" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-16x16.png">
    <link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/fonts/inter/inter.css">
    <link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/elsa-workflows-studio.css">
    <script src="/_content/Elsa.Designer.Components.Web/monaco-editor/min/vs/loader.js"></script>
    <script type="module" src="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/elsa-workflows-studio.esm.js"></script>
</head>
<body>
<elsa-studio-root server-url="@serverUrl" monaco-lib-path="_content/Elsa.Designer.Components.Web/monaco-editor/min">
    <elsa-studio-dashboard></elsa-studio-dashboard>
</elsa-studio-root>
</body>
</html>

启动项目

运行该项目,打开浏览器访问//localhost:5001/,页面如下所示:

第一个HTTP Endpoint工作流

我们先定义一个简单的工作流,后续会实现启动参数与返回特定格式数据的流程。

定义工作流的方式有两种,使用设计器和代码。设计器定义的好处是可以在运行时动态添加与修改流程,并且是直接在流程图上进行修改,但是只能使用已注册的Activity,如果业务需要自定义Activity,则还是需要先写一些代码。

通过流程设计器定义

新建流程

选择菜单中的Workflow Definitions,进入工作流定义页,点击Create Workflow创建一个新的工作流。

点击Start,然后选择Http里面的HTTP Endpoint创建一个接口用来做为流程的入口。

设置参数并保存

  • Path: /design/hello-world
  • Methods: GET

接下来设置该接口的返回值。在流程的Done节点下点加号,选择HTTP里面的HTTPResponse,设置参数并保存:

  • Content: <h1>Hello World! </h1><p>这是通过设计器实现的流程</p>
  • Content Type: text/html
  • Status Code: OK

设置流程名称,点击右上角的设置按钮,设置Name为hello-world-design,Display Name为hello-world by design

点击右下角的publish发布流程。此时返回到Workflow Definitions中可以看到刚刚定义好的流程。

启动流程

因为hello-world-design这个流程是由HTTP Endpoint作为起点,所以我们可以通过接口来启动该流程。
访问hello-world-design可以看到如下效果

此时我们点击Workflow Instances
可以看到刚刚执行的工作流实例,点击进入可以看到流程执行的详细过程。

使用代码定义

我们通过代码的方式实现上述流程。

新建流程

新建一个Workflows目录用于存放工作流。

创建一个类名为:HelloWorldWorkflow,并实现IWorkflow接口。具体代码如下:

using Elsa.Builders;
using Elsa.Activities.Http;

namespace ElsaCore.Server.Workflows
{
    public class HelloWorldWorkflow : IWorkflow
    {
        public void Build(IWorkflowBuilder builder)
        {
            builder.HttpEndpoint(setup =>
            {
                setup.WithMethod(HttpMethod.Get.Method).WithPath("/code/hello-world");
            })
           
                .Then<WriteHttpResponse>(setup =>
                {
                    setup.WithContentType("text/html")
                    .WithContent("<h1>Hello World! </h1><p>这是通过代码实现的流程</p>")
                    .WithStatusCode(System.Net.HttpStatusCode.OK);
                });
        }
    }
}

因为我们在Program.cs中配置Elsa的时候使用了AddWorkflowsFrom<Program>(),所以会自动扫描目标类所在的程序集下所有实现IWorkflow接口的工作流自动注册。
否则需要调用AddWorkflow<HelloWorldWorkflow>()手动注册流程。

查看流程

启动项目并点击Workflow Registry可以看到我们刚刚创建的流程

点进去可以看到流程图,但因为是代码实现的所以是只读。

启动流程

访问//localhost:5001/code/hello-world即可。

小结

本次我们创建了一个新项目,引入了一些Elsa相关的包,完成了工作流服务+图形化工作流仪表盘。创建了一个简单的工作流,但是这样是远远不够的,我们需要更加复杂的工作流,比如自定义参数、不同参数返回不同结果,模拟一些真实的业务场景,慢慢熟悉此框架,应用到真实的业务场景中,将在后续文章中体现,未完待续…