Dependency injection (with IOptions) in Console Apps in .NET

When you are used to building web applications, you kind of get hooked to the ease of Dependency Injection (DI) and the way settings can be specified in a JSON file and accessed through DI (IOptions<T>). It's only logical to want the same feature in your Console app.

After reading many - many! - outdated blogs, I decided to add my 50 cents specifically on a Console app. It all comes down to packages and separation of concerns.

The first version of this article was written in 2018. It has gotten lots of views since and it should stay green. That's why I've rewritten the article to include instructions for .NET 7.

👋 from 2023! Console Applications evolve and Microsoft is working on a System.CommandLine package that you might want to include in your CLI. Read more at .NET Console Application with injectable commands. I've changed this article to include the RootCommand.

Nuget me!

First, we need the right packages. And that can be quite a hassle - because the ASP.NET Core All package will include them for you when you are building an ASP.NET application - but there's no such thing for a Console app. They might be hard to google find:

  • Install-Package Microsoft.Extensions.Configuration -Version 7.0.0
    Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables -Version 7.0.0
    Install-Package Microsoft.Extensions.Configuration.FileExtensions -Version 7.0.0
    Install-Package Microsoft.Extensions.Configuration.Json -Version 7.0.0
    Install-Package Microsoft.Extensions.DependencyInjection -Version 7.0.0
    Install-Package Microsoft.Extensions.DependencyInjection.Abstractions -Version 7.0.0
    Install-Package Microsoft.Extensions.Logging -Version 7.0.0
    Install-Package Microsoft.Extensions.Logging.Console -Version 7.0.0
    Install-Package Microsoft.Extensions.Logging.Debug -Version 7.0.0
    Install-Package Microsoft.Extensions.Options -Version 7.0.1
    Install-Package System.CommandLine -Version 2.0.0-beta4.22272.1
  • dotnet add package Microsoft.Extensions.Configuration --version 7.0.0
    dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables --version 7.0.0
    dotnet add package Microsoft.Extensions.Configuration.FileExtensions --version 7.0.0
    dotnet add package Microsoft.Extensions.Configuration.Json --version 7.0.0
    dotnet add package Microsoft.Extensions.DependencyInjection --version 7.0.0
    dotnet add package Microsoft.Extensions.DependencyInjection.Abstractions --version 7.0.0
    dotnet add package Microsoft.Extensions.Logging --version 7.0.0
    dotnet add package Microsoft.Extensions.Logging.Console --version 7.0.0
    dotnet add package Microsoft.Extensions.Logging.Debug --version 7.0.0
    dotnet add package Microsoft.Extensions.Options --version 7.0.1
    dotnet add package System.CommandLine --version 2.0.0-beta4.22272.1
  • <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />

The project file

Edit your console project file and replace its contents with this.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
  </ItemGroup>

</Project>

Note: this project uses implicit usings, a C# 10 feature, so we don't have to declare all usings.

Now you can upgrade the packages to the latest version in your Nuget manager. If you're not on .NET 5 or higher, you might need to add a <LangVersion>latest</LangVersion> to the first PropertyGroup. This will enable the latest version of C# including the async main (needed for .NET Core < 3).

AppSettings class + json

This blog has been around for a while now and people asked for an example of the actual settings. I use this class as AppSettings:

public class AppSettings
{
    public string Greeting { get; set; }
}

Next, you can add an appsettings.json to the root of your application:

{
  "App": {
    "Greeting": "Hello {0}! Welcome to the tool."
  }
}

Make sure the file has properties: Build Action must be None and Copy to Output Directory must be Copy always. It should like this in your project file:

  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>

The JSON file works with sections, and we'll see later in the article how we bind the App section to an AppSettings instance. Want to know more about the setup with json files? Please read Setup multiple setting-files with a .NET Console Application.

Program vs. App

Normally the program class contains the main method and all the program-logic. If you want to do DI, it might be handy to separate the application from the setup that's required for your DI. So I propose to build an App class that does the actual running. For argument's sake, I've created such a class with a logger and some settings. The class is derived from RootCommand that will do the argument parsing.

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.CommandLine;

public class App: RootCommand
{
    private readonly ILogger<App> _logger;
    private readonly AppSettings _appSettings;

    public App(IOptions<AppSettings> appSettings, ILogger<App> logger):
        base("Sends a nice greeting to the user.")
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _appSettings = appSettings?.Value ?? throw new ArgumentNullException(nameof(appSettings));

        var nameArgument = new Argument<string>("name", "The name of the person to greet.");
        AddArgument(nameArgument);

        this.SetHandler(Execute, nameArgument);
    }

    private async Task Execute(string name)
    {
        _logger.LogInformation("Starting...");
        var greeting = String.Format(_appSettings.Greeting, name);
        _logger.LogDebug($"Greeting: {greeting}");

        Console.WriteLine(greeting);

        _logger.LogInformation("Finished!");

        await Task.CompletedTask;
    }
}

This basically gives us an abstracted application class that is even testable. It also gives us the opportunity to focus on setting up the application in the program class (separating the actual application logic).

Setting up the program

In the program, we'll focus on DI. The App will also be added through DI. We're using the C# 10 top level statement, so we don't need a Program class:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.CommandLine;

static void ConfigureServices(IServiceCollection services)
{
    // configure logging
    services.AddLogging(builder =>
    {
        builder.AddConsole();
        builder.AddDebug();
    });

    // build config
    var configuration = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", optional: false)
        .AddEnvironmentVariables()
        .Build();

    services.Configure<AppSettings>(configuration.GetSection("App"));

    // add services:
    // services.AddTransient<IMyRespository, MyConcreteRepository>();

    // add app
    services.AddTransient<App>();
}

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

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

// entry to run app
await serviceProvider.GetService<App>().InvokeAsync(args);

Note: since C# 8, the using statement does not need braces anymore.

Invoke the program

When you do a dotnet run -- --help in your project dir, you'll see the following:

Description:
  Sends a nice greeting to the user.

Usage:
  ConsoleApp <name> [options]

Arguments:
  <name>  The name of the person to greet.

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

When you do a dotnet run -- "peeps from KeesTalksTech" you'll see:

info: App[0]
      Starting...
Hello peeps from KeesTalksTech! Welcome to the tool.
info: App[0]
      Finished!

Final thoughts

We've injected a few things:

  • Logging − a lot of my applications run as stand-alone services in Docker containers, so logging is key. For convenience, I added a console logger. You can add any form of logging to the LoggerFactory.
  • Configuration − I use an appsettings.json file in my application. It will contain a section with the app variables. The DI will auto-map it to the setting object. To make things easier I added the AddEnvironmentVariables() so I'm able to override settings in production.
  • Services − this gives us nice abstractions.
  • RootCommand − this provides command line parsing. It also gives us a nice --help feature.

The program will resolve the App through DI and start your program. So that's it. Easy to use.

Need the code? Check GitHub.

Changelog

  • 2023-03-11: Added .NET 7 and RootCommand support. Added the Invoke the program section.
  • 2022-04-09: Added .NET 6 support with implicit usings and top level statements. Also updated the code on GitHub.
  • 2021-07-23: Added .NET 5 support and a link to a GitHub repo.
  • 2020-04-07: Added .NET 3.1 and support for args in the App.
  • 2019-09-25: Added "Hello World" message and .NET Core 3.0 support.
  • 2019-01-10: Added AppSettings class and JSON config examples.
  • 2018-12-14: Added 2 packages missing in the list of Nuget packages.
  • 2018-12-14: Logging is now done using the logging builder - the other version is obsolete.
  • 2018-12-14: Added async main support by specifying the project example.
  1. Mika Genic says:

    I get the type or namespace ‘App’ could not be found

    1. Kees C. Bakker says:

      Can you share your code? App is the application class.

  2. Fran says:

    Hi,
    I think that using Microsoft.Extensions.Options package is missing

  3. Fran says:

    … and, in addition to ‘Microsoft.Extensions.Options’ package, it is also required the ‘Microsoft.Extensions.Options.ConfigurationExtensions’, for the call to ‘services.Configure(…);’ to work properly.

    Thanks for the post, very interesting and useful!

  4. Carl Sixsmith says:

    You are missing a nuget dependency

    AddEnvironmentVariables() requires the Microsoft.Extensions.Configuration.EnvironmentVariables package.

  5. abrasat says:

    Thank you for this useful article. Could you please post also the AppSettings class definition and also the content of the appsettings.json file?

    1. Kees C. Bakker says:

      Done. It got its own section now. If you want to read more about console applications and json appsetting files, you could also read: https://keestalkstech.com/2019/01/setup-multiple-setting-files-with-a-net-console-application/

  6. JWomack says:

    Any ideas how to use the similar “Logging” property in the appsettings.json that you get with asp.net setup? I am using your samples but my logger won’t log debug or trace flagged messages to the console. Something like
    "Logging": {
    "LogLevel": {
    "Default": "Debug",
    "Microsoft": "Warning"
    }

  7. Jarda says:

    Kees, thank you so very much. So perfect and simple solution. I love it :) .

  8. Tom Woodforde says:

    This was really helpful, many thanks

  9. AjithC says:

    This is awesome. You saved me after 48hours tries.

  10. Mihail Stratan says:

    Thank you very much. You saved me a lot of time!

  11. Chris says:

    Thank you – this worked for me (using .Net 6 preview). Though I would have preferred a more vanilla implementation at first, perhaps somehow without the App class. That way it would be easy to see the bare minimum implementation without any abstractions. I think the concepts would be easier to understand then. Then build out from there and introduce the abstractions on top for a better solution.

    Anyway, thanks , saved me a lot of time.

expand_less