Dependency Injection, Logging and Configuration In A .NET Core Console Application

With .NET Core, we get a lot of stuff for free. If you so choose, wiring up your applications (ASP.NET or Console) with things like Dependency Injection, Configuration and Logging is as simple as can be. .NET Core bakes these features into the framework and usage of does not require a trade-off of power or flexibility.

Following, I will walk you through getting up and running with all three of these items in a .NET Core console application.

Be sure to head over to the git repository that supports this post at - Pioneer Console Boilerplate

Setup

Project

First thing first, let's create a new project. Open up Visual Studio, hit Ctrl+Shift+N and select the Console Application (.NET Core) project type from the templates.

Visual studio will scaffold a project for you which will end up looking like this.

Dependencies

.NET Core is all about opting into dependencies as opposed to opting out. That being said we will need to pull in a few NuGet packages to facilitate our needs. Open up your project.json file and add the following packages to your dependencies node. Versions might differ depending on what version of .NET Core you are using.

  "dependencies": {
    "Microsoft.NETCore.App": "1.1.0",
    "Microsoft.Extensions.DependencyInjection": "1.1.0",
    "Microsoft.Extensions.Configuration.Json": "1.1.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0",
    "Microsoft.Extensions.Configuration": "1.1.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
    "Microsoft.Extensions.Logging": "1.1.0",
    "Microsoft.Extensions.Logging.Console": "1.1.0",
    "Microsoft.Extensions.Logging.Debug": "1.1.0"
  }

Fair warning, project.json is on its way out in favor of .csproj. As of the date this was published, this transition is already available in the Visual Studio 2017 release candidate.

Dependency Injection

Now that our project is ready, we need to wire up our dependency injection container so we have a means to register the individual components we will be using in our program. For this demonstration, we will be registering a new service called TestService. Go ahead and create a new file called TestService.cs and add the following.

public interface ITestService
{
}

class TestService : ITestService
{
}

To maintain a separation of concern between our business based logic and the logic we use to configure and run the actual console application, let's create a new class called App.cs. The goal being that we will use Program.cs to bootstrap everything need to support our application and then fire off all the logic that is needed to support out "business" needs by way of executing the Run() method in App.cs.

public class App
{
    private readonly ITestService _testService;

    public App(ITestService testService)
    {
        _testService = testService;
    }

    public void Run()
    {
        _testService.Run();
        System.Console.ReadKey();
    }
}

App.cs needs an object that meets the ITestService interface contract. This object will be passed in through our dependency manager.

Open up your Program.cs file and add the following.

public class Program
{
    public static void Main(string[] args)
    {
        // create service collection
        var serviceCollection = new ServiceCollection();
        ConfigureServices(serviceCollection);

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

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

    private static void ConfigureServices(IServiceCollection serviceCollection)
    {
        // add services
        serviceCollection.AddTransient<ITestService, TestService>();

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

Our console app starts out in Main. Here we create a new ServiceCollection object and configure it inside of ConfigureServices. In ConfigureServices we add our dependencies to the container collection, of which can have a lifetime of Scoped, Transient or Singleton. Once the ServiceCollection object is configured, we then need to request an IServiceProvider (Dependency Management Container) from our ServiceCollection object in order to manually resolve our App class and kick our logical loop off by calling its Run() method.

For more information on service lifetime and registration options, visit .NET Dependency Injection.

Logging

Now that we have our Dependency Injection wired up, we need to setup logging. More specifically, we need to log to the console so that we can visually verify that not only our App.Run() method is being called but also that our ITestService is being injected and ran from the App class.

Setup

Open of Program.cs again and update the ConfigureServices method with the following.

private static void ConfigureServices(IServiceCollection serviceCollection)
{
    // add logging
    serviceCollection.AddSingleton(new LoggerFactory()
    .AddConsole()
    .AddDebug());
    serviceCollection.AddLogging(); 

    // add services
    serviceCollection.AddTransient<ITestService, TestService>();

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

We first add new instances of ILoggerFactory for the Console (outputs to the console) and Debug (writes log output by way of System.Diagnostics.Debug) providers. Both providers will have a Singleton lifetime. Finally, we add our logging services to your service collection.

Usage

Update the App class with the following.

    public class App
    {
        private readonly ITestService _testService;
        private readonly ILogger<App> _logger;

        public App(ITestService testService,
            ILogger<App> logger)
        {
            _testService = testService;
            _logger = logger;
        }

        public void Run()
        {
            _logger.LogInformation($"This is a console application for {_config.Title}");
            _testService.Run();
            System.Console.ReadKey();
        }
    }

We now are injecting an instance of ILogger<App>. Of which we are using to LogInfromation inside our Run() method. Additionally, we are calling the ITestService.Run() of our injected service to further illustrate that our dependency injection is actually managing dependencies correctly.

Update the TestService class and interface with the following.

public interface ITestService
{
    void Run();
}

class TestService : ITestService
{
    private readonly ILogger<TestService> _logger;

    public TestService(ILogger<TestService> logger)
    {
        _logger = logger;
    }

    public void Run()
    {
        _logger.LogWarning("Wow! We are now in the test service.");
    }
}

Much like App.cs we inject an instance of ILogger<TestService> and perform a LogWarning inside of ITestService.Run().

If we run our console app now, we should end up with something like the following. Of which shows our path down to the ITestService instance through our logging.

Configuration

Typically you will need some kind of configuration to manage your app's deployment. Luckily, .NET Core offers up a clean and lightweight method of pulling in configuration through a JSON file.

Create a file called app-settings.json and add the following.

{
  "Configuration": {
    "Title": "Pioneer Console Boilerplate"
  }
}

Create a file called AppSettings.cs and add the following. We will use this model to deserialize our app-settings.json file into.

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

Update Program.ConfigureServices with the following.

private static void ConfigureServices(IServiceCollection serviceCollection)
{
    // add configured instance of logging
    serviceCollection.AddSingleton(new LoggerFactory()
        .AddConsole()
        .AddDebug());

    // add logging
    serviceCollection.AddLogging(); 

    // build configuration
    var configuration = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("app-settings.json", false)
        .Build();
    serviceCollection.AddOptions();
    serviceCollection.Configure<AppSettings>(configuration.GetSection("Configuration"));

    // add services 
    serviceCollection.AddTransient<ITestService, TestService>();

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

We use a ConfigurationBuilder object to...

  1. Set an absolute path scoped to the directory our app-settings.json file lives.
  2. Add our app-settings.json file to IConfigurationRoot object, denoting it as not optional.
  3. Build and return our IConfigurationRoot object.

We then call AddOptions() on our IServiceCollection object to add services needed to use the Options pattern inside of our app. In short, the Options Pattern allows us to decouple feature configuration in our application and bind said feature configuration to independent models.

Finally, we register the IConfigurationRoot instance, which our Options will bind against.

Usage

Open up your App class and update it with the following.

public class App
{
    private readonly ITestService _testService;
    private readonly ILogger<App> _logger;
    private readonly AppSettings _config;

    public App(ITestService testService,
        IOptions<AppSettings> config,
        ILogger<App> logger)
    {
        _testService = testService;
        _logger = logger;
        _config = config.Value;
    }

    public void Run()
    {
        _logger.LogInformation($"This is a console application for {_config.Title}");
        _testService.Run();
        System.Console.ReadKey();
    }
}

Open up your TestService class and update it with the following.

class TestService : ITestService
{
    private readonly ILogger<TestService> _logger;
    private readonly AppSettings _config;

    public TestService(ILogger<TestService> logger,
        IOptions<AppSettings> config)
    {
        _logger = logger;
        _config = config.Value;
    }

    public void Run()
    {
        _logger.LogWarning($"Wow! We are now in the test service of: {_config.Title}");
    }
}

In both updates, we inject an IOptions<t> with a type of AppSettings. This means we will get an object that has our app-settings.json config file deserialized into that object. Of which, we use to print our Title variable out with our logging message.

Run the application and you should now get something that looks like this.

And that is that. .NET Core makes adding configuration, logging and dependency injection simple to add into your app no matter the type. As always, if you have any comments be sure to let me know below.

Be sure to head over to the git repository that supports this post at - Pioneer Console Boilerplate