一、搜"dotnet 工作流引擎",你会先搜到什么
场景很常见:.NET 系统里要加个审批流。请假、报销、采购,单子从申请人出发,走到部门领导,复杂一点的要会签、按比例通过、退回发起人改材料、抄送一把手。
你去搜,会搜到 Elsa Workflows、Workflow Core,再往外还有 Temporal、Camunda。这些名字都很强,但花一个下午读下来你会发现,它们的主场是编排:长事务、Saga、可视化活动流、分布式补偿。把它们请进一个 CRUD 系统里审批三张单子,就像开卡车去楼下取快递。
而你真正想要的,其实是件小事:一段审批语义,嵌进自己的系统,用自己已有的 MySQL,配一个能画流程的前端。
jeeflow 就是做这件小事的引擎:串行/并行/按比例会签、一票否决、退回发起人、委托代理、抄送------这套 OA 审批语义,引擎核心自己扛;业务方接进来只欠它两样东西:一张用户表(SPI 接口)、五张 wf_ 前缀的表。它之前已经在 Java/Go/Python/Node/PHP/Rust/MoonBit 七种语言上跑着,同一份 LogicFlow 流程 JSON 八门语言通用。今天的主角是刚收口的第 8 门语言:C#/.NET。
先把"它不是什么"也说清楚,省得你装错:
- 不是 BPM 平台:不带用户体系、不带表单引擎,审批人解析要你自己接组织架构(也就一个接口的事);
- 不带 UI :但有配套开源前端 jeeflow-ui,
?lang=csharp直连 C# demo 可用; - 数据库只支持 MySQL:内存仓储开箱即用(测试/内嵌场景),生产走 MySqlConnector 的 MySQL 仓储;
- 不碰你的业务表:表单数据落哪张表、哪些字段谁可见,由你配置,引擎只管流程本身。
安装就一行,四件东西按需拿:
bash
dotnet add package Mldong.Jeeflow.Facade # 统一门面(传递 Core + Persist)
dotnet add package Mldong.Jeeflow.Repository.MySql # 生产 MySQL 仓储(可选)
# 内嵌/测试场景只要 Core 一个包:零第三方依赖
下一节直接跑。
二、5 分钟跑通一条审批流:新工程实录
光说不练是伪代码,下面是一个全新 console 工程 的真实记录------dotnet new console 之后从 nuget.org 拉 Mldong.Jeeflow.Facade 1.0.1,到一条请假审批走完,全程 5 分钟(版本号 1.0.1 是写稿时 nuget.org 上的最新版)。
流程用联邦共享测试资产里最简单的一条(01-simple.json):开始 → 申请(assignee=applicant)→ 上级审批(assignee=leader)→ 结束。LogicFlow 格式,和 Java/Go/Python 等七门语言用的是同一份文件。
第 1 步:组装引擎
csharp
using System.Text;
using Mldong.Jeeflow.Core;
// 1. 内存仓储 + 服务上下文;引擎唯一的必填 SPI 是 IUserProvider(按 id 取用户)
var repo = new MemoryRepository();
var ctx = new ServiceContext(repo);
ctx.UserProvider = new FixedUserProvider();
// 2. 组装引擎
var engine = new JeeflowEngine(ctx);
IUserProvider 是引擎对"用户"的全部认知------一个方法 GetUserAsync(userId)。真实系统里换成查你的用户表即可,demo 里先来个内存版:
csharp
class FixedUserProvider : IUserProvider
{
public Task<IUserProvider.UserInfo?> GetUserAsync(string userId) =>
Task.FromResult<IUserProvider.UserInfo?>(new IUserProvider.UserInfo { UserId = userId });
}
第 2 步:部署一份流程定义
csharp
// 3. 部署流程定义:LogicFlow JSON,八门语言共用同一份
var content = File.ReadAllText("01-simple.json");
var define = new ProcessDefine
{
Id = 1, // 内存仓储不自增,主键应用侧生成
Name = "simple", DisplayName = "简单审批流程", Type = "approval",
State = 1, Version = 1,
Content = Encoding.UTF8.GetBytes(content),
};
await repo.SaveDefineAsync(define);
这里插一个我第一次跑就踩到的真坑 :Id 不赋值,后面按 id 发起会直接报"没有流程定义"。原因是联邦契约里那 5 张 wf_ 表没有自增主键,主键全部由应用侧雪花生成,内存仓储也不会替你补号------这个设计在任何一门语言里都一样。
第 3 步:发起、审批、走完
csharp
// 4. 发起:user1 提交请假。引擎只把流程推到第一个任务节点,不会替你办它
var inst = await engine.StartProcessInstanceByIdAsync(define.Id, "user1",
new FlowData { ["f_days"] = 3, ["f_reason"] = "年假" });
Console.WriteLine($"实例已发起:id={inst.InstanceId} state={inst.State}(10=进行中)");
// 5. 当前待办是"申请"节点,处理人是发起人自己(applicant 契约)
var apply = (await repo.FindDoingTasksAsync(inst.InstanceId!.Value, null))[0];
Console.WriteLine($"待办:{apply.TaskName} → {string.Join(",", await repo.FindTaskActorsAsync(apply.TaskId!.Value))}");
// 6. user1 提交申请(submitType=1 同意),流程推进到"上级审批"
await engine.ExecuteProcessTaskAsync(apply.TaskId!.Value, "user1",
new FlowData { ["submitType"] = 1 });
var review = (await repo.FindDoingTasksAsync(inst.InstanceId.Value, null))[0];
Console.WriteLine($"待办:{review.TaskName} → {string.Join(",", await repo.FindTaskActorsAsync(review.TaskId!.Value))}");
// 7. leader 点同意,流程走到终点
await engine.ExecuteProcessTaskAsync(review.TaskId!.Value, "leader",
new FlowData { ["submitType"] = 1 });
var after = await repo.FindInstanceByIdAsync(inst.InstanceId);
Console.WriteLine($"审批后实例 state={after!.State}(20=已完成)");
dotnet run,真实输出:
ini
实例已发起:id=2096621026388475904 state=10(10=进行中)
待办:apply → user1
待办:task1 → leader
审批后实例 state=20(20=已完成)
四行输出里有三处契约,值得停一下:
一,state=10 到 state=20。 实例状态机就三档:10 进行中、20 已完成、99 已撤销(任务侧另有两档)。这是系列第 3 篇讲过的那套状态机,C# 一个数都没跑偏。
二,第一站待办的处理人是 user1 自己。 流程 JSON 里申请节点写的是 assignee=applicant------applicant 是引擎级特殊值,运行时替换成流程发起人。这就是系列第 6 篇讲过的 applicant 契约:申请节点让发起人把材料确认一遍再往下走,"退回发起人重办"能闭环,靠的也是它。
三,注意第 4 步的注释------引擎不会替你办任何节点。 StartProcessInstanceByIdAsync 只把流程推到第一个任务节点就停。我第一次跑时想当然以为"发起"会顺手把申请节点办掉,让 leader 直接去执行,结果引擎扔过来一个异常:"当前参与者不能执行该流程任务" ------leader 不在 user1 那条待办的处理人名单里,引擎直接拒了。后来才知道,要"发起即自动办完申请节点"得用 startAndExecute(门面里的对应 action,或者引擎侧两步连调),这是 mldong 契约的显式设计:每一步都是有人点的,引擎只认提交。
顺带一提,那个异常是引擎的负向行为------错误信息就是给前端 msg 字段的原话。想看门面的 HTTP 契约长什么样,下一节换生产姿势。

三、从内存到生产:MySQL 仓储与 45 action 门面
内存仓储适合测试和内嵌,生产要落库。换 MySQL 仓储,业务代码一行不用改:
csharp
using Mldong.Jeeflow.Repository.MySql;
var factory = MySqlConnectionFactory.FromEnv(); // 读 JEFFLOW_DB_HOST/PORT/USER/PWD/NAME
var repo = new MySqlRepository(factory); // 上面 MemoryRepository 换成它
var ctx = new ServiceContext(repo);
repo.Configure(ctx); // 两阶段接线(ctx ↔ repo 解循环)
ctx.TransactionTemplate = new MySqlTransactionTemplate(factory, repo); // 可选真事务
- 建表 SQL 随包内嵌(
schema-mysql.sql),5 张wf_表,无自增,主键应用侧雪花生成; - 语句级 autocommit 是联邦现状;要同事务回滚,注入
ITransactionTemplate,同事务内所有仓储方法共用同一连接; IProcessRepository是全异步接口------整条引擎链路没有一个.Result、.Wait(),仓库里有条 CI 级门禁:grep 这三个词必须为零。
如果你在做一个要给前端用的服务,别一个 action 一个 action 地拼------引擎的 45 个 action 全部从一个门面入口进:
csharp
using Mldong.Jeeflow.Facade;
var facade = new JeeflowFacade(ctx);
var json = await facade.FlowJsonAsync("processInstance/startAndExecute", new FlowData
{
["processDefineId"] = "1",
["operator"] = "user1",
["f_days"] = 3,
});
真实返回(写稿时真机输出):
json
{"code":0,"msg":"成功","data":{"processInstanceId":"2096621342496391168"}}
三个细节都在这一行里:
| 细节 | 契约 |
|---|---|
{code,msg,data} 信封 |
成功恒 code=0;失败只发明 99999999,错误语义在 msg |
processInstanceId 是字符串 |
雪花 id 超出 JavaScript Number.MAX_SAFE_INTEGER,出口全字符串化(含嵌套数组),前端不会再丢精度 |
| 集成方只写一个转发 controller | HTTP body → FlowData → FlowAsync(action, args),45 个 action 一个循环搞定 |
45 个 action 覆盖流程定义 8 个、流程设计 9 个、实例 14 个(含统计 3 个)、委托 5 个、任务 9 个------就是 jeeflow-ui 前端要调的全集。不想自己写 controller,可以看在线演示站:jeeflow-ui ?lang=csharp 直连 C# demo(:8093),画流程、发起、审批、看统计,整条前端链路开着箱。点这里直达。
包矩阵放一张图收个尾:

四、两个半小时的移植:第八语言怎么进联邦
写到这里该讲讲这块 NuGet 包是哪来的了。答案在 git 时间线里(jeeflow-csharp 仓真实提交记录):
vbnet
09-06 00:05 移植方案 v1.1 定稿(此时未动一行代码)
09-06 01:07 M0 仓骨架:45 action manifest 与 Java 实查 diff 无差集,5 个 spike 全绿
09-06 01:33 M1 Core 全模块 + 内存仓储,T0 100 用例全绿
09-06 02:00 M2 MySQL 仓储 + 事务模板 + 命令级串行化,T1 120 用例(160 真库)
09-06 02:53 M3 Persist 动态入库 + Facade 45 action + 契约出口层,153/153 全绿
09-06 03:07 M4 轻量 demo :8093 + T2 冒烟 20/20
09-06 03:28 M5 一致性快照与七语言逐字段全等,联邦登记完成
09-06 09:47 tag v1.0.0 → CI 推 NuGet(Trusted Publishing,免 API key)
09-06 10:20 tag v1.0.1 修正包元数据
从方案定稿到引擎六里程碑全绿,两小时二十一分;到 NuGet 可装,一个上午。这套"八小时从零到发版"能跑通,一半靠前辈铺路,一半靠 C# 自己争气。
前辈铺路:第七语言 MoonBit(jeeflow-moon)前一周刚趟完整条路------45 action 清单怎么固化对账、五张表怎么逐字对齐、22 个合规场景怎么建、一致性快照怎么逐字段比对,全部成了可复制的模板。C# 的移植方案就是照着它写的,工程上的新决策只剩一个:要不要真事务(答案:给,MySqlConnector 环境事务,联邦里第一个)。
C# 自己争气,是它把前两门语言撞的"墙"全绕开了:
| 墙 | Rust 移植时 | MoonBit 移植时 | C# 这次 |
|---|---|---|---|
| 异步墙 | 嵌套 block_on panic(同步门面 + async 引擎组合独有) |
async test + wasm 运行时组合拳 | 无 ------.NET 原生 async,全链 await 一把梭 |
| 构建目标墙 | 无 | wasm-gc/native 双目标,client 连接要 vendored 解锁 | 无------本机 native 直跑 |
| 工具链风险 | 链接器环境敏感 | 工具链未成熟,版本钉死 | 无------官方 SDK zip 解压即用 |
结果就是引擎核心本身一个坑都没踩 ------153 个用例(含负向变异破坏)一次推过,反倒是坑都埋在引擎外面:老 docker 的 seccomp 拦 CoreCLR 系统调用(容器起循环,加 seccomp=unconfined 才好)、托管机内存紧张要给 GC 上限(GCHeapHardLimit 256MB,常驻约 22MB)、NuGet 官方 Action 输出名是大写 NUGET_API_KEY(小写引用取空导致 push 401)。这些属于"把一个 .NET 服务跑进任意老旧生产环境"的通用成本,引擎本身无责。

五、什么时候用它,什么时候别用
最后摆正预期,这张表比任何吹捧都有用:
| 你的需求 | 建议 |
|---|---|
| 系统里嵌审批流:请假/报销/采购,会签、退回、委托、抄送 | 正解。五张表 + 一个用户 SPI + 一个门面,jeeflow-ui 直连可用 |
| 前端还没有流程设计器 | 用 jeeflow-ui(开源,Vue3),?lang=csharp 就是给 C# 后端留的档位 |
| 多语言技术栈,流程定义要共用 | 同一份 LogicFlow JSON 八门语言跑,迁移引擎/混合栈不锁语言 |
| BPMN 建模、长时编排、Saga 补偿、分钟级以上的人类等待混合机器任务 | 别用,去 Elsa Workflows / Temporal / Camunda,它们是那个赛道的 |
| 数据库不是 MySQL | 等后续版本,或者自己实现 IProcessRepository(接口就在 Core 包里,PgSQL 仓储大概是几百行 ADO.NET 的事) |
Mldong.Jeeflow.* 四个包都在 nuget.org,Apache-2.0,net8.0;net10.0 双目标框架。Core 包零第三方依赖(csproj 里连一个 PackageReference 都没有,编译产物约 180KB),MySqlConnector 只在仓储包里。装之前想先玩,演示站在跑着;想看代码,仓库和文档站都在下面。
一条审批流的复杂度,值得一个 180KB 的引擎来扛,而不是一辆卡车。
参考资料
- jeeflow-csharp 仓库(2026-09-06 核对):NuGet 四包 Mldong.Jeeflow.* 1.0.1;45 action 清单
docs/action-manifest.json(Java↔规范↔MoonBit↔C# 四方对账);测试矩阵 T0 153 用例 / T1 MySQL 真库 / T2 冒烟 20 项 / 一致性 15 key 与七语言全等 - 系列前篇:第 3 篇《工作流引擎的"灵魂":状态机与 submitType》、第 6 篇《"applicant" 契约:退回发起人的闭环设计》、第 13 篇《同一份 15 个流程 JSON,第六种语言也跑通了》、第 16 篇《引擎从不发一条消息》
- 在线体验:C# 演示站 jeeflow-demo.mldong.com/?lang=cshar...
- 仓库:github.com/mldong/jeef...(NuGet:Mldong.Jeeflow.Facade)· jeeflow-ui · 文档站 · 开源演示站 · 集成演示站