System.CommandLine 与依赖注入:完整解决方案
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; }
}
验证逻辑、描述和别名都已封装且可重用。该库甚至随附了常用的选项,如 InputDirectoryOption、OutputFileOption 和 FormatExpressionOption,它们位于 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.CommandLine 和 Microsoft.Extensions.Hosting 构建,让你在无需任何样板代码的情况下,充分利用这两个生态系统的全部功能。