Try   HackMD

Workflow Core 簡介

Workflow Core 是一個基於 .NET 的輕量級工作流程引擎,用於建構長期執行的工作流程與業務邏輯,支援分散式系統、事件驅動以及持久化, 可用電子商務訂單處理、自動化任務或複雜事件調度等等工作流程。

特點

  • 跨平台:可使用 .NET 進行開發,並且與 .NET 8 兼容
  • 事件驅動:支援事件等待,事件觸發後繼續執行流程
  • 持久化:支援 PostgreSQL、SQL Server、Redis,來儲存 Workflow 的流程和狀態

TL;DR

  • Workflow Core 為輕量版引擎套件,適用於簡單的流程作業
  • 不支援 BPMN 2.0 協議,不利於引擎套件的更換
  • 可將流程產出 Json 格式,但支援的可視化工具有限
  • 官方文件的範例說明不夠齊全,需以原始碼輔助

版本資訊

  • .NET 8
  • Workflow Core Version 3.13.0

開始使用

基本範例

先以 Console 專案來實際操作 Workflow Core 的使用,建立好 Console 專案後輸入以下指令安裝 Workflow Core。

  • 套件安裝
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging
dotnet add package WorkflowCore
  • 情境模擬

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

以下開始說明程式,拆分成主程式以及各步驟的類別實作,原始碼請參考這裡

  • Program.cs
using Microsoft.Extensions.DependencyInjection;
using WorkflowCore.Interface;
using WorkflowCore.Models;

var serviceProvider = new ServiceCollection()
  .AddLogging()
  .AddWorkflow()
  .BuildServiceProvider();

var host = serviceProvider.GetService<IWorkflowHost>();
if (host == null) throw new Exception("Fail");

host.RegisterWorkflow<ShipmentWorkflow, ShipmentData>();
host.Start();

Console.Write("輸入數量:");
var quantity = Console.ReadLine();
if (quantity == null || !int.TryParse(quantity, out int parseQuantity))
{
  throw new Exception("Fail");
}

var shipData = new ShipmentData();
shipData.Id = Guid.NewGuid();
shipData.Quantity = parseQuantity;

await host.StartWorkflow("ShipmentWorkflow", shipData);

Console.ReadLine();
host.Stop();
  • 工作流類別 ShipmentWorkflow.cs
public class ShipmentWorkflow : IWorkflow<ShipmentData>
{
  public string Id => "ShipmentWorkflow";
  public int Version => 1;

  public void Build(IWorkflowBuilder<ShipmentData> builder)
  {
    builder.StartWith<CheckInventory>()
    .Input(step => step.Quantity, data => data.Quantity)
    .Output(data => data.IsInventorySufficient, step => step.IsInventorySufficient)
    .If(data => data.IsInventorySufficient).Do(
        builder => builder
        .If(data => data.Quantity > 100).Do(
            builder => builder.StartWith<SupervisorApproval>()
            .Output(data => data.IsApproved, step => step.IsApproved)
            .If(data => data.IsApproved).Do(
                builder => builder.StartWith<ShipItems>()
                .Input(step => step.Quantity, data => data.Quantity)
            )
        )
        .If(data => data.Quantity <= 100).Do(
            builder => builder.StartWith<ShipItems>()
            .Input(step => step.Quantity, data => data.Quantity)
        )
    )
    .Then(ctx => Console.WriteLine("流程結束"))
    .EndWorkflow();
}
  • 官方文件連結
  • 檢查庫存 CheckInventory.cs
public class CheckInventory : StepBody
{
  public int Quantity { get; set; }
  public bool IsInventorySufficient { get; set; }

  public override ExecutionResult Run(IStepExecutionContext context)
  {
    Console.WriteLine($"檢查庫存: {Quantity}");
    IsInventorySufficient = Quantity <= 200;
    if (!IsInventorySufficient) Console.WriteLine("庫存不足");
    return ExecutionResult.Next();
  }
}
  • 主管簽核 SupervisorApproval.cs
public class SupervisorApproval : StepBody
{
  public bool IsApproved { get; set; }

  public override ExecutionResult Run(IStepExecutionContext context)
  {
    Console.WriteLine("等待主管簽核中...");

    IsApproved = true;
    var output = IsApproved ? "通過" : "不通過";
    Console.WriteLine($"主管簽核:{output}");

    return ExecutionResult.Next();
  }
}
  • 出貨 ShipItems.cs
public class ShipItems : StepBody
{
  public int Quantity { get; set; }

  public override ExecutionResult Run(IStepExecutionContext context)
  {
    Console.WriteLine($"確定出貨,數量:{Quantity}");
    return ExecutionResult.Next();
  }
}

搭配 WebAPI 專案

開放 WebAPI 給外部程式做存取,以及開啟持久化設定,可以支援外部事件的觸發,範例將實現以下功能,原始碼請參考這裡

  • Store workflow in postgres.
  • Start or suspend workflow.
  • Event trigger in workflow.
  • Get specific workflow information.
  • Program.cs
// 開啟持久化設定
// UsePostgreSQL(string connectionString, bool canCreateDB, bool canMigrateDB, [string schemaName = "wfc"])
builder.Services.AddWorkflow(x => x.UsePostgreSQL("<connectionstring>", true, true));

// 中間省略...

var host = app.Services.GetService<IWorkflowHost>() ?? throw new Exception("fail");

host.RegisterWorkflow<ShipmentWorkflow, ShipmentData>();
host.Start();

app.Lifetime.ApplicationStopped.Register(() => host.Stop());

app.Run();

SupervisorApproval.cs 特別提出簽核流程 API 的處理過程,需要使用 ExecutionResult.WaitForEvent 來等待簽核事件的觸發,並且需要開啟持久化設定

補充說明

需要開啟持久化設定的原因為工作流程的狀態預設是存在記憶體中,當程式結束後工作狀態會跟著消失。

  • SupervisorApproval.cs
public class SupervisorApproval : StepBody
{
    public bool IsApproved { get; set; }

    public override ExecutionResult Run(IStepExecutionContext context)
    {
        if (!context.ExecutionPointer.EventPublished)
        {
            Console.WriteLine("等待主管簽核中...");
            return ExecutionResult.WaitForEvent("Approve", context.Workflow.Id, DateTime.Now);
        }

        IsApproved = (bool)context.ExecutionPointer.EventData;
        var output = IsApproved ? "通過" : "不通過";
        Console.WriteLine($"主管簽核:{output}");
        return ExecutionResult.Next();
    }
}

踩雷

從原始碼 WaitFor 中可以看到如果執行流程中有事件觸發的等待,必須去判斷事件 context.ExecutionPointer.EventPublished 是否已經觸發

  • 觸發簽核事件
[HttpPost("[action]")]
public async Task<IActionResult> Approve(ApproveData approveData)
{
    await _workflowHost.PublishEvent("Approve", approveData.WorkflowId, approveData.Approve, DateTime.Now);
    return Ok("OK");
}

/// <summary>
/// PublishEvent
/// </summary>
/// <param name="eventName"></param>
/// <param name="eventKey"></param>
/// <param name="eventData"></param>
/// <param name="effectiveDate"></param>
/// <returns></returns>
Task PublishEvent(string eventName, string eventKey, object eventData, DateTime? effectiveDate = null);

參考資料

延伸閱讀