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

**Date:** 2018-04-26  
**Author:** Kees C. Bakker  
**Categories:** .NET / C#  
**Tags:** Console Application  
**Original:** https://keestalkstech.com/dependency-injection-with-ioptions-in-console-apps-in-dotnet/

![Dependency injection (with IOptions) in Console Apps in .NET](https://keestalkstech.com/wp-content/uploads/2018/04/alice-gu-Xh9vvqf_3G4-unsplash.jpg)

---

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`). 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.

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](https://keestalkstech.com/2023/03/net-console-application-with-injectable-commands/).

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 8** (with is LTS).

## 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:

```sh
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
dotnet add package Microsoft.Extensions.Configuration.FileExtensions
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Microsoft.Extensions.Logging.Configuration
dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.Logging.Debug
dotnet add package Microsoft.Extensions.Options
dotnet add package Microsoft.Extensions.Options.DataAnnotations
```

## The project file

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

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
	<Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.0" />
  </ItemGroup>

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

</Project>
```

This project uses [implicit usings, a C# 10 feature](https://devblogs.microsoft.com/dotnet/welcome-to-csharp-10/#global-and-implicit-usings), 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 `latest` to the first `PropertyGroup`. This will enable the latest version of C# including the `async` main (needed for .NET Core < 3).

## AppOptions class + json

This blog has been around for a while now and people asked for an example of the actual options to inject. Let's create a class that uses data annotation to validate the configuration:

```cs
using System.ComponentModel.DataAnnotations;

public class AppOptions
{
    public const string SectionName = "App";

    [Required(AllowEmptyStrings = false)]
    public string Greeting { get; set; } = String.Empty;
}
```

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

```cs
{
  "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:

```xml
<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 `AppOptions` instance. Want to know more about the setup with JSON files? Please read [Setup multiple setting-files with a .NET Console Application](https://keestalkstech.com/2019/01/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 the `AppOptions`. The class is derived from `RootCommand` that will do the argument parsing.

```cs
using Microsoft.Extensions.Logging;

public class App(ILogger<App> _logger, AppOptions _options)
{
    public async Task Execute(string[] args)
    {
        var name = args.Length == 0 ? "World" : args[0];

        _logger.LogInformation("Starting...");
        var greeting = string.Format(_options.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](https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements), so we don't need a `Program` class:

```cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

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

    // build config
    services.AddSingleton<IConfiguration>(_ => 
        new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false)
            .AddEnvironmentVariables()
            .Build());

    void Configure<TConfig>(string sectionName) where TConfig : class
    {
        services
            .AddSingleton(p => p.GetRequiredService<IOptions<TConfig>>().Value)
            .AddOptionsWithValidateOnStart<TConfig>()
            .BindConfiguration(sectionName);
            .Validate(options =>
            {
                var results = new List<ValidationResult>();
                var context = new ValidationContext(options);
                if (!Validator.TryValidateObject(options, context, results, validateAllProperties: true))
                {
                    throw new OptionsValidationException(
                        sectionName,
                        typeof(TOptions),
                        results.Select(r => $"[{sectionName}] {r.ErrorMessage}")
                    );
                }
                return true;
            });
    }

    Configure<AppOptions>(AppOptions.SectionName);

    // 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.GetRequiredService<App>().Execute(args);
```

[Since C# 8, the using statement does not need braces anymore](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement#example).

## Invoke the program

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

```txt
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.

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

The code is on GitHub, so check it out: [code gallery / 1. Dependency injection (with IOptions) in Console Apps in .NET](https://github.com/KeesCBakker/keestalkstech-code-gallery/tree/main/01.dependency-injection-with-ioptions-in-console-apps).

## Changelog

- 2025-05-23: Improved the validation exception with the name of the section for better discoverability.
- 2024-12-13: Added the .NET 8 `AddOptionsWithValidateOnStart`. Removed the `RootCommand` as the blog [.NET Console Application with injectable commands](https://keestalkstech.com/2023/03/net-console-application-with-injectable-commands/) has all the information on it.
- 2024-11-30: Added .NET 8 and converted `AppSettings` to `AppOptions` with data annotation validation on startup. I also enabled `Nullable` in the project.
- 2023-03-11: Added .NET 7 ~~and `RootCommand` support~~. Added the [Invoke the program](#invoke-the-program) section.
- 2022-04-09: Added .NET 6 support with *[implicit usings](https://devblogs.microsoft.com/dotnet/welcome-to-csharp-10/#global-and-implicit-usings)* and *[top level statements](https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements)*.
- 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. Logging is now done using the logging builder - the other version is obsolete. Added `async` main support by specifying the project example.
