- 示例工程
【免费下载链接】practical-aspnetcore
Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.
导读
本文以practical-aspnetcore仓库中的projects/elsa/composite-activity示例为核心,讲解 Elsa 工作流引擎中Composite Activity(复合活动)的完整用法:如何把一组活动(Activity)封装成一个独立活动,如何通过Composite<T>对外暴露类型化结果,以及如何在多个工作流中复用它。读完本文,你将能够基于 Elsa 3.x 的代码优先(code-first)方式编写自定义复合活动,并掌握Root、Variables、OnCompleted与工作流变量之间的协作机制。
Composite Activity 是什么
Elsa 是运行在 .NET 之上的工作流引擎库,既可以作为独立应用使用,也可以嵌入 ASP.NET Core 应用。projects/elsa/README.md中明确给出复合活动的定义:
Composite Activity is a way to encapsulate a set of activities into a single activity. It is a way to reuse activities in different workflows.
即:复合活动是把一组活动封装成一个整体活动的手段,其根本目的是在不同工作流中复用这组活动逻辑。这与编程中的"函数抽取""组件化"思想一脉相承——当多个工作流都要执行"生成随机数并输出"这类固定步骤时,不必在每个工作流里重复堆砌WriteLine、SetVariable,而是把这组步骤收敛为一个GetRandom复合活动。
本示例对应的源码位于 projects/elsa/composite-activity/Program.cs,项目级说明见 projects/elsa/README.md(其中 "Composite Activity" 条目)。
项目结构与运行环境
composite-activity是一个独立的控制台风格示例项目,目录内仅包含 4 个文件:
Program.cs—— 全部示例代码(工作流定义 + 复合活动定义)composite-activity.csproj—— 项目文件,声明目标框架与 Elsa 包引用composite-activity.sln—— Visual Studio 解决方案文件README.md—— 复合活动概念说明
目标框架与依赖
查看 composite-activity.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>true</ImplicitUsings> <NoWarn>ASP0000</NoWarn> <LangVersion>preview</LangVersion> </PropertyGroup> <ItemGroup> <PackageReference Include="Elsa" Version="3.7.1" /> </ItemGroup> </Project>要点说明:
- 项目以
net10.0为目标框架,并启用LangVersion=preview;仓库根目录的 global.json 指定 SDK 版本为10.0.100并允许rollForward: major,因此运行本示例需要 .NET 10 SDK。 - 使用
Microsoft.NET.Sdk.WebSDK,但本示例并不启动 Kestrel 服务器,只是利用 Web SDK 提供的宿主能力来承载 Elsa 的服务注册。 - 唯一的第三方依赖是
Elsa 3.7.1,这是代码优先(code-first)工作流所需的核心程序集。 NoWarn=ASP0000用于抑制在非 Web 场景下解析服务提供程序时可能出现的分析器警告。
如何运行
在仓库根目录执行以下命令即可体验:
dotnet run --project projects/elsa/composite-activity运行后,控制台会依次输出复合活动内部的两条信息(随机数生成提示与随机数值),再回到外层工作流输出最终的 "magic number" 行。
整体代码解剖:外层工作流如何消费复合活动
Program.cs采用 Elsa 3 的 code-first 写法,完整流程如下(完整源码见 Program.cs):
using System.Security.Cryptography; using Elsa.Extensions; using Elsa.Workflows; using Elsa.Workflows.Activities; using Elsa.Workflows.Memory; var services = new ServiceCollection(); services.AddElsa(); var serviceProvider = services.BuildServiceProvider(); var runner = serviceProvider.GetRequiredService<IWorkflowRunner>(); var magicNumber = new Variable<int>("magic-number", 0); var workflow = new Sequence { Variables = { magicNumber }, Activities = { new GetRandom { Result = new(magicNumber) }, new WriteLine(ctx => { var number = magicNumber.Get(ctx); return $"The magic number is { number }"; }) } }; await runner.RunAsync(workflow);这段代码可以拆解为四个环节:
1. 初始化 Elsa 运行时
var services = new ServiceCollection(); services.AddElsa(); var serviceProvider = services.BuildServiceProvider(); var runner = serviceProvider.GetRequiredService<IWorkflowRunner>();services.AddElsa()注册 Elsa 全部核心服务,包括活动发现、工作流执行器、内存存储等。这个扩展方法来自Elsa.Extensions命名空间。IWorkflowRunner是工作流执行入口,通过 DI 解析后即可用RunAsync驱动任意工作流。这也是仓库中其它 Elsa 示例(如 sequence-activity、setvariable-activity)完全一致的启动模式,可见这是 code-first 工作流的通用骨架。
2. 声明工作流变量
var magicNumber = new Variable<int>("magic-number", 0);Variable<T>来自Elsa.Workflows.Memory命名空间,用于在工作流内部传递值。这里声明了一个名为magic-number、初始值为0的整型变量,它的角色是"承接"复合活动产生的结果。
3. 组装 Sequence 工作流
工作流本体是一个Sequence(顺序容器),内部依次执行两个活动:
- 第一个是自定义复合活动
GetRandom,并通过对象初始化器把它的输出结果绑定到magicNumber:new GetRandom { Result = new(magicNumber) }Result = new(magicNumber)表示把复合活动的类型化结果输出到变量magicNumber(new(...)是目标类型化 new 表达式,等价于new Output<int>(magicNumber))。 - 第二个是
WriteLine,用 lambda 读取变量并打印:new WriteLine(ctx => { var number = magicNumber.Get(ctx); return $"The magic number is { number }"; })
magicNumber.Get(ctx)在活动执行上下文中读取变量当前值——这正是 Elsa 变量系统的典型用法:定义时用Variable<T>,读取时借助ActivityExecutionContext(即ctx)。
4. 执行工作流
await runner.RunAsync(workflow);Sequence会按顺序执行内部活动。先运行GetRandom(内部完成随机数生成并回填Result),再运行WriteLine输出最终结果,控制台打印内容形如:
GetRandom compositve is generating random information <随机整数> The magic number is <随机整数>核心:自定义复合活动 GetRandom 的实现
复合活动的精髓在于Composite<T>基类。示例中GetRandom : Composite<int>表明它是一个向外暴露int类型结果的复合活动:
class GetRandom : Composite<int> { private readonly Variable<int> _random = new(); public GetRandom() { var random = RandomNumberGenerator.Create(); var bytes = new byte[sizeof(int)]; random.GetNonZeroBytes(bytes); var result = BitConverter.ToInt32(bytes); Root = new Sequence { Variables = { _random }, Activities = { new WriteLine($"{nameof(GetRandom)} compositve is generating random information"), new SetVariable<int>(_random, result) } }; } protected override void OnCompleted(ActivityCompletedContext context) { var random = _random.Get<int>(context.ChildContext); context.ChildContext.Set(Result, random); } }Root:复合活动的内部编排
Composite<T>的核心是Root属性——它指定复合活动内部要执行的工作流。这里把内部逻辑定义为一个Sequence:
- 内部声明私有变量
_random(Variable<int>),并将其注册进Root.Variables,供内部活动共享。 - 第一步
WriteLine打印"正在生成随机信息"的提示。 - 第二步
SetVariable<int>(_random, result)把构造阶段计算好的随机数写入_random。
随机数本身在构造函数中一次性生成:RandomNumberGenerator.Create()创建加密随机数生成器,GetNonZeroBytes(bytes)填充 4 字节,再通过BitConverter.ToInt32(bytes)转为int。这个生成逻辑与仓库中 workflow-3 生成随机整数的方式如出一辙,属于仓库内的惯例做法。
OnCompleted:把内部结果"带出"复合活动
OnCompleted是复合活动的关键钩子——它在内部Root工作流执行完成后被调用,负责把内部计算结果暴露给外部调用者:
protected override void OnCompleted(ActivityCompletedContext context) { var random = _random.Get<int>(context.ChildContext); context.ChildContext.Set(Result, random); }context.ChildContext是内部Root子工作流的执行上下文,_random.Get<int>(context.ChildContext)从中读取内部变量_random的值。context.ChildContext.Set(Result, random)把读取到的随机数写入复合活动的Result输出。由于Result已被外层绑定到magicNumber变量,值随即流入外层工作流。
这正是"封装 + 结果回传"的完整闭环:外部调用者只关心GetRandom的Result,不需要知道内部用了几个活动、生成了什么中间变量——复用的边界由此形成。
数据流全景:变量如何贯通内外
综合整个示例,数据流可以归纳为:
- 外层定义
magicNumber变量(初始0),并声明GetRandom.Result指向它; - 执行
GetRandom时,构造函数预生成随机整数,内部Sequence通过SetVariable将其写入内部变量_random; Root完成后触发OnCompleted,从ChildContext读取_random,再Set到Result,从而写入外层magicNumber;- 外层
WriteLine通过magicNumber.Get(ctx)读取并打印。
可以看到,Variable<T>既承载内部中间状态,也承载跨层结果;而Composite<T>的泛型参数决定了结果类型,使得复合活动在语法层面就是"一个返回int的活动"。
复合活动与基础活动的搭配关系
理解复合活动,离不开它内部用到的两个基础活动。仓库中这两个活动各有独立示例:
Sequence:顺序容器,按添加顺序执行内部活动。复合活动的Root本身就是Sequence,其行为可对照 projects/elsa/sequence-activity/Program.cs。SetVariable:向指定变量写入值。GetRandom内部用SetVariable<int>(_random, result)保存随机数;单独使用时可参考 projects/elsa/setvariable-activity/Program.cs——该示例展示了SetVariable反复改写message变量的效果。WriteLine:向控制台输出一行文本,是最基础的活动,见 projects/elsa/writeline-activity/Program.cs。
复合活动本质上是"容器活动 + 结果回传"的组合:把Sequence/SetVariable/WriteLine等原子能力收纳进Root,再用OnCompleted向上层暴露结果。相比直接用Fork分支(见 projects/elsa/fork-activity/Program.cs)或If条件(见 projects/elsa/if-activity/Program.cs)编排流程,复合活动的优势在于抽象与复用:调用方无需关心内部实现细节。
对比:两种"返回结果"的 code-first 写法
Elsa 3 中,让工作流对外产出值有两条常见路径,仓库恰好都提供了示例:
| 方式 | 载体 | 适用场景 | 仓库示例 |
|---|---|---|---|
复合活动 +Composite<T> | Root+OnCompleted回写Result | 把"一组活动"整体复用为一个可参数化的活动 | composite-activity/Program.cs |
自定义工作流 +WorkflowBase<T> | 内置Result变量 +RunAsync<T>返回值 | 把整个工作流当作一个返回T的单元 | workflow-3/Program.cs |
workflow-3的写法是通过RunAsync<int>(new RandomWorkflow())拿到result.Result,本质是把"工作流整体"作为返回值单元;而复合活动是把"工作流内的一段逻辑"作为返回值单元,粒度更细,也更贴近"自定义活动"的语义。
复用场景与设计建议
结合projects/elsa/README.md对复合活动的定位(encapsulate a set of activities into a single activity, reuse in different workflows),在实际项目中可以这样使用:
- 沉淀业务原子操作:如"生成订单号""计算折扣价""校验用户权限"等由多个活动组成的固定逻辑,抽成复合活动后,多个工作流直接
new使用。 - 类型化输出降低耦合:使用
Composite<T>让结果有明确类型,调用方通过Result = new(变量)即可接住输出,无需在多个工作流中重复声明内部中间变量。 - 内部细节私有化:内部
Variables、中间活动全部封装在复合活动内部(示例中的_random即声明为私有字段),外部无法直接触碰,从而保证逻辑一致性与可维护性。
小结
composite-activity示例用约 60 行代码完整展示了 Elsa 3 复合活动的三个核心机制:
Root:用Sequence等容器组织内部活动,形成可执行的整体;Variables:内部状态通过Variable<T>在工作流内流动;OnCompleted+Result:在内部完成后把结果回传外层,让复合活动成为"返回类型化值"的自定义活动。
它是 Elsa 生态中"封装与复用"的基石能力。掌握它之后,你可以进一步探索projects/elsa目录下的Fork(并行分支)、While/For/ForEach(循环)与WorkflowBase<T>(返回结果的工作流)等示例,组合出更复杂、可维护的业务编排逻辑。
延伸阅读
- 复合活动示例源码:projects/elsa/composite-activity/Program.cs
- Elsa 系列示例总览:projects/elsa/README.md
- 顺序容器
Sequence:projects/elsa/sequence-activity/Program.cs - 变量写入
SetVariable:projects/elsa/setvariable-activity/Program.cs - 返回结果的工作流(另一种结果回传方式):projects/elsa/workflow-3/Program.cs
- 示例工程
【免费下载链接】practical-aspnetcore
Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.
相关推荐
practical-aspnetcore 实战:用 Elsa `ForEach` 活动基于集合驱动循环工作流
practical aspnetcore 实战:用 Elsa ForEach 活动基于集合驱动循环工作流 本指南以 foreach activity https
示例工程PerfView 开源协作工作流:基于 Visual Studio 的 Fork、upstream 同步与 Pull Request 实战指南
PerfView 开源协作工作流:基于 Visual Studio 的 Fork、upstream 同步与 Pull Request 实战指南 PerfView
示例工程Apache Beam Python Kata 实战:用 Composite Transform 封装可复用的复杂处理逻辑
Apache Beam Python Kata 实战:用 Composite Transform 封装可复用的复杂处理逻辑 本篇指南以 Apache Beam
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考