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 is 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 concern.

Note: it feels like a bit of over-engineering, but you’ll get some nice features in return; the program becomes more pluggable and more testable. It also provides a nice async main function.

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:

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.EnvironmentVariables
  • Microsoft.Extensions.Configuration.FileExtensions
  • Microsoft.Extensions.Configuration.Json
  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.DependencyInjection.Abstractions
  • Microsoft.Extensions.Logging
  • Microsoft.Extensions.Logging.Console
  • Microsoft.Extensions.Logging.Debug
  • Microsoft.Extensions.Options

The project file

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

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
    <PackageReference Include="Microsoft.Extensions.Logging" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" />
    <PackageReference Include="Microsoft.Extensions.Options" />
  </ItemGroup>
</Project>

Now you can upgrade the packages to the latest version in your Nuget manager (I’ve omitted versions, as they become outdated and you want the latest). The <LangVersion>latest</LangVersion> will enable the latest version of C# including the async main.

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 TempDirectory { get; set; }
}

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

{
    "App": {
        "TempDirectory": "c:\\temp\\rss-hero\\tmp\\"
    }
}

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:

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

    public App(IOptions<AppSettings> appSettings, ILogger<App> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _appSettings = appSettings?.Value ?? throw new ArgumentNullException(nameof(appSettings));
    }

    public async Task Run()
    {
        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. Not that the entry point of the program is already async so we can use await.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading.Tasks;

internal class Program
{
    public static async Task Main(string[] args)
    {
        // create service collection
        var services = new ServiceCollection();
        ConfigureServices(services);

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

        // entry to run app
        await serviceProvider.GetService<App>().Run();
    }

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

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

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

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

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

We’ve injected a few things:

  • Logging − a lot of my application 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 a 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 will give us nice abstractions.

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

Improvements
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.