如何通过System.CommandLine命令行参数动态添加HostedService
如何通过System.CommandLine动态添加Worker Service
我需要实现多个Worker Service,能根据命令行参数动态添加。比如执行Background --service1 --service2时,程序启动对应的后台服务;如果不带参数执行,程序直接终止。
当前代码无法正常工作,因为命令行还没解析时,services对象是空的。虽然可以分离命令行解析和主机构建,但System.CommandLine有宿主集成包,应该有更合适的实现方式。
原代码实现:
using System.CommandLine; using System.CommandLine.Builder; using System.CommandLine.Hosting; using System.CommandLine.Parsing; class Program { public static async Task<int> Main(string[] args) { IServiceCollection commandServices = new ServiceCollection(); var parser = BuildCommandLine(ref commandServices) .UseHost(_ => Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) => { ConfigureServices(context, services, commandServices); AddLogging(context, services); }), hostBuilder => { hostBuilder.UseCommandHandler<BackgroundCommand, BackgroundCommand.BackgroundCommandHandler>(); }) .UseDefaults() // Ensure defaults are added last in the chain .Build(); return await parser.InvokeAsync(args); static CommandLineBuilder BuildCommandLine(ref IServiceCollection services) { // Create root command with description var rootCommand = new RootCommand("This is your command-line app's root command."); var backgroundCommand = new BackgroundCommand(); backgroundCommand.Handler = new BackgroundCommand.BackgroundCommandHandler(ref services); rootCommand.AddCommand(backgroundCommand); return new CommandLineBuilder(rootCommand); } } }
public class BackgroundCommand : Command { public BackgroundCommand() : base("Background", "Runs background workers based on options") { var service1Option= new Option<bool>(aliases: ["--service1"], description: "", getDefaultValue: () => false); var service2Option= new Option<bool>(aliases: ["--service2"], description: "", getDefaultValue: () => false); AddOption(service1Option); AddOption(service2Option); } public class BackgroundCommandHandler : ICommandHandler { private readonly IServiceCollection _services; public bool Service1 { get; set; } = false; public bool Service2 { get; set; } = false; public BackgroundCommandHandler(ref IServiceCollection services) { _services = services; } public int Invoke(InvocationContext context) { throw new NotImplementedException(); } public async Task<int> InvokeAsync(InvocationContext context) { if (Service1) { _services.AddHostedService<Service1Worker>(); } if(Service2) { _services.AddHostedService<Service2Worker>(); } return 0; } } }
解决方案
核心问题是原代码在命令处理阶段才尝试添加服务,但此时宿主已经完成构建。正确的做法是在宿主构建阶段,利用已解析的命令行参数动态注册服务,而非在命令处理器中修改服务集合。
修改后的完整代码:
Program.cs
using System.CommandLine; using System.CommandLine.Builder; using System.CommandLine.Hosting; using System.CommandLine.Parsing; class Program { public static async Task<int> Main(string[] args) { var rootCommand = new RootCommand("后台服务启动工具"); var backgroundCommand = new BackgroundCommand(); rootCommand.AddCommand(backgroundCommand); var parser = new CommandLineBuilder(rootCommand) .UseHost(_ => Host.CreateDefaultBuilder(args), hostBuilder => { // 宿主构建阶段根据命令行参数注册服务 hostBuilder.ConfigureServices((context, services) => { var parseResult = context.GetInvocationResult()?.ParseResult; if (parseResult is not null && parseResult.CommandResult.Command is BackgroundCommand) { var service1Enabled = parseResult.GetValueForOption(backgroundCommand.Service1Option); var service2Enabled = parseResult.GetValueForOption(backgroundCommand.Service2Option); if (service1Enabled) { services.AddHostedService<Service1Worker>(); } if (service2Enabled) { services.AddHostedService<Service2Worker>(); } } AddLogging(context, services); }); hostBuilder.UseCommandHandler<BackgroundCommand, BackgroundCommandHandler>(); }) .UseDefaults() .Build(); return await parser.InvokeAsync(args); } private static void AddLogging(HostBuilderContext context, IServiceCollection services) { services.AddLogging(config => { config.AddConsole(); config.SetMinimumLevel(LogLevel.Information); }); } }
BackgroundCommand.cs
using System.CommandLine; using System.CommandLine.Invocation; public class BackgroundCommand : Command { public Option<bool> Service1Option { get; } public Option<bool> Service2Option { get; } public BackgroundCommand() : base("Background", "根据指定参数启动后台服务") { Service1Option = new Option<bool>(aliases: ["--service1"], description: "启动Service1后台服务", getDefaultValue: () => false); Service2Option = new Option<bool>(aliases: ["--service2"], description: "启动Service2后台服务", getDefaultValue: () => false); AddOption(Service1Option); AddOption(Service2Option); } } public class BackgroundCommandHandler : ICommandHandler { public bool Service1 { get; set; } public bool Service2 { get; set; } public async Task<int> InvokeAsync(InvocationContext context) { // 未指定任何服务时直接退出 if (!Service1 && !Service2) { return 0; } // 启动宿主并保持后台服务运行 var host = context.BindingContext.GetService<IHost>(); await host.RunAsync(); return 0; } public int Invoke(InvocationContext context) { return InvokeAsync(context).GetAwaiter().GetResult(); } }
关键改进点
- 服务注册时机调整:将动态添加HostedService的逻辑移到宿主构建阶段的
ConfigureServices中,此时宿主尚未完成初始化,服务集合可正常修改。 - 命令处理器职责简化:仅负责判断是否需要启动宿主——当指定了服务参数时启动宿主运行后台服务,无参数则直接终止程序。
- 正确获取命令行参数:通过
context.GetInvocationResult()获取已解析的命令行结果,避免了原代码中错误传递服务集合的问题。
使用方式
- 启动指定后台服务:
dotnet run Background --service1 --service2 - 不带参数直接终止:
dotnet run Background
内容的提问来源于stack exchange,提问作者David Turley
相关产品推荐
相关产品推荐

