# .NET Console Application with injectable commands

**Date:** 2023-03-07  
**Author:** Kees C. Bakker  
**Categories:** .NET / C#  
**Tags:** Console Application  
**Original:** https://keestalkstech.com/net-console-application-with-injectable-commands/

![.NET Console Application with injectable commands](https://keestalkstech.com/wp-content/uploads/2023/03/william-daigneault-oWrZoAVOBS0-unsplash.jpg)

---

Console applications are alive and kicking. Setting them up might be a bit hard. In this article I'll explore how to create a .NET console application that provides commands using the new `System.CommandLine` package. This will provide arguments to command mapping out of the box. I'll be showing how to combine it with dependency injection for even more power ⚡.

[outline]

## Goals

We want to create a CLI application with the following goals:

- **System.CommandLine** — this is a [fairly new project](https://learn.microsoft.com/en-us/dotnet/standard/commandline/) by .NET that helps to create better CLI applications. It offers the ability to add *commands*, *arguments* and *options* to your application. It comes with a `--help` feature and it will do the command line argument mapping for you.
- **Dependency Injection** — why go anywhere without it? Dependency injection has made ASP.NET way more composable. [I wrote an entire article on how to add it to console applications as well](https://keestalkstech.com/2018/04/dependency-injection-with-ioptions-in-console-apps-in-net-core-2/). We'll be reusing some of the code.
- **Environment variable injection support** — some of the configuration should be overridable using environment variables.

We're making a CLI, so what's a better way to describe it than showing what the `--help` should look like?

```txt
Description:
  Weather information using a very unreliable weather service.

Usage:
  MyCli [command] [options]

Options:
  --version       Show version information
  -?, -h, --help  Show help and usage information

Commands:
  current   Gets the current temperature.
  forecast  Get the forecast. Almost always wrong.
```

*Note: if you want to use command line argument when executing a `dotnet run`, you can use `--` to feed the arguments to the application instead of the .NET CLI (so `dotnet run -- --help` in this case).*

## NuGet Packages

If you say .NET, you say NuGet packages. We'll be using the following packages:

```sh
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Options
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions
dotnet add package System.CommandLine
```

The `System.CommandLine` package is still in ~~beta~~ preview. I expect it to be released soon, but things might still change. It is [used by the .NET `dotnet` CLI](https://github.com/dotnet/sdk/blob/main/src/Cli/dotnet/dotnet.csproj#L96).

## Project structure

I'm using the following project structure:

```txt
.
├── src/
│   └── MyCli/
│       ├── Commands/
│       │   ├── CurrentCommand.cs
│       │   └── ForcastCommand.cs
│       ├── Services/
│       │   ├── FakeWeatherService.cs
│       │   └── FakeWeatherServiceSettings.cs
│       └── Program.cs
└── MyCli.sln
```

## Weather service

What is injection without a good service? Let's create a *weather service* that returns the temperature based on a randomizer:

```cs
namespace MyCli.Services;

class WeatherService()
{
    public WeatherServiceOptions Options { get; } = new WeatherServiceOptions();

    public Task<string> GetTemperature(string? city = null)
    {
        if (city == null) city = Options.DefaultCity;

        var report = $"In {city} it is now {Random.Shared.Next(-20, 40)} degrees celcius.";
        return Task.FromResult(report);
    }

    public Task<string[]> Forecast(int days, string? city = null)
    {
        if (city == null) city = Options.DefaultCity;

        var reports = new List<string>
        {
            $"Report for {city} for the next {days} days:"
        };

        for (var i = 0; i<days; i++)
        {
            var date = DateTime.Now.AddDays(i + 1).ToString("yyyy-MM-dd");
            var report = $"- {date}: {Random.Shared.Next(-20, 40),3} degrees celcius.";
            reports.Add(report);
        }

        return Task.FromResult(reports.ToArray());
    }
}

class WeatherServiceOptions
{
    public string DefaultCity { get; set; } = "Amsterdam, NLD";

    public int DefaultForecastDays { get; set; } = 5;
}
```

## Commands

Commands are implementations of the `System.CommandLine.Command` class. To make them injectable, we create classes that are derived from the `Command` class (see [dependency injection section](#dependency-injection)).

### Current Temperature Command

To get our *current temperature* command, we'll need to do the following:

- Call the base constructor with the *name* and *description* of the command. This will be used by the `--help` feature.
- Inject the `WeatherService`, as it does the actual work.
- Use the `WeatherService.`Options to get the default value for the `--city` option.
- Map it all together using a `SetHandler`. The option in automatically mapped to the `city` parameter of the `Execute` method.

Now the implementation is very easy:

```cs
using MyCli.Services;
using System.CommandLine;

namespace MyCli.Commands;

class CurrentCommand : Command
{
    private readonly WeatherService _weather;

    public CurrentCommand(WeatherService weather) : base("current", "Gets the current temperature.")
    {
        _weather = weather ?? throw new ArgumentNullException(nameof(weather));

        var cityOption = new Option<string>("--city", () => _weather.Options.DefaultCity, "The city.");

        AddOption(cityOption);
        
        this.SetHandler(Execute, cityOption);
    }

    private async Task Execute(string city)
    {
        var report = await _weather.GetTemperature(city);
        Console.WriteLine(report);
    }
}
```

What I like about the setup is that we can add *optional arguments* with *defaults*. Here we get the default value from an object from our dependency injection. When we do a `current --help`, we can a nice description and the *actual injected value*:

```txt
Description:
  Gets the current temperature.

Usage:
  MyCli current [options]

Options:
  --city <city>   The city. [default: Amsterdam, NLD]
  -?, -h, --help  Show help and usage information
```

### Forecast Command

The same goes for the forecast command, but now we have 2 options: `--city` and `--days`.

```cs
using MyCli.Services;
using System.CommandLine;

namespace MyCli.Commands;

class ForecastCommand : Command
{
    private readonly WeatherService _weather;

    public ForecastCommand(WeatherService weather) : base("forecast", "Get the forecast. Almost always wrong.")
    {
        _weather = weather;

        var cityOption = new Option<string>("--city", ()=> _weather.Options.DefaultCity, "The city.");
        var daysOption = new Option<int>("--days", () => _weather.Options.DefaultForecastDays, "Number of days.");

        AddOption(cityOption);
        AddOption(daysOption);

        this.SetHandler(Execute, cityOption, daysOption);
    }

    private async Task Execute(string city, int days)
    {
        var report = await _weather.Forecast(days, city);
        foreach (var item in report)
        {
            Console.WriteLine(item);
        }
    }
}
```

## Dependency injection

Now, let's tie it all together using dependency injection. We need to do the following:

- Setup a `ServiceCollection` to store our dependencies.
- Add the commands `CurrentCommand` and `ForecastCommand` to the service collection.
- Add the `WeatherService` to the service collection.
- Create a `System.CommandLine.RootCommand` and tie it to the registered `Command` implementation.
- Invoke the root command with the given command line arguments.

This leads to the following `Program` code:

```cs
using Microsoft.Extensions.DependencyInjection;
using MyCli.Commands;
using MyCli.Services;
using System.CommandLine;

static void ConfigureServices(IServiceCollection services)
{
    // add commands:
    services.AddTransient<Command, CurrentCommand>();
    services.AddTransient<Command, ForecastCommand>();

    // add services:
    services.AddTransient<WeatherService>();
}

// create service collection
var services = new ServiceCollection();
ConfigureServices(services);

// create service provider
using var serviceProvider = services.BuildServiceProvider();

// entry to run app
var rootCommand = new RootCommand("Weather information using a very unreliable weather service.");
serviceProvider
    .GetServices<Command>()
    .ToList()
    .ForEach(rootCommand.AddCommand);

await rootCommand.InvokeAsync(args);
```

To make dependency injection work, we do a `GetServices` to retrieve all the commands and add them to the root command.

## Final thoughts

And that's all: now you have a CLI that supports commands and a `--help` feature out of the box!

The code is on GitHub, so check it out: [code gallery / 4. .NET Console Application with injectable commands](https://github.com/KeesCBakker/keestalkstech-code-gallery/tree/main/04.command-line-di-poc).

## Changelog

- 2024-11-30: converted the code to .NET 8 and added it to the [gallery](https://github.com/KeesCBakker/keestalkstech-code-gallery).
- 2023-07-03: initial article.
