commandline
commandline copied to clipboard
Is there support for Dependency Injection
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?
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
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
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)
{
//...
}
}
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
- 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!
- 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.
- [Optional] Implement a service that implements IExecuteParsingFailure<out TResult>. This will allow you to handle errors from the parser.
- 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.
- After building your IServiceProvider, you can request the parser or have it injected in a service constructor. i.e. request service ICommandLineParser
- 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, 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 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!
Thanks @JaronrH for clarification.
@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?
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;
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 commandlineparserpopulates properties
thoughts?
@JaronrH thank you for your implementation of DI really great :D
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.