Table of Contents

命令壳生成器(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 传入命令。

怎么选

  • 命令行工具/管理 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(命令源生成立项)