Skip to content

jcharlesworthuk/CommandLineInjector

Repository files navigation

Command Line Injector

Parameter binding + Dependency Injection for .NET Core console apps.

> dotnet myapp mycommand --option="Bind Me!" --awesomeness=100

public async Task MyCommand(string option, int awesomeness = 50) {
  // ... Well that was easy
}

Available on Nuget CommandLineInjector

Documentation

Table of Contents

Overview

Let's say you have some code in a class like this that calls an injected dependency...

public class SendEmailCommand
{
    private readonly IEmailService _service; 

    // inject a service class that can perform the actual sending
    // (eg. SendGrid/Mailchimp client)
    public SendEmailCommand(IEmailService service)
    {
        _service = service; 
    }

    // Invokes this command to send an email
    public async Task Invoke(string address, string content = "Empty")
    {
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        // Build the email
        var email = new Email { Address = address, Content = content };

        // Send the email using the injected service
        await _service.Send(email);
    }
}

If we were using ASP.Net the above command class is pretty easy to wire up and call from a controller

public class EmailsController : Controller
{
    private readonly SendEmailCommand _command;

    public EmailsController(SendEmailCommand command)
    {
        _command = command;
    }

    [HttpPost]
    public async Task<IActionResult> Send(string address, string content)
    {
        await _command.Invoke(address, content);
        return Accepted();
    }
}

In the above example the parameter binding and dependency injection are handled for you in ASP.Net. Making a POST request to ~/emails?address=test@example.com?content=Hello will bind the 'address' and 'content' parameters for you and let you call your command with the SendEmailCommand injected in...

But what about Console apps?

Maybe you want to execute your command manaully from a console app, build a full command line runner for your application, or maybe you just want to use DI in console apps to keep your code organised.

The Command Line Injector package will allow you to bind the command line parameters to class methods, and resolve the dependencies at runtime like this.

namespace EmailSenderConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Register dependencies to a new collection
            var serviceCollection = new ServiceCollection()
                                        .AddTransient<IEmailService, SendGridEmailService>()
                                        .AddTransient<SendEmailCommand>();

            // Create the application
            var app = new CommandLineInjectingApplication("emailsender", new MicrosoftDependencyInjectionAdapter(serviceCollection, null));

            // Add our command class
            app.Command<SendEmailCommand>("send");

            // Execute the application with the command line argument array
            app.Execute(args);
        }
    }
}

The above console app can be run with

> dotnet emailsender send test@example.com --content="Hello World"

The package uses Microsoft's Microsoft.Extensions.CommandLineUtils package, extending the base CommandLineApplication by adding a generic overload of the Command<T>() method. This means you can decorate your command class and get help text

[Description("Sends an email with content to the given address")]
public class SendEmailCommand
{
    private readonly IEmailService _service; 

    public SendEmailCommand(IEmailService service)
    {
        _service = service;
    }

    public async Task Invoke(
            [Description("Email address to send to")] string address, 
            [Description("Optional email body content")] string content = "Empty")
    {
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        await _service.Send(new Email { Address = address, Content = content });

        // ... more code if you like
    }
}

Passing the help option --help (or -?) will print the contents of the description attributes.

> dotnet emailsender send --help

  Usage: dotnet emailsender send [arguments] [options]

  Arguments:
    [address] Email address to send to

  Options:
    -c|--content <value>      Optional email body content
    -?|-h|--help              Show help information

Try It!

There are examples in the /examples folder of this repository using popular DI frameworks. You can build and run them using the batch files, for example

> example-autofac --help

Usage: example-autofac [options] [command]

Options:
  -?|-h|--help  Show help information

Commands:
  simple  A simple example command with injected dependencies

Use "example-autofac [command] --help" for more information about a command.

Multiple Commands

The true power of all of this comes when you add multiple command registrations to build up a complex and multi-functional command line application. Let's add a second command to Retry and email.

[Description("Retries the last email sent to an address")]
public class RetryEmailCommand
{
    private readonly IEmailService _service; 

    public SendEmailCommand(IEmailService service)
    {
        _service = service;
    }

    public async Task Invoke([Description("Email address to resend to")] string address)
    {
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        await _service.Retry(address);

        // ... more code if you like
    }
}


namespace EmailSenderConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Register dependencies to a new collection
            var serviceCollection = new ServiceCollection()
                                        .AddTransient<IEmailService, SendGridEmailService>()
                                        .AddTransient<SendEmailCommand>()
                                        .AddTransient<RetryEmailCommand>();

            // Create the application
            var app = new CommandLineInjectingApplication("emailsender", new MicrosoftDependencyInjectionAdapter(serviceCollection, null));

            // Add our send command
            app.Command<SendEmailCommand>("send");

            // Add our retry command
            app.Command<RetryEmailCommand>("retry");

            // Execute the application with the command line argument array
            app.Execute(args);
        }
    }
}

Now if we check the help text you can see we have two commands available

> dotnet emailsender --help

  Usage: dotnet emailsender [arguments] [options]

  Options:
    -?|-h|--help  Show help information

  Commands:
    send     Sends an email with content to the given address
    retry    Retries the last email sent to an address

  Use "dotnet  [command] --help" for more information about a command.    

Service Classes

You don't have to have a command class for every console command. You can also specify a method from a "service" type class to use as a console app command. For example we could place both our Send and Retry methods onto one EmailCommandService class

public class EmailCommands
{
    private readonly IEmailService _service; 

    public EmailCommands(IEmailService service)
    {
        _service = service;
    }

    [Description("Retries the last email sent to an address")]
    public async Task Retry([Description("Email address to resend to")] string address)
    {
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        await _service.Retry(address);
    }


    [Description("Sends an email with content to the given address")]
    public async Task Send(
            [Description("Email address to send to")] string address, 
            [Description("Optional email body content")] string content = "Empty")
    {
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        await _service.Send(new Email { Address = address, Content = content });
    }
}


namespace EmailSenderConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Register dependencies to a new collection
            var serviceCollection = new ServiceCollection()
                                        .AddTransient<IEmailService, SendGridEmailService>()
                                        .AddTransient<EmailCommands>();

            // Create the application
            var app = new CommandLineInjectingApplication("emailsender", new MicrosoftDependencyInjectionAdapter(serviceCollection, null));

            // Add our sendand retry command methods on the EmailCommands class
            app.CommandService<EmailCommands>(nameof(EmailCommands.Send), nameof(EmailCommands.Retry));

            // Execute the application with the command line argument array
            app.Execute(args);
        }
    }
}

The above example will produce the same result as having two command classes.

> dotnet emailsender --help

  Usage: dotnet emailsender [arguments] [options]

  Options:
    -?|-h|--help  Show help information

  Commands:
    send     Sends an email with content to the given address
    retry    Retries the last email sent to an address

  Use "dotnet  [command] --help" for more information about a command.    

Global Option Flags

The console app also supports adding command-line options that are common to every command you register, and can be used to configure the DI container before it is resolved. For example the following snippet adds the "provider" option to all commands registered

namespace EmailSenderConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create our global option flag
            var providerOption = new ContainerConfigurationOption
                    {
                        Name = "provider",
                        ShortcutName = "p",
                        HelpText = "Sets the provider in the DI container",
                        HasValue = true
                    };

            // Register dependencies to a new collection
            var serviceCollection = new ServiceCollection()
                                        .AddTransient<EmailCommands>();

            // Create the application
            var app = new CommandLineInjectingApplication("emailsender", new MicrosoftDependencyInjectionAdapter(serviceCollection, (scopeBuilder, commands) =>
            {
                
                // *** ALTER THE SERVICE COLLECTION HERE **
                scopeBuilder.AddTransient<IEmailService>(...);

                return scopeBuilder;
            });

            // Add the global option to all commands
            app.AddToSubsequentAllCommands(providerOption);

            // Add our send and retry command methods on the EmailCommands class
            app.Commands<EmailCommands>(nameof(EmailCommands.Send), nameof(EmailCommands.Retry));

            // Execute the application with the command line argument array
            app.Execute(args);
        }
    }
}

Now when we query the available options we will see our extra "--provider" option has been applied to both the commands:

> dotnet emailsender send --help

  Usage: dotnet emailsender send [arguments] [options]

  Arguments:
    [address] Email address to send to

  Options:
    -c|--content <value>      Optional email body content
    -p|--provider <value>     Sets the provider in the DI container
    -?|-h|--help              Show help information

If we now run any command with the new "--provider" option set the code will drop into the scope builder expression and allow us to modify the IEmailService dependency based on the value of the "--provider=XXX" option

> dotnet emailsender send test@example.com --provider=Mailchimp

DI Frameworks

The base CommandLineInjector package contains the console app class (inherited from Microsoft.Extensions.CommandLineUtils.CommandLineApplication) and the interfaces required for dependency injection, but you have to also include the adapter for your DI Framework. Adapters are available for the following Dependency Injection frameworks

CommandLineInjector.Microsoft.DependencyInjection

This adapter uses the Microsoft.Extensions.DependencyInjection package which is commonly used in ASP.Net Core and EF Core scenarios. Availble on nuget CommandLineInjector.Microsoft.DependencyInjection

namespace CommandLineInjector.Microsoft.DependencyInjection.Example
{
    class Program
    {
        static void Main(string[] args)
        {
            var serviceCollection = new ServiceCollection()
                .AddTransient<IExampleService, ExampleService>()
                .AddTransient<TestSimpleCommandClass>();

            var containerAdapter = new MicrosoftDependencyInjectionAdapter(serviceCollection, (scopeBuilder, commands) =>
            {
                scopeBuilder.AddSingleton<IExampleConfiguration>(new ExampleConfiguration(commands));
                return scopeBuilder;
            });

            var app = new CommandLineInjectingApplication("commandlineinjector-example", containerAdapter);

            app.RequiresCommand();
            app.AddToSubsequentAllCommands(ExampleConfiguration.ConfigValueOption);

            app.Command<TestSimpleCommandClass>("simple");
            app.Execute(args);

            Console.ReadLine();
        }
    }
}

CommandLineInjector.Autofac

Adapter for the popular Autofac IoC Container. Availble on nuget CommandLineInjector.Autofac

namespace CommandLineInjector.Autofac.Example
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<ExampleService>().As<IExampleService>();
            builder.RegisterType<TestSimpleCommandClass>().AsSelf();

            var autofacContainer = builder.Build();

            var containerAdapter = new AutofacContainerAdapter(autofacContainer, (scopeBuilder, commands) =>
            {
                scopeBuilder.RegisterInstance(new ExampleConfiguration(commands)).As<IExampleConfiguration>();
                return scopeBuilder;
            });

            var app = new CommandLineInjectingApplication("commandlineinjector-example", containerAdapter);

            app.RequiresCommand();
            app.AddToSubsequentAllCommands(ExampleConfiguration.ConfigValueOption);

            app.Command<TestSimpleCommandClass>("simple");
            app.Execute(args);
        }
    }
}

CommandLineInjector.StructureMap

Adapter for StructureMap. Availble on nuget CommandLineInjector.StructureMap

namespace CommandLineInjector.StructureMap.Example
{
    class Program
    {
        static void Main(string[] args)
        {
            var structureMapContainer = new Container(config =>
            {
                config.For<IExampleService>().Use<ExampleService>();
            });

            var containerAdapter = new StructureMapContainerAdapter(structureMapContainer, (container, commands) =>
            {
                container.Configure(config =>
                {
                    config.For<IExampleConfiguration>().Use(new ExampleConfiguration(commands));
                });
                return container;
            });
            
            var app = new CommandLineInjectingApplication("commandlineinjector-example", containerAdapter);

            app.RequiresCommand();
            app.AddToSubsequentAllCommands(ExampleConfiguration.ConfigValueOption);

            app.Command<TestSimpleCommandClass>("simple");
            app.Execute(args);
        }
    }
}

CommandLineInjector.Lamar

Lamar is the long-term replacement for StructureMap, the adapter for Lamar looks very similar to the StructureMap adapter. Availble on nuget CommandLineInjector.Lamar

namespace CommandLineInjector.Lamar.Example
{
    class Program
    {
        static void Main(string[] args)
        {
            var structureMapContainer = new Container(config =>
            {
                config.For<IExampleService>().Use<ExampleService>();
                config.For<IExampleConfiguration>().Use<ExampleConfiguration>();
                config.Injectable<ExampleConfiguration>();
            });

            var containerAdapter = new LamarContainerAdapter(structureMapContainer, (container, commands) =>
            {
                container.Inject(new ExampleConfiguration(commands));
                return container;
            });

            var app = new CommandLineInjectingApplication("commandlineinjector-example", containerAdapter);

            app.RequiresCommand();
            app.AddToSubsequentAllCommands(ExampleConfiguration.ConfigValueOption);

            app.Command<TestSimpleCommandClass>("simple");
            app.Execute(args);
        }
    }
}

About

Framework for adding dependency injection to .NET Core console apps

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published