dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.35k stars 376 forks source link

Dependency injection confusion - How to use CommandLineBuilder, UseHost, CommandHandlers etc #1858

Open voltagex opened 1 year ago

voltagex commented 1 year ago

I'm lost in the weeds with dependency injection here.

using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;

namespace Test2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            CommandLineBuilder builder = new CommandLineBuilder();
            var parser = builder.UseHost((host) => {
                host.UseCommandHandler<MyCommand, MyCommandHandler>();
            }).Build();

            var parseResult = parser.Parse(args);
            var invokeResult = parser.Invoke(args);
        }
    }

    internal class MyCommandHandler : ICommandHandler
    {
        public int Invoke(InvocationContext context)
        {
            throw new NotImplementedException();
        }

        public Task<int> InvokeAsync(InvocationContext context)
        {
            throw new NotImplementedException();
        }
    }

    internal class MyCommand : Command
    {
        public MyCommand() : base("Test2")
        {

        }
    }

    public interface IHelloService
    {
        string Hello();
    }

    public class HelloService : IHelloService
    {
        public HelloService()
        {
        }

        public string Hello()
        {
            return "Hello!";
        }
    }
}

Before I even get to using the Hosting stuff, why is RootCommand set to Test2 when that's not in args[] and why aren't MyCommand and MyCommandHandler not called at all - shouldn't I be getting a NotImplementedException?

I've looked at https://github.com/dotnet/command-line-api/blob/5618b2d243ccdeb5c7e50a298b33b13036b4351b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs and https://github.com/dotnet/command-line-api/issues/1025#issuecomment-678609352 but have not been able to make much progress.

https://learn.microsoft.com/en-us/dotnet/standard/commandline/dependency-injection isn't entirely suitable because I need multiple services after my command line is parsed.

Further away from my simplified example, I'd like something like the following to work:

        var parser = commandBuilder.UseHost(host => host.ConfigureDefaults(args).ConfigureServices(s=> 
        {
            s.AddLogging(l=> l.AddConsole());
            s.AddOptions();
            s.AddOptions<DatabaseOptions>();
            s.Configure<DatabaseOptions>(c => c.ConnectionString = "Data Source=mydatabase.db");
            //Bind --host 127.0.0.1 to configuration somehow
           //s.Configure<RemoteHostOptions>(h=> h.Host = ParseResult.GetOption(...))
            s.AddSingleton<IFilesDatabase, FilesDatabase>();
        }).UseCommandHandler<ScanCommand, ScanCommandHandler>()).Build();

So I am stuck on:

  1. Parsing command line options and arguments
  2. Getting access to services in an IHost's ServiceCollection
  3. Converting command lines to configuration for services
  4. Invoking it all and having it "just work"

Thanks in advance - sorry if I've completely missed the mark here.

voltagex commented 1 year ago

Whoops! This works a lot better when I pass MyCommand into the builder itself!

new CommandLineBuilder(new MyCommand());

Will keep this open and see if I can get a complete example working.

voltagex commented 1 year ago
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;

namespace Test2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var parser = new CommandLineBuilder(new MyCommand())
                .UseHost(host =>
                {
                    host.ConfigureServices(services =>
                    {
                        services.AddSingleton<IGreetingService, HelloService>();
                        services.AddSingleton<IFarewellService, FarewellService>();
                        services.Configure<FarewellOptions>(f => f.CustomFarewell = "Until we meet again");
                    })
                    .UseCommandHandler<MyCommand, MyCommandHandler>();
                })
                .Build();

            var parseResult = parser.Parse(args);
            var invokeResult = parser.Invoke(args);
        }
    }

    public class MyCommandHandler : ICommandHandler
    {
        private readonly IGreetingService _greetingService;
        private readonly IFarewellService _farewellService;
        private readonly IOptions<FarewellOptions> _options;
        public MyCommandHandler(IGreetingService greetingService, IFarewellService farewellService, IOptions<FarewellOptions>? farewellOptions)
        {
            _greetingService = greetingService;
            _farewellService = farewellService;
            _options = farewellOptions;
        }
        public int Invoke(InvocationContext context)
        {
            throw new NotImplementedException();
        }

        public Task<int> InvokeAsync(InvocationContext context)
        {
            Console.WriteLine(_greetingService.Greet());
            Console.WriteLine(_farewellService.Farewell());
            return Task.FromResult(0);
        }
    }

    internal class MyCommand : RootCommand
    {
        public MyCommand() : base("")
        {
            var farewellOption = new Option<string>("--farewell");
            this.AddOption(farewellOption);
        }
    }

    public class FarewellOptions
    {
        public string CustomFarewell { get; set; }
    }

    public interface IGreetingService
    {
        string Greet();
    }

    public interface IFarewellService
    {
        string Farewell();
    }

    public class HelloService : IGreetingService
    {
        public HelloService()
        {
        }

        public string Greet()
        {
            return "Hello!";
        }
    }

    public class FarewellService : IFarewellService
    {
        private IOptions<FarewellOptions> _options;
        public FarewellService(IOptions<FarewellOptions> options)
        {
            _options = options;
        }

        public string Farewell()
        {
            if (!string.IsNullOrEmpty(_options.Value.CustomFarewell))
            {
                return _options.Value.CustomFarewell;
            }

            return "Farewell!";
        }
    }
}

Gets me much closer - from my initial list I can do everything except configure services from the command line.

For my contrived example, --farewell Bye! should change the program's output to

Hello!
Bye!
smiggleworth commented 1 year ago

Do you know if there is a working example where multiple commands/handlers are wired with host.UseCommandHandler<,>?

jonsequitur commented 1 year ago

Does this test case help illustrate the usage?

https://github.com/dotnet/command-line-api/blob/5618b2d243ccdeb5c7e50a298b33b13036b4351b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs#L57-L86

voltagex commented 1 year ago

Does this test case help illustrate the usage?

https://github.com/dotnet/command-line-api/blob/5618b2d243ccdeb5c7e50a298b33b13036b4351b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs#L57-L86

Sort of, I don't think it shows how to get access to multiple services though.

bhehe commented 10 months ago

One scenario I wasn't quite able to figure out myself was if my root command wanted to leverage constructor DI, I wasn't seeing how to implement that when you need to create an instance of the root to pass into the CommandLineBuilder constructor.

Or is it simply root commands have to be constrained to something very simplistic and they should be thought of as more of an 'anchor point' for your set of top-level sub-commands than providing functionality in the root itself.

fredrikhr commented 1 week ago

I have started work on PR #2450 where I will add new examples to HostingPlyground on how to use the .NET Generic Host. This PR also introduces a new feature that provides first-class support for using a Hosted service for executing your CLI Command.