.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

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM