This blog post describes how to setup logging and dependency injection in a modern .net application so that the same components can be re-used across both runtimes — in the classical .NET framework and in the new .NET Core 2.0.

We’ll create a console application and a web service that will both output the same value from a log-enabled library. The target audience is the blog’s author 17 years ago, who was suddenly transferred by a time machine to 2018.

The final code is available in a GitHub repo.

Preparation

We’ll need Visual Studio 2017 Community.

When installing, make sure to tick both .NET desktop development and web components:

While, strictly speaking, it is possible to use Visual Studio Code and the .NET Core 2.0 SDK, the full version of Visual Studio has finally matured enough to support the latest .NET Core, so given that it has all the needed templates, and the Community edition is free for personal use, there is no reason not to utilise it.

Introduction and history

The current state of frameworks

Earlier there existed another technology to write cross-platform libraries in C# called Portable Class Libraries (PCL). It introduced in the later stages of Windows Phone/Silverlight lifetime and ended up being a mess. The most important problems with PCLs were the ever-growing list of platform combinations one could target and a very narrow set of intersecting APIs in most of them. These lead to an unpleasant environment for library developers who had to support at least a few combinations, what required knowing special hacks and workarounds, setting up multiple build configurations etc. The outcome was of course a stale ecosystem of components not working together. In practice it was impossible to create a new library or component that was truly re-usable in all .NET incarnations.

The other source of problems was the non-stoping bifurcation of terms, versions, namings, workflows, approaches, project file types (project.json, anyone?), SDKs and so on up until the release of .NET Core 2.0. This made it very difficult for a new person to start developing anything using .NET Core, as most tutorials were outdated in a month time, and one would have to dig through lots of information to grasp the present state of the technology.

PCLs were gradually replaced over the last couple of years with the new .NET Standard approach. It was initially introduced during .NET Core 1.0 times, in 2015, and has matured since then. .Net Standard libraries are consumable both in classical .NET Framework and in .NET Core, as well as in other implementations like Mono or Xamarin.

As one can see at the .NET Standard page, there are a few versions of .NET Standard. The table there shows minimum versions of frameworks that support the libraries. It is not 100% correct about version 2.0 though — it is practically supported by the classical framework only starting from 4.7.1, not 4.6.1. 4.6.1 actually requires some kind of patches to fully use it. Despite this fact, 4.7.1 is installable on all Windows versions since 7 / 2008 R2 SP1, so it doesn’t make sense to use lower versions of .NET Standard if there are no specific requirements for that.

Project types and libraries

In this tutorial we’ll be using:

  • Autofac for dependency injection and
  • NLog for logging

We’ll create a .NET Standard library with a class that will be consumed in both a classical console application and an ASP.NET Core 2.0 project.

Why not just abandon .NET Framework?

In other words, why create a classical .NET Framework console application and not a .NET Core app?

Because .NET Core applications do not compile into .exe files. They instead get compiled into .dll’s that are meant to be run with a dotnet runtime command. (So a consumer will have to install the runtime.) There is, actually, a way to produce a platform-specific runtime-independent executable:

dotnet publish xxx --self-contained --runtime win-x64

The resulting binary will, in this case, have an .exe extension and will run natively on Windows. The penalty for that is 65 megabytes of libraries that will have to be distributed together with the app. Unfortunately, unlike Go language toolchain that creates small native executables, .NET Core does not discard unused code from the referenced libraries.

Using dotnet to run the compiled .dlls has another unpleasant effect — you won’t see the actual application in Task Manager, only a useless «dotnet» instance.

Why Autofac, not some other DI library?

Autofac is one of the most popular ones. It currently has 8.3M downloads on Nuget, Unity has 8.6M, Ninject — 5.5M, StructureMap — 3M. Ninject has been much slower than the others in some tests.

The particular choice between Unity and Autofac is mostly a personal preference. The author’s experience of implementing a logger injection with Unity consisted of creating a set of seven classes with complicated logic of keeping a stack of resolved items.

Why not log4net or some other logging library?

NLog is fast, and, again, the common experience of log4net version hell in the world of corporate development required some fresh alternatives for the author.

Scaffolding

Creating the solution

  1. Open Visual Studio and create a new blank solution.

  2. Add a .NET Standard Library project to the solution — CatLibrary. It will be created as a .NET Standard 2.0 project. (This can be verified and changed later in the project’s properties. The framework selector above which is set to .NET Framework 4.6.1 is irrelevant for this.)

  3. Add a .NET Framework Console App (from «Windows Classic Desktop») — ConsoleApp. Keep the Framework selector above at version 4.6.1.

  4. Add a ASP.NET Core Web Application — CoreWebApplication. (Ignore the Framework selector this time again.)

  5. On the next screen use the defaults — «Web API» template and no authentication.

  6. Now for each of the application projects right-click on them in the Solution Explorer and add a project reference to CatLibrary.

  7. Now select Build -> Build Solution from the Visual Studio menu and make sure the scaffolding builds.

We now have the following project structure.

A note on project files

Interestingly, the library and the web project have the new simplified structure of .csproj files.

This is what the library has:

1<Project Sdk="Microsoft.NET.Sdk">
2
3  <PropertyGroup>
4    <TargetFramework>netstandard2.0</TargetFramework>
5  </PropertyGroup>
6
7</Project>

This is the project file of the web application:

 1<Project Sdk="Microsoft.NET.Sdk.Web">
 2
 3  <PropertyGroup>
 4    <TargetFramework>netcoreapp2.0</TargetFramework>
 5  </PropertyGroup>
 6
 7  <ItemGroup>
 8    <Folder Include="wwwroot\" />
 9  </ItemGroup>
10
11  <ItemGroup>
12    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
13  </ItemGroup>
14
15  <ItemGroup>
16    <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.1" />
17  </ItemGroup>
18
19  <ItemGroup>
20    <ProjectReference Include="..\AutofacTools\AutofacTools.csproj" />
21    <ProjectReference Include="..\CatLibrary\CatLibrary.csproj" />
22  </ItemGroup>
23
24</Project>

Unlike the classical .NET Framework project files, they don’t list all files explicitly and don’t specify anything not specifically required. Our console application’s project file still looks like it did 15 years ago:

 1<?xml version="1.0" encoding="utf-8"?>
 2<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 3  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
 4  <PropertyGroup>
 5    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
 6    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
 7    <ProjectGuid>{24421341-FA27-427A-9C43-1C8B9941D600}</ProjectGuid>
 8    <OutputType>Exe</OutputType>
 9    <RootNamespace>ConsoleApp</RootNamespace>
10    <AssemblyName>ConsoleApp</AssemblyName>
11    <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
12    <FileAlignment>512</FileAlignment>
13    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
14  </PropertyGroup>
15  ...
16  <ItemGroup>
17    <Reference Include="System" />
18    <Reference Include="System.Core" />
19    <Reference Include="System.Xml.Linq" />
20    <Reference Include="System.Data.DataSetExtensions" />
21    <Reference Include="Microsoft.CSharp" />
22    <Reference Include="System.Data" />
23    <Reference Include="System.Net.Http" />
24    <Reference Include="System.Xml" />
25  </ItemGroup>
26  <ItemGroup>
27    <Compile Include="Program.cs" />
28    <Compile Include="Properties\AssemblyInfo.cs" />
29  </ItemGroup>
30  <ItemGroup>
31    <None Include="App.config" />
32  </ItemGroup>
33  <ItemGroup>
34    <ProjectReference Include="..\AutofacTools\AutofacTools.csproj">
35      <Project>{afd70917-1ff1-4e5d-8709-992b1e1f7fe7}</Project>
36      <Name>AutofacTools</Name>
37    </ProjectReference>
38    <ProjectReference Include="..\CatLibrary\CatLibrary.csproj">
39      <Project>{bdef2489-1bff-4a0e-9a68-6de9e6f255f9}</Project>
40      <Name>CatLibrary</Name>
41    </ProjectReference>
42  </ItemGroup>
43  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
44</Project>

Cat library

Let’s proceed with our solution. In CatLibrary rename the file Class1.cs to Cat.cs and open it for editing. Change the class inside to the following:

namespace CatLibrary
{
    public class Cat
    {
        public string MakeSound()
        {
            return "Meow!";
        }
    }
}

Cat will produce the sound, and both our applications will output it to the user. We’ll now add infrastructure for logging to be able to see the inner workings of our cat before it actually makes the sound.

ILogger interface

We’ll be using ILogger<T> (src) interface defined in Microsoft.Extensions.Logging.Abstractions package. T here will be the name of the class it will be injected to. So Cat will have an ILogger<Cat> argument in its constructor.

Why? The reasons for this choice are:

  1. No need to reinvent the bicycle and create something own.
  2. It is used internally by ASP.NET classes in a rather complicated way, so using something else together with it will add a degree of complication.
  3. Logging libraries now support it natively via extensions and wrappers.

This interface is a generic version of the interface ILogger (src). ILogger basically has a single function that is utilised by various extension methods.

void Log<TState>(
  LogLevel logLevel,
  EventId eventId,
  TState state,
  Exception exception,
  Func<TState, Exception, string> formatter);

The typed version is used instead of ILogger because:

  1. It already has the user type in it, so it is easier to create the proper logger with the knowledge of the user type. (Otherwise we’d have to pass Cat based on the type being resolved etc., this would be much more complicated. The logger needs to know the class it is being used in to write its name in the log, which is a common practice.)
  2. It is a restraint that prevents us from passing the same instance of ILogger to some other class. (That we may create manually when cutting corners and writing some not properly dependency-injected code.)

Adding logging

First, install the logging abstractions library from Microsoft.

  1. Open Package Manager Console via View -> Other Windows -> Package Manager Console menu item.
  2. Make sure the «Default project» selected above is our CatLibrary.
  3. Execute the following:
Install-Package Microsoft.Extensions.Logging.Abstractions

Add a constructor with an ILogger<Cat> argument to Cat class and save the value to a new field in the class:

public class Cat
{
+   private readonly ILogger<Cat> _logger;
+
+   public Cat(ILogger<Cat> logger)
+   {
+       _logger = logger;
+   }

    public string MakeSound()
    {
        return "Meow!";
    }
}

ILogger<Cat> is highlighted in red as it is not defined in CatLibrary namespace that our class resides in. Add the following line to the top of the file to reference its namespace:

using Microsoft.Extensions.Logging;

(using System; may be removed.) Visual Studio (as well as ReSharper plugin, if you have it installed) may suggest to implement this fix automatically by showing a lightbulb on the left or by underlining the code with a «wiggly line». Know your tools!

Finally, let’s add the line that actually performs logging in the beginning of MakeSound:

public string MakeSound()
{
+   _logger.LogInformation("Cat is going to meow now...");
    return "Meow!";
}

Now, everytime a consumer of a Cat class object wants it to make a sound, the object will log this string. It is its internal workings and its responsibility. This does not interfere with the actual returned value, so this happens transparently for consumers.

One might ask — but shouldn’t consumers actually pass an instance of ILogger<Cat> to the Cat they create? Doesn’t this mean that some knowledge of the Cat’s dependency on logging still lays on its every consumer? That might be the case, but we are designing our solution with the dependency injection approach which exact purpose is to eliminate this problem. Our consumers will simply get a cat that they will be asking to make some noise, and that’s it.

A note on packages

Microsoft.Extensions.Logging.Abstractions was installed from NuGet, that is the website and the set of tools that comprise the workflow to install libraries into .NET programs. It is created and developed by Microsoft. NuGet is the same thing for .NET as npm for node.js, or gems in the Ruby world.

The library we installed has its own page. One can manually download the library (actually, a .nupkg archive with possibly a few binaries compiled for different frameworks). It is available at the link «Manual download» on the right.

The package manager console command we used is one of the ways to automatically download it and install it into a project. On the same page we can see commands for other clients — the .NET Core CLI itself and an independent client called Paket. Visual Studio additionally provides its own UI to install NuGet packages, we’ll use it in the next section.

These clients are also called «package managers», as their additional purpose is to manage the whole set of dependencies in the project. This requirement comes from a set of problems naturally arising from such automation. Say, for example, we are referencing packages A and B, and each of them is dependent on its own on some third package C.

Ideally, the client should simply additionally automatically install C into our project. But practically we can face the situation when A wants C of version from 1.5 up to 2.3, and B wants a version 2.25 or higher (for example, if some functionality was added there). Such complications should be handled by the package manager — it should install the matching version. In this case it may be 2.3, but if later we add D which wants C of specifically version 2.27, the installed version of C would be lowered to 2.27 as the only matching one.

This has never worked perfectly with standard Microsoft clients. All of them: Install-Package command, .NET Core CLI and the Visual Studio UI use the same technology behind the scenes. (i.e. all of them call nuget.exe.) The main problem has been that in classical .NET Framework projects they added the package version explicitly into .csproj files (we’ll see it later). This caused constant file changes if a referenced package was a part of another build and updated often.

The alternative client Paket is an attempt to fix that, but it is beyond the scope of this blog post. Even despite the whole feel of modularity and diversity in the modern .NET stack. The reasons are:

  1. Given the velocity of changes in .NET Core world it is still not as smooth in it as it is for complex classical .NET Framework environments, and cannot be used as easily. Especially in mixed solutions like ours.
  2. .NET Core is aimed at development of small modular microservice-like applications. We most likely won’t have multiple solutions each producing a NuGet package, as is common in corporate .NET Framework world.
  3. The current trend of using Git as a version control system eliminates the problem of checking out files for work. So they may change often, it won’t be as much a problem as it is when using Perforce of SVN.
  4. After all, the current versions of the Microsoft NuGet clients are more robust than what was on the table in 2012.

So, basically, the bar above which it becomes worth it, is too high.

Why is logger something «external»?

Mainly because the whole paradigm of dependency injection evolved only by the end of 2000s.

The common practice before that was to create a static instance of a Log4Net’s logger inside each class. This obviously prevented proper unit testing (as it is impossible to replace a private static instance with something else). So the need for a common way to log things across the .NET codebase was eventually realised by Microsoft around 2014 when ASP.NET evolved into the modular .NET Core. And in this era the new libraries provided by Microsoft are the same kinds of independent NuGet packages as anything 3rd party.

What was changed in the project after installation?

We got a few new lines:

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
+  </ItemGroup>

</Project>

What this does is a bit of magic, one is supposed to think that the NuGets are referenced as first-class local projects. .NET Core will automatically download the package behind the scenes and use it.

Console app

Switch package restore style

We have to switch the style of package restoring in the console application before we proceed.

If we do Install-Package Autofac right away, we’ll get the following change in ConsoleApp.csproj:

   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="Autofac, Version=4.8.0.0, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
+      <HintPath>..\packages\Autofac.4.8.0\lib\net45\Autofac.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
...
   <ItemGroup>
     <None Include="App.config" />
+    <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>

This structure is a sign of the classical NuGet mechanics. They are different from what is used in the new world of .NET Standard and .NET Core. So if our CatLibrary uses some NuGet reference, it won’t be correctly copied to the console application’s output folder, and the console application won’t work.

Right-click the project item in Solution Explorer and select «Open Folder in File Explorer». Find the file ConsoleApp.csproj and open it in a text editor.

Add the following line to the first <PropertyGroup> section:

     <FileAlignment>512</FileAlignment>
     <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+    <RestoreProjectStyle>PackageReference</RestoreProjectStyle>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

Save the file and accept the suggestion to reload from Visual Studio. Now the installation of the same Autofac package would result in the following.

     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="Autofac">
+      <Version>4.8.0</Version>
+    </PackageReference>
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

As one can see, this is the same <PackageReference> tag we saw in the CatLibrary project file after we installed the logging abstractions library to it.

Add packages

Now we’ll explore the Visual Studio UI to install packages. (As a reminder, the result is the same as using Install-Package command, but the UI may be more useful sometimes, so we’ll use it as an option.)

Right-click on the ConsoleApp project in Solution Explorer and select «Manage NuGet packages…». You’ll see the window below. Select the Browse tab in the top-left corner.

First let’s install Autofac. Search for it, select the top result (that will be called exactly Autofac), and click the «Install» button on the right. This one is simple.

Next is NLog. Don’t install it directly: it itself does not contain implementations for ILogger interface we started using. The library’s author provides an additional library that has an implementation. It is called NLog.Extensions.Logging (and it has a dependency on NLog, so we’ll get NLog automatically). Search for it.

If you scroll the right window to the bottom, you’ll see the set of dependencies of NLog.Extensions.Logging.

We have a .NET Framework 4.6.1 project, so the second section matters for us. We can see that it references both the library itself, NLog, and Microsoft.Extensions.Logging. The latter references Microsoft.Extensions.Logging.Abstractions (that contains ILogger) which we used in CatLibrary.

Click the «Install» button. Accept the licenses to continue. That’s it with packages. Close the NuGet window.

If we open the project file, we’ll see that the references were indeed added in the new style:

     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="Autofac">
+      <Version>4.8.0</Version>
+    </PackageReference>
+    <PackageReference Include="NLog.Extensions.Logging">
+      <Version>1.0.1</Version>
+    </PackageReference>
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

Now let’s make the app work. We won’t do the dependency injection now, but we’ll set up the Autofac container and will enable logging just to see that it works.

Add nlog.config

For NLog to work, the application must contain nlog.config file with configuration. Add a new text file to the project with that name. In its properties, set «Build Action» to None and «Copy to Output» to «If Newer».

Put the following to the file. We’ll only log to the console itself for simplicity.

<nlog>
  <targets>
    <target type="Console" name="consoleTarget"
            layout="${logger}: ${message} ${exception}" />
  </targets>

  <rules>
    <logger name="*" minlevel="Info" writeTo="consoleTarget" />
  </rules>
</nlog>

Enable logging

We’ll register the standard Logger<T> (src) as the implementation of ILogger<T>. This class is actually a wrapper that accepts an ILoggerFactory and uses it to create the real ILogger inside. We’ll use NLogLoggerFactory that will create an ILogger implementation from NLog.

We are setting up Autofac to handle all these things. Add the following code to the Main function in Program.cs.

    internal class Program
    {
        private static void Main(string[] args)
        {
            // Autofac container.
            var builder = new ContainerBuilder();
            
            // The type Cat is added to container so that the container
            // would be able to provide instances of it.
            builder.RegisterType<Cat>();

            // Create Logger<T> when ILogger<T> is required.
            builder.RegisterGeneric(typeof(Logger<>))
                .As(typeof(ILogger<>));

            // Use NLogLoggerFactory as a factory required by Logger<T>.
            builder.RegisterType<NLogLoggerFactory>()
                .AsImplementedInterfaces().InstancePerLifetimeScope();
            
            // Finish registrations and prepare the container that can resolve things.
            var container = builder.Build();

            // Entry point. This provides our logger instance to a Cat's constructor.
            Cat cat = container.Resolve<Cat>();

            // Run.
            string result = cat.MakeSound();
            Console.WriteLine(result);
        }
    }

Here are the detailed mechanics of what happens when we call container.Resolve<Cat>().

  1. Autofac found the Cat’s constructor argument ILogger<Cat>.
  2. To resolve it, it had to, according to our registration, create an instance of Logger<Cat>.
  3. But the Logger<T> class has the constructor that accepts some ILoggerFactory (src). So now Autofac needed to create an instance of the factory.
  4. We had registered NLog’s NLogLoggerFactory (src) as the implementation of this interface. So Autofac then created a NLogLoggerFactory (and kept it for future usages, thanks to InstancePerLifetimeScope() call), and used it to instantiate a Logger<Cat>. The created logger was then passed to Cat’s constructor.

The consumer only had to request a Cat - no specific knowledge of loggers was needed.

Execute

Right-click the ConsoleApp project file and select «Set as StartUp project». (Note it will become bold.) Now we can press F5 key to run the application. You’ll see the following output:

CatLibrary.Cat: Cat is going to meow now...
Meow!

It all works! Also note that the first line in the output is the log output itself, while the second is something we explicitly log to console. Had we changed minLevel in the nlog.config file to, for example, ERROR, the first line won’t be written as being logged in the code with INFO verbosity level.

Web app

Now let’s make Cat usable also in the .NET Core Web project that we’ve created.

Introduction

But first, an eyeball tour over what is added to the project by default.

Program.cs. It calls a WebHost’s static method to create a webhost builder that uses the neighbour Startup class for configuration. Then the webhost is built and run.

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args)
    {
        return WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
    }
}

This is Startup.cs. It is what is referenced in BuildWebHost above. There is a convention on what methods this class should contain, as it does not implement any interfaces. It is, of course, a terrible design decision, yet, that is how it is done in ASP.NET.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment()) app.UseDeveloperExceptionPage();

        app.UseMvc();
    }
}

ConfigureServices by default calls the AddMvc extension method (specified in Microsoft.AspNetCore.Mvc assembly, src) that adds various ASP.NET things to services, which is a collection of mappings for a DI engine. As the code above doesn’t explicitly reference any external things, how are they used? Some other dependency injection thing, you might ask?

The thing is, ASP.NET Core has its own built-in dependency injection pipeline. It’s tailored for the needs of a web application, and it isn’t as robust or matured as libraries like Autofac. Unfortunately, it still has to be used, as configuration in ASP.NET Core is done in this manner by means of extension methods.

Configure’s both arguments are from sub-namespaces of Microsoft.AspNetCore. This method sets up the logic of responding to HTTP requests. By default, again, some extension is used that sets up all the things that are enough in a common ASP.NET app.

The third file is Controllers/ValuesController.cs. Let’s clean it up right away.

  1. Rename it to AnimalController.
  2. Leave only the first Get method inside - that has the [HttpGet] attribute on it. Change its return value to simple string. We’re going to have a Cat inside that will return the same sound. For now return a stub string.

Something like the following:

[Route("api/[controller]")]
public class AnimalController : Controller
{
    [HttpGet]
    public string Get()
    {
        return "temp";
    }
}

Now let’s do a hello world run.

  1. Set the debugging to start at our AnimalController. Go to the project’s properties select the Debug tab on the left and change the value against «Launch browser» to api/animal.
  2. Mark the web project as a startup one.
  3. Hit the F5 key to launch debugging.

The browser will open and you’ll see the following:

Web app: make it work

Install two packages into the web project:

Install-Package Autofac.Extensions.DependencyInjection
Install-Package NLog.Web.AspNetCore

AnimalController changes

Add the cat and the logger to AnimalController. The class should look like this:

[Route("api/[controller]")]
public class AnimalController : Controller
{
    private readonly Cat _cat;
    private readonly ILogger<AnimalController> _logger;

    public AnimalController(Cat cat, ILogger<AnimalController> logger)
    {
        _cat = cat;
        _logger = logger;
    }

    [HttpGet]
    public string Get()
    {
        _logger.LogInformation("Get is called on Values controller.");
        var result = _cat.MakeSound();
        return result;
    }
}

Program changes

In Program.cs first add NLog to the webhost builder:

  {
      return WebHost.CreateDefaultBuilder(args)
+         .UseNLog()
          .UseStartup<Startup>()
          .Build();

This extension method UseNLog (src) registers a singleton NLogLoggerProvider as the implementer of ILoggerProvider into the same collection of DI mappings. Why is it now a provider and not a factory that we saw in the console application earlier? Because there already is some default factory in ASP.NET Core, and it receives a list of providers in its constructor. By registering a provider, NLog doesn’t interfere with this existing infrastructure. This built-in factory will actually create a complex logger that will call other loggers supplied by different providers. So, in other words, creating a provider is another level of abstraction around the loggers.

Additionally, modify Main to log exceptions that occur before the application initialises. Basically, we hard code NLog usage separately for this only purpose. THe function should look like this:

public static void Main(string[] args)
{
    // NLog: setup the logger first to catch all errors
    Logger logger = NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger();
    try
    {
        logger.Debug("init main");
        BuildWebHost(args).Run();
    }
    catch (Exception e)
    {
        //NLog: catch setup errors
        logger.Error(e, "Stopped program because of exception");
        throw;
    }
}

Startup changes

Here all changes lie in ConfigureServices method. First, we need to switch from controller object creation from the specialised code to the used common DI container. Otherwise we won’t be able to use dependency injection for our AnimalController.

- services.AddMvc();
+ services.AddMvc().AddControllersAsServices();

Then, change the return type of the method from void to IServiceProvider. Service provider is the ASP.NET term for a DI container. We’ll use our own Autofac one, so we’ll return it from this method.

- public void ConfigureServices(IServiceCollection services)
+ public IServiceProvider ConfigureServices(IServiceCollection services)

Next, add these lines to the function to create an Autofac container, populate it from the ASP.NET DI mappings collection, register Cat and return it from the function.

+ ContainerBuilder builder = new ContainerBuilder();
+ builder.Populate(services);
+ builder.RegisterType<Cat>();
+ IContainer container = builder.Build();

+ return new AutofacServiceProvider(container);

Method looks like this in the end:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddControllersAsServices();

    ContainerBuilder builder = new ContainerBuilder();
    builder.Populate(services);
    builder.RegisterType<Cat>();
    IContainer container = builder.Build();
    
    return new AutofacServiceProvider(container);
}

nlog.config

As there is no console in the web app, this time we’ll be logging to file. Copy nlog.config from the console application by dragging it while holding the left Ctrl key. This will copy it with its properties. Then change the file to log to a file:

<nlog>
  <targets>
    <target type="File" name="fileTarget" fileName="example.log"
            layout="${date}|${level:uppercase=true}|${message} ${exception}|${logger}|${all-event-properties}" />
  </targets>

  <rules>
    <logger name="*" minlevel="Info" writeTo="fileTarget" />
  </rules>
</nlog>

Run

Now everything is ready. Hit F5 to start debugging. The browser will open and you will see:

Stop the application. Now right click the web project in the Solution Explorer and select «Open Folder in File Explorer». Navigate to bin\Debug\netcoreapp2.0 subfolder. You’ll see that example.log file is there. Open it. Note that among many technical lines we have two created by our code:

2018/04/29 23:08:50.089|INFO|Cat is going to meow now... |CatLibrary.Cat|
2018/04/29 23:08:50.089|INFO|Get was called on Values controller. |CoreWebApplication.Controllers.AnimalController|

That’s all, folks!