[.NET專案實戰] Elsa開源工作流元件應用(二):核心解讀

林晓lx發表於2024-03-20

@

目錄
  • 定義
    • 變數
      • 記憶體暫存器類
      • 暫存器中的儲存區塊類
      • 變數到儲存的對映類
    • 上下文物件
      • 活動上下文(ActivityExecutionContext)
      • 工作流執行上下文(WorkflowExecutionContext)
      • 表示式執行上下文(ExpressionExecutionContext)
  • 構建
    • 構建活動
    • 構建工作流
  • 執行
    • 註冊
      • 註冊工作流
      • 註冊活動
      • 填充
    • Invoke活動
  • 可觀測性
    • 設計器與APIs
    • 工作流配置
    • 檢視工作流狀態

本篇將帶你深入分析Elsa工作流原理,排除干擾展示關鍵程式碼段,以加深理解

定義

變數

Elsa工作原理可以抽象理解為管道中介軟體 + 非同步模型

Elsa中,活動的變數的獲取和設定都是非同步的。Elsa定義了Variable型別作為非同步操作的結果或者說是非同步操作的佔位符,這個變數在執行的時候才會填充數值。這與我們熟悉C#中的Task,或者js裡的promise物件作用相同。輸入Input,OutPut都屬於 Variable。

Elsa模擬了記憶體暫存器(MemoryRegister)以及Set和Get訪問器實現非同步模型。

記憶體暫存器類

public class MemoryRegister
{
    ...
    public IDictionary<string, MemoryBlock> Blocks { get; }
    
}

暫存器中的儲存區塊類

public class MemoryBlock
{
    ...
    /// <summary>
    /// The value stored in this block.
    /// </summary>
    public object? Value { get; set; }
    
    /// <summary>
    /// Optional metadata about this block.
    /// </summary>
    public object? Metadata { get; set; }
}

變數到儲存的對映類

Id可以代表變數在記憶體區塊中的引用地址

public class MemoryBlockReference
{
    
    /// <summary>
    /// The ID of the memory block.
    /// </summary>
    public string Id { get; set; } = default!;

    public object? Get(MemoryRegister memoryRegister) => GetBlock(memoryRegister).Value;
}

構建活動時將建立活動中變數到儲存區塊的對映,分配一個引用給變數

 public void AssignInputOutputs(IActivity activity)
 {
     var activityDescriptor = _activityRegistry.Find(activity.Type, activity.Version) ?? throw new Exception("Activity descriptor not found");
     var inputs = activityDescriptor.GetWrappedInputProperties(activity).Values.Cast<Input>().ToList();
     var seed = 0;

     foreach (var input in inputs)
     {
         var blockReference = input?.MemoryBlockReference();

         if (blockReference != null!)
             if (string.IsNullOrEmpty(blockReference.Id))
                 blockReference.Id = $"{activity.Id}:input-{seed}";

         seed++;
     }
    ...
 }

非同步變數獲取和設定:

可以透過上下文物件的Set,和Get方法,非同步獲取和設定非同步變數。

上下文物件

檢視原始碼可以看到Elsa定義瞭如下Context

在這裡插入圖片描述

其中比較重要的上下文物件:

活動上下文(ActivityExecutionContext)

活動上下文物件由Elsa.Runtime提供,在工作流執行函式中可供訪問。透過它可訪問包含活動例項、當前輸入和輸出等。透過它可以訪問當前活動所在的工作流執行上下文。

工作流執行上下文(WorkflowExecutionContext)

工作流上下文物件由Elsa.Runtime提供,可透過活動上下文(ActivityExecutionContext)訪問其所屬工作流執行上下文。透過它可訪問包含工作流例項、當前活動、當前輸入和輸出等。

表示式執行上下文(ExpressionExecutionContext)

表示式執行上下文用於在構建活動時傳遞記憶體變數(輸入,輸出),其中包含MemoryRegister物件。

透過表示式執行上下文(ExpressionExecutionContext)獲取到變數的值:
在這裡插入圖片描述

構建

構建活動

Elsa預設幫我們建立了這些活動:

在這裡插入圖片描述

在這裡插入圖片描述

他們都實現了IActivity介面,Activity和CodeActivity是IActivity的實現類,對應的是一個空的活動,(CodeActivity是帶有自動完成功能的空活動)

我們要做的是繼承這個活動,重寫Execute方法以實現我們自己的業務。比如:

public class HelloWorld : Activity
{
    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        Console.WriteLine("Hello World!");
        await CompleteAsync();
    }
}

以官方預設的WiteLine為例,這個類的Execute程式碼如下:

protected override void Execute(ActivityExecutionContext context)
{
    var text = context.Get(Text);
    var provider = context.GetService<IStandardOutStreamProvider>() ?? new StandardOutStreamProvider(System.Console.Out);
    var textWriter = provider.GetTextWriter();
    textWriter.WriteLine(text);
}

構建工作流

首要目標是拿到一個工作流物件(Workflow),Elsa啟動時會從工作流提供者(IWorkflowProvider)獲取所有能用的工作流。並註冊到資源池中

public interface IWorkflowProvider
{
    string Name { get; }

    ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowsAsync(CancellationToken cancellationToken = default);
}

Elsa預設的實現類是如下兩種,BlobStorageWorkflowProvider將從資料庫(BlobStorage)中反序列化來註冊。ClrWorkflowProvider使用工作流構建器註冊。

在這裡插入圖片描述

我們先定義工作流描述類,它繼承自IWorkflow, WorkflowBase是IWorkflow的抽象基類

class SequentialWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder workflow)
    {
        workflow.Root = new Sequence
        {
            Activities =
            {
                new WriteLine("Line 1"),
                new WriteLine("Line 2"),
                new WriteLine("Line 3")
            }
        };
    }
}
 

Elsa初始化時,WorkflowBuilder會構建程式集中所有實現IWorkflow的類。

WorkflowBuilder中的BuildWorkflowAsync方法會將工作流描述類IWorkflow物件構建成Workflow物件。

在這裡插入圖片描述

這裡思考一個問題:終執行的程式碼是在活動中定義的,但為什麼返回的是Workflow物件?透過程式碼研讀,實際上Workflow也是一個IActivity活動,只不過它具有一個Root根節點的複合活動。活動的定義請參考官方文件

BuildWorkflowAsync中的具體實現如下:

在這裡插入圖片描述

執行

註冊

註冊包括註冊工作流和註冊活動,配置Elsa時需要使用如下兩個方法:

.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>()

註冊工作流

工作流可以透過ClrWorkflowProvider,使用工作流構建器註冊,也可以從本地儲存(BlobStorage)中反序列化來註冊。
程式碼構建的工作流是透過實現IWorkflow介面,在Elsa初始化時將工作流注冊到工作流定義持久化到資料庫的WorkflowDefinition表中

透過工作流構建器註冊:

在這裡插入圖片描述

註冊活動

Elsa使用描述器(IActivityDescriber)提供一個描述符(ActivityDescriptor),這裡比較繞,閱讀原始碼可以發現,其實是透過各種反射獲取活動派生類的特徵資料(有的系統喜歡將稱之為後設資料),封裝這些資料的型別稱之為描述符,特徵資料可以作為在介面上顯示,分組,排序的資訊。

在這裡插入圖片描述

活動不同於工作流,它在執行中不持久化於資料庫,而是以登錄檔的形式儲存於記憶體中。

IDictionary<(string Type, int Version), ActivityDescriptor> _activityDescriptors

在構建工作流的時候自動註冊活動,也可以透過實現IActivity介面,在Elsa初始化時將所有活動註冊到登錄檔中
在這裡插入圖片描述

Elsa啟動時將所有實現了IActivity介面的型別註冊為活動:

在這裡插入圖片描述

填充

啟動時填充活動登錄檔和工作流定義表。
官方也給出了說明,各填充兩次確保活動登錄檔和工作流定義表都是最新的:

在這裡插入圖片描述

階段一:填充活動登錄檔
因為工作流定義可以用作活動,需要確保在填充工作流定義表之前填充活動登錄檔。

階段二:填充工作流定義表

階段三:重新填充活動登錄檔
填充了工作流定義表之後,我們需要重新填充活動登錄檔,以確保活動描述符是最新的。

階段四:用當前的活動集重新更新工作流定義表。
最後,需要重新填充工作流定義表,以確保工作流定義是最新的。

Invoke活動

Elsa預設的管道中介軟體:

在這裡插入圖片描述

Elsa註冊執行活動的中介軟體(DefaultActivityInvokerMiddleware):

public static class ActivityInvokerMiddlewareExtensions
{
    /// <summary>
    /// Adds the <see cref="DefaultActivityInvokerMiddleware"/> component to the pipeline.
    /// </summary>
    public static IActivityExecutionPipelineBuilder UseDefaultActivityInvoker(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<DefaultActivityInvokerMiddleware>();
}

在這裡插入圖片描述

在執行活動的中介軟體(DefaultActivityInvokerMiddleware),最終活動被呼叫的程式碼如下:

在這裡插入圖片描述

可以看見,Elsa最終以反射的方式建立一個Activity例項,然後呼叫它的ExecuteAsync方法。

可觀測性

設計器與APIs

實際上,Elsa的執行時和設計器是完全分離的。Elsa提供了一個基於Blazor的設計工具,它作為獨立的專案釋出在Github上: Elsa-Studio

在這裡插入圖片描述

因為和介面互動是透過REST API實現的,所以你也可以使用任何你想要的客戶端來實現。

接設計器預設的HTTP API實現在Elsa.Workflows.Api庫中,用於支援設計器的增刪改查業務。

如果僅要使用工作流引擎,可以使用Elsa.Workflows.Management庫,它只包含對於工作流的管理而不涉及HTTP介面。

工作流配置

開啟設計器,點選“工作流(Workflow)”選單,然後單擊“定義(Definition)”選項卡。可以看到一個工作流定義的列表。點選右上角新增按鈕,

在這裡插入圖片描述

在開啟的頁面中,拖拽活動到工作流圖上,然後單擊“儲存(Save)”按鈕。

在這裡插入圖片描述

在瀏覽器的網路請求中可以看到一個POST請求,請求地址為/workflow/definitions,請求引數為JSON格式,後端服務中WorkflowDefinitions的Endpoint中將對編輯器的“儲存”請求進行處理

在請求負載中,WorkflowDefinitionModel欄位會包含工作流定義和Root活動。

預設實現會將工作流定義和根活動序列化為JSON,並將其儲存到資料庫中。其中根活動在資料庫WorkflowDefinition表的StringData列中儲存。

在這裡插入圖片描述

當工作流執行時,Elsa會例項化(Materialize)Workflow物件

在這裡插入圖片描述
在這裡插入圖片描述

在這裡插入圖片描述

其中RootActivity會反序列化,可以看到StringData會被反序列化為IActivity物件

在這裡插入圖片描述

在這裡插入圖片描述

檢視工作流狀態

Elsa定義了不同的介面和資料庫

主要的介面如下:

workflowDefinition:工作流定義介面,資料來自WorkflowDefinition表
workflowInstance:工作流例項介面,資料來自WorkflowInstance表
activity-execution:活動執行介面,查詢活動的Id、狀態以及結果,輸入輸出等上下文資料,資料主要透過查詢ActivityExecutionRecords表來獲取。
journal: 活動執行日誌,資料來自WorkflowExecutionLogRecords表

開啟設計器,點選“工作流(Workflow)”選單,然後單擊“例項(Instance)”選項卡。可以看到一個工作例項列表

在這裡插入圖片描述

點選條目即可檢視工作流的執行日誌和各活動的執行資訊。Web頁面中各片區的資料來源分佈大致如下:

在這裡插入圖片描述

其中頁面中央的工作流編輯器顯示了工作流的結構,結合工作流的執行日誌,可以直觀的看到工作流的執行情況。可觀測到執行的步驟,以及執行的耗時。

--完結--

相關文章