Search code examples
c#.net-corecommand-linebackground-servicesystem.commandline

System.CommanLine Adding services.UseHostedService<T>() to commandHandler


I am currently trying to implement a couple of worker services. It would be great if I can tell it to add a service based on the command line.

Lets say I use the command "Background --service1 --service2". I would want the commandhandler to add the services to the host. And lets say I run it without any of the arguments, it would just run and stop straight away.

My current implementation wont work due to the fact that the services object is always going to be empty when it is added to the hosted services, as the command line has not yet been parsed.

The only way that I could imagine this working, is by seperating the command line and the host builder. Then getting the result from the command line and using that to build the builder. But I expect there is a better way of doing this as there is a host package for the 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;
        }
    }
}

Solution

  • So the issue I was running into was this. The way the hosting works is that it adds the services, and then runs the command line. So once the command line is parsed, you cannot add any more services.

    There is an easy workaround, and that is to just use the standard host, and use the CommandLineBuilder().

    Progam.cs

    var builder = Host.CreateApplicationBuilder(args);
    ConfigureWorkerServices(builder.Services, args);
    
    void ConfigureWorkerServices(IServiceCollection services, string[] args)
    {
        var commandLineBuilder = new CommandLineBuilder();
    
        commandLineBuilder.Command.AddCommand(new WorkerCommand(services));
    
        var parser = commandLineBuilder.Build();
        parser.InvokeAsync(args);
    }
    
    

    Base Command

        public abstract class BaseCommand<T> : Command where T : ICommandModel
        {
            public IServiceCollection Services { get; init; }
    
            public BaseCommand(string name, IServiceCollection services, string? description = null) : base(name, description)
            {
                Services = services;
    
                Handler = CommandHandler.Create<T>(ParseOptions);
            }
    
            /// <summary>
            /// This fills the Settings.
            /// </summary>
            /// <param name="model">Moddel to fill.</param>
            public abstract void ParseOptions(T model);
        }
    

    Background Command

    public abstract class BackgroundCommand : BaseCommand<BackgroundCommandModel>
    {
        public Option<bool> Background = new Option<bool>(aliases: ["--Background", "--background", "-b", "-B"], description: "Run the process as a background task.", getDefaultValue: () => false);
    
        public BackgroundCommand(string name, IServiceCollection services, string? description = null) : base(name, services, description)
        {
            AddOption(Background);
        }
    
        public override void ParseOptions(BackgroundCommandModel model)
        {
            ArgumentNullException.ThrowIfNull(model);
    
            Services.AddSingleton<Processing>();
            if (model.Background)
            {
                Services.AddHostedService<ProcessingWorker>();
            }
            else
            {
                Services.AddHostedService<OneTimeRunHostedService<Processing>>();
            }
    }}
    

    Background command moddel

        public class BackgroundCommandModel : ICommandModel
        {
            public bool Background { get; set; }
        }
    

    So you run the CommandLineBuiler on it's own before you build the host. That way you can inject all the services you might need.