System.CommandLine 与依赖注入:完整解决方案

发布: (2026年1月12日 GMT+8 09:38)
8 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的完整文本内容(除代码块和 URL 之外),我将为您翻译成简体中文并保持原有的 Markdown 格式。

问题

当你尝试在 System.CommandLine 项目中加入依赖注入时,通常会出现以下情况:

  • 需要手动配置 IServiceCollection
  • 必须想办法在命令处理程序中解析服务。
  • 使用带有取消令牌的异步处理程序需要额外的工作。
  • 在命令之间共享代码(例如公共选项)会变得重复。
  • 对外部服务(数据库、API)进行预验证输入时会显得笨拙。

下面看看 Albatross.CommandLine 如何逐一解决这些问题。

快速开始:开箱即用的 DI

安装包

dotnet add package Albatross.CommandLine

一个完整、可运行的 CLI 应用程序,具备完整的 DI 支持

using Albatross.CommandLine;
using Albatross.CommandLine.Annotations;
using Microsoft.Extensions.DependencyInjection;
using System.CommandLine;

// 入口点
await using var host = new CommandHost("MyApp")
    .RegisterServices(RegisterServices)
    .AddCommands()               // 由源生成器生成
    .Parse(args)
    .Build();

return await host.InvokeAsync();

static void RegisterServices(ParseResult result, IServiceCollection services)
{
    services.RegisterCommands(); // 由源生成器生成
    services.AddSingleton();
}

就这样。CommandHost 管理 DI 容器的生命周期,源生成器会生成所有的 wiring 代码。

定义命令:属性 + 源生成

[Verb("greet", Description = "Greet a user")]
public class GreetParams
{
    [Option(Description = "Name of the person to greet")]
    public required string Name { get; init; }

    [Option("f", Description = "Use formal greeting")]
    public bool Formal { get; init; }
}

public class GreetHandler : BaseHandler
{
    private readonly IMyService _service;

    public GreetHandler(ParseResult result, GreetParams parameters, IMyService service)
        : base(result, parameters)
    {
        _service = service; // Fully injected!
    }

    public override Task InvokeAsync(CancellationToken cancellationToken)
    {
        var greeting = parameters.Formal ? "Good day" : "Hey";
        Writer.WriteLine($"{greeting}, {parameters.Name}!");
        return Task.FromResult(0);
    }
}

源生成器会创建一个 GreetCommand 类,将所有内容注册到 DI,并完成选项解析的连接。你只需要编写业务逻辑即可。

超越基础:可重用参数

在任何真实的 CLI 应用中,你都会有跨多个命令出现的通用选项:输入目录、输出格式、API 密钥、详细程度标志等。与其重复这些选项,不如创建可重用的参数类。

可重用选项定义

[DefaultNameAliases("--input", "-i")]
public class InputFileOption : Option
{
    public InputFileOption(string name, params string[] aliases) : base(name, aliases)
    {
        Description = "Input file path";
        this.AddValidator(result =>
        {
            var file = result.GetValueForOption(this);
            if (file != null && !file.Exists)
            {
                result.ErrorMessage = $"File not found: {file.FullName}";
            }
        });
    }
}

使用可重用选项

[Verb("process")]
public class ProcessParams
{
    [UseOption]
    public required FileInfo Input { get; init; }

    [Option(Description = "Output directory")]
    public DirectoryInfo? Output { get; init; }
}

验证逻辑、描述和别名都已封装且可重用。该库甚至随附了常用的选项,如 InputDirectoryOptionOutputFileOptionFormatExpressionOption,它们位于 Albatross.CommandLine.Inputs 包中。

高级:使用 DI 进行预处理

有时你需要在命令执行之前对输入进行外部服务的验证——例如,验证数据库中是否存在某个仪器 ID。这时 选项处理程序 就显得非常有用。

使用处理程序定义选项

[DefaultNameAliases("--instrument", "-i")]
[OptionHandler]
public class InstrumentOption : Option
{
    public InstrumentOption(string name, params string[] aliases) : base(name, aliases)
    {
        Description = "Instrument Id";
    }
}

处理程序拥有完整的 DI 支持

public class VerifyInstrumentId : IAsyncOptionHandler
{
    private readonly IInstrumentService _service;
    private readonly ICommandContext _context;

    public VerifyInstrumentId(IInstrumentService service, ICommandContext context)
    {
        _service = service;
        _context = context;
    }

    public async Task InvokeAsync(InstrumentOption option, ParseResult result, CancellationToken token)
    {
        var id = result.GetValue(option);
        if (id != 0)
        {
            var valid = await _service.IsValidInstrument(id, token);
            if (!valid)
            {
                // Short‑circuit: command handler won't execute
                _context.SetInputActionStatus(/* ... */);
                // You can also set an error message or exit code here
            }
        }
    }
}

处理程序在命令 之前 运行

如果验证失败,命令将永不执行。这使得你的命令处理程序可以专注于业务逻辑,而不是输入验证。

TL;DR

  • Albatross.CommandLine 为您提供即插即用的 System.CommandLine 应用,几乎无需样板代码。
  • 源生成器自动创建命令类、选项解析器以及 DI 注册。
  • 可复用的选项类使代码保持 DRY 并易于维护。
  • 选项处理器允许您在完整 DI 支持下运行预验证逻辑。
new OptionHandlerStatus(option.Name, false, $"Instrument {id} not found"));
            }
        }
    }
}

魔法:输入转换

这是最强大的模式。如果你不想让命令处理器接收 int 类型的乐器 ID,而是完整的 InstrumentSummary 对象,该怎么办?

// Add a third generic argument: the output type
[DefaultNameAliases("--instrument", "-i")]
[OptionHandler]
public class InstrumentOption : Option {
    public InstrumentOption(string name, params string[] aliases) : base(name, aliases) {
        Description = "Instrument identifier";
    }
}

// The transformer fetches and returns the full object
public class InstrumentTransformer : IAsyncOptionHandler {
    private readonly IInstrumentService _service;

    public InstrumentTransformer(IInstrumentService service) {
        _service = service;
    }

    public async Task InvokeAsync(
        InstrumentOption option, ParseResult result, CancellationToken token) {
        var identifier = result.GetValue(option);
        if (string.IsNullOrEmpty(identifier)) {
            return new OptionHandlerResult();
        }
        var summary = await _service.GetSummary(identifier, token);
        return new OptionHandlerResult(summary);
    }
}

现在你的参数类接收到转换后的类型:

[Verb("price")]
public class GetPriceParams {
    // Property type is InstrumentSummary, not string!
    [UseOption]
    public required InstrumentSummary Instrument { get; init; }
}

public class GetPriceHandler : BaseHandler {
    public override Task InvokeAsync(CancellationToken token) {
        // Direct access to the full object
        Writer.WriteLine($"Price for {parameters.Instrument.Name}: ${parameters.Instrument.Price}");
        return Task.FromResult(0);
    }
}

用户输入 --instrument AAPL,但你的处理器收到的是一个已完全填充的 InstrumentSummary 对象。转换过程完全透明。

为什么这很重要

这些模式实现了关注点的清晰分离:

职责
可复用选项验证规则、描述、别名
选项处理器外部验证、数据获取
命令处理器纯业务逻辑

你的命令处理器变得简洁且易于测试。基础设施处理繁杂的部分。

摘要

如果你正在使用 .NET 构建 CLI 应用,并且需要:

  • 一流的依赖注入
  • 支持取消的异步处理程序
  • 可复用、已验证的参数
  • 使用外部服务的预处理
  • 输入转换

请查看 Albatross.CommandLine。它基于 System.CommandLineMicrosoft.Extensions.Hosting 构建,让你在无需任何样板代码的情况下,充分利用这两个生态系统的全部功能。

链接

Back to Blog

相关文章

阅读更多 »