5.Elsa源码探索-表达式引擎-Expression
一、这个模块是做什么的?
如果说工作流是一条流水线,活动是流水线上的每一道工序,那表达式引擎就是工序中的"万能转换器"——它让每个活动的输入属性可以在运行时动态求值,而不是设计时写死。分支条件判断、变量取值赋值...凡是需要"活"数据的地方,背后都是表达式引擎在工作。
用一句话概括:Elsa.Expressions 把"活动的参数"从静态常量变成了可以动态计算的表达式。
Elsa 引入了注册表抽象——每种语言实现 IExpressionHandler 并注册 ExpressionDescriptor,框架只认接口,不关心背后是 JavaScript 还是 C#。这让 Elsa 官方在不改动核心代码的前提下,内置了四种求值后端:
| 后端 | 表达式类型 | 适用场景 |
|---|---|---|
| Jint | JavaScript |
逻辑运算、变量读写、通用脚本 |
| Fluid | Liquid |
文本 / HTML 模板渲染 |
| Roslyn | CSharp |
强类型 C# 脚本 |
| IronPython | Python |
Python 脚本 |
二、核心数据结构
Expression — 一个表达式就是"类型 + 值"
// Elsa.Expressions/Models/Expression.cs
public class Expression
{
public string Type { get; set; } // "Literal" / "JavaScript" / "Liquid" / 自定义
public object? Value { get; set; } // 表达式原文字符串(或对象)
}
Type 字段是查找注册表的键,Value 是传递给 Handler 的原始内容。
ExpressionDescriptor — 描述一种表达式类型
// Elsa.Expressions/Models/ExpressionDescriptor.cs
public class ExpressionDescriptor
{
public string Type { get; init; } // 类型名
public string DisplayName { get; set; } // UI 显示名
public bool IsSerializable { get; set; } = true; // 是否可序列化
public bool IsBrowsable { get; set; } = true; // 是否在 UI 中展示
public Func<IServiceProvider, IExpressionHandler> HandlerFactory { get; set; } // 求值器工厂
public Func<MemoryBlockReference> MemoryBlockReferenceFactory { get; set; } // 内存块引用工厂
public Func<ExpressionSerializationContext, Expression> Deserialize { get; set; } // 反序列化函数
}
每种表达式类型在启动时向 IExpressionDescriptorRegistry 注册一个描述符,相当于"插件声明"。
三、求值流程
IExpressionEvaluator — 统一求值入口
// Elsa.Expressions/Services/ExpressionEvaluator.cs
public class ExpressionEvaluator(IExpressionDescriptorRegistry registry, IServiceProvider serviceProvider)
: IExpressionEvaluator
{
public async ValueTask<object?> EvaluateAsync(
Expression expression, Type returnType,
ExpressionExecutionContext context,
ExpressionEvaluatorOptions? options = null)
{
var expressionType = expression.Type;
// 1. 从注册表查找描述符
var expressionDescriptor = registry.Find(expressionType);
if (expressionDescriptor == null)
throw new($"Could not find a descriptor for expression type \"{expressionType}\".");
// 2. 用工厂创建 Handler(每次按需实例化)
var handler = expressionDescriptor.HandlerFactory(serviceProvider);
options ??= new();
// 3. 委托给具体 Handler 求值
return await handler.EvaluateAsync(expression, returnType, context, options);
}
}
整个流程:调用方 → IExpressionEvaluator.EvaluateAsync → 查注册表找 IExpressionHandler → 执行求值 → 返回结果。
四、内存模型:MemoryRegister / MemoryBlock
Elsa 的表达式执行不直接操作 CLR 变量,而是通过一套虚拟内存寻址系统传递数据,使得同一套变量可以在不同活动之间共享和隔离。
MemoryBlock — 最小内存单元
// Elsa.Expressions/Models/MemoryBlock.cs
public class MemoryBlock
{
public object? Value { get; set; }
public IDictionary<object, object> Metadata { get; set; } = new Dictionary<object, object>();
}
MemoryRegister — 一个作用域内的"变量表"
// Elsa.Expressions/Models/MemoryRegister.cs
public class MemoryRegister
{
public IDictionary<string, MemoryBlock> Blocks { get; }
// 按引用声明一个内存块(懒初始化)
public MemoryBlock Declare(MemoryBlockReference blockReference) { ... }
// 按 ID 查找
public bool TryGetBlock(string id, out MemoryBlock block) { ... }
}
ExpressionExecutionContext — 求值上下文(支持父子链)
// Elsa.Expressions/Models/ExpressionExecutionContext.cs
public class ExpressionExecutionContext(
IServiceProvider serviceProvider,
MemoryRegister memory,
ExpressionExecutionContext? parentContext = null, // 父上下文(支持作用域链)
IDictionary<object, object>? transientProperties = null,
Action? onChange = null,
CancellationToken cancellationToken = default)
{
// 向上遍历父链查找变量
private MemoryBlock? GetBlockInternal(MemoryBlockReference blockReference)
{
var currentContext = this;
while (currentContext != null)
{
if (currentContext.Memory.TryGetBlock(blockReference.Id, out var block))
return block;
currentContext = currentContext.ParentContext; // 作用域向上查找
}
return null;
}
}
这套设计非常类似于编程语言的词法作用域(Lexical Scope):内层找不到就往外层找,直到根上下文。
五、JavaScript 求值后端(Jint)
JintJavaScriptEvaluator
Elsa 使用 Jint 这个纯 .NET 的 JavaScript 解释器,不依赖 V8 或 Node.js。
// Elsa.Expressions.JavaScript/Services/JintJavaScriptEvaluator.cs
public class JintJavaScriptEvaluator(
IConfiguration configuration,
INotificationSender mediator, // 通知钩子
IOptions<JintOptions> scriptOptions,
IMemoryCache memoryCache) // 预编译脚本缓存
: IJavaScriptEvaluator
{
public async Task<object?> EvaluateAsync(string expression, Type returnType,
ExpressionExecutionContext context, ...)
{
var engine = await GetConfiguredEngine(configureEngine, context, options, cancellationToken);
// 触发通知:EvaluatingJavaScript(允许外部注入变量/函数)
await mediator.SendAsync(new EvaluatingJavaScript(engine, context, expression), cancellationToken);
var result = ExecuteExpressionAndGetResult(engine, expression);
// 触发通知:EvaluatedJavaScript(结果后处理)
await mediator.SendAsync(new EvaluatedJavaScript(engine, context, expression, result), cancellationToken);
return result.ConvertTo(returnType);
}
}
脚本预编译缓存(避免每次解析 AST):
private Prepared<Script> GetOrCreatePrepareScript(string expression)
{
var cacheKey = "jint:script:" + Hash(expression); // SHA256 哈希作为缓存键
return memoryCache.GetOrCreate(cacheKey, entry =>
{
if (_jintOptions.ScriptCacheTimeout.HasValue)
entry.SetSlidingExpiration(_jintOptions.ScriptCacheTimeout.Value);
return PrepareScript(expression); // 解析为 Jint Prepared<Script>
})!;
}
引擎配置扩展点(JintOptions):
| 配置项 | 类型 | 说明 |
|---|---|---|
AllowClrAccess |
bool |
是否允许脚本访问任意 CLR 类型(不受信任的工作流请勿开启) |
AllowConfigurationAccess |
bool |
是否注入 getConfig(key) 函数(不受信任的工作流请勿开启) |
ScriptCacheTimeout |
TimeSpan? |
预编译脚本缓存过期时间,默认 1 天,null 为永不过期 |
DisableWrappers |
bool |
禁用 getMyVar() 语法糖,只保留 getVariable("MyVar") |
DisableVariableCopying |
bool |
禁用变量自动同步,提升性能但无法用 variables.xxx 语法 |
ConfigureEngineOptionsCallback |
Action<Options>? |
深度定制 Jint 引擎选项(沙箱、超时等) |
ConfigureEngineCallback |
Action<Engine>? |
获得 Engine 实例,可手动注入全局变量/函数 |
通知钩子机制允许外部(如 Elsa.Workflows.Core)在求值前往引擎注入工作流变量:
EvaluatingJavaScript 通知
└── Handler: 注入 variables.xxx、getVariable()、setVariable() 等全局函数
JavaScript 内置函数完整列表
Jint 引擎启动时,以下函数通过 ConfigureEngineWithCommonFunctions 和 ConfigureEngineWithVariablesAndInputOutputAccessors 两个 Handler 自动注入,无需任何 import。
工作流上下文
| 函数 | 返回类型 | 说明 |
|---|---|---|
getWorkflowDefinitionId() |
string |
获取工作流定义 ID(DefinitionId) |
getWorkflowDefinitionVersionId() |
string |
获取工作流定义当前版本记录 ID |
getWorkflowDefinitionVersion() |
number |
获取工作流定义版本号 |
getWorkflowInstanceId() |
string |
获取当前工作流实例 ID |
getCorrelationId() |
string? |
获取关联 ID |
setCorrelationId(id) |
void |
设置关联 ID |
getWorkflowInstanceName() |
string? |
获取工作流实例名称 |
setWorkflowInstanceName(name) |
void |
设置工作流实例名称 |
变量与输入输出
| 函数 | 返回类型 | 说明 |
|---|---|---|
getVariable(name) |
any |
按名称获取当前作用域内的变量 |
setVariable(name, value) |
void |
设置当前作用域内的变量 |
getInput(name) |
any |
获取工作流输入参数 |
getOutputFrom(activityId, outputName?) |
any |
获取指定活动的输出值 |
getLastResult() |
any |
获取上一个活动的返回值 |
getXxx() |
any |
语法糖:等价于 getVariable("Xxx"),变量名 PascalCase 自动生成 |
setXxx(value) |
void |
语法糖:等价于 setVariable("Xxx", value) |
getXxxFromYyy() |
any |
语法糖:获取活动 Yyy 的输出 Xxx,如 getStatusCodeFromHttpRequest() |
变量访问的三种方式(优先推荐第三种,最简洁):
getVariable("MyVar") // 方式一:通用函数 variables.MyVar // 方式二:variables 容器(求值前自动同步) getMyVar() // 方式三:语法糖(DisableWrappers=false 时可用)
字符串工具
| 函数 | 返回类型 | 说明 |
|---|---|---|
isNullOrWhiteSpace(str) |
boolean |
等价于 string.IsNullOrWhiteSpace |
isNullOrEmpty(str) |
boolean |
等价于 string.IsNullOrEmpty |
JSON
| 函数 | 返回类型 | 说明 |
|---|---|---|
toJson(obj) |
string |
序列化为 JSON 字符串(枚举输出字符串而非数字,支持全 Unicode) |
GUID
| 函数 | 返回类型 | 说明 |
|---|---|---|
newGuid() |
Guid |
生成新 GUID 对象 |
newGuidString() |
string |
生成新 GUID 字符串,如 "a1b2c3..." |
newShortGuid() |
string |
生成 Base64 短 GUID(去除 /+= 字符) |
parseGuid(str) |
Guid |
将字符串解析为 GUID 对象 |
编码与二进制
| 函数 | 返回类型 | 说明 |
|---|---|---|
bytesToString(bytes) |
string |
byte[] → UTF-8 字符串 |
bytesFromString(str) |
byte[] |
UTF-8 字符串 → byte[] |
bytesToBase64(bytes) |
string |
byte[] → Base64 字符串 |
bytesFromBase64(str) |
byte[] |
Base64 字符串 → byte[] |
stringToBase64(str) |
string |
UTF-8 字符串 → Base64 字符串 |
stringFromBase64(str) |
string |
Base64 字符串 → UTF-8 字符串 |
streamToBytes(stream) |
byte[] |
Stream → byte[] |
streamToBase64(stream) |
string |
Stream → Base64 字符串 |
注册的 .NET 类型(可直接在 JS 中使用)
// 这些 CLR 类型通过 engine.RegisterType<T>() 注入,可直接构造和调用静态方法
new DateTime(2026, 1, 1)
new DateTimeOffset(...)
new TimeSpan(1, 0, 0) // 1小时
new Guid("...")
new Random().nextInt(1, 100) // 注意:Jint 方法名首字母小写
HTTP 模块扩展函数(需引入 Elsa.Http)
| 函数 | 说明 |
|---|---|
createEventTriggerUrl(eventName) |
生成事件触发 URL(永久有效) |
createEventTriggerUrl(eventName, lifetime: TimeSpan) |
生成有时效限制的事件触发 URL |
createEventTriggerUrl(eventName, expiresAt: DateTimeOffset) |
生成指定过期时间的事件触发 URL |
六、Liquid 模板求值后端(Fluid)
Elsa 使用 Fluid 实现 Liquid 模板语法,适合输出 HTML/文本模板。
LiquidTemplateManager
// Elsa.Expressions.Liquid/Services/LiquidTemplateManager.cs
public class LiquidTemplateManager : ILiquidTemplateManager
{
public async Task<string?> RenderAsync(string template,
ExpressionExecutionContext expressionExecutionContext,
CancellationToken cancellationToken = default)
{
// 1. 从内存缓存中获取已解析的模板对象(IFluidTemplate)
var result = GetCachedTemplate(template);
// 2. 构建模板上下文,并触发 RenderingLiquidTemplate 通知(允许外部注入变量)
var templateContext = await CreateTemplateContextAsync(expressionExecutionContext, cancellationToken);
// 3. 渲染模板
return await result.RenderAsync(templateContext, _options.Encoder);
}
private IFluidTemplate GetCachedTemplate(string source)
{
return _memoryCache.GetOrCreate(source, entry =>
{
// 解析失败时返回包含错误信息的原始模板(不抛异常,降级显示)
if (!TryParse(source, out var parsed, out var error))
{
error = "{% raw %}\n" + error + "\n{% endraw %}";
TryParse(error, out parsed, out error);
entry.SetSlidingExpiration(TimeSpan.FromMilliseconds(100)); // 错误模板短缓存
return parsed;
}
entry.SetSlidingExpiration(TimeSpan.FromSeconds(30));
return parsed;
})!;
}
}
关键设计:解析失败不抛异常,而是将错误信息包裹在 {% raw %} 块中返回,保证工作流不因模板语法错误而崩溃。
Liquid 内置变量完整列表
Liquid 模板求值前,ConfigureLiquidEngine Handler 向 TemplateContext 注入以下上下文变量,可在模板中直接使用 {{ Variable }} 语法访问。
工作流上下文变量
| 变量 | 示例 | 说明 |
|---|---|---|
{{ WorkflowDefinitionId }} |
"wf-order-process" |
工作流定义 ID |
{{ WorkflowDefinitionVersionId }} |
"abc123" |
工作流定义当前版本记录 ID |
{{ WorkflowDefinitionVersion }} |
3 |
工作流定义版本号 |
{{ WorkflowInstanceId }} |
"inst-456" |
当前工作流实例 ID |
{{ CorrelationId }} |
"order-789" |
关联 ID(可为空) |
工作流变量与输入
| 变量 | 示例 | 说明 |
|---|---|---|
{{ Variables.OrderId }} |
"ORD-001" |
按名称访问工作流变量 |
{{ Input.UserId }} |
"user-42" |
按名称访问工作流输入参数 |
配置访问(需开启 AllowConfigurationAccess)
{# 访问 appsettings.json 中的配置节 #}
{{ Configuration.MyApp.ApiUrl }}
Liquid vs JavaScript 的选型建议:
- 需要生成 HTML/邮件正文/文本模板 → 用 Liquid,语法更安全、可读性好
- 需要逻辑运算、条件判断、调用函数 → 用 JavaScript,表达能力更强
七、C# 求值后端(Roslyn)
Elsa 还提供了基于 Microsoft.CodeAnalysis(Roslyn) 的 C# 表达式后端,与 JavaScript 后端并列,适合已有大量 C# 知识的开发者或需要强类型安全的场景。
CSharpEvaluator 核心流程
// Elsa.Expressions.CSharp/Services/CSharpEvaluator.cs
public class CSharpEvaluator(INotificationSender notificationSender, IOptions<CSharpOptions> scriptOptions, IMemoryCache memoryCache)
: ICSharpEvaluator
{
public async Task<object?> EvaluateAsync(string expression, Type returnType, ExpressionExecutionContext context, ...)
{
// 1. 创建全局变量(Globals 持有 ExecutionContextProxy)
var globals = new Globals(context, options.Arguments);
var script = CSharpScript.Create("", scriptOptions, typeof(Globals));
// 2. 触发通知:EvaluatingCSharp(注入变量访问代码、引用程序集等)
var notification = new EvaluatingCSharp(options, script, scriptOptions, context);
await notificationSender.SendAsync(notification, cancellationToken);
// 3. 追加用户表达式,编译并执行
script = notification.Script.ContinueWith(expression, notification.ScriptOptions);
var runner = GetCompiledScript(script); // SHA256 缓存已编译脚本
return await runner(globals, cancellationToken: cancellationToken);
}
}
在 C# 表达式中访问工作流数据
GenerateWorkflowVariableAccessors Handler 在求值前自动生成 WorkflowVariablesProxy(内嵌 class),并注入 Variable 和 Variables 两个全局变量:
// 访问变量(强类型)
Variable.OrderId // 等价于 GetVariable<string>("OrderId")
Variable.Amount // 等价于 GetVariable<decimal>("Amount")
Variables.Get<int>("Age") // 泛型版本
// 访问工作流输入
ExecutionContext.GetInput<string>("UserId")
// 直接返回值(C# 表达式的最后一个语句的值即为结果)
Variable.Price * 1.1
C# 后端同样使用
IMemoryCache对编译结果做 SHA256 缓存,避免重复调用 Roslyn 编译器。
八、Python 求值后端(IronPython)
Elsa 通过 Elsa.Expressions.Python 模块集成 IronPython 提供 Python 表达式支持,架构与 JavaScript/C# 一致:PythonExpressionHandler 实现 IExpressionHandler,委托 IPythonEvaluator 执行。
// Elsa.Expressions.Python/Expressions/PythonExpressionHandler.cs
public async ValueTask<object?> EvaluateAsync(Expression expression, Type returnType,
ExpressionExecutionContext context, ExpressionEvaluatorOptions options)
{
var pythonExpression = expression.Value.ConvertTo<string>() ?? "";
return await _evaluator.EvaluateAsync(pythonExpression, returnType, context);
}
Python 后端适合已有 Python 脚本积累的团队,但 IronPython 与 CPython 存在一定兼容性差异,性能也弱于 Jint,一般场景建议优先考虑 JavaScript 或 C#。
九、注册机制
每种表达式后端通过 IExpressionDescriptorProvider 向注册表贡献描述符:
// 注册 JavaScript 表达式
services.AddExpressionDescriptorProvider<JavaScriptExpressionDescriptorProvider>();
// 注册 Liquid 表达式
services.AddExpressionDescriptorProvider<LiquidExpressionSyntaxProvider>();
启动时 IExpressionDescriptorRegistryPopulator 遍历所有 Provider,统一填充 IExpressionDescriptorRegistry。
十、内置表达式类型
| 类型名 | 模块 | 求值引擎 | 说明 |
|---|---|---|---|
Literal |
Elsa.Expressions |
无(直接返回) | 字面量,原样返回字符串 |
JavaScript |
Elsa.Expressions.JavaScript |
Jint | 使用纯 .NET JS 解释器求值 |
Liquid |
Elsa.Expressions.Liquid |
Fluid | 使用 Liquid 模板语法渲染 |
CSharp |
Elsa.Expressions.CSharp |
Roslyn | 使用 Roslyn 动态编译 C# 脚本 |
Python |
Elsa.Expressions.Python |
IronPython | 使用 IronPython 执行 Python 代码 |
Object |
Elsa.Expressions |
无(反序列化) | JSON 对象字面量,反序列化为目标类型 |
Variable |
Elsa.Workflows.Core |
无(内存查找) | 工作流变量引用(MemoryBlockReference) |
Output |
Elsa.Workflows.Core |
无(内存查找) | 其他活动输出值引用 |
十一、整体流程图
活动需要求值某个 Input 属性
│
▼
IExpressionEvaluator.EvaluateAsync(expression, returnType, context)
│
├─ 查 IExpressionDescriptorRegistry(按 expression.Type)
│ │
│ └── ExpressionDescriptor.HandlerFactory(serviceProvider)
│ │
│ ▼
│ IExpressionHandler(具体实现)
│ ├── LiteralHandler: 直接返回 expression.Value
│ ├── JintJavaScriptEvaluator: Jint 引擎求值
│ ├── LiquidExpressionHandler: Fluid 渲染
│ ├── CSharpExpressionHandler: Roslyn 编译执行
│ └── PythonExpressionHandler: IronPython 执行
│
▼
返回 object? → ConvertTo<T>() → 赋值给活动属性
│
├─ 查 IExpressionDescriptorRegistry(按 expression.Type)
│ │
│ └── ExpressionDescriptor.HandlerFactory(serviceProvider)
│ │
│ ▼
│ IExpressionHandler(具体实现)
│ ├── LiteralHandler: 直接返回 expression.Value
│ ├── JintJavaScriptEvaluator: Jint 引擎求值
│ ├── LiquidExpressionHandler: Fluid 渲染
│ ├── CSharpExpressionHandler: Roslyn 编译执行
│ └── PythonExpressionHandler: IronPython 执行
│
▼

浙公网安备 33010602011771号