.Net工作流elsa-workflows官方文档学习:简单的文件审批工作流

网页:https://elsa-workflows.github.io/elsa-core/docs/guides-document-approval

在本节中,我们将执行以下操作:

  • 以编程方式定义长时间运行的工作流,在HTTP请求到达指定URL时执行,接受带有JSON负载的POST请求,该JSON负载表示要检查的文档。
  • 相关活动组件:ReceiveHttpRequest,WriteHttpResponse,Fork,Join,SetVariable,Signaled,SendEmail和IfElse。

此工作流的目的是允许作者提交文档(建模为JSON对象),并允许审阅者批准或拒绝此文档。 此外,如果审阅者花费的时间太长而无法采取行动,则会定期提醒他。

我们将发布到工作流中的JSON有效负载如下所示:

{
    "Id": "1",
    "Author": {
        "Name": "John",
        "Email": "john@gmail.com"
    },
    "Body": "This is sample document."
}
View Code

使用访问此模型的JavaScript表达式查看活动时,请牢记此结构。

创建ASP.NET Core项目

创建一个名为Elsa.Guides.ContentApproval.WebApp的新的空ASP.NET Core项目,并添加以下程序包:

  • Elsa.Core
  • Elsa.Activities.Email
  • Elsa.Activities.Http
  • Elsa.Activities.Timers

创建工作流类

using System;
using System.Dynamic;
using System.Net;
using System.Net.Http;
using Elsa.Activities.ControlFlow;
using Elsa.Activities.Email.Activities;
using Elsa.Activities.Http.Activities;
using Elsa.Activities.Primitives;
using Elsa.Activities.Timers.Activities;
using Elsa.Activities.Workflows;
using Elsa.Expressions;
using Elsa.Services;
using Elsa.Services.Models;

namespace Elsa.Guides.DocumentApproval.WebApp
{
    public class DocumentApprovalWorkflow : IWorkflow
    {
        public void Build(IWorkflowBuilder builder)
        {
            builder
                .StartWith<ReceiveHttpRequest>(
                    x =>
                    {
                        x.Method = HttpMethod.Post.Method;
                        x.Path = new Uri("/documents", UriKind.Relative);
                        x.ReadContent = true;
                    }
                )
                .Then<SetVariable>(
                    x =>
                    {
                        x.VariableName = "Document";
                        x.ValueExpression = new JavaScriptExpression<ExpandoObject>("lastResult().Body");
                    }
                )
                .Then<SendEmail>(
                    x =>
                    {
                        x.From = new LiteralExpression("approval@acme.com");
                        x.To = new JavaScriptExpression<string>("Document.Author.Email");
                        x.Subject =
                            new JavaScriptExpression<string>("`Document received from ${Document.Author.Name}`");
                        x.Body = new JavaScriptExpression<string>(
                            "`Document from ${Document.Author.Name} received for review. " +
                            "<a href=\"${signalUrl('Approve')}\">Approve</a> or <a href=\"${signalUrl('Reject')}\">Reject</a>`"
                        );
                    }
                )
                .Then<WriteHttpResponse>(
                    x =>
                    {
                        x.Content = new LiteralExpression(
                            "<h1>Request for Approval Sent</h1><p>Your document has been received and will be reviewed shortly.</p>"
                        );
                        x.ContentType = "text/html";
                        x.StatusCode = HttpStatusCode.OK;
                        x.ResponseHeaders = new LiteralExpression("X-Powered-By=Elsa Workflows");
                    }
                )
                .Then<SetVariable>(
                    x =>
                    {
                        x.VariableName = "Approved";
                        x.ValueExpression = new JavaScriptExpression<bool>("false");
                    }
                )
                .Then<Fork>(
                    x => { x.Branches = new[] { "Approve", "Reject", "Remind" }; },
                    fork =>
                    {
                        fork
                            .When("Approve")
                            .Then<Signaled>(x => x.Signal = new LiteralExpression("Approve"))
                            .Then("Join");

                        fork
                            .When("Reject")
                            .Then<Signaled>(x => x.Signal = new LiteralExpression("Reject"))
                            .Then("Join");

                        fork
                            .When("Remind")
                            .Then<TimerEvent>(
                                x => x.TimeoutExpression = new LiteralExpression<TimeSpan>("00:00:10"),
                                name: "RemindTimer"
                            )
                            .Then<IfElse>(
                                x => x.ConditionExpression = new JavaScriptExpression<bool>("!!Approved"),
                                ifElse =>
                                {
                                    ifElse
                                        .When(OutcomeNames.False)
                                        .Then<SendEmail>(
                                            x =>
                                            {
                                                x.From = new LiteralExpression("reminder@acme.com");
                                                x.To = new LiteralExpression("approval@acme.com");
                                                x.Subject =
                                                    new JavaScriptExpression<string>(
                                                        "`${Document.Author.Name} is awaiting for your review!`"
                                                    );
                                                x.Body = new JavaScriptExpression<string>(
                                                    "`Don't forget to review document ${Document.Id}.<br/>" +
                                                    "<a href=\"${signalUrl('Approve')}\">Approve</a> or <a href=\"${signalUrl('Reject')}\">Reject</a>`"
                                                );
                                            }
                                        )
                                        .Then("RemindTimer");
                                }
                            );
                    }
                )
                .Then<Join>(x => x.Mode = Join.JoinMode.WaitAny, name: "Join")
                .Then<SetVariable>(
                    x =>
                    {
                        x.VariableName = "Approved";
                        x.ValueExpression = new JavaScriptExpression<object>("input('Signal') === 'Approve'");
                    }
                )
                .Then<IfElse>(
                    x => x.ConditionExpression = new JavaScriptExpression<bool>("!!Approved"),
                    ifElse =>
                    {
                        ifElse
                            .When(OutcomeNames.True)
                            .Then<SendEmail>(
                                x =>
                                {
                                    x.From = new LiteralExpression("approval@acme.com");
                                    x.To = new JavaScriptExpression<string>("Document.Author.Email");
                                    x.Subject =
                                        new JavaScriptExpression<string>("`Document ${Document.Id} approved!`");
                                    x.Body = new JavaScriptExpression<string>(
                                        "`Great job ${Document.Author.Name}, that document is perfect! Keep it up.`"
                                    );
                                }
                            );

                        ifElse
                            .When(OutcomeNames.False)
                            .Then<SendEmail>(
                                x =>
                                {
                                    x.From = new LiteralExpression("approval@acme.com");
                                    x.To = new JavaScriptExpression<string>("Document.Author.Email");
                                    x.Subject =
                                        new JavaScriptExpression<string>("`Document ${Document.Id} rejected`");
                                    x.Body = new JavaScriptExpression<string>(
                                        "`Sorry ${Document.Author.Name}, that document isn't good enough. Please try again.`"
                                    );
                                }
                            );
                    }
                );
        }
    }
}
View Code

代码较多! 让我们一步一步地从上到下查看。

 ReceiveHttpRequest
 
.StartWith<ReceiveHttpRequest>(
    x =>
    {
        x.Method = HttpMethod.Post.Method;
        x.Path = new Uri("/documents", UriKind.Relative);
        x.ReadContent = true;
    }
)

由于存在 ReceiveHttpRequest 活动,每次接收到与路径/文档匹配的 HTTP POST 请求时,都将执行工作流。

我们将其 ReadContent 设置为 true,以便读取和解析请求主体。解析内容是通过适当的 IContentFormatter 完成的,该 IContentFormatter 是根据请求体的内容类型选择的。目前,只支持 application/json 和 text/json 内容类型,但也将添加对 application/x-www-form-urlencoded 和 multipart/form-data 的支持。它将把 JSON 内容解析为 ExpandoObject。

将 ReadContent 设置为 true 后,我们可以从工作流中的其他活动访问解析后的 JSON。活动将使用“ Content”键以及工作流执行上下文的 LastResult 属性将此值存储在其输出字典中。

 SetVariable
.Then<SetVariable>(
    x =>
    {
        x.VariableName = "Document";
        x.ValueExpression = new JavaScriptExpression<ExpandoObject>("lastResult().Body");
    }
)

然后我们连接到 SetVariable 活动,该活动在我们称为 Document 的工作流上设置一个自定义变量。我们使用一个 JavaScript 表达式来分配作为 HTTP 请求的一部分收到的对象。

 该表达式的工作方式如下:
  •  首先,我们调用一个名为 lastResult 的函数。此函数返回工作流执行上下文的 LastResult 值。因为这是由 ReceiveHttpRequest 设置的,所以它将包含一个对象,该对象保存有关接收到的 HTTP 请求的详细信息,包括一个 Body 属性,该属性包含已解析的 JSON 对象。
正如我们将在下一个活动中看到的,我们使用 SetVariable 活动来简化从其他活动访问它的过程。
 
SendEmail
.Then<SendEmail>(
   x =>
   {
       x.From = new LiteralExpression("approval@acme.com");
       x.To = new JavaScriptExpression<string>("Document.Author.Email");
       x.Subject =
           new JavaScriptExpression<string>("`Document received from ${Document.Author.Name}`");
       x.Body = new JavaScriptExpression<string>(
           "`Document from ${Document.Author.Name} received for review. " +
           "<a href=\"${signalUrl('Approve')}\">Approve</a> or <a href=\"${signalUrl('Reject')}\">Reject</a>`"
       );
   }
)
View Code

我们要做的第二件事是通知审阅者新的文件已提交。 为此,我们使用SendEmail活动发送电子邮件。 我们使用LiteralExpression和JavaScriptExpression对象的混合配置此活动。 LiteralExpression返回传递给其构造函数的文字字符串值,如果使用泛型类型重载,则可以选择将其转换为给定类型。 在这种情况下,我们只需要指定一个文字电子邮件地址:“ approval@acme.com。

 To,Subject和Body的表达式更有趣,因为它们演示了如何使用JavaScript表达式访问我们之前定义的Document变量。

 Body属性表达式使用一个名为signalUrl的JavaScript函数,该函数接受一个表示信号名称的参数。 这样做是生成一个包含安全令牌的绝对URL,该令牌包含以下信息:

  •  工作流实例ID
  • 信号名称

 当对生成的URL发出HTTP请求时(例如,在收到电子邮件时单击该请求),Elsa将识别该URL并触发与安全令牌携带的工作流实例ID相匹配的工作流实例。 更具体地说,将通过信号事件触发工作流程,如果该事件被该类型的活动阻止,则导致工作流程恢复。 我们将在短期内通知您。

 首先,我们要向客户端发送HTTP响应,并说文档已成功接收。

WriteHttpResponse

.Then<WriteHttpResponse>(
    x =>
    {
        x.Content = new LiteralExpression(
            "<h1>Request for Approval Sent</h1><p>Your document has been received and will be reviewed shortly.</p>"
        );
        x.ContentType = "text/html";
        x.StatusCode = HttpStatusCode.OK;
        x.ResponseHeaders = new LiteralExpression("X-Powered-By=Elsa Workflows");
    }
)
View Code

WriteHttpResponse活动只是将响应写回到客户端。 通过该活动,我们可以配置状态码,内容类型,内容主体和响应头以发送回去。

 SetVariable

.Then<SetVariable>(
 x =>
 {
     x.VariableName = "Approved";
     x.ValueExpression = new LiteralExpression<bool>("false");
 }
) 

这次,我们使用SetVariable活动来初始化另一个名为Approved的变量。 稍后将使用此变量来检查审阅者是否单击了“批准”或“拒绝”链接(触发了适当的信号)。 我们需要预先初始化此变量,因为在下一个活动中,我们会将执行分叉到3个分支中,其中一个会启动一个定期检查此变量的计时器。 如果未定义变量,则工作流程将出错。

 Fork, IfElse

.Then<Fork>(
    x => { x.Branches = new[] { "Approve", "Reject", "Remind" }; },
    fork =>
    {
        fork
            .When("Approve")
            .Then<Signaled>(x => x.Signal = new LiteralExpression("Approve"))
            .Then("Join");

        fork
            .When("Reject")
            .Then<Signaled>(x => x.Signal = new LiteralExpression("Reject"))
            .Then("Join");

        fork
            .When("Remind")
            .Then<TimerEvent>(
                x => x.TimeoutExpression = new LiteralExpression<TimeSpan>("00:00:10"),
                name: "RemindTimer"
            )
            .Then<IfElse>(
                ...
            );
    }
)
View Code

Fork活动使我们可以将工作流执行分为多个分支。 在这种情况下,我们将分支到以下分支:

  • Approve
  • Reject
  • Remind

对于Approve和Reject分支,我们都连接到前面提到的Signaled活动。 因为我们分叉执行,所以这两个活动的工作流程都将被阻塞。 当审阅者单击电子邮件中的任一链接时,将触发这些活动之一并恢复工作流程的执行。 发生这种情况时,工作流程将继续执行“加入”活动,稍后将进行介绍。

 连接活动

请注意,我们可以连接到作为泛型类型参数指定的活动以外的其他活动。我们可以改为指定活动的 ID,而不是使用类型参数指定接下来要执行的活动。实际上,Next < t > 方法只是简单地定义一个活动,然后自动地在当前活动和正在定义的活动之间创建一个连接。Next 方法没有类型参数,只有一个名称: string 参数,它只是在当前活动和 ID 指定的活动之间创建一个连接。

 工作流不仅会在两个有信号的活动上被阻止,而且还会在 TimerEvent 活动上被阻止。此活动被配置为每10秒触发一次。每隔10秒,工作流的这个分支将继续执行 IfElse 活动。注意,我们为 TimerEvent 活动指定了一个 ID 值: “ RemindTimer”。指定显式 ID 允许我们引用工作流其他部分的活动。
 
 IfElse
.Then<IfElse>(
    x => x.ConditionExpression = new JavaScriptExpression<bool>("!!Approved"),
    ifElse =>
    {
        ifElse
            .When(OutcomeNames.False)
            .Then<SendEmail>(
                x =>
                {
                    x.From = new LiteralExpression("reminder@acme.com");
                    x.To = new LiteralExpression("approval@acme.com");
                    x.Subject =
                        new JavaScriptExpression<string>(
                            "`${Document.Author.Name} is awaiting for your review!`"
                        );
                    x.Body = new JavaScriptExpression<string>(
                        "`Don't forget to review document ${Document.Id}.<br/>" +
                        "<a href=\"${signalUrl('Approve')}\">Approve</a> or <a href=\"${signalUrl('Reject')}\">Reject</a>`"
                    );
                }
            )
            .Then("RemindTimer");
    }
);
View Code

IfElse 活动将执行分成两个分支。根据其 ConditionExpression 计算的布尔值,将继续在 True 分支或 False 分支上执行。在我们的示例中,条件检查 Approved 工作流变量的值是否等于 true。

 如果为 false,则使用 SendEmail 活动发送电子邮件(我们以前见过) ,并使用 Then (“ Reminder”)循环回到 TimerEvent。现在您了解了为什么我们为 TimerEvent 提供一个 ID 值。
 
 Join
.Then<Join>(x => x.Mode = Join.JoinMode.WaitAny, name: "Join")
.Then<SetVariable>(
    x =>
    {
        x.VariableName = "Approved";
        x.ValueExpression = new JavaScriptExpression<object>("input('Signal') === 'Approve'");
    }
)
View Code

Join 变量将工作流执行合并回单个分支。指定 WaitAny 的 JoinMode 将导致该活动在任何传入的活动执行之后立即继续工作流。换句话说,一旦“批准”或“拒绝”信号被触发,工作流将立即恢复。当这样做时,我们将 Approved 工作流变量分别设置为 true 或 false。

 IfElse
 
.Then<IfElse>(
    x => x.ConditionExpression = new JavaScriptExpression<bool>("!!Approved"),
    ifElse =>
    {
        ifElse
            .When(OutcomeNames.True)
            .Then<SendEmail>(
                x =>
                {
                    x.From = new LiteralExpression("approval@acme.com");
                    x.To = new JavaScriptExpression<string>("Document.Author.Email");
                    x.Subject =
                        new JavaScriptExpression<string>("`Document ${Document.Id} approved!`");
                    x.Body = new JavaScriptExpression<string>(
                        "`Great job ${Document.Author.Name}, that document is perfect! Keep it up.`"
                    );
                }
            );

        ifElse
            .When(OutcomeNames.False)
            .Then<SendEmail>(
                x =>
                {
                    x.From = new LiteralExpression("approval@acme.com");
                    x.To = new JavaScriptExpression<string>("Document.Author.Email");
                    x.Subject =
                        new JavaScriptExpression<string>("`Document ${Document.Id} rejected`");
                    x.Body = new JavaScriptExpression<string>(
                        "`Sorry ${Document.Author.Name}, that document isn't good enough. Please try again.`"
                    );
                }
            );
    }
);
View Code

最后,我们只需检查 Approved 的值,然后向文档的作者发送适当的电子邮件,最后完成工作流。

 更新Startup文件

现在已经定义了工作流,我们应该更新 Startup 类如下:

 
using Elsa.Activities.Email.Extensions;
using Elsa.Activities.Http.Extensions;
using Elsa.Activities.Timers.Extensions;
using Elsa.Extensions;
using Elsa.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Elsa.Guides.DocumentApproval.WebApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services
                .AddElsa()
                .AddHttpActivities(options => options.Bind(Configuration.GetSection("Http")))
                .AddEmailActivities(options => options.Bind(Configuration.GetSection("Smtp")))
                .AddTimerActivities(options => options.Bind(Configuration.GetSection("BackgroundRunner")))
                .AddWorkflow<DocumentApprovalWorkflow>;
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseHttpActivities();
        }
    }
}
View Code

更新Appsettings.json

正如您所看到的,我们正在通过将 HTTP、 Email 和 Timer 活动的选项绑定到 Configuration 来配置它们。虽然您可以手动进行配置,但让我们按照以下方式更新 appsettings.json:

 
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Http": {
    "BaseUrl": "http://localhost:5000"
  },
  "Smtp": {
    "Host": "localhost",
    "Port": "2525"
  },
  "BackgroundRunner": {
    "SweepInterval": "PT01S"
  }
}
View Code

运行

为了尝试这个工作流程,需要使用以下两个工具: 

Postman 使我们能够轻松地将 JSON 内容发布到我们的工作流中。Smtp4Dev 使我们能够在本地启动 SMTP 服务,拦截所有即将发出的电子邮件,而不需要实际将它们发送给收件人。我将我的设置为侦听端口2525。

 Docker Compose和.HTTP文件:或者,您可以选择使用随附的docker-compose文件从docker容器中运行Smtp4Dev服务,然后使用http-request.http文件触发HTTP请求。 这需要在您的计算机上安装Docker Desktop。

 首先,启动应用程序。 如果一切顺利,则Web主机将准备在http:// localhost:5000处接收传入的HTTP请求:

 控制台窗口显示:

Hosting environment: Production
Content root path: C:\Projects\Elsa\elsa-guides\src\Elsa.Guides.DocumentApproval.WebApp
Now listening on: http://localhost:5000
Now listening on: https://localhost:5001
Application started. Press Ctrl+C to shut down.

接下来,发送以下 HTTP 请求:

 
POST /documents HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{
    "Id": "3",
    "Author": {
        "Name": "John",
        "Email": "john@gmail.com"
    },
    "Body": "This is sample document."
}
View Code

或者使用 cUrl 格式:

 
curl --location --request POST "http://localhost:5000/documents" \
--header "Content-Type: application/json" \
--data "{
    \"Id\": \"3\",
    \"Author\": {
        \"Name\": \"John\",
        \"Email\": \"john@gmail.com\"
    },
    \"Body\": \"This is sample document.\"
}"
View Code

响应应该是这样的:

<h1>Request for Approval Sent</h1>
<p>Your document has been received and will be reviewed shortly.</p>

当你启动 Smtp4Dev Web UI 时,你应该看到这个:

 

 

 每隔10秒左右,提醒电子邮件信息就应该出现:

 

 

 这将一直继续,直到您单击“批准”或“拒绝”链接为止。

 总结

在这个演练中,我们已经看到了如何在 ReceiveHttpRequest、 signalUrl JavaScript 函数的帮助下实现长时间运行的工作流,我们还看到了如何使用各种其他活动来实现提醒循环。

 源码

https://github.com/elsa-workflows/elsa-guides/tree/master/src/Elsa.Guides.DocumentApproval.WebApp

 

 

 

posted @ 2021-02-23 19:14  顺其自然,道法自然  阅读(6547)  评论(4编辑  收藏  举报