Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
voltagex opened this issue Sep 22, 2022 · 7 comments

Comments

@voltagex
Copy link

voltagex commented Sep 22, 2022

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 #1025 (comment) 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
Copy link
Author

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
Copy link
Author

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
Copy link

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

@jonsequitur
Copy link
Contributor

Does this test case help illustrate the usage?

[Fact]
public static async Task Can_have_diferent_handlers_based_on_command()
{
var root = new RootCommand();
root.AddCommand(new MyCommand());
root.AddCommand(new MyOtherCommand());
var parser = new CommandLineBuilder(root)
.UseHost(host =>
{
host.ConfigureServices(services =>
{
services.AddTransient<MyService>(_ => new MyService()
{
Action = () => 100
});
})
.UseCommandHandler<MyCommand, MyCommand.MyHandler>()
.UseCommandHandler<MyOtherCommand, MyOtherCommand.MyHandler>();
})
.Build();
var result = await parser.InvokeAsync(new string[] { "mycommand", "--int-option", "54" });
result.Should().Be(54);
result = await parser.InvokeAsync(new string[] { "myothercommand", "--int-option", "54" });
result.Should().Be(100);
}

@voltagex
Copy link
Author

Does this test case help illustrate the usage?

[Fact]
public static async Task Can_have_diferent_handlers_based_on_command()
{
var root = new RootCommand();
root.AddCommand(new MyCommand());
root.AddCommand(new MyOtherCommand());
var parser = new CommandLineBuilder(root)
.UseHost(host =>
{
host.ConfigureServices(services =>
{
services.AddTransient<MyService>(_ => new MyService()
{
Action = () => 100
});
})
.UseCommandHandler<MyCommand, MyCommand.MyHandler>()
.UseCommandHandler<MyOtherCommand, MyOtherCommand.MyHandler>();
})
.Build();
var result = await parser.InvokeAsync(new string[] { "mycommand", "--int-option", "54" });
result.Should().Be(54);
result = await parser.InvokeAsync(new string[] { "myothercommand", "--int-option", "54" });
result.Should().Be(100);
}

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

@bhehe
Copy link

bhehe commented Sep 12, 2023

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
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants