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 引擎启动时,以下函数通过 ConfigureEngineWithCommonFunctionsConfigureEngineWithVariablesAndInputOutputAccessors 两个 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),并注入 VariableVariables 两个全局变量:

// 访问变量(强类型)
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 执行

posted @ 2026-04-25 11:33  叨奈特挖井人  阅读(29)  评论(0)    收藏  举报