1. Introduction

I was recently tasked with developing a new console app at work and thought I would see how an implementation in .NET Core 3.1 would work.

Dependency Injection is an increasingly popular prominent design pattern in modern application development. Check out this article to learn about dependency injection in-depth (Coming soon).

ASP.NET Core 3.x is platformed on top of the generic host. A versatile object that let's us encapsulates an app's resources at start up and configure dependency injection. This looks and works a bit different to previous incarnations of .NET.

I'm going to share how I configured my generic host with a bare-bones approach you can adapt. It implements logging, autofac and is configurable with appsettings.json.

1.1 Quick recap of Dependency Injection

Dependency Injection, in a nutshell, is where components are supplied their dependencies through their constructors, methods, or directly into fields.

This means that you do not need to instantiate dependencies inside your classes, they are configured at start up and injected when needed.

A "dependency" is an object that your classes need to function, for example, a class from your business logic layer that interacts with your database.

Why should I care?

  • Maintainability - Dependency injection lets us configure and control our dependency's configuration from a central location outside of the classes that use them.

  • Reusability - External configuration also promotes reusability. If a different implementation of an interface is needed in a different context, the component is configurable externally without any need of changing the code.

  • Low coupling - When two things are closely coupled, changing one means changing the other. Bugs and restrictions in one will induce the same in the other. One class instantiating another and it's objects is indicative of very strong coupling.

  • Unit Testing - Loose coupling makes unit testing easier. When testing a class that instantiates other objects, we're no longer just testing our desired unit of code having increased the number of places that the test can fail. In addition, it is possible to inject mock implementations of dependencies which are replace the real implementation. the behaviour of the mock object can be configured and tested to handle all behaviours correctly.

I've got an article coming soon which will expand on the advantages of using DI.

Getting started with the generic host is as including the following packages:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; 

and instantiate a Host object in our start up class:

using Microsoft.Extensions.Hosting.Host;

var host = Host.CreateDefaultBuilder() 

2. The code

We do our dependency injection in the main method:

public static async Task Main(string[] args)
        {
            Console.WriteLine("Starting Application");

            // Dependency injection
            using var host = Host.CreateDefaultBuilder()
                .UseServiceProviderFactory(new AutofacServiceProviderFactory())
                .ConfigureHostConfiguration(Builder =>
                {
                    Builder.AddJsonFile("appsettings.json");
                })
                .ConfigureAppConfiguration((hostContext, builder) =>
                {
                    builder.AddJsonFile("appsettings.json");
                    builder.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json",
                    optional: true);
                })
                .ConfigureLogging((hostContext, loggingBuilder) =>
                {
                    Log.Logger = new LoggerConfiguration()
                            .ReadFrom.Configuration(hostContext.Configuration)
                            .CreateLogger();
                    loggingBuilder.AddSerilog();
                })
                .ConfigureContainer<ContainerBuilder>(autofacContainer =>
                {
                    autofacContainer.RegisterType<AppServices>().InstancePerLifetimeScope();
                })
                .ConfigureServices((hostBuilderContext, serviceCollection) =>
                {
                    serviceCollection.AddHostedService<ApplicationRoot>();
                    serviceCollection.AddTransient(typeof(LogClass));
                    serviceCollection.Configure<ConsoleLifetimeOptions>(options => options.SuppressStatusMessages = true); 
                    serviceCollection.Configure<TestOptions>(hostBuilderContext.Configuration.GetSection("Test")); 
                }).Build();
            {
                using (new TER_SYNAPSE_RCT_Entities())
                {
                    await host.StartAsync();
                }
                host.Dispose();
            }
        }

There's a fair bit going on here, so if you’ve got time, let’s geek out and break down the magic of Generic Host.

2.1 Add our dependency injection framework

  .UseServiceProviderFactory(new AutofacServiceProviderFactory())

After initialising the host, we start by telling our app which _Dependency Injection Container we want to use.

Note: these containers may also be referred to as_ Inversion of Control Containers_.

A Dependency Injection Container is the central location in your app that manages your dependencies and provides the features to inject and configure their lifetime scopes.

Autofac is my container framework of choice in the code template. Autofac is a popular, opensource DI framework for the .NET platform. Other popular C# choices include Hiro and Simple Injector.

We could in theory write our own framework but that’s not the point of our application. We don't want to divert time writing lots of code dealing with code dependencies!

Make sure to Include Autofac and Autofac.Extensions.DependencyInjection namespace for this to work.

2.2 Load appsettings.json

.ConfigureHostConfiguration(Builder =>
                {
                    Builder.AddJsonFile("appsettings.json");
                })
.ConfigureAppConfiguration((hostContext, builder) =>
                {
                    builder.AddJsonFile("appsettings.json");
                    builder.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json",
                    optional: true);
                })

What is the difference between ConfigureHostConfiguration() and ConfigureAppConfiguration()?

Host Configuration refers to any configuration needed when building the host itself such as current environment, environment variables, kestrel, content root. Everything else falls under App Configuration refers to any configuration the App needs (after the host is built) during its building process and while it is running. I've included both for example's sake, although if you only want to load appsettings.json, use one or the other.

Some appsettings.json trivia Since the beginning of the .NET Framework, the XML file app.config was typically where we stored variable configuration for our apps. It provided an easy to navigate space where we could apply behavioural changes outside the IDE without any need to recompile the application.

However, app.config had some drawbacks.

  • Loosely typed – Each setting variable must be parsed by the application.
  • Configuration Galore – All application configuration is stored in a single file. This includes everything from connection strings, security settings, binding redirects, etc.
  • No Overrides – There is no configuration hierarchy meaning that settings cannot be overridden by things such as environment variables.

A new more flexible configuration paradigm was introduced in ASP.NET Core. This approach used a .JSON file, usually referred to as appsettings.json You can read more about appsettings.json here (dedicated article coming soon).

Don't forget to include the following extension packages to use appsettings.json :

Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.FileExtensions
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.Configuration.Binder

2.3 Configure logging

.ConfigureLogging((hostContext, loggingBuilder) =>
{
    Log.Logger = new LoggerConfiguration()
            .ReadFrom.Configuration(hostContext.Configuration)
            .CreateLogger();
    loggingBuilder.AddSerilog();
})

I’m setting up [Serilog](https://serilog.net/) as our logging framework of choice - and often the choice of a good bulk of .NET developers!

Make sure to download and include the Serilog nuget package into your app

Logging configuration is contained in appsettings.json, which we can now read from by using: .ReadFrom.Configuration(hostContext.Configuration)

There's a lot of interesting configuration we can apply to various logging frameworks through our appsettings.json file, such as where to log to, fonts, additional information to print, etc. - You can check out my appsettings.json on the accomapny GitHub project.

2.4 Configure Container

Here we configure our Autofac based DI container and finally (the exciting stuff) register dependencies.

.ConfigureContainer<ContainerBuilder>(autofacContainer =>
  {
      autofacContainer.RegisterType<AppServices>().InstancePerLifetimeScope();
  })

We’re going to register our service class to the container which will be accessible to the application through constructor injection.

That's our services class, and let's say it contains various business logic methods required by our application to carry out it's tasks.

So each time the application needs it, we will inject it as a constructor parameter in the signature of classes that depend on it.

InstancePerLifetimeScope() instructs Autofac to apply at most a single instance per nested lifetime scope for our service class, ie. it's contained and resolved within the scope of the host.

2.5 Configure Services

 .ConfigureServices((hostBuilderContext, serviceCollection) =>
                {
                    serviceCollection.AddHostedService<ApplicationRoot>();
                    serviceCollection.AddTransient(typeof(LogClass));
                    serviceCollection.Configure<ConsoleLifetimeOptions>(options => options.SuppressStatusMessages = true); 
                    serviceCollection.Configure<TestOptions>(hostBuilderContext.Configuration.GetSection("Test")); 
                }

This is where we register our worker services. These are the processes that run asynchronously in the background of our application. (like daemons in Linux).

An example of background processing tasks that we might see in a console application could be a process handling messages on a queue. These types of long running process are pretty common in cloud-based container-based architecture.

This is going to include our class ApplicationRoot - which implements IHostedService and must implement StartAsync().

When the application starts, it will call all services implementing StartAsync().

We also register the Log class here as a transient. Transient lifetime services are recreated each time they're requested from the service container.

serviceCollection.AddTransient(typeof(LogClass));

Again, this will be managed by the Autofac container we configured in section 2.1.

Finally, Configure<TOptions>(IServiceCollection, IConfiguration) registers a configuration instance which TOptions will bind against. TOptions refers to type of options being configured.

serviceCollection.Configure<ConsoleLifetimeOptions>(options => options.SuppressStatusMessages = true);

This is not particularly important but I implement it. ASP.NET Core writes environment and configuration information to the console on startup. By setting the SuppressStatusMessages configuration value to true, I prevent the startup and shutdown messages outputting to the console and log avoiding some unneeded clutter! (This might be useful in some cases however!)

Image of this here! WIth box and explanation

Finally Build() initializes the host and returns the disposable IHost interface which let’s us call StartAsync(CancellationToken cancellationToken) to run the services implementing StartAsync().

3. End

As someone who builds software, I'm often thinking a lot about how I should organise my code. Good code organisation is fundamental in ensuring that a large app remains scalable, easy to understand, easy to change and easy to test.

My template, which you can use to get going with your own .NET CORE 3 console apps should help to encourage some good practices from the offset, of which, Dependency Injection remains an important part.