Logging and DI in modern .net applications
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 .dll
s 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
Open Visual Studio and create a new blank solution.
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.)Add a .NET Framework Console App (from «Windows Classic Desktop») —
ConsoleApp
. Keep the Framework selector above at version 4.6.1.Add a ASP.NET Core Web Application —
CoreWebApplication
. (Ignore the Framework selector this time again.)On the next screen use the defaults — «Web API» template and no authentication.
Now for each of the application projects right-click on them in the Solution Explorer and add a project reference to
CatLibrary
.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:
- No need to reinvent the bicycle and create something own.
- 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.
- 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:
- 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.) - 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.
- Open Package Manager Console via
View
->Other Windows
->Package Manager Console
menu item. - Make sure the «Default project» selected above is our
CatLibrary
. - 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:
- 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.
- .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.
- 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.
- 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>()
.
- Autofac found the
Cat
’s constructor argumentILogger<Cat>
. - To resolve it, it had to, according to our registration, create an instance
of
Logger<Cat>
. - But the
Logger<T>
class has the constructor that accepts someILoggerFactory
(src). So now Autofac needed to create an instance of the factory. - We had registered NLog’s
NLogLoggerFactory
(src) as the implementation of this interface. So Autofac then created aNLogLoggerFactory
(and kept it for future usages, thanks toInstancePerLifetimeScope()
call), and used it to instantiate aLogger<Cat>
. The created logger was then passed toCat
’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.
- Rename it to
AnimalController
. - Leave only the first
Get
method inside - that has the[HttpGet]
attribute on it. Change its return value to simplestring
. We’re going to have aCat
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.
- Set the debugging to start at our
AnimalController
. Go to the project’s properties select theDebug
tab on the left and change the value against «Launch browser» toapi/animal
. - Mark the web project as a startup one.
- 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!