☰
OrchardCore 工作流活动(Workflow Activity)开发完全指南:从基类、生命周期到显示驱动与表达式求值
2026/9/27 9:01:30 网站建设 项目流程
  • CMS
  • 后端
  • Web框架

【免费下载链接】OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.

项目地址:https://gitcode.com/gh_mirrors/or/OrchardCore
点击查看免费下载

导读

本文是 Orchard Core 工作流模块中自定义活动(Activity)开发的实战参考,基于仓库内OrchardCore.Workflows模块与OrchardCore.Workflows.Abstractions项目的真实源码编写。你将掌握:任务型活动(Task)与事件型活动(Event)的区别与生命周期、GetProperty/SetProperty属性持久化机制、WorkflowExpression<T>表达式(Liquid/JavaScript)的求值方式、ActivityDisplayDriver显示驱动的形状(Shape)约定,以及如何通过AddActivity将活动注册进工作流运行时。读完本文,你可以直接照着源码写出一个带编辑器界面、可暂停/恢复、可被外部事件唤醒的自定义工作流活动。

从基类说起:任务与事件的分工

OrchardCore.Workflows.Abstractions/Activities目录下定义了活动的核心抽象。TaskActivity<TActivity>与EventActivity是两个最重要的基类,其区别决定了活动在运行时的行为:

类型命名空间用途
TaskActivity<TActivity>OrchardCore.Workflows.Activities类型化任务;Name默认取类型名
TaskActivity同上任务基类(Activity, ITask)
EventActivity同上事件基类(Activity, IEvent);Execute返回Halt()
ActivityDisplayDriver<TActivity>OrchardCore.Workflows.Display只提供缩略图与设计时形状
ActivityDisplayDriver<TActivity, TEditViewModel>同上额外提供编辑形状与模型映射

从源码看(TaskActivity.cs):

public abstract class TaskActivity : Activity, ITask; public abstract class TaskActivity<TActivity> : TaskActivity where TActivity : ITask { // 工作流定义中使用的技术名称,默认就是类型的 Name public override string Name => typeof(TActivity).Name; }

即:继承TaskActivity<LogTask>的活动,其技术名称自动就是LogTask,无需手工指定。而事件基类(EventActivity.cs)则直接覆盖了Execute:

public abstract class EventActivity : Activity, IEvent { public override ActivityExecutionResult Execute(WorkflowExecutionContext workflowContext, ActivityContext activityContext) { // 挂起工作流,等待事件发生 return Halt(); } }

这是"任务"与"事件"在行为上的本质分界:任务执行一次即完成,事件则把工作流挂起等待外部触发。

Activity抽象类(Activity.cs)是两者的共同祖先,它提供:

  • Properties:一个JsonObject类型的属性袋(bag),用于持久化活动状态;
  • GetProperty/SetProperty辅助方法:通过[CallerMemberName]自动以成员名为键读写Properties;
  • 静态帮助方法:Outcome/Outcomes/Halt/Noop,用于构造执行结果与可能的端口集合;
  • 虚方法:GetPossibleOutcomes、CanExecute(Async)、Execute(Async),供子类按需重写。

其中GetProperty<T>的签名值得注意:

protected virtual T GetProperty<T>(Func<T> defaultValue = null, [CallerMemberName] string name = null) { var item = Properties[name]; return item != null ? item.ToObject<T>() : defaultValue != null ? defaultValue() : default; }

Properties是以 JSON 形式存储的,因此任何可 JSON 序列化的类型都可以安全地作为活动属性。

任务(Task)生命周期:五个关键阶段

一个任务型活动从被设计师拖入画布到最终执行,会依次经历以下阶段:

  1. GetPossibleOutcomes:设计器与运行时都会调用它来获知该活动有哪些输出端口(ports);
  2. CanExecuteAsync(可选):执行前的门卫(gate),返回false则跳过本次执行;
  3. ExecuteAsync:真正干活的入口,返回Outcome("...")指示走向哪个端口;
  4. (如果返回了Halt())ResumeAsync稍后被调用:活动挂起后,由运行时在合适的时机恢复;
  5. 结果流转:ActivityExecutionResult决定工作流下一步走向。

以仓库中最简单的任务 LogTask.cs 为例,它完整演示了任务活动的骨架:

public class LogTask : TaskActivity<LogTask> { private readonly ILogger _logger; private readonly IWorkflowExpressionEvaluator _expressionEvaluator; protected readonly IStringLocalizer S; public LogTask(ILogger<LogTask> logger, IWorkflowExpressionEvaluator expressionEvaluator, IStringLocalizer<LogTask> localizer) { _logger = logger; _expressionEvaluator = expressionEvaluator; S = localizer; } public override LocalizedString DisplayText => S["Log Task"]; public override LocalizedString Category => S["Primitives"]; public LogLevel LogLevel { get => GetProperty(() => LogLevel.Information); set => SetProperty(value); } public WorkflowExpression<string> Text { get => GetProperty(() => new WorkflowExpression<string>()); set => SetProperty(value); } public override IEnumerable<Outcome> GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext) => Outcome(S["Done"]); public override async Task<ActivityExecutionResult> ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext) { var text = await _expressionEvaluator.EvaluateAsync(Text, workflowContext, null); var logLevel = LogLevel; _logger.Log(logLevel, 0, text, null, (state, error) => state.ToString()); return Outcome("Done"); } }

从中可以看到任务型活动的三个要点:

  • 通过构造函数注入ILogger、IWorkflowExpressionEvaluator、IStringLocalizer等服务;
  • 属性用GetProperty(() => 默认值)/SetProperty模式声明,保证未设置时返回非 null 的默认实例;
  • GetPossibleOutcomes返回本地化的端口名称,ExecuteAsync返回对应的字符串端口名。

事件(Event)生命周期:挂起、唤醒与恢复

事件型活动的生命周期与任务截然不同,其核心是"挂起-唤醒"模式:

  1. EventActivity.Execute返回Halt()——工作流挂起,实例状态持久化到数据库;
  2. 外部代码触发事件,例如调用IWorkflowManager.TriggerEventAsync;
  3. CanExecuteAsync决定当前挂起实例是否应当恢复;
  4. Resume/ResumeAsync返回输出端口,工作流继续向下执行。

仓库中最典型的例子是 SignalEvent.cs:

public class SignalEvent : EventActivity { public static string EventName => nameof(SignalEvent); private readonly IWorkflowExpressionEvaluator _expressionEvaluator; protected readonly IStringLocalizer S; public override string Name => EventName; public override LocalizedString DisplayText => S["Signal Event"]; public override LocalizedString Category => S["HTTP"]; public WorkflowExpression<string> SignalName { get => GetProperty(() => new WorkflowExpression<string>()); set => SetProperty(value); } public override async Task<bool> CanExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext) { var signalName = await _expressionEvaluator.EvaluateAsync(SignalName, workflowContext, null); return string.Equals(workflowContext.Input.GetValue<string>("Signal"), signalName, StringComparison.OrdinalIgnoreCase); } public override IEnumerable<Outcome> GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext) => Outcome(S["Done"]); public override ActivityExecutionResult Resume(WorkflowExecutionContext workflowContext, ActivityContext activityContext) => Outcome("Done"); }

它的工作方式很精妙:

  • CanExecuteAsync把活动上配置的SignalName与外部传入的Input["Signal"]做不区分大小写的比较,作为"是否该由本实例响应"的门卫;
  • Resume直接返回Outcome("Done"),表示事件命中后继续沿Done端口前进。

外部代码如何触发?IWorkflowManager接口(IWorkflowManager.cs)提供了入口:

Task<IEnumerable<WorkflowExecutionContext>> TriggerEventAsync(string name, IDictionary<string, object> input = null, string correlationId = null, bool isExclusive = false, bool isAlwaysCorrelated = false);

还提供了一个便捷扩展方法,把匿名对象转换为RouteValueDictionary作为 input 传入。correlationId用于把事件与特定业务实体(如内容项 ID)关联,isExclusive控制是否只恢复一个匹配实例。

另一个事件型活动的例子是 TimerEvent.cs,它演示了带持久化状态的事件:活动挂起后把StartedUtc写入Properties,ResumeAsync时用NCrontab解析CronExpression,通过IClock计算下一次触发时间,未到期就再次Halt(),到期则返回Outcome("Done")。默认表达式为"*/5 * * * *"(每 5 分钟),并支持UseLocalTime开关把站点设置的时区纳入计算。这正是"挂起状态在进程重启后依然有效"的绝佳例证。

属性持久化:GetProperty / SetProperty 的底层机制

活动属性持久化是活动开发中最常用的基础设施。其核心模式如下:

public WorkflowExpression<string> Text { get => GetProperty(() => new WorkflowExpression<string>()); set => SetProperty(value); }

需要记住的三条规则:

  • 键是成员名:通过[CallerMemberName]自动捕获属性名作为Properties字典的键,无需手工指定;
  • 以 JSON 存储:属性被序列化进Properties并随工作流实例落库,因此能跨越挂起/恢复乃至进程重启;
  • 提供默认工厂:GetProperty(() => new ...)让未设置的属性返回合理的非 null 默认值,避免空引用问题。

注意GetProperty<T>在不传默认工厂时会返回default(引用类型为 null),所以凡是会被直接使用的属性,都建议提供默认值工厂,这一点在TimerEvent.CronExpression(默认"*/5 * * * *")、LogTask.LogLevel(默认LogLevel.Information)等源码中处处可见。

表达式(Expression):让活动接受 Liquid 与 JavaScript

面向用户的输入应当声明为WorkflowExpression<T>,这样工作流作者就可以在编辑器中写 Liquid 模板或 JavaScript 脚本。WorkflowExpression<T>(WorkflowExpression.cs)本质上是包了一层原始模板字符串:

public class WorkflowExpression<T> { public WorkflowExpression() { } public WorkflowExpression(string expression) { Expression = expression; } public string Expression { get; set; } }

求值时注入两个求值器:

  • IWorkflowExpressionEvaluator(IWorkflowExpressionEvaluator.cs):用于Liquid表达式,签名如下:
Task<T> EvaluateAsync<T>(WorkflowExpression<T> expression, WorkflowExecutionContext workflowContext, TextEncoder encoder);
  • IWorkflowScriptEvaluator(IWorkflowScriptEvaluator.cs):用于JavaScript脚本,支持传入作用域方法提供者:
Task<T> EvaluateAsync<T>(WorkflowExpression<T> expression, WorkflowExecutionContext workflowContext, params IGlobalMethodProvider[] scopedMethodProviders);

典型用法(摘自 LogTask.cs):

var text = await _expressionEvaluator.EvaluateAsync(Text, workflowContext, null);

视图模型(View Model)通常暴露一个.Expression属性(即原始模板字符串),供编辑器直接绑定,驱动层负责在模型与活动之间搬运。

SetOutputTask(SetOutputTask.cs)更进一步演示了语法切换:它同时持有Value(JavaScript)与LiquidValue(Liquid)两个表达式,由Syntax属性(WorkflowScriptSyntax.JavaScript/WorkflowScriptSyntax.Liquid)决定用哪个求值器:

var value = Syntax switch { WorkflowScriptSyntax.Liquid => await _expressionEvaluator.EvaluateAsync(LiquidValue, workflowContext, null), WorkflowScriptSyntax.JavaScript => await _scriptEvaluator.EvaluateAsync(Value, workflowContext), _ => throw new NotSupportedException($"The syntax {Syntax} isn't supported for SetOutputTask.") }; workflowContext.Output[OutputName] = value;

这就是一个"写输出"任务的完整实现:把求值结果写入工作流上下文的Output字典。

输出端口(Outcomes)与 ActivityExecutionResult

端口是工作流连线的依据。活动需要在GetPossibleOutcomes中声明(本地化显示),并在Execute/Resume中按字符串名返回:

public override IEnumerable<Outcome> GetPossibleOutcomes(...) => Outcome(S["Yes"], S["No"]); public override async Task<ActivityExecutionResult> ExecuteAsync(...) => condition ? Outcome("Yes") : Outcome("No");

从 Activity.cs 的源码可以看到Outcome系列帮助方法的完整形态:Outcome(params LocalizedString[])返回IEnumerable<Outcome>用于声明,Outcome(params string[])/Outcome(params IEnumerable<string>)返回ActivityExecutionResult用于执行;旧的Outcomes(...)重载均已被标记[Obsolete],新代码应使用Outcome。

ActivityExecutionResult(ActivityExecutionResult.cs)是活动的"返回值"类型:

  • Outcomes(params string[])/Outcome(...)—— 沿指定端口继续执行;
  • Halt()—— 返回ActivityExecutionResult.Halted,IsHalted = true,工作流挂起;
  • Noop()—— 返回ActivityExecutionResult.Empty,不做任何事继续。

其内部实现非常直观:

public class ActivityExecutionResult { public static readonly ActivityExecutionResult Empty = new([]); public static readonly ActivityExecutionResult Halted = new([]) { IsHalted = true }; public IEnumerable<string> Outcomes { get; private set; } public bool IsHalted { get; private set; } }

工作流上下文数据:Input / Output / Properties

WorkflowExecutionContext(WorkflowExecutionContext.cs)是活动与整个工作流实例之间的数据通道,三个字典分工明确:

public sealed class WorkflowExecutionContext { public IDictionary<string, object> Input { get; } // 来自发起方 public IDictionary<string, object> Output { get; } // 回传给发起方 public IDictionary<string, object> Properties { get; } // 跨活动共享的状态 }

读取输入与写出输出的惯用法:

var signal = workflowContext.Input.GetValue<string>("Signal"); // 读输入 workflowContext.Output[OutputName] = value; // 写输出

此外,从构造函数可以看出上下文还携带WorkflowType、Workflow(实例)、已执行活动栈ExecutedActivities、LastResult以及按ActivityId索引的活动字典,为活动提供了完整的运行环境。SignalEvent正是通过workflowContext.Input.GetValue<string>("Signal")读取外部信号,而SetOutputTask通过workflowContext.Output[OutputName] = value把结果回传给发起方——读写两端的范例都在仓库源码中。

显示驱动(Display Driver)内部机制

每个活动通常配套一个显示驱动,负责在设计师画布上呈现缩略图、设计形状以及编辑表单。基类ActivityDisplayDriver<TActivity>与ActivityDisplayDriver<TActivity, TEditViewModel>(ActivityDisplayDriver.cs)从活动的技术名称推导出三种形状类型:

{ActivityName}_Fields_Thumbnail {ActivityName}_Fields_Design {ActivityName}_Fields_Edit

源码中用typeof(TActivity).Name静态缓存了ActivityName,并拼接出s_thumbnailShapeType与s_designShapeType;带视图模型的派生类再拼接出s_editShapeType:

protected static readonly string ActivityName = typeof(TActivity).Name; private static readonly string s_thumbnailShapeType = $"{ActivityName}_Fields_Thumbnail"; private static readonly string s_designShapeType = $"{ActivityName}_Fields_Design"; // 派生类中: private static readonly string s_editShapeType = $"{ActivityName}_Fields_Edit";

DisplayAsync把缩略图形状放在"Thumbnail"/"Content"位置、设计形状放在"Design"/"Content"位置;Edit用Initialize<TEditViewModel>(s_editShapeType, viewModel => EditActivityAsync(activity, viewModel))初始化编辑形状。

需要重写的映射钩子只有两个方向:

protected override void EditActivity(TActivity activity, TEditViewModel model) { /* activity -> model */ } protected override void UpdateActivity(TEditViewModel model, TActivity activity) { /* model -> activity */ } // 异步变体:EditActivityAsync, UpdateActivityAsync

值得注意:UpdateAsync已经替你完成了context.Updater.TryUpdateModelAsync(viewModel, Prefix)模型绑定,随后调用UpdateActivity,最后返回Edit(...)重新渲染编辑界面。也就是说,你只需要写双向映射逻辑,绑定与形状渲染都由基类完成。默认的EditActivity/UpdateActivity是空实现,未重写时编辑界面只显示空白表单,所以务必备齐这对钩子。

注册活动:AddActivity 与服务装配

活动写好后,需要在模块的Startup.ConfigureServices中注册:

services.AddActivity<LogTask, LogTaskDisplayDriver>(); services.AddActivity<SignalEvent, SignalEventDisplayDriver>();

AddActivity一次完成三件事:注册活动类型、注册其显示驱动、把活动加入WorkflowOptions。它要求两个类型参数——活动本身与配套的显示驱动(无驱动时可传只有一个泛型参数的版本)。

仓库中的实际用法可参考 OrchardCore.Workflows/Startup.cs(注册WorkflowFaultEvent等)与 Http/Startup.cs:

[Feature("OrchardCore.Workflows.Http")] public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { // ... services.AddActivity<HttpRequestEvent, HttpRequestEventDisplayDriver>(); services.AddActivity<HttpRedirectTask, HttpRedirectTaskDisplayDriver>(); services.AddActivity<SignalEvent, SignalEventDisplayDriver>(); // ... } }

注意:依赖 HTTP 功能或特定 Feature 的活动,应放在带[RequireFeatures(...)](如[Feature("OrchardCore.Workflows.Http")])的独立Startup中注册,仓库对OrchardCore.Workflows.Http正是这样处理的——这保证了功能开关关闭时相关活动不会被暴露到设计器中。

仓库中的实战范例清单

下表汇总了可直接对照学习的仓库范例,覆盖了从最小任务到带持久化状态事件的各类写法:

活动文件示范要点
LogTaskActivities/LogTask.cs最小任务、表达式求值、日志输出
NotifyTask+ 驱动OrchardCore.Workflows/Activities 与 Drivers任务与显示驱动的成对写法
SetOutputTaskActivities/SetOutputTask.cs写入Output、语法切换
SetPropertyTask同上目录写入Properties(跨活动共享状态)
SignalEventHttp/Activities/SignalEvent.cs事件门卫 + 恢复
TimerEventTimers/TimerEvent.cs带持久化状态的事件、Cron 调度

动手写一个活动的完整检查清单

综合以上源码分析,一个完整的自定义活动需要覆盖这些环节:

  1. 选择基类:一次性执行选TaskActivity<TActivity>;需要挂起等外部触发选EventActivity;
  2. 声明属性:全部用GetProperty(() => 默认值)/SetProperty模式,让属性可持久化、不返回 null;
  3. 设计输入:用户可编辑的输入用WorkflowExpression<T>,注入IWorkflowExpressionEvaluator(Liquid)和/或IWorkflowScriptEvaluator(JS)求值;
  4. 声明端口:在GetPossibleOutcomes中用Outcome(S["..."])(本地化)返回端口;
  5. 实现执行逻辑:在ExecuteAsync中干活并返回Outcome("...");需要挂起时返回Halt(),之后实现ResumeAsync;
  6. 读写上下文:用workflowContext.Input.GetValue<T>(key)读、workflowContext.Output[key] = value写;
  7. 编写驱动:继承ActivityDisplayDriver<TActivity, TEditViewModel>,实现EditActivity/UpdateActivity(或异步变体),配合{Name}_Fields_Edit.cshtml等视图;
  8. 注册:在Startup.ConfigureServices中调用services.AddActivity<TActivity, TDisplayDriver>();依赖特定功能的放到带[RequireFeatures(...)]的Startup中。

按照这个清单,对照LogTask、SetOutputTask、SignalEvent、TimerEvent四个范例源码,你就能完整掌握 Orchard Core 工作流活动开发的全部要点。

  • CMS
  • 后端
  • Web框架

【免费下载链接】OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.

项目地址:https://gitcode.com/gh_mirrors/or/OrchardCore
点击查看免费下载
上一篇:hashsigs-ts工程实践:tsup+Vitest构建并开源TypeScript密码学库的完整流程
下一篇:字符的视觉革命:ASCII艺术生成器探索指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询