網頁: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." }
使用訪問此模型的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.`" ); } ); } ); } } }
代碼較多! 讓我們一步一步地從上到下查看。
.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 屬性將此值存儲在其輸出字典中。
.Then<SetVariable>( x => { x.VariableName = "Document"; x.ValueExpression = new JavaScriptExpression<ExpandoObject>("lastResult().Body"); } )
然后我們連接到 SetVariable 活動,該活動在我們稱為 Document 的工作流上設置一個自定義變量。我們使用一個 JavaScript 表達式來分配作為 HTTP 請求的一部分收到的對象。
- 首先,我們調用一個名為 lastResult 的函數。此函數返回工作流執行上下文的 LastResult 值。因為這是由 ReceiveHttpRequest 設置的,所以它將包含一個對象,該對象保存有關接收到的 HTTP 請求的詳細信息,包括一個 Body 屬性,該屬性包含已解析的 JSON 對象。


.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>`" ); } )
我們要做的第二件事是通知審閱者新的文件已提交。 為此,我們使用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"); } )
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>( ... ); } )
Fork活動使我們可以將工作流執行分為多個分支。 在這種情況下,我們將分支到以下分支:
- Approve
- Reject
- Remind
對於Approve和Reject分支,我們都連接到前面提到的Signaled活動。 因為我們分叉執行,所以這兩個活動的工作流程都將被阻塞。 當審閱者單擊電子郵件中的任一鏈接時,將觸發這些活動之一並恢復工作流程的執行。 發生這種情況時,工作流程將繼續執行“加入”活動,稍后將進行介紹。
連接活動
請注意,我們可以連接到作為泛型類型參數指定的活動以外的其他活動。我們可以改為指定活動的 ID,而不是使用類型參數指定接下來要執行的活動。實際上,Next < t > 方法只是簡單地定義一個活動,然后自動地在當前活動和正在定義的活動之間創建一個連接。Next 方法沒有類型參數,只有一個名稱: string 參數,它只是在當前活動和 ID 指定的活動之間創建一個連接。


.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"); } );
IfElse 活動將執行分成兩個分支。根據其 ConditionExpression 計算的布爾值,將繼續在 True 分支或 False 分支上執行。在我們的示例中,條件檢查 Approved 工作流變量的值是否等於 true。


.Then<Join>(x => x.Mode = Join.JoinMode.WaitAny, name: "Join") .Then<SetVariable>( x => { x.VariableName = "Approved"; x.ValueExpression = new JavaScriptExpression<object>("input('Signal') === 'Approve'"); } )
Join 變量將工作流執行合並回單個分支。指定 WaitAny 的 JoinMode 將導致該活動在任何傳入的活動執行之后立即繼續工作流。換句話說,一旦“批准”或“拒絕”信號被觸發,工作流將立即恢復。當這樣做時,我們將 Approved 工作流變量分別設置為 true 或 false。


.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.`" ); } ); } );
最后,我們只需檢查 Approved 的值,然后向文檔的作者發送適當的電子郵件,最后完成工作流。
現在已經定義了工作流,我們應該更新 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(); } } }
更新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" } }
運行
為了嘗試這個工作流程,需要使用以下兩個工具:
Postman 使我們能夠輕松地將 JSON 內容發布到我們的工作流中。Smtp4Dev 使我們能夠在本地啟動 SMTP 服務,攔截所有即將發出的電子郵件,而不需要實際將它們發送給收件人。我將我的設置為偵聽端口2525。
首先,啟動應用程序。 如果一切順利,則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." }
或者使用 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.\" }"
響應應該是這樣的:
<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 函數的幫助下實現長時間運行的工作流,我們還看到了如何使用各種其他活動來實現提醒循環。