C#工作流——elsa-workflows

 

介绍

Elsa Workflows 是一个功能强大且灵活的执行引擎,封装为一组开源 .NET 库,旨在为 .NET 应用程序注入工作流功能。 借助 Elsa,开发人员可以将逻辑直接编织到他们的系统中,从而增强功能和自动化,并与应用程序的核心功能无缝对齐。

Elsa 中的工作流程可以通过两种方式定义:

  • 以编程方式:通过编写 .NET 代码,开发人员可以定义针对特定业务需求量身定制的复杂工作流。
  • 可视化:使用内置设计器,非开发人员或喜欢可视化方法的人可以轻松创建和修改工作流。

Elsa 的多功能性使其适用于广泛的应用,从简单的任务自动化到复杂的业务流程管理。 其模块化架构允许与现有系统轻松集成,使其成为希望为其项目添加工作流功能的开发人员的首选。

 

elsa-workflows官网

esla-workflows项目


开始

踏上您的 Elsa Workflows 之旅!无论您是工作流程新手还是经验丰富的开发人员,我们都有资源帮助您将 Elsa 无缝集成到您的项目中。

安装

从我们的分步指南开始,设置您的系统并安装必要的库。

活动库

深入了解各种活动库,发现工作流程的构建块。

概念

掌握 Elsa 背后的基本概念并了解其强大的功能。

指南

卷起袖子,通过动手指南和教程学习。


Elsa 工作流程概述

Elsa Workflows 不仅仅是另一个工作流引擎;它是一种强大而灵活的解决方案,旨在为您的 .NET 应用程序注入活力。借助 Elsa,您可以对简单和复杂的业务流程进行建模,确保您的应用程序不仅功能强大,而且高效且用户友好。

为什么选Elsa-Workflow?

  • 灵活性:无论您是想自动化简单的任务还是设计复杂的多步骤业务流程,Elsa 都能满足您的需求。其模块化设计确保您可以对其进行定制以满足您的确切需求。

  • 视觉和程序化设计:使用Elsa,您不必在编码和设计之间做出选择。您可以通过编程方式定义工作流以提高精度,也可以使用直观的可视化设计器进行快速开发和协作。

  • 集成就绪:Elsa 旨在与他人很好地玩耍。无论您是与第三方服务还是现有系统集成,Elsa 都能轻松实现。

  • 开源:作为开源,Elsa 受益于不断增强其功能的贡献者社区。您将获得一个不仅功能强大而且符合最新行业标准的工具。

主要特点

  • 活动库:一组丰富的现成活动,可作为工作流的构建基块。

  • 触发器:根据特定事件或条件自动启动工作流。

  • 长期和短期运行的工作流程:无论您是需要运行数天、等待用户输入的工作流程,还是在几毫秒内完成的工作流程,Elsa 都可以处理。

  • 可扩展性:需要独特的东西吗?Elsa 被设计为可扩展的,允许您添加自定义活动或与其他系统无缝集成。

可能的用例

  • 业务流程自动化:简化订单处理、HR 入职或内容审批等业务流程

  • 任务自动化:自动执行从数据输入到报告生成的重复性任务

  • 集成工作流:连接不同的系统并确保它们之间的数据顺畅流动。

  • 警报和监控:设置工作流以监控系统并自动发送警报或采取纠正措施。

深入了解文档,了解 Elsa Workflows 提供的所有功能,并开始创建动态、工作流驱动的应用程序的旅程!


使用 elsa-workflows程的好处

elsa-workflows 带来了许多优势,使其成为开发人员和企业的首选。这就是为什么您应该考虑 Elsa 来满足您的工作流程需求:

  • 快速开发:借助 Elsa 的可视化设计器和广泛的活动库,您可以在创纪录的时间内设计和部署工作流,从而加快开发周期。

  • 可扩展性:Elsa 基于 .NET 构建,具有固有的可扩展性,确保随着业务的增长,工作流可以轻松处理增加的负载。

  • 成本效益:使用 Elsa 实现流程自动化可以通过减少人工工作、最大限度地减少错误和优化资源使用来节省大量成本。

  • 灵活性:Elsa 的程序化和视觉设计的双重方法意味着您可以制作适合您确切要求的工作流程,无论多么复杂。

  • 开源优势:作为开源,Elsa 由开发人员社区不断改进。这可确保您始终能够访问最新功能和最佳实践。

  • 无缝集成:Elsa 旨在与其他系统顺利集成,无论是数据库、第三方服务还是自定义应用程序,确保生态系统的凝聚力。

  • 增强的用户体验:自动化和优化的流程可以缩短响应时间、一致的结果并改善整体用户体验。

  • 强大的安全性:借助内置的身份验证和授权功能,Elsa 可确保您的工作流程和数据保持安全。

  • 透明的监控和日志记录:利用Elsa的监控功能密切关注您的工作流程。跟踪进度、诊断问题并确保一切顺利进行。

  • 社区与支持:加入一个蓬勃发展的艾尔莎爱好者社区。无论您是有疑问、需要指导,还是想分享您的专业知识,Elsa 社区都随时为您服务。

利用 Elsa Workflows 的强大功能,将您的应用程序提升到效率和用户满意度的新高度!


深入了解 Elsa 的编程 API

elsa-workflows 提供了一个编程 API,该 API 从 Windows Workflows Foundation 4 中汲取灵感。此 API 为开发人员提供了一个强大的工具集,可以精确、灵活地定义工作流。

顺序工作流

顺序工作流允许您定义按特定顺序执行的一系列活动。下面是一个简单的示例,提示用户输入其名称,然后显示它:

// Define a workflow variable to capture the output of the ReadLine activity.
var nameVariable = new Variable<string>();

// Define a simple sequential workflow:
var workflow = new Sequence
{
    // Register the name variable.
    Variables = { nameVariable }, 
    
    // Setup the sequence of activities to run.
    Activities =
    {
        new WriteLine("Please tell me your name:"), 
        new ReadLine(nameVariable),
        new WriteLine(context => $"Nice to meet you, {nameVariable.Get(context)}!")
    }
};

输出

流程图工作流程

对于更复杂的工作流程,Elsa 支持活动。 这些允许您将工作流定义为相互关联的活动的图形,从而提供更大的灵活性:Flowchart

public class BraidedWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder workflow)
    {
        var writeLine1 = new WriteLine("WriteLine1");
        var writeLine2 = new WriteLine("WriteLine2");
        var writeLine3 = new WriteLine("WriteLine3");
        var writeLine4 = new WriteLine("WriteLine4");
        var writeLine5 = new WriteLine("WriteLine5");
        var writeLine6 = new WriteLine("WriteLine6");
        var writeLine7 = new WriteLine("WriteLine7");

        workflow.Root = new Flowchart
        {
            Start = writeLine1,
            
            Activities =
            {
                writeLine1,
                writeLine2,
                writeLine3,
                writeLine4,
                writeLine5,
                writeLine6,
                writeLine7,
            },

            Connections =
            {
                new Connection(writeLine1, writeLine2),
                new Connection(writeLine1, writeLine3),

                new Connection(writeLine2, writeLine4),
                new Connection(writeLine2, writeLine5),

                new Connection(writeLine3, writeLine5),
                new Connection(writeLine3, writeLine6),

                new Connection(writeLine4, writeLine7),
                new Connection(writeLine5, writeLine7),
                new Connection(writeLine6, writeLine7),
            }
        };
    }
}

可视化后,流程图工作流显示为:

设计师

可视化设计器

对于那些喜欢可视化方法或希望与非开发人员合作的人,Elsa 提供了一个直观的设计器:

设计师

在 .NET 项目中释放工作流的强大功能

通过将工作流集成到您的应用程序中,您可以打开通往无数可能性的大门。 从轻松更新业务逻辑和编排微服务,到处理重复性任务、数据处理和消息处理,潜力巨大。 有了艾尔莎,你只受限于你的想象力!


最新消息

Elsa 3 代表了其前身 Elsa 2 的巨大飞跃。这个版本不仅仅是一个升级;这是一次彻底的重写,带来了大量的改进、新功能和架构变化。以下是 Elsa 3 带来的全面概述:

核心增强功能

  • .NET 6 面向:Elsa 3 面向未来,面向 .NET 6 及更高版本。

  • 架构清晰度:核心库、管理库和运行时库之间有明显的区别。这种分离确保了增强的灵活性和集成能力。

  • 减少依赖性:由于依赖性减少,将 Elsa 集成到您现有的应用程序中现在比以往任何时候都更加无缝。

设计器和编程模型

  • 改进的视觉设计器:体验最先进的设计器,配备拖放、多选、撤消、重做、复制和粘贴等功能。

  • 创新的编程模型:借助新的编程模型,从代码中制作工作流和开发自定义活动从未如此简单。

  • 多样化的图表支持:除了传统的流程图和序列图之外,Elsa 3 为未来对状态机和 BPMN 2.0 图的支持铺平了道路。

执行和运行时

  • 基于队列的调度器:Elsa 3 采用基于队列的工作流调度器,从以前版本的基于堆栈的方法过渡而来。此更改促进了广度优先的执行。

  • 并行活动执行:现在可以使用 和 活动类型并发运行活动。TaskJob

  • 多功能工作流运行时:虽然默认运行时是基于数据库的,但 Elsa 3 引入了对使用 Proto.Actor 的分布式运行时的支持,从而实现了跨多个节点的无锁工作流执行。

  • 中间件管道:已为工作流和活动执行实现中间件管道体系结构。

持久性和安全性

  • 灵活的持久性:Elsa 3 中简化的持久性抽象允许您从一系列持久性技术中进行选择。无论是 SQL Server、MongoDB 还是 Elasticsearch,Elsa 3 都能满足您的需求。

  • 受保护的 API 端点:默认情况下,API 端点现在是安全的。您可以灵活地使用 JWT 令牌、API 密钥配置身份验证,甚至可以选择不进行身份验证。

  • 非持久性活动数据:活动输入和输出默认为非持久性,确保数据安全。但是,如果需要持久性,则可以使用工作流变量来捕获和存储数据。

工作流上下文

  • 增强的工作流上下文:Elsa 3 引入了多功能的工作流上下文支持,允许为每个工作流配置多个工作流上下文提供程序。

特征

Elsa Workflows 是一个强大的平台,旨在简化工作流驱动应用程序的创建和管理。以下是其功能集的全面概述:

工作流创建

  • 编程工作流:使用代码以编程方式创建工作流。此方法提供强大的类型化功能,并能够在不同方案中重用工作流。

  • 可视化设计器:使用 Elsa 直观的可视化设计器创建工作流。这种图形方法非常适合那些喜欢视觉表示并提供相同可重用性优势的人。

工作流执行类型

  • 短时间运行的工作流:这些工作流从头到尾无缝执行,没有任何暂停。它们非常适合发送电子邮件或执行一系列步骤等任务。

  • 长时间运行的工作流:专为需要暂停和恢复的任务而设计。示例包括等待外部事件的工作流或编排冗长任务的工作流。

活动管理

  • 复合活动:类似 和 的活动可以封装其他活动。您可以通过编程方式或使用可视化设计器来设计自己的复合活动。这些可以在不同的工作流中重复使用。SequenceFlowchart

  • 触发器:旨在启动工作流的活动。示例包括 和 。HttpEndpointTimer

  • 活动:工作流的基本构建块。Elsa 附带了一系列丰富的活动,例如 、 、 等。该平台的可扩展性允许您根据需要添加自定义活动。WriteLineSendEmailHttpRequest

  • 活动提供程序:这些提供程序向 Elsa 提供活动类型。例如,提供基于实现的活动类型。Elsa 的灵活性将很快支持 GraphQL、OpenAPI、JavaScript 函数等活动类型。TypedActivityProviderIActivity

动态表达式

  • 表达式:在运行时动态计算值。Elsa 原生支持 JavaScript 和 Liquid 表达式,但它旨在适应自定义表达式计算器。

    • JavaScript 示例

      `The current date and time is: ${new Date()}`
    • 液体示例

      The current date and time is: {{ "now" | date: "%Y-%m-%d %H:%M:%S" }}

持久性和托管

  • 持久性:Elsa 允许将工作流保存到数据库中,即使在应用程序重新启动后也能恢复。持久性机制是抽象的,允许自定义持久性提供程序。

  • 工作流托管:直接在应用程序中集成和执行工作流,提供无缝的用户体验。

集成能力

  • 外部应用程序集成:可以从任何能够发出 HTTP 请求的外部应用程序触发 Elsa 工作流。相反,Elsa 工作流可以通过 HTTP 请求、Webhook、服务总线消息、gRPC 等与外部应用程序进行交互。

设置

使用以下命令创建新的空 ASP.NET 应用:

dotnet new web -n "ElsaStudio" -f net7.0

CD 进入项目的根目录,并添加 and 包:ElsaWorkflowDesigner.Web

cd WokflowDesigner.Web
dotnet add package Elsa.Workflows.Designer

接下来,打开文件并将其内容替换为以下代码:Program.cs

程序.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();

app.Run();

请注意,此处未添加任何特定于 Elsa 的内容。这是具有 Razor 页面的 ASP.NET Web 应用的基本设置。

在项目中,创建一个名为“的新文件夹”,并创建一个包含以下内容的新文件:Pages_ViewImports.cshtml

@namespace WorkflowDesigner.Web.Pages
@using Elsa.Workflows.Designer
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

创建另一个包含以下内容的文件:Index.cshtml

@page "/"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Elsa Workflows 3.0</title>
    <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
    <link rel="stylesheet" href="_content/Elsa.Workflows.Designer/elsa-workflows-designer/elsa-workflows-designer.css">
    <script src="_content/Elsa.Workflows.Designer/monaco-editor/min/vs/loader.js"></script>
    <script type="module" src="_content/Elsa.Workflows.Designer/elsa-workflows-designer/elsa-workflows-designer.esm.js"></script>
</head>
<body>

<elsa-studio server="https://localhost:5001/elsa/api" monaco-lib-path="/_content/Elsa.Workflows.Designer/monaco-editor/min"></elsa-studio>

</body>
</html>

这将包括包提供的必要脚本和样式。Elsa.Workflows.Designer

确保 的 URL 包含指向 Elsa 工作流服务器的有效 URL(例如,我们在上一章中创建的服务器)。 运行工作流服务器时,请注意正在使用的端口号 - 这可能与此处使用的端口号不同。server

尝试一下

启动应用程序时,您应该会看到登录屏幕:

响应

如果将工作流服务器配置为创建默认用户,则应该能够使用默认用户名和密码登录,即:

Username: admin
Password: password

登录后,您将看到主页:

响应

现在,我们可以通过右上角显示的“新建”按钮开始创建工作流:

响应

到目前为止,我们已经了解了如何设置单独的工作流服务器和设计器应用程序。如果您想知道,是的,也可以将服务器 + 设计器组合到单个 ASP.NET 应用程序中

如何使用以下 HTTP 活动:

  • Http端点
  • SendHttp请求
  • WriteHttpResponse

项目设置

若要创建项目,请运行以下命令:

dotnet new webapi -n WorkflowApp.Web --no-openapi -f net7.0

运行项目

执行以下命令,进入已创建的项目目录,运行项目。

cd WorkflowApp.Web
dotnet run

当应用运行时,记下它正在侦听的 URL。例如:。 若要调用天气预报控制器,请导航到 ,这应生成类似于以下内容的输出(为清楚起见,进行了简化):http://localhost:5085http://localhost:5085/weatherforecast

[
  {
    "date": "2023-01-20",
    "temperatureC": 54,
    "temperatureF": 129,
    "summary": "Hot"
  },
  {
    "date": "2023-01-21",
    "temperatureC": 48,
    "temperatureF": 118,
    "summary": "Balmy"
  }
]

Elsa 集成

接下来,让我们安装和配置 Elsa。

安装以下软件包:

dotnet add package Elsa --prerelease
dotnet add package Elsa.EntityFrameworkCore.Sqlite --prerelease
dotnet add package Elsa.Http --prerelease
dotnet add package Elsa.Identity --prerelease
dotnet add package Elsa.Liquid --prerelease
dotnet add package Elsa.Workflows.Api --prerelease

程序

使用以下代码进行更新:Program.cs

using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddRazorPages();

// Add Elsa services.
builder.Services.AddElsa(elsa =>
{
    elsa.UseIdentity(identity =>
    {
        identity.UseAdminUserProvider();
        identity.TokenOptions = tokenOptions => tokenOptions.SigningKey = "my-secret-signing-key";
    });
    elsa.UseDefaultAuthentication();
    elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore());
    elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore());
    elsa.UseJavaScript();
    elsa.UseLiquid();
    elsa.UseWorkflowsApi();
    elsa.UseHttp(http => http.ConfigureHttpOptions = options => options.BasePath = "/workflows");
});

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.UseWorkflows();
app.MapControllers();
app.MapRazorPages();
app.Run();

路径前缀

默认情况下,HTTP 工作流路径以 为前缀。这是一项优化,可防止每个入站 HTTP 请求都通过负责调用工作流的随附中间件组件。/workflows

您可以将前缀更改为其他内容。例如,以下代码将前缀更改为:"/wf"

elsa.UseHttp(http => http.ConfigureHttpOptions = options => options.BasePath = "/wf");

若要完全删除前缀,请改为提供空字符串:

elsa.UseHttp(http => http.ConfigureHttpOptions = options => options.BasePath = "");

如前所述,这将导致任何入站 HTTP 请求与工作流匹配,这可能会降低应用程序的整体性能,因此请谨慎使用。

代码工作流

我们将创建的工作流将能够执行以下操作:

  • 处理入站 HTTP 请求
  • 将 HTTP 请求发送到 WeatherForecast API 端点。
  • 将天气预报结果写回 HTTP 响应。

让我们看看如何在代码中创建工作流。

创建一个名为 的新文件夹,并添加以下类:Workflows

using System.Net;
using System.Net.Mime;
using System.Text;
using Elsa.Http;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;

namespace WorkflowApp.Web.Workflows;

public class WeatherForecastWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        var serverAddress = new Uri(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")!.Split(';').First());
        var weatherForecastApiUrl = new Uri(serverAddress, "weatherforecast");
        var weatherForecastResponseVariable = builder.WithVariable<ICollection<WeatherForecast>>();

        builder.Root = new Sequence
        {
            Activities =
            {
                // Expose this workflow as an HTTP endpoint.
                new HttpEndpoint
                {
                    Path = new("/weatherforecast"),
                    SupportedMethods = new(new[] { HttpMethods.Get }),
                    CanStartWorkflow = true
                },

                // Invoke another API endpoint. Could be a remote server, but here we are invoking an API hosted in the same app.
                new SendHttpRequest
                {
                    Url = new(weatherForecastApiUrl),
                    Method = new(HttpMethods.Get),
                    ParsedContent = new(weatherForecastResponseVariable)
                },

                // Write back the weather forecast.
                new WriteHttpResponse
                {
                    ContentType = new(MediaTypeNames.Text.Html),
                    StatusCode = new(HttpStatusCode.OK),
                    Content = new(context =>
                    {
                        var weatherForecasts = weatherForecastResponseVariable.Get(context)!;
                        var sb = new StringBuilder();

                        sb.AppendLine(
                            """
<!doctype html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://cdn.tailwindcss.com"></script>
        <title>yswenli-elsa-workflow</title>
    </head>
    <body>
        <div class="px-4 sm:px-6 lg:px-8">
        <div class="mt-8 flex flex-col">
        <div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
          <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
            <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
              <table class="min-w-full divide-y divide-gray-300">
                <thead class="bg-gray-50">
                  <tr>
                    <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Date</th>
                    <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Temperature (C/F)</th>
                    <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Summary</th>
                  </tr>
                </thead>
                <tbody class="divide-y divide-gray-200 bg-white">
""");
                        foreach (var weatherForecast in weatherForecasts)
                        {
                            sb.AppendLine("<tr>");
                            sb.AppendLine($"""<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{weatherForecast.Date}</td>""");
                            sb.AppendLine($"""<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{weatherForecast.TemperatureC}/{weatherForecast.TemperatureF}</td>""");
                            sb.AppendLine($"""<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{weatherForecast.Summary}</td>""");
                            sb.AppendLine("</tr>");
                        }

                        sb.AppendLine(
"""
                </tbody>
            </table>
        </div>
        </div>
        </div>
        </div>
    </body>
    </html>
""");
                        return sb.ToString();
                    })
                }
            }
        };
    }
}

CanStart工作流

请注意,我们将活动的属性设置为 。 这是向工作流运行时发出的信号,用于从活动中提取触发器。CanStartWorkflowHttpEndpointtrue

如果未设置此属性,则不会自动触发工作流以响应入站 HTTP 请求。

若要将工作流注册到工作流运行时,请返回并通过添加以下行来更新 Elsa 设置代码:Program.cs

elsa.AddWorkflow<WeatherForecastWorkflow>();

重新启动应用程序,这次导航到 。http://localhost:5085/workflows/weatherforecast

结果应如下所示:

Weather forecast response

来自 Designer 的工作流

在代码中创建工作流的替代方法是使用 Elsa Studio,这是一个允许您创建和管理工作流的 Web 应用程序。 要设置托管 Elsa Studio 的 ASP.NET 应用程序,请按照此处的说明进行操作。

将 Elsa Studio 连接到 Elsa 服务器

确保您的 Elsa Studio 应用程序配置为指向我们在本指南中创建的 Elsa 服务器 URL。例如:http://localhost:5085/elsa/api。

在 Elsa Studio 中设计工作流程时,您可以使用 Liquid 表达式语言动态生成值。这在编写 HTML 响应时特别有用。 因为我们想在 HTML 响应中使用该模型,所以我们需要将其注册到 Liquid 引擎。WeatherForecast

当我们注册为工作流变量类型时,Elsa 将为我们执行此操作。WeatherForecast

为此,请通过添加以下行来更新 Program.cs 中的工作流管理设置:

management.AddVariableType<WeatherForecast>(category: "Weather");

设置 Elsa Studio 后,启动应用程序,创建新的工作流并导入以下 JSON 文件:

weatherforecast-workflow.json

发布工作流并导航到 。http://localhost:5085/workflows/weatherforecast-from-designer

在此处找到本f示例的最终源代码。

外部应用程序集成

一个常见的体系结构是具有一个工作流引擎,该引擎负责协调工作流的执行,以及一个单独的应用程序,该引擎负责执行构成工作流的任务。 这些任务可以用任何编程语言实现,并且可以托管在任何应用程序中。

为了了解其工作原理,我们将创建两个使用 Webhook 相互通信的应用程序。 应用程序表示员工入职流程,其中工作流引擎负责编排流程,任务执行程序负责执行构成流程的任务。

这些任务将由人类完成,但同样的方法也可用于运行自动化任务。 归根结底,它是在任务完成后与工作流服务器进行通信。

RunTask 活动

为了请求执行任务,我们将使用该活动。 执行此活动时,它将执行两个步骤:RunTask

  1. 它发布一个名为 的域事件。RunTaskRequest
  2. 它创建一个书签并等待系统恢复它。

开箱即用,Elsa 不提供任何处理域事件的方法。 您可以选择自己处理此问题,也可以使用包来处理此问题,这就是我们将在此示例中执行的操作。RunTaskRequestElsa.Webhooks

Elsa.Webhooks 包

该包提供了一种处理域事件的方法,方法是向与此事件匹配的所有已注册 Webhook 触发 HTTP 请求。 Webhook 端点在工作流服务器应用程序中配置,并指向将执行任务的外部应用程序。 然后,外部应用程序将执行该任务,并在完成后将响应发送回工作流服务器应用程序。Elsa.WebhooksRunTaskRequest


为了了解其工作原理,我们将创建两个应用程序:

  1. 承载工作流引擎的工作流服务器应用程序。
  2. 一个任务执行器应用程序,用于执行构成工作流的任务。

载入工作流将协调以下任务:

  1. 接收新员工的姓名和电子邮件地址作为输入。
  2. 运行名为“创建电子邮件帐户”的任务。
  3. 运行名为“创建 Slack 帐户”的任务。
  4. 运行名为“创建 GitHub 帐户”的任务。
  5. 运行名为“添加到 HR 系统”的任务。
  6. 运行名为“添加到工资单系统”的任务。
  7. 向新员工发送电子邮件,欢迎他们加入公司。

我们将让工作流运行第一个任务,一旦完成,其余任务将并行运行,因为它们彼此独立。

工作流服务器

工作流服务器应用程序是承载工作流引擎的简单 ASP.NET Core 应用程序。 它使用包来处理域事件。Elsa.WebhooksRunTaskRequest

要设置此应用程序,请按照 ASP.NET Core 应用程序指南中的步骤操作,并按如下方式更新 Program.cs 文件:

services
    .AddElsa(elsa => elsa
        // ...
        // Left out for brevity.
        // ...
        .UseWebhooks(webhooks => webhooks.WebhookOptions = options => builder.Configuration.GetSection("Webhooks").Bind(options))
    );

Update and add the following sections:appsettings.json

{
  "Webhooks": {
    "Endpoints": [
      {
        "EventTypes": [
          "RunTask"
        ],
        "Url": "https://localhost:5002/api/webhooks/run-task"
      }
    ]
  }
}

此配置指示工作流服务器在发布域事件时向任务执行程序应用程序发送 Webhook 请求。RunTaskRequest

运行工作流服务器应用程序并创建以下工作流:

上述工作流对流程进行建模,如上所述:

  1. 接收新员工的姓名和电子邮件地址作为输入。
  2. 运行名为“创建电子邮件帐户”的任务。
  3. 运行名为“创建 Slack 帐户”的任务。
  4. 运行名为“创建 GitHub 帐户”的任务。
  5. 运行名为“添加到 HR 系统”的任务。
  6. 运行名为“添加到工资单系统”的任务。
  7. 向新员工发送电子邮件,欢迎他们加入公司。

除第一个和最后一个任务外,所有任务都由外部应用程序执行。

让我们回顾一下工作流中的每个活动:

从输入设置员工

这是将变量设置为输入值的活动:SetVariableEmployeeEmployee

请注意,此活动设置了一个名为 的工作流变量。 确保首先从“变量”选项卡创建此变量,如上面的屏幕截图 (1) 所示。Employee

创建电子邮件帐户

这是请求外部应用程序执行“创建电子邮件帐户”任务的活动:RunTask

创建 Slack 帐户

这与上一个活动相同,但适用于“创建 Slack 帐户”任务,并使用以下表达式来设置输入:Payload

return {
    employee: getEmployee(),
    description: "Create a Slack account for the new employee."
}

创建 GitHub 帐户

这与上一个活动相同,但用于“创建 GitHub 帐户”任务,并使用以下表达式设置输入:Payload

return {
    employee: getEmployee(),
    description: "Create a GitHub account for the new employee."
}

添加到人力资源系统

这与上一个活动相同,但适用于“添加到 HR 系统”任务,并使用以下表达式设置输入:Payload

return {
    employee: getEmployee(),
    description: "Add the new employee to the HR system."
}

添加到薪资系统

这与上一个活动相同,但适用于“添加到 HR 系统”任务,并使用以下表达式设置输入:Payload

return {
    employee: getEmployee(),
    description: "Add the new employee to the Payroll system."
}

发送欢迎电子邮件

这是向新员工发送欢迎电子邮件的活动,包含以下设置:SendEmail

hr@acme.com
getEmployee().Email
主题
`Welcome aboard, ${getEmployee().Name}!`
身体
`Hi ${getEmployee().Name},<br><br>All of your accounts have been setup. Welcome aboard!`

外部应用程序

外部应用程序是一个 ASP.NET 核心 MVC 应用程序,用于执行构成工作流的任务。 该应用程序包含一个视图,用于显示用户应完成的任务列表,以及一个用于处理来自工作流服务器的 Webhook 请求的控制器。

设置

运行以下 CLI 命令以搭建新项目的基架:

dotnet new mvc -o EmployeeOnboarding.Web

添加以下 NuGet 包:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Sqlite.Design
dotnet add package Elsa.EntityFrameworkCore

对于此应用程序,我们将使用 Entity Framework Core 将载入任务存储在 SQLite 数据库中。 首先,让我们像这样对载入任务实体进行建模:

namespace EmployeeOnboarding.Web.Entities;

/// <summary>
/// A task that needs to be completed by the user.
/// </summary>
public class OnboardingTask
{
    /// <summary>
    /// The ID of the task.
    /// </summary>
    public long Id { get; set; }

    /// <summary>
    /// An external ID that can be used to reference the task.
    /// </summary>
    public string ExternalId { get; set; } = default!;

    /// <summary>
    /// The ID of the onboarding process that the task belongs to.
    /// </summary>
    public string ProcessId { get; set; } = default!;

    /// <summary>
    /// The name of the task.
    /// </summary>
    public string Name { get; set; } = default!;
    
    /// <summary>
    /// The task description.
    /// </summary>
    public string Description { get; set; } = default!;

    /// <summary>
    /// The name of the employee being onboarded.
    /// </summary>
    public string EmployeeName { get; set; } = default!;

    /// <summary>
    /// The email address of the employee being onboarded.
    /// </summary>
    public string EmployeeEmail { get; set; } = default!;

    /// <summary>
    /// Whether the task has been completed.
    /// </summary>
    public bool IsCompleted { get; set; }

    /// <summary>
    /// The date and time when the task was created.
    /// </summary>
    public DateTimeOffset CreatedAt { get; set; }

    /// <summary>
    /// The date and time when the task was completed.
    /// </summary>
    public DateTimeOffset? CompletedAt { get; set; }
}

接下来,让我们创建数据库上下文:

namespace EmployeeOnboarding.Web.Data;

public class OnboardingDbContext : DbContext
{
    public OnboardingDbContext(DbContextOptions<OnboardingDbContext> options) : base(options)
    {
    }

    public DbSet<OnboardingTask> Tasks { get; set; } = default!;
}

最后,让我们在以下位置配置数据库上下文:Startup.cs

builder.Services.AddControllersWithViews();
builder.Services.AddDbContextFactory<OnboardingDbContext>(options => options.UseSqlite("Data Source=onboarding.db"));

请注意,我们使用 a 来创建数据库上下文。这使我们能够从托管服务自动运行迁移,接下来我们将创建该托管服务。DbContextFactory

迁移

让我们创建一个托管服务,该服务将在应用程序启动时自动运行迁移:

namespace EmployeeOnboarding.Web.HostedServices;

public class MigrationsHostedService : IHostedService
{
    private readonly IServiceProvider _serviceProvider;

    public MigrationsHostedService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        using var scope = _serviceProvider.CreateScope();
        var dbContextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<OnboardingDbContext>>();
        await using var dbContext = dbContextFactory.CreateDbContext();
        await dbContext.Database.MigrateAsync(cancellationToken);
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

在以下位置注册托管服务:Program.cs

builder.Services.AddHostedService<MigrationsHostedService>();

运行以下 CLI 命令以生成初始迁移:

dotnet ef migrations add Initial

任务列表

现在我们已经设置了数据库访问层,让我们更新 Home 控制器以显示需要完成的任务列表。 为此,我们将引入一个视图模型,该模型称为 :IndexViewModelIndexHomeController

using EmployeeOnboarding.Web.Entities;

namespace EmployeeOnboarding.Web.Views.Home;

public class IndexViewModel
{
    public IndexViewModel(ICollection<OnboardingTask> tasks)
    {
        Tasks = tasks;
    }

    public ICollection<OnboardingTask> Tasks { get; set; }
}

然后更新 操作以使用视图模型:IndexHomeController

using EmployeeOnboarding.Web.Data;
using EmployeeOnboarding.Web.Views.Home;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EmployeeOnboarding.Web.Controllers;

public class HomeController : Controller
{
    private readonly OnboardingDbContext _dbContext;

    public HomeController(OnboardingDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    
    public async Task<IActionResult> Index(CancellationToken cancellationToken)
    {
        var tasks = await _dbContext.Tasks.Where(x => !x.IsCompleted).ToListAsync(cancellationToken: cancellationToken);
        var model = new IndexViewModel(tasks);
        return View(model);
    }
}

Finally, let's update the view to display the list of tasks:Index.cshtml

@model EmployeeOnboarding.Web.Views.Home.IndexViewModel
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Tasks</h1>
    <p>Please complete the following tasks.</p>
</div>

<div class="container">
    <table class="table table-bordered table-hover">
        <thead class="table-light">
        <tr>
            <th scope="col">Task ID</th>
            <th scope="col">Name</th>
            <th scope="col">Description</th>
            <th scope="col">Employee</th>
            <th scope="col"></th>
        </tr>
        </thead>
        <tbody>
        @foreach (var task in Model.Tasks)
        {
            <tr>

                <th scope="row">@task.Id</th>
                <td>@task.Name</td>
                <td>@task.Description</td>
                <td>@($"{task.EmployeeName} <{task.EmployeeEmail}>")</td>
                <td>
                    <form asp-action="CompleteTask">
                        <input type="hidden" name="TaskId" value="@task.Id"/>
                        <button type="submit" class="btn btn-primary">Complete</button>
                    </form>
                </td>
            </tr>
        }
        </tbody>
    </table>
</div>

接收任务

现在,我们已经有了一种显示任务列表的方法,让我们设置一个可以从工作流服务器应用程序接收任务的 Webhook 控制器。

首先,让我们创建一个名为 :WebhookController

using EmployeeOnboarding.Web.Data;
using EmployeeOnboarding.Web.Entities;
using EmployeeOnboarding.Web.Models;
using Microsoft.AspNetCore.Mvc;

namespace EmployeeOnboarding.Web.Controllers;

[ApiController]
[Route("api/webhooks")]
public class WebhookController : Controller
{
    private readonly OnboardingDbContext _dbContext;

    public WebhookController(OnboardingDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    
    [HttpPost("run-task")]
    public async Task<IActionResult> RunTask(WebhookEvent webhookEvent)
    {
        var payload = webhookEvent.Payload;
        var taskPayload = payload.TaskPayload;
        var employee = taskPayload.Employee;
        
        var task = new OnboardingTask
        {
            ProcessId = payload.WorkflowInstanceId,
            ExternalId = payload.TaskId,
            Name = payload.TaskName,
            Description = taskPayload.Description,
            EmployeeEmail = employee.Email,
            EmployeeName = employee.Name,
            CreatedAt = DateTimeOffset.Now
        };

        await _dbContext.Tasks.AddAsync(task);
        await _dbContext.SaveChangesAsync();

        return Ok();
    }
}

上面的清单使用该模型来反序列化 Webhook 有效负载。该模型定义如下:WebhookEventWebhookEvent

public record WebhookEvent(string EventType, RunTaskWebhook Payload, DateTimeOffset Timestamp);

该模型定义如下:RunTaskWebhook

public record RunTaskWebhook(string WorkflowInstanceId, string TaskId, string TaskName, TaskPayload TaskPayload);

该模型定义如下:TaskPayload

public record TaskPayload(Employee Employee, string Description);

该模型定义如下:Employee

public record Employee(string Name, string Email);

完成任务

让我们将操作添加到:CompleteTaskHomeController

public async Task<IActionResult> CompleteTask(int taskId, CancellationToken cancellationToken)
{
    var task = _dbContext.Tasks.FirstOrDefault(x => x.Id == taskId);
    
    if (task == null)
        return NotFound();
    
    await _elsaClient.ReportTaskCompletedAsync(task.ExternalId, cancellationToken: cancellationToken);
    
    task.IsCompleted = true;
    task.CompletedAt = DateTimeOffset.Now;
    
    _dbContext.Tasks.Update(task);
    await _dbContext.SaveChangesAsync(cancellationToken);
    
    return RedirectToAction("Index");
}

上面的清单使用 将任务报告为已完成,其定义如下:ElsaClient

namespace EmployeeOnboarding.Web.Services;

/// <summary>
/// A client for the Elsa API.
/// </summary>
public class ElsaClient
{
    private readonly HttpClient _httpClient;

    public ElsaClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    /// <summary>
    /// Reports a task as completed.
    /// </summary>
    /// <param name="taskId">The ID of the task to complete.</param>
    /// <param name="result">The result of the task.</param>
    /// <param name="cancellationToken">An optional cancellation token.</param>
    public async Task ReportTaskCompletedAsync(string taskId, object? result = default, CancellationToken cancellationToken = default)
    {
        var url = new Uri($"tasks/{taskId}/complete", UriKind.Relative);
        var request = new { Result = result };
        await _httpClient.PostAsJsonAsync(url, request, cancellationToken);
    }
}

HttpClient 的配置如下:Program.cs

// Configure Elsa API client.
services.AddHttpClient<ElsaClient>(httpClient =>
{
    var url = configuration["Elsa:ServerUrl"]!.TrimEnd('/') + '/';
    var apiKey = configuration["Elsa:ApiKey"]!;
    httpClient.BaseAddress = new Uri(url);
    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("ApiKey", apiKey);
});

中的部分定义如下:Elsaappsettings.json

{
  "Elsa": {
    "ServerUrl": "https://localhost:5001/elsa/api",
    "ApiKey": "00000000-0000-0000-0000-000000000000"
  }
}

Running the Workflow

Now that we have a way to display the list of tasks and to complete tasks, let's run the workflow.

发送以下请求以运行工作流:

curl --location 'https://localhost:5001/elsa/api/workflow-definitions/{workflow_definition_id}/execute' \
--header 'Content-Type: application/json' \
--header 'Authorization: ApiKey 00000000-0000-0000-0000-000000000000' \
--data-raw '{
    "input": {
        "Employee": {
            "Name": "Alice Smith",
            "Email": "alice.smith@acme.com"
        }
    }
}'

请确保替换为我们之前创建的实际工作流定义 ID。{workflow_definition_id}

上述请求的效果是将在数据库中创建一个新任务,该任务将显示在 Web 应用程序中。

单击该按钮时,该任务将在数据库中标记为已完成,工作流将在其他应用程序中异步继续。 刷新“任务列表”页时,任务将消失,但将在数据库中创建 4 个新任务:Complete

完成所有任务后,工作流将向员工发送欢迎电子邮件,工作流将完成。

从 JSON 加载工作流

从 JSON 加载工作流是将工作流存储在数据库或文件系统中的好方法。本指南将向您展示如何从 JSON 文件加载工作流。

控制台应用程序

从 JSON 文件加载工作流的最直接方法是简单地加载 JSON 文件的内容,对其进行反序列化,然后执行反序列化的工作流。

下面是控制台应用程序的完整 Program.cs 文件,演示了如何从 JSON 文件加载工作流并执行它:

using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Microsoft.Extensions.DependencyInjection;

// Setup service container.
var services = new ServiceCollection();

// Add Elsa services.
services.AddElsa();

// Build service container.
var serviceProvider = services.BuildServiceProvider();

// Populate registries. This is only necessary for applications  that are not using hosted services.
await serviceProvider.PopulateRegistriesAsync();

// Import a workflow from a JSON file.
var workflowJson = await File.ReadAllTextAsync("HelloWorld.json");

// Get a serializer to deserialize the workflow.
var serializer = serviceProvider.GetRequiredService<IActivitySerializer>();

// Deserialize the workflow.
var workflow = serializer.Deserialize<Workflow>(workflowJson);

// Resolve a workflow runner to run the workflow.
var workflowRunner = serviceProvider.GetRequiredService<IWorkflowRunner>();

// Run the workflow.
await workflowRunner.RunAsync(workflow);

HelloWorld.json 是与 Program.cs 存储在同一目录中的文件,如果输出目录比输出目录中的文件新,则配置为复制到输出目录。

{
  "id": "HelloWorld-v1",
  "definitionId": "HelloWorld",
  "name": "Hello World",
  "root": {
    "id": "Flowchart1",
    "type": "Elsa.Flowchart",
    "activities": [
      {
        "id": "WriteLine1",
        "type": "Elsa.WriteLine",
        "text": {
          "typeName": "String",
          "expression": {
            "type": "Literal",
            "value": "Hello World!"
          },
          "memoryReference": {
            "id": "WriteLine1:input-1"
          }
        }
      }
    ]
  }
}

ASP.NET 应用

在 ASP.NET 应用程序中运行 Elsa 时,您可能希望将存储在 JSON 中的工作流提供给工作流运行时。

工作流运行时依赖于实现来为其提供工作流定义。 Elsa 开箱即用,附带以下提供程序:IWorkflowDefinitionProvider

  • ClrWorkflowDefinitionProvider(英语:ClrWorkflowDefinitionProvider)
  • FluentStorageWorkflowDefinitionProvider(英语:FluentStorageWorkflowDefinitionProvider)

负责提供创建为实现 .NET 类型的工作流定义。 负责提供通过 提供的工作流定义,这是 FluentStorage 库提供的 API。ClrWorkflowDefinitionProviderIWorkflowFluentStorageWorkflowDefinitionProviderIBlobStorage

除了内置提供程序之外,您还可以实现自己的提供程序,以从所需的任何源提供工作流定义。IWorkflowDefinitionProvider

但是,在本章中,使用 从存储在文件系统中的 JSON 文件提供工作流定义就足够了。 要进行此设置,您需要执行以下操作:FluentStorageWorkflowDefinitionProvider

  1. 按照 ASP.NET 工作流服务器一章中所述设置工作流服务器应用程序。
  2. 添加对工作流服务器项目的包引用。Elsa.WorkflowProviders.FluentStorage
  3. 更新程序 .cs 以使用 :FluentStorageWorkflowDefinitionProvider
// Add Elsa services.
services.AddElsa(elsa => elsa
    // Add the Fluent Storage workflow definition provider.
    .UseFluentStorageProvider()
    ... // Other configuration.

默认情况下,Fluent 存储提供程序将在文件夹中查找工作流定义。 若要试用,请在项目的根目录中创建文件夹,并创建一个名为“hello-world.jsonz”的新文件,其中包含以下内容:WorkflowsWorkflows

{
  "id": "hello-world-v1",
  "definitionId": "hello-world",
  "name": "Hello World",
  "isLatest": true,
  "isPublished": true,
  "version": 1,
  "root": {
    "id": "Flowchart1",
    "type": "Elsa.Flowchart",
    "activities": [
      {
        "id": "WriteLine1",
        "type": "Elsa.WriteLine",
        "text": {
          "typeName": "String",
          "expression": {
            "type": "Literal",
            "value": "Hello World!"
          },
          "memoryReference": {
            "id": "WriteLine1:input-1"
          }
        }
      }
    ]
  }
}

记下工作流定义 ID,即 。 例如,这是您在使用 REST API 调用工作流时使用的 ID。hello-world

curl --location --request POST 'https://localhost:5001/elsa/api/workflow-definitions/hello-world/execute' \
--header 'Authorization: ApiKey {your_api_key}'

调用工作流

有多种方法可以执行工作流:

  • 使用触发器。
  • 使用 REST API。
  • 使用 C# 代码。

使用触发器

当工作流配置为从触发器启动时,只要激活触发,该工作流就会启动。 触发器的激活方式取决于触发器类型。

例如,可以通过调用触发器中配置的路径来启动以 HTTP 终结点触发器开头的工作流。

Http endpoint configuration

触发器未触发

仅当触发器的“可以启动”工作流处于启用状态时,触发器才会处于活动状态。

Can start workflow

否则,触发器将不会启动工作流(但它仍将用作阻止活动)。

可以通过以下请求触发此工作流:

curl --location --request POST 'https://localhost:5001/workflows/test' \
--header 'Content-Type: application/json'

触发器的其他示例包括:

  • 计时器触发器,用于在特定时间启动工作流。
  • 事件触发器,在收到事件时启动工作流。
  • 消息触发器,在收到消息时启动工作流。

使用 REST API

该包提供了两个用于执行工作流的终结点:Elsa.Workflows.Api

  • /workflow-definitions/{workflow_definition_id}/execute同步执行工作流定义。
  • /workflow-instances/{workflow_instance_id}/dispatch异步执行工作流实例。

同步执行

在此上下文中同步并不意味着工作流将在与调用方相同的线程中执行。它仍然使用基于任务的异步编程。 不同之处在于,调用方将等待工作流完成,然后才能获得响应。

若要同步执行工作流定义,可以使用以下请求:

curl --location --request POST 'https://localhost:5001/elsa/api/workflow-definitions/<workflow-definition-id>/execute' \
--header 'Content-Type: application/json' \
--header 'Authorization: ApiKey {api_key}'
--data-raw '{
    "input": {
        "InputData": {
            "dummyProp": "hello"
        }
    }
}'

授权

可以按照工作流服务器/安全性中所述获取授权令牌 ASP.NET。

您可以从“输入/输出”选项卡定义每个工作流的输入和输出Workflow input and output

响应应如下所示:

{
    "workflowState": {
        "definitionId": "2f6ba5802e254082b00bd4dab00e650a",
        "definitionVersion": 1,
        "status": "Finished",
        "subStatus": "Finished",
        "bookmarks": [],
        "completionCallbacks": [],
        "activityExecutionContexts": [],
        "output": {
            "OutputData": "hello there!"
        },
        "properties": {
            "PersistentVariablesDictionary": {
                "f765d2c1ef7843ed862488ae6f191884:Workflow1:subvar": ""
            }
        },
        "id": "f765d2c1ef7843ed862488ae6f191884"
    }
}

该字段包含工作流实例 ID。 这些字段包含工作流的输出。idoutput

有关输入和输出的详细信息,请参阅输入/输出一章。

异步执行

以下终结点异步执行工作流:

/workflow-definitions/{workflow_definition_id}/dispatch`

该请求与终结点几乎相同,只是它不等待工作流完成。/execute

相反,端点会将工作流排入队列以供执行,并返回工作流实例 ID。

下面是一个响应示例:

{
    "workflowInstanceId": "b235209459294b97904eaee89fe7f930"
}

工作流实例 ID

工作流实例 ID 是在实际创建工作流实例之前生成的。这意味着工作流实例 ID 可用于跟踪工作流实例,即使它尚不存在。

使用 C# 代码

有两种方法可以从 C# 代码执行工作流:

  • 使用服务。IWorkflowRunner
  • 使用服务。IWorkflowRuntime

使用服务IWorkflowRunner

是可以执行任何对象的低级服务。IWorkflowRunnerIActivity

例如,以下代码执行一个名为:HelloWorld

var workflow = new Workflow
{
   Root = new WriteLine("Hello world!")
};

var workflowRunner = serviceProvider.GetRequiredService<IWorkflowRunner>();

await workflowRunner.RunWorkflowAsync(workflow);

工作流实例 ID

Workflow 类本身也是一个活动!

使用服务IWorkflowRuntime

使用起来很简单,但它需要您知道要执行的工作流程,并自己处理它创建的任何书签。 此外,您还必须自己管理持久化工作流实例。IWorkflowRunner

另一方面,该服务是一项高级服务,允许您触发一个或多个工作流,具体取决于您提供的输入。 工作流运行时与 一起从 中查找工作流。IWorkflowRuntimeIWorkflowDefinitionServiceIWorkflowDefinitionStore

这意味着,在使用服务触发工作流之前,必须注册工作流。IWorkflowRuntime

使用工作流设计器(或直接使用 REST API)创建工作流时,将自动注册工作流。 如果您有自定义,其工作流也将自动注册。 但是,如果需要,也可以手动注册工作流。例如:IWorkflowDefinitionProvider

public class HelloWorldWorkflow : WorkflowBase
{
   protected override void Build(IWorkflowBuilder builder)
   {
        builder.Root = new WriteLine("Hello world!");
   }
}

services.AddElsa(elsa => elsa.AddWorkflow<MyWorkflow>());

以下示例演示了如何使用该服务调用现有工作流:IWorkflowRuntime


var workflowRuntime = serviceProvider.GetRequiredService<IWorkflowRuntime>();
var result = await workflowRuntime.StartWorkflowAsync("HelloWorldWorkflow");

工作流实例 ID

当您希望从事件处理程序触发工作流时,通常使用 IWorkflowRuntime 服务。例如,如果实现监视目录的文件更改的自定义触发器活动,则可以使用 IWorkflowRuntime 服务在创建文件时触发工作流。 另一个示例是当您希望从消息队列触发工作流时。在这种情况下,可以使用 IWorkflowRuntime 服务在收到消息时触发工作流。

编程工作流与设计器工作流

从本质上讲,艾尔莎执行对象。这些对象是以编程方式或使用设计器创建的。 使用设计器创建时,工作流定义以 JSON 形式存储,并用于在运行时重新构造实际对象(实现)。IActivityWorkflowIActivity

让我们来看看这两种方法之间一些更重要的区别和相似之处。

编程工作流

编程工作流是通过实例化对象并设置其属性来创建的。例如,以下代码创建一个工作流,提示用户输入其名称并将其打印到控制台:IActivity

// Define a workflow variable to capture the output of the ReadLine activity.
var nameVariable = new Variable<string>();

// Define a simple sequential workflow:
var workflow = new Sequence
{
    // Register the name variable.
    Variables = { nameVariable }, 
    
    // Setup the sequence of activities to run.
    Activities =
    {
        new WriteLine("Please tell me your name:"), 
        new ReadLine(nameVariable),
        new WriteLine(context => $"Nice to meet you, {nameVariable.Get(context)}!")
    }
};

输出:

Please tell me your name:
Elsa
Nice to meet you, Elsa!

这里需要注意的是,我们使用该活动来定义顺序工作流。此外,Elsa 还支持活动,它允许您通过声明活动之间的连接将工作流定义为活动图。 下面是一个示例,它使用 :SequenceSequenceFlowchartFlowchart

// Define a workflow variable to capture the output of the ReadLine activity.
var nameVariable = new Variable<string>();

// Define the activities to put in the flowchart:
var writeLine1 = new WriteLine("Please tell me your name:");
var writeLine2 = new ReadLine(nameVariable);
var writeLine3 = new WriteLine(context => $"Nice to meet you, {nameVariable.Get(context)}!");

// Define a flowchart workflow:
var workflow = new Flowchart
{
    // Register the name variable.
    Variables = { nameVariable }, 
    
    // Add the activities.
    Activities =
    {
        writeLine1, 
        writeLine2,
        writeLine3
    },
    
    // Setup the connections between activities.
    Connections =
    {
        new Connection(writeLine1, writeLine2),
        new Connection(writeLine2, writeLine3)
    }
};

虽然实现不同,但输出将是相同的:

Please tell me your name:
Elsa
Nice to meet you, Elsa!

设计器工作流程

使用设计器时,可以通过将活动拖放到画布上并连接它们来定义工作流。 基础数据模型与用于编程工作流的数据模型相同,并使用 Flowchart 活动。 换言之,使用设计器创建工作流时,您创建的工作流的属性设置为活动。RootFlowchart

但是,一个重要的区别是,使用设计器创建的工作流(当前)不支持 C# lambda 表达式。 相反,我们使用 JavaScript 表达式。

让我们看一个例子。

使用设计器创建以下工作流:

Designer workflow

让我们依次看一下每个活动:

活动 1

此活动是一项活动。其属性设置为 。WriteLineTextPlease tell me your name:

Designer workflow

活动 2

此活动是一项活动。此活动没有输入,但其属性设置为名为 的工作流变量。ReadLineOutputName

Designer workflow

此变量在工作流的设置中定义:Variables

Designer workflow

活动 3

此活动是一项活动。其属性设置为 JavaScript 表达式:。WriteLineTextNice to meet you, ${getName()}!\

Designer workflow

Accessing workflow variables from JavaScript

To access workflow variables from JavaScript, you can use the following naming convention: . For example, to access the variable, you can use .get{nameOfVariable}()NamegetName()

JSON representation

Let's export the workflow from the designer and take a look at its JSON structure (formatted and simplified for readability):

{
  "name": "Nice to meet you",
  "variables": [
    {
      "id": "d6d10d5433e646b9bc02f1d05efeb584",
      "name": "Name",
      "typeName": "String"
    }
  ],
  "root": {
    "type": "Elsa.Flowchart",
    "id": "Flowchart1",
    "start": "WriteLine1",
    "activities": [
      {
        "text": {
          "typeName": "String",
          "expression": {
            "type": "Literal",
            "value": "Please tell me your name:"
          },
          "memoryReference": {
            "id": "WriteLine1:input-1"
          }
        },
        "id": "WriteLine1",
        "type": "Elsa.WriteLine"
      },
      {
        "result": {
          "typeName": "String",
          "memoryReference": {
            "id": "d6d10d5433e646b9bc02f1d05efeb584"
          }
        },
        "id": "ReadLine1",
        "type": "Elsa.ReadLine"
      },
      {
        "text": {
          "typeName": "String",
          "expression": {
            "type": "JavaScript",
            "value": "\u0060Nice to meet you, ${getName()}!\u0060"
          },
          "memoryReference": {
            "id": "WriteLine2:input-1"
          }
        },
        "id": "WriteLine2",
        "type": "Elsa.WriteLine"
      }
    ],
    "connections": [
      {
        "source": "WriteLine1",
        "target": "ReadLine1",
        "sourcePort": "Done",
        "targetPort": "In"
      },
      {
        "source": "ReadLine1",
        "target": "WriteLine2",
        "sourcePort": "Done",
        "targetPort": "In"
      }
    ]
  }
}

这里需要注意的几个关键点:

  • 该属性包含工作流变量。variables
  • 该属性包含工作流的根活动,即活动。rootFlowchart
  • 该属性包含属于工作流的活动。activities
  • 该属性包含活动之间的连接。connections
  • 该属性用于引用工作流变量。memoryReference
  • 该属性用于定义 JavaScript 表达式。expression
  • 该属性用于定义属性的类型。typeName
  • 该属性用于定义活动的类型。type
  • 该属性用于唯一标识活动或连接。id
  • 和 属性用于定义连接的源活动和目标活动。sourcetarget
  • 和 属性用于定义连接的源端口和目标端口。sourcePorttargetPort

JSON 结构与 C# 数据模型非常相似。主要区别在于,我们使用 JavaScript 表达式而不是 C# lambda 表达式,并且设计器创建一个对象,然后使用其属性包装对象。WorkflowFlowchartRoot

总结

在本章中,我们了解了 Elsa 工作流程的核心概念。 我们已经看到,工作流由活动组成,并且活动可以相互连接。我们还看到,可以通过编程方式或使用设计器来定义工作流。

posted @ 2023-10-30 11:58  yswenli  阅读(10183)  评论(6编辑  收藏  举报