命令壳生成器(CommandShell)——CLI / HTTP 双执行面
从 [CommandGroup] 标注的命令组编译期生成 CLI 命令行壳与 HTTP API 代理:参数绑定、帮助文本、
路由分发、错误映射全部由生成物承担——零运行时反射,零手写解析。一份命令定义,两个执行面。
快速上手
定义命令组(标注在普通类上——组即服务,方法即命令):
[CommandGroup("traffic", Description = "TierTraffic 管理根")]
public sealed class TrafficRoot
{
public bool Ping([CommandOption(LongName = "verbose", ShortName = 'v')] bool verbose)
=> verbose;
[Command("set-level", Description = "设置等级")]
[CommandError(typeof(ArgumentException), 422)]
public async Task<long> SetLevelAsync(
[CommandArg(0)] Level level, // 位置参数(枚举)
[CommandOption(LongName = "limit")] long limit = 10, // 具名选项(带默认值)
[CommandBody] RouteSpec spec, // 请求体(CLI=stdin / HTTP=JSON)
CancellationToken ct = default) // 框架注入,不参与绑定
{
// ...业务...
}
}
编译期生成两个静态类(挂在根组类的命名空间下):
| 生成类 | 执行面 | 关键成员 |
|---|---|---|
TrafficRootCli |
命令行 | Run(args, service, stdout, stderr[, json][, ct][, results])(返回退出码)、WriteHelp、CompleteNext(补全)、CommandPaths |
TrafficRootHttp |
HTTP | TryHandleAsync(request, service, results)——单一分发入口 |
CLI 接线(Main 里三行):
int exitCode = TrafficRootCli.Run(args, new TrafficRoot(), Console.Out, Console.Error, results: results);
return exitCode;
HTTP 接线(ASP.NET Core 路由兜底形态——不匹配返回 null,宿主续走自己的 404/静态面):
app.Map("/traffic/{*path}", async ctx =>
{
var result = await TrafficRootHttp.TryHandleAsync(ctx.Request, new TrafficRoot(), results);
if (result is not null)
{
ctx.Response.StatusCode = result.Status; // CommandHttpResponse = 纯数据载体(Status/ContentType/Body)——写响应归宿主
}
});
results 为消费方实现的 ICommandResults(成功载荷渲染 + 错误写出——模式匹配自家异常提取
错误码,零反射)。
概念
- 组(
[CommandGroup]):命令命名空间。根组 = 服务本身;嵌套组实例获取沿嵌套链逐级解析—— 父组 → 祖辈 → 根上首个持有同型 public 属性/字段的宿主即命中(链式取值service.Sys.Discovery), 全链未命中回落可访问无参构造——服务注入与嵌套深度解耦,组不必提升为根级属性; 无可访问无参构造 = TCSG061 编译期报在组声明处(生成物不落注定失败的new)。 组路径即 CLI 子命令路径(traffic route add)与 HTTP 路径段。 - 命令(
[Command("name")]):命令名必填(组内唯一——重名 = TCSG054,非法名 = TCSG055); HTTP 缺省路由 =/{各级组名}/{命令名}([Command]的Route/Method可覆写)。 - 参数三角色(每个参数必须显式标注其一——未标注 = TCSG056):
[CommandArg(position)]位置参数——CLI 按 token 位绑定;[CommandOption]具名选项——--long/-s;LongName缺省 = 参数名 kebab-case,ShortName缺省无短名,布尔选项支持--flag裸形态;[CommandBody]请求体——HTTP = JSON 反序列化,CLI = stdin 读入;GET 命令带 body = TCSG058。
- 可绑定类型集(编译期 TryParse 直调——零反射):
string/bool/整数族/double/decimal/Guid/DateTimeOffset/任意枚举(含IsDefined校验)/Nullable<T>包裹。复杂类型走[CommandBody]。 CancellationToken:任意参数位框架注入,调用方取消透传,不参与绑定。- 错误映射(
[CommandError(typeof(异常), 状态码)]):声明式异常→状态码/退出码映射, 方法与组两级可标(AllowMultiple);未声明的异常 → 500 兜底。
body 绑定
[CommandBody] 参数类型任意复杂——POCO 强类型(record/类)、JsonElement 直通、可空声明 T?
皆可,按参数类型自然分流,CLI/HTTP 双面同语义:
public sealed record OrchSaveBody(string OrchId, long IfRev);
[Command("orch-save")]
public async Task<Receipt> SaveAsync(
[CommandArg(0)] string orchId,
[CommandBody] OrchSaveBody body, ...) // POCO 绑定——schema 可从类型反射派生
- 命令 I/O JSON 契约(生成器自动登记——#484):每根组生成
JsonContext([JsonSerializable]覆盖全部命令返回类型 + body 类型——新增命令零登记步骤,AOT 契约由 生成器单点维护)与JsonResults(默认ICommandResults:成功载荷/错误信封{code,message}经生成 context 源生成序列化)。TryHandleAsync/Run/RunAsync的results/json参数 缺省即消费生成面——零注入、零登记;自定义命名策略/错误信封 = 自建 JsonSerializerContext + ICommandResults 显式传入即覆盖(context 未登记的 body 类型 → 400 / exit 2 回执指名)。 - 错误回执语义(HTTP 400 / CLI exit 2):
- JSON 语法/形状非法——
JsonException→body JSON 解析失败: <原因>; - JSON null——非可空声明(
T)→body 不能为 null;可空声明(T?)→ 绑定为 null 传入命令。
- JSON 语法/形状非法——
怎么选
- 命令行工具/管理 CLI → 只接
*Cli; - 同一组命令要暴露 HTTP API → 再接
*Http——命令定义不改一行,双面行为由同一模型保证; - 退出码/状态码语义 →
[CommandError]声明式映射,禁止在命令方法内手写 exit code 分支。
反模式
- 在命令方法里自己解析
args——绑定是生成器的职责;手写解析 = 双份真相; - 参数忘标角色注解——编译期 TCSG056 直接报(不存在"静默落 Body"的宽容形态);
- 参数名以
__tcsg_开头——生成代码自有标识符(私有处理函数形参+局部变量)的保留命名空间, 占用即 TCSG060 编译期报;此外生成器标识符已全面保留前缀化,用户参数名任意取不撞名; - 嵌套组全链无同型成员且无可访问无参构造——TCSG061 编译期报在组声明处(生成物获取表达式 落占位,不再产生生成文件里的 CS7036)——把组实例暴露为任一祖辈组的 public 属性/字段,或提供 可访问无参构造;
- 命令重名/空名——编译期诊断 TCSG054/TCSG055,不要指望运行期覆盖;
- body 类型忘了注册进 JsonSerializerContext——生成
JsonContext已自动登记全部命令 I/O 类型(缺省注入面零登记);仅当显式传入自建 context 且漏登记该类型时 400 / exit 2 回执指名, 补[JsonSerializable]或改用缺省注入即可;
想深入
- 生成器与诊断码:
src/KernLab.Tier.CodeGen/CommandGenerator.cs(TCSG054-061 族) - 双面行为契约的权威样例:
tests/KernLab.Tier.CodeGen.Tests/CommandSourceGenTests.cs - 关联:#435(命令源生成立项)