You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过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();
    }
}

关键改进点

  1. 服务注册时机调整:将动态添加HostedService的逻辑移到宿主构建阶段的ConfigureServices中,此时宿主尚未完成初始化,服务集合可正常修改。
  2. 命令处理器职责简化:仅负责判断是否需要启动宿主——当指定了服务参数时启动宿主运行后台服务,无参数则直接终止程序。
  3. 正确获取命令行参数:通过context.GetInvocationResult()获取已解析的命令行结果,避免了原代码中错误传递服务集合的问题。

使用方式

  • 启动指定后台服务:dotnet run Background --service1 --service2
  • 不带参数直接终止:dotnet run Background

内容的提问来源于stack exchange,提问作者David Turley

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 23:11:17