commandline icon indicating copy to clipboard operation
commandline copied to clipboard

Is there support for Dependency Injection

Open rshillington opened this issue 6 years ago • 12 comments

When constructing the instance of from the array of types provided to the ParseArguments it would be nice if the instance supplied had it's dependencies resolved via constructor arguments. Perhaps this is already in place and I'm just not seeing how it's configured?

rshillington avatar Jun 29 '19 13:06 rshillington

Welcome @rshillington Dependency Injection isn't supported by the library. The parser supported Types are: the primitive data type: string,int,..., enum and the collection IEnumerable by using the Option and Value Attributes. Can you describe by example the scenario you want to implement?

moh-hassan avatar Jun 29 '19 14:06 moh-hassan

Given the following snippet:

abstract class Verb
{
    public abstract void Run();
}
[Verb("inspect")] class InspectVerb : Verb
{
}

[Verb("run")] class RunVerb : Verb
{
    RunVerb(IDoSomething doSomething) { ... }
}
   ...

var result = parser.ParseArguments(args,
                                                    typeof(InspectVerb),
                                                    typeof(RunVerb)
            );

            var verbToRun = result.MapResult(o => o as Verb, _ => null);
            if (verbToRun == null) Environment.Exit(-1);
            
            await verbToRun.Run();

If I try and use the run verb I will get a MissingMethodException No parameterless constructor defined for this object. Ideally the RunVerb instance would be created from DI

rshillington avatar Jun 29 '19 14:06 rshillington

No parameterless constructor defined for this object

If the Verb/Option type is Mutable then it must have a default parameter-less constructor If Immutable Type, a constructor that takes the options/args needed to create the type.

The class RunVerb constructor has a parameter need to be injected and this not supported.

The next scenario may help:

       [Verb("run")] class RunVerb : Verb
       {
       [Option]
       public string PluginName {get;set;}	

     //     based on the value of PluginName option you RunPlugin with the corresponding plugin
     public  void   RunPlugin(IDoSomething doSomething) 
      { 
      //...
      }          
    
    }

moh-hassan avatar Jun 29 '19 15:06 moh-hassan

I'm a huge fan of Micorosft's DI so I ended up writing my own DI extension for CommandLineParser. You're welcome to try it out for yourself here: https://github.com/JaronrH/CommandLineParser.DependencyInjection

  1. Add your Options as classes (Verbs or single options class) and have them implement ICommandLineOptions (interface does nothing on it's own). These should not require DI injection as they are just used to identify Options using DI!
  2. Create a service that implements IExecuteCommandLineOptions<TCommandLineOptions, TResult>. TCommandLineOptions should be an options class from step 1 and TResult is what the parser will ultimately return as a result.
  3. [Optional] Implement a service that implements IExecuteParsingFailure<out TResult>. This will allow you to handle errors from the parser.
  4. Setup DI and use the AddCommandLineParser() extension to IServiceCollection to add the CommandLineParser DI extensions (pass in the assemblies that contain the above implementations). This uses Scrutor (https://github.com/khellang/Scrutor) to scan the assemblies and add them as needed.
  5. After building your IServiceProvider, you can request the parser or have it injected in a service constructor. i.e. request service ICommandLineParser
  6. Use parser.ParseArguments(args) to parse and execute. Behind the scenes, this creates the parser using the class type(s) registered as ICommandLineOptions in DI from step 1. If the parser is successful, the DI attempts to resolve the corresponding IExecuteCommandLineOptions<TCommandLineOptions, TResult> service from DI (step 2). Otherwise, the optional IExecuteParsingFailure<out TResult> service from DI is used from step 3 (if defined, otherwise, we return default(TResult)).

If you want to manually control what gets pulled into DI, just use AddCommandLineParser() without passing in any assemblies and then register the implementations yourself.

Note: If you want the help information dispatched to the console, you will need to specify that in the configuration. For example: parser.ParseArguments(args, o => { o.HelpWriter = System.Console.Error; });

Hope that helps!

JaronrH avatar Sep 16 '19 15:09 JaronrH

@JaronrH, Adding DI to CLP increase the complexity and violate the concept of SR (single responsibility).

Why not simplify the problem and re-design the system by moving all DI parts outside the Verb classes to a new class which is DI support and use one of DI containers like Microsoft DI / autofac or whatever to resolve the dependency.

Also , CLP is a nuget package with no external dependency at all, and using DI will need to reference these DI libraries which will cause a major Break Change. . Verbs are ParameterLess Ctor classes and are instantiated by Reflection,

moh-hassan avatar Dec 14 '19 00:12 moh-hassan

@moh-hassan I did not add add DI to CLP. Instead, the DI package I built uses CLP as one of its dependencies (along with DI) and doesn't change anything about CLP itself. It did not violate the concept of SR either. In fact, what I did is almost exactly what you described!

JaronrH avatar Dec 14 '19 01:12 JaronrH

Thanks @JaronrH for clarification.

moh-hassan avatar Dec 14 '19 20:12 moh-hassan

@moh-hassan Dependency injection can prove very useful. For instance I can configure a logging framework and may want to inject it across different verb classes. Also I may want to inject other services as needed to call APIs.

Adding DI to CLP increase the complexity and violate the concept of SR (single responsibility).

How?

ankitbko avatar Mar 31 '20 17:03 ankitbko

My simple workaround:

  • Create simple interface IInjectableServiceProvider
public interface IInjectableServiceProvider
{
    public IServiceProvider ServiceProvider { get; set; }
}
  • Implement it in your command by public IServiceProvider ServiceProvider { get; set; }
  • Push serviceProvider like this:
if (command is IInjectableServiceProvider injectable)
    injectable.ServiceProvider = _serviceProvider;

TheTrigger avatar Feb 17 '23 08:02 TheTrigger

i haven't looked at the library code yet, but i think you can keep single responsibility and without adding aspnetcore dependency. How? Allowing us to intervene between data type detection and creation of the instance

  • Parse command to only detect type
  • allow us to run ActivatorUtilities.CreateInstance and give back the instance to commandlineparser
  • commandlineparser populates properties

thoughts?

TheTrigger avatar Feb 17 '23 08:02 TheTrigger

@JaronrH thank you for your implementation of DI really great :D

salama135 avatar Oct 26 '23 09:10 salama135

Would LOVE to see this incorporated within the core library. This would add a nice improvement that is inline with the direction Microsoft is going to extend the core application host.

ravensorb avatar Feb 06 '24 11:02 ravensorb