dotnet / command-line-api

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

Allow a simple implementation of enabling dependency injection #826

Open taori opened 4 years ago

taori commented 4 years ago

These days it would be nice if there was a simple way of utilizing dependency injection in command line applications as well.

Currently i am using the latest version, using middleware to populate the bindingcontext with services, but that does not allow open generic registration (for easy logger registration). Perhaps that is a scenario worth keeping in mind.

jonsequitur commented 4 years ago

The way you're doing it is a simple approach that lets you use a dependency injection library of your choice but was intended for the 80% cases.

For more complex cases, Microsoft.Extensions.DependencyInjection is supported via the System.CommandLine.Hosting package. Would this work for you?

taori commented 4 years ago

The way you're doing it is a simple approach that lets you use a dependency injection library of your choice but was intended for the 80% cases.

For more complex cases, Microsoft.Extensions.DependencyInjection is supported via the System.CommandLine.Hosting package. Would this work for you?

That sounds very promising actually. Somehow i only stumbled upon DragonFruit and dotnet-suggest through the wiki of this project. I'll have a look at it and get back to this issue with feedback

taori commented 4 years ago

This certainly seems to be what i am looking for. What i plan on doing is having an invocation structure where i implement something like ITypedCommand<TArg1, TArg2, ...> so i can utilize constructor injection and map arguments through Attributes. Do you think this is doable right now? Essentially i need something similar to how things work with mvc actions.

taori commented 4 years ago

@jonsequitur This is my current state.

I can't seem to get it working unfortunately. At least i would expect it to forward input to the "testCommand" handler, but not even that appears to work.

Do you think what i am attempting to do is doable with *.Hosting?

using System;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Reflection;
using System.Threading.Tasks;
using Generator.CLI.Component.Parser;
using Generator.CLI.Dependencies;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Generator.CLI
{
    class Program
    {
        static async Task<int> Main(string[] args)
        {
            var builder = new CommandLineBuilder()
                .UseHost(host => { host.ConfigureServices((context, services) =>
                {
                    services.AddTransient(typeof(ILogger<>), typeof(Logger<>));
                    services.AddTransient(typeof(ILoggerFactory), typeof(LoggerFactory));
                }); })
                .UseHelp()
                .UseTypoCorrections()
                .UseAttributedCommands()
                .UseSuggestDirective();

            var parser = builder.Build();
            var parseResult = parser.Parse("test -v hi");
            var invoke = await parseResult.InvokeAsync();
            return invoke;
        }
    }

    public static class BuilderExtensions
    {
        public static CommandLineBuilder UseAttributedCommands(this CommandLineBuilder source)
        {
            source.UseMiddleware(async (context, next) =>
            {
                var rootCommand = new RootCommand();
                var testCommand = new Command("test")
                {
                    new Argument<string>("v")
                };

                // this is where i want to construct classes inject registered services and register commands to the root command
                rootCommand.AddCommand(testCommand);
                var method = typeof(BuilderExtensions).GetMethod(nameof(Test), BindingFlags.Static | BindingFlags.NonPublic);
                // testCommand.Handler = CommandHandler.Create(method, null);
                testCommand.Handler = CommandHandler.Create<string>(p => Console.WriteLine(p));
                source.AddCommand(rootCommand);
                await next.Invoke(context);
            });

            return source;
        }

        private static void Test(string param)
        {
            throw new NotImplementedException();
        }
    }
}
taori commented 4 years ago

I did get it to work like this:

using System;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Generator.CLI
{
    class Program
    {
        static async Task<int> Main(string[] args)
        {
            var builder = new CommandLineBuilder()
                .UseHost(host => { host.ConfigureServices((context, services) =>
                {
                    services.AddTransient(typeof(ILoggerFactory), typeof(LoggerFactory));
                    services.AddTransient(typeof(ILogger<>), typeof(Logger<>));
                }); })
                .UseDebugDirective()
                .UseHelp()
                .UseTypoCorrections()
                .UseAttributedCommands()
                .UseSuggestDirective();

            var parser = builder.Build();
            var parseResult = parser.Parse("test -b \"parameter b\" -a \"parameter a\"");
            var invoke = await parseResult.InvokeAsync();
            return invoke;
        }
    }

    public static class BuilderExtensions
    {
        public static CommandLineBuilder UseAttributedCommands(this CommandLineBuilder source)
        {
            var method = typeof(BuilderExtensions).GetMethod(nameof(Test), BindingFlags.Static | BindingFlags.NonPublic);
            var testCommand = new Command("test");
            testCommand.AddOption(new Option("-a") { Argument = new Argument("-a"){ Arity = ArgumentArity.ExactlyOne}});
            testCommand.AddOption(new Option("-b") { Argument = new Argument("-b") { Arity = ArgumentArity.ExactlyOne } });
            testCommand.Handler = CommandHandler.Create(method, null);
            source.AddCommand(testCommand);

            return source;
        }

        private static void Test(IHost host, string a, string b)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();
            // logger.LogDebug("just a test really");
            Console.WriteLine(a);
            Console.WriteLine(b);
        }
    }
}

Ideally the BindingContext should also try to use the host to resolve services instead of having to go through the host for everything.

jonsequitur commented 4 years ago

Would your expectation be that you can inject, e.g. a parameter of ILogger<Something> directly into the Test method?

taori commented 4 years ago

If i am using a host yes, otherwise it would make sense if that did not work.

jonsequitur commented 4 years ago

Related: #671.

lawrencek76 commented 3 years ago

The following pattern should also be enabled by making di "simple" using constructor injection and non static methods.

    class TestCommand
    {
        MyService _service;
        TestCommand(MyService service)
        {
            _service = service;
        }

        public void Test(string a, string b)
        {
            Console.WriteLine($"hello {_service} {a} {b}");
        }
    }

attempting this today creates an exception "Unhandled exception: System.Reflection.TargetException: Non-static method requires a target."