- 后端
- 工作流自动化
- 流程编排
- 低代码
【免费下载链接】elsa-core
The Workflow Engine for .NET
本文基于
specs/012-output-converters/data-model.md展开。Elsa 3 的活动输出转换器(Output Converters)是一套可扩展机制:工作流作者可以在"活动输出 → 变量/工作流输出"这条绑定链路上,显式挂载一个已注册的转换器,让目标变量拿到的是转换后的 Bound Value,而活动自身的原生输出(Native Output)原封不动地保留在活动输出寄存器与诊断日志中。读完本文,你将掌握该特性的完整数据模型——输出绑定、转换器配置、注册与描述符、转换上下文、目标解析、结构化错误与状态机——并能结合仓库源码理解其端到端实现。
特性定位:为什么需要"绑定级"输出转换
Elsa 3 中,活动的每个输出(Activity Output)通过Output/Output<T>绑定(Output Binding)关联到一个目标(Destination),这个目标通常是一个变量(variable)或工作流输出(workflow output)。传统绑定把活动原生值直接写入目标;而输出转换器允许在写入前插入一次显式、同步、确定性的变换。
这套模型的核心原则体现在四个"不变式"上:
- 原生值不变:活动输出寄存器、工作流日志、API 响应与诊断永远暴露原生 Activity Output(参见 spec.md 的 FR-005、FR-006);
- 转换只作用于绑定边界:只有交付给目标的 Bound Value 才经历转换(FR-007 明确"转换必须在 Output Binding 边界同步完成");
- 显式选择、绝不自动推断:系统不会根据源/目标类型猜测该用哪个转换器(FR-015),必须由作者显式配置 Converter ID;
- 可选、向后兼容:未配置转换器的绑定,序列化形状与赋值行为与旧版本完全一致(FR-003)。
一、输出绑定(Output Binding):一条可选的新关系
在既有Output/Output<T>绑定模型之上,本特性只新增一个可选关系:
| 成员 | 说明 |
|---|---|
Converter | 零或一个转换器配置(Converter Configuration) |
| 内存引用(Memory Reference) | 保持不变,它仍然是目标身份(destination identity)的唯一标识 |
| 原生输出类型 | 保持不变:强类型输出为T,无类型输出为object |
两条验证规则构成了绑定的合法性前提:
- 转换器配置必须指向可解析的目标:若绑定了
Converter,则该输出必须关联到一个可解析的变量或工作流输出; - 无配置则行为不变:没有
Converter的绑定,序列化结果与既有赋值路径完全一致,不触发任何转换器查找、校验或调用。
仓库中Output模型的Converter属性正是这一关系的落地(见 src/modules/Elsa.Workflows.Core/Models/Output.cs)。在运行时边界ActivityExecutionContext.Set中,代码先检查output?.Converter == null:为空则走原有赋值路径(写入表达式内存块 + 记录活动输出),非空才进入转换流程(见 ActivityExecutionContext.cs)。
二、转换器配置(Converter Configuration):持久化的唯一入口
每个绑定可携带一份持久化的转换器配置,字段只有两个:
Id:必填、非空的稳定转换器 ID;Settings:可选 JSON 对象,在持久化或调用前会被克隆,保证不可变语义。
序列化形态如下(绑定 JSON 中新增一个可选converter对象):
{ "typeName": "String", "memoryReference": { "id": "resultVariable" }, "converter": { "id": "sample.to-text", "settings": { "format": "compact" } } }注意两个约束:配置中只允许出现 Converter ID 与设置,绝不允许持久化实现类型名、实例、描述符或显示元数据(FR-004)。代码层面,OutputConverterConfiguration是sealed record,构造时即对Settings执行settings?.Clone(),Id空值时被规整为空字符串(见 OutputConverterConfiguration.cs)。
三、转换器注册(Output Converter Registration):Keyed DI 与唯一性
注册信息由三个要素组成:
| 成员 | 说明 |
|---|---|
Descriptor | 不可变的转换器描述符(Converter Descriptor) |
ServiceKey | 精确的 Converter ID,作为 Keyed DI 的解析键 |
ServiceLifetime | 注册生命周期:Transient/Scoped/Singleton三者之一 |
唯一性规则非常严格:
- 精确的、区分大小写的 ID 是唯一的(查找使用 ordinal + 大小写敏感比较);
- 仅大小写不同的 ID 同样禁止——注册期即抛错,而不是依赖注册顺序。
仓库中OutputConverterServiceCollectionExtensions.AddOutputConverter<TConverter>实现了注册入口:它先校验描述符、用StringComparison.OrdinalIgnoreCase检查历史注册,再把不可变OutputConverterRegistration注册为单例,同时通过ServiceDescriptor.DescribeKeyed以 Converter ID 为 key 注册实现(见 OutputConverterServiceCollectionExtensions.cs)。注册集合构造时,OutputConverterRegistry会再次以StringComparer.OrdinalIgnoreCase分组检查重复,并拒绝 open-generic 的源/结果类型(见 OutputConverterRegistry.cs)。
注册代码示例(摘自 doc/wiki/output-converters.md):
services.AddOutputConverter<NumberToTextConverter>( new OutputConverterDescriptor( "sample.number-to-text.v1", typeof(decimal), typeof(string), "Number to text", "Formats a decimal using an explicit invariant format.", schemaDocument.RootElement));默认生命周期为Scoped:实现类从当前工作流执行作用域(workflow execution scope)中按 ID 解析,注册表只缓存描述符与注册信息,绝不缓存 scoped 转换器实例(FR-024)。
四、转换器描述符(Converter Descriptor):面向发现的中立元数据
描述符是"服务器拥有"(server-owned)的发现元数据,字段如下:
| 字段 | 说明 |
|---|---|
Id | 稳定的语义标识(持久化公共契约) |
SourceType | 支持的源 CLR 类型 |
ResultType | 声明的结果 CLR 类型 |
DisplayName | 可发现的展示文本 |
Description | 可选的可发现描述 |
SettingsSchema | 可选的克隆 JSON Schema |
关键点:API 投影时,CLR 类型会被替换为注册的类型别名或安全类型名,并省略所有服务注册数据(生命周期、实现类型、keyed 服务细节一律不外泄)。参考 API 客户端模型 src/clients/Elsa.Api.Client/Resources/OutputConverters/Models/OutputConverterDescriptor.cs。
兼容性判定在FindCompatible中实现:描述符兼容当且仅当SourceType.IsAssignableFrom(声明的输出类型)且ResultType可赋值给目标类型(对Nullable<T>目标做了底层类型展开),见 OutputConverterRegistry.cs。
五、转换上下文(Output Conversion Context):极简且不可变
每次转换调用,转换器只拿到一个不可变的上下文:
| 成员 | 说明 |
|---|---|
Value | 非空的原生活动输出 |
SourceType | 声明的活动输出类型 |
DestinationType | 解析出的声明目标类型 |
Settings | 不可变 / 克隆后的 JSON 设置 |
上下文里既没有工作流执行对象,也没有服务提供者(FR-021/FR-022)——这从机制上杜绝了转换器偷偷修改工作流状态或执行"服务定位"式取依赖。依赖只能通过构造函数注入获得。
仓库实现OutputConversionContext同样在构造时克隆Settings(见 OutputConversionContext.cs)。转换器接口契约(见 IOutputConverter.cs):
public interface IOutputConverter { object? Convert(OutputConversionContext context); IEnumerable<string> ValidateSettings(JsonElement? settings) => []; }实现参考NumberToTextConverter的典型写法:从context.Settings读取format,用CultureInfo.InvariantCulture显式格式化,不做任何 I/O 与状态变更(完整示例见 doc/wiki/output-converters.md)。
六、目标(Destination):变量或工作流输出的统一视图
目标对象的四个字段:
| 字段 | 说明 |
|---|---|
Id | 内存引用或工作流输出身份 |
Type | 解析后的 CLR 类型 |
AllowsNull | 引用类型与Nullable<T>为true,其他值类型为false |
Kind | Variable或WorkflowOutput |
目标的解析分两个层面:
- 定义期解析:从最近的变量容器作用域逐级向外查找,找不到再查工作流输出;
- 运行时解析:使用已声明的内存块元数据(memory-block metadata)。
由于 Elsa 目前没有变量/工作流输出的可空引用元数据,AllowsNull完全由 CLR 可表示性决定(引用类型或Nullable<T>允许 null),这是 research.md 中记录的设计决策,避免把本特性扩成全局可空性模型。
七、输出转换错误(Output Conversion Error):结构化、隐私安全的故障
转换失败不会静默吞掉,而是抛出专门的OutputConversionException(Exceptions/OutputConversionException.cs),字段如下:
| 字段 | 说明 |
|---|---|
ConverterId | 配置的转换器 ID |
Stage | 失败阶段(见下) |
ActivityId/ActivityType | 产生输出的活动身份与类型 |
OutputName | 声明的输出名 |
DestinationId | 可选的目标 ID |
SourceTypeName/DestinationTypeName | 声明类型的安全名称 |
InnerException | 可选的原始转换器异常 |
失败阶段枚举(Enums/OutputConversionFailureStage.cs):
Resolution | SettingsValidation | SourceCompatibility | Invocation | ResultValidation安全策略有两条硬约束:
- 只有安全字段会被复制到持久化的异常元数据(
GetSafeMetadata只导出 ID、阶段、活动信息、输出名、类型名); - 原生值与原始设置永不进入异常消息——默认消息仅包含 Converter ID、阶段、输出名与活动 ID,防止敏感数据泄漏(FR-032)。
在OutputConverterInvoker中可以看到完整的五阶段编排:解析注册 → 校验源兼容性与结果可赋值性 → 从活动作用域解析 keyed 服务 → 校验设置(JSON Schema + 转换器自有校验)→ 调用Convert→ 校验结果与可空性,任一步失败都生成带阶段信息的OutputConversionException(见 OutputConverterInvoker.cs)。
八、状态转换:一条清晰的赋值决策路径
data-model.md 给出的状态机完整描述了从绑定到写入的全过程:
Unconfigured Binding └─ assign native value using existing path Configured Binding ├─ record native output ├─ native null → validate destination nullability → write null └─ non-null ├─ resolve registration and destination ├─ validate source, destination, and settings ├─ invoke converter ├─ validate result and nullability ├─ success → write Bound Value └─ failure → fault activity; destination unchanged几个必须强调的语义:
- 先记录、后转换:原生值先写入活动输出寄存器,即便随后转换失败,诊断里仍能看到原生值(research.md 明确这是选择
ActivityExecutionContext.Set作为编排点的理由); - null 绕过:原生值为 null 时直接跳过
Convert调用(FR-008),仅在目标允许 null 时才写入 null; - 原子写入:转换与结果校验全部完成之后才写目标,因此失败时目标保持原值不变(FR-010/FR-011);
- 失败即故障:失败通过 Elsa 常规的活动故障管道上报(不引入转换器专属重试机制),同时保留原始异常作为内层异常(FR-029/FR-031)。
这一流程在 ActivityExecutionContext.cs 中有完整代码印证:先RecordActivityOutput记录原生值,再解析 destination,处理 null 分支,随后调用 invoker 完成转换链。
九、定义期校验与运行时复核:双重防线
为确保"持久化工作流活得比部署长",系统在两个时点执行同样的安全校验(FR-026/FR-027):
- 定义接受/物化时:
ValidateOutputConverters作为WorkflowDefinitionValidating通知处理器,遍历物化后的工作流图,对每个带Converter的绑定依次校验:ID 非空 → 目标可解析 → 转换器已注册 → 源类型兼容 → 结果类型可赋值 → 设置校验通过,任何问题都以可操作的验证错误返回(见 ValidateOutputConverters.cs); - 运行时赋值时:
OutputConverterInvoker重复全部安全检查,专门捕获"校验时注册过、运行时已删除/被不兼容替换"这类部署注册漂移。
十、API 发现与 Studio:服务器拥有的描述符查询
转换器的发现完全由服务器端持有,Studio 不维护硬编码目录(FR-036)。查询端点:
GET /descriptors/output-converters?sourceType=Decimal&destinationType=String- 两个查询参数均为必填的注册类型别名或可解析的安全类型名;缺失或不可解析返回
400; - 授权要求
read:*或read:output-converters(见 Endpoint.cs); - 响应仅包含:
id、sourceTypeName、resultTypeName、displayName、description、可选的settingsSchema——绝不包含实现类型、实例、服务键、生命周期或工作流里的设置值; - API 客户端契约
IOutputConvertersApi.ListAsync以SourceType/DestinationType查询并镜像安全描述符形状(见 src/clients/Elsa.Api.Client/Resources/OutputConverters/Contracts/IOutputConvertersApi.cs)。
Studio 端遵循完整作者工作流:选择目标 → 按声明类型过滤兼容转换器 → 有 Schema 时用 schema 驱动的字段编辑器、无 Schema 时用原始 JSON 编辑器 → 保存/重开不丢配置 → 清除转换器后恢复未转换赋值行为。版本偏斜也有明确约定:旧服务器不提供发现能力时,Studio 隐藏/禁用新控件但不删除已持久化的转换器配置;未知的持久化 Converter ID 保持可见并标注校验状态(详见 contracts/studio-contract.md 与 contracts/rest-api.md)。
十一、测试覆盖与边界情况
仓库的单元测试与组件测试为该特性提供了完整验证网,可作为阅读源码的路线图:
- 注册与唯一性:test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputConverterRegistrationTests.cs、OutputConverterRegistryTests.cs;
- 调用链与阶段错误:OutputConverterInvokerTests.cs、OutputConversionExceptionStateTests.cs;
- 运行时边界:ActivityExecutionContextOutputConversionTests.cs;
- 目标解析:OutputBindingDestinationResolverTests.cs;
- 定义期校验:test/unit/Elsa.Workflows.Management.UnitTests/Handlers/Notifications/ValidateOutputConvertersTests.cs;
- API 端点:test/unit/Elsa.Workflows.Api.UnitTests/OutputConverters/OutputConverterEndpointTests.cs;
- 端到端场景与 API 客户端:test/component/Elsa.Workflows.ComponentTests/Scenarios/OutputConverters/OutputConverterTests.cs。
data-model.md 与 spec.md 明确要求测试覆盖:序列化、类型兼容、可空性、生命周期、隐私、重放/重试行为、API 发现、Studio 往返、版本偏斜以及"未配置转换器的原路径不变"(FR-042)。边界情况还包括:配置了转换器但没有变量/工作流输出目标、目标类型为object/可空值类型/不可空值类型/未知类型、非空输入转换为 null、源类型是基类/接口、结果声明不可赋值给目标、两次注册仅大小写不同、设置缺失/为空/畸形/违反 Schema、旧客户端编辑含转换器配置的工作流、以及尝试配置多个转换器或 open-generic 转换器。
十二、操作建议与设计约束
从 doc/wiki/output-converters.md 提炼的实战纪律:
- ID 是持久化公共契约:查找必须大小写敏感;行为、设置或结果语义发生破坏性变更时,换一个新版本化 ID,绝不复用旧 ID;
- 确定性优先:locale、时区、舍入等环境性选择必须显式化为设置项;转换器内不得执行 I/O、不得变更工作流状态;
- 把移除当部署漂移:已发布工作流引用的转换器被移除后,赋值时活动会按正常故障管道报错,目标保持原值、原生输出仍可诊断;
- 异步或有副作用请换姿势:需要异步或副作用的变换,应使用活动输入或显式活动完成,而不是输出转换器。
设计决策的完整论证记录在 ADR 0011(绑定期同步转换)、ADR 0012(显式稳定标识) 与 ADR 0013(服务器拥有发现) 中。
小结
从数据模型视角看,Elsa 3 的输出转换器是一套克制而完整的设计:绑定只新增一个可选关系、配置只持久化 ID 与设置、上下文只携带四个不可变字段、错误只导出结构化安全元数据。它把"显式、同步、确定性、可发现、隐私安全"五条原则落到了每个数据实体上,同时通过"无配置路径零开销、行为不变"保证了向后兼容。无论你是要注册自定义转换器的扩展开发者,还是想在设计器中配置转换的工作流作者,都可以以本文的数据模型为索引,直接进入 specs/012-output-converters 目录下的 spec、contracts 与测试继续深入。
- 后端
- 工作流自动化
- 流程编排
- 低代码
【免费下载链接】elsa-core
The Workflow Engine for .NET
相关推荐
Elsa 3 输出转换器(Output Converters)完全指南:在绑定边界同步、显式、可发现地转换 Activity 输出
Elsa 3 输出转换器(Output Converters)完全指南:在绑定边界同步、显式、可发现地转换 Activity 输出 本篇技术指南聚焦 Elsa
后端工作流自动化流程编排低代码Elsa Workflows 输出转换同步绑定机制:Activity Output 与 Bound Value 的边界设计深度解析
Elsa Workflows 输出转换同步绑定机制:Activity Output 与 Bound Value 的边界设计深度解析 导读 本文基于 Elsa W
后端工作流自动化流程编排低代码ThingsBoard 数据转换器 v2:simple-json 解码器输出(Decoder Output)格式深度解析
ThingsBoard 数据转换器 v2:simple json 解码器输出(Decoder Output)格式深度解析 在 ThingsBoard 的集成(I
物联网后端数据可视化消息队列
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考