[.NET Core] Easy logging with log4net

Logging what’s happening in your application is something necessary and useful for your debugging sessions. As .NET Core is a fair new technology, you’re going to find a lot of guides on how to configure log4net for a .NET Framework application but it’s far less clearer for a .NET Core one.

In this guide, we’re going to create a new solution with multiple projects that will use our logging object. The goal is to never depend from the log4net package in any project but, instead, to depend on a local solution that will present a Logger object with static methods to log. Thus, we will only have to add an using and to call Logger.Log() to log something.

The simpler, the better, huh? Let’s go!

You’ll be able to download the example solution at the end of this guide! 😊

Create a new project

Create a new solution with a Console App (.NET Core) project and call it whatever you want, I chose Banana because why not.

Then add a new Class Library (.NET Core) project and call it the same name with a .Toolbox appended at the end (Banana.Toolbox for me).

I use the “Toolbox” name because I use this kind of project as a… Toolbox, where I have the logger object, some extensions methods, … that I can reuse between multiple projects (like a NuGet package). Don’t forget to delete the useless Class1.cs file.

You should have this:

Adding Log4net to the tools

Prepare the static object

I love my solutions well sorted so, in the toolbox project, add a new Logger folder. You will place all the needed files for the logging there.

In this folder, add a new C# file and name it Logger.cs. Make this a public and static class…

1
2
3
4
5
6
7
8
9
10
using System;
using System.Collections.Generic;
using System.Text;

namespace Banana.Toolbox.Logger
{
public static class Logger
{
}
}

Now we need to download the log4net NuGet package in the Toolbox project (this will be the only project where log4net is downloaded):

Log4Net instantiation

We now have Log4Net and a static object ready to use it. How are we going to do it? Simple: with static methods!

But before logging anything, we need to instantiate log4net somewhere. Add a new static property to your Logger class:

1
2
3
4
5
6
7
namespace Banana.Toolbox.Logger
{
public static class Logger
{
private static ILog _log;

[...]

Visual Studio will complain that it does’t find the “ILog” interface. Let VS add itself the right using or write it manually: using log4net;

Then, add a new static method to instantiate the new logger object:

1
2
3
4
private static void EnsureLogger()
{

}

ILog is the interface provided by Log4Net to use their logger object. To instantiate a new Log4Net logger, we’re going to use the GetLogger method from the LogManager object with this signature :

1
LogManager.GetLogger(Assembly repositoryAssembly, string name)

Method documentation here

The repositoryAssembly property is the “Assembly” object that represents the project that is currently executed. The name property is the name to show in the log file.

To instantiate a new logger in our _log class property, add this code to EnsureLogger:

1
2
3
4
5
6
7
8
private static void EnsureLogger()
{
if (_log != null) return;

var assembly = Assembly.GetEntryAssembly();

_log = LogManager.GetLogger(assembly, assembly.ManifestModule.Name.Replace(".dll", "").Replace(".", " "));
}

First, we check if the logger is already created, it this is the case we don’t need to continue so we exit the method immediatly.

Then, as the logger is not instantiated, we first get the “Entry Assembly” (the project that has been executed first) and we use this assembly to create a new Log4Net logger.

We also remove the “.dll” from the project filename and the others “.” because Log4Net will not write what’s after the first dot. This is not useful in this specific case but you should keep it in mind for your next projects.

Log4Net configuration file

ATM the logger won’t work. We still need to configure it to tell Log4Net where and how to save the log files. Each executing project will need its own Log4Net configuration file, here we only have one executing project (Banana) so we’ll only need one file.

In the Banana project, add a Config folder then create a new log4net.config file (you can add any type of file and rename to .config later). This will contains the Log4Net configuration for this project:

Put the following in this file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<log4net debug="true">
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="LOGS/Banana.log"/>
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="25MB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%-5p %d %5rms %-22.22c{1} - %m%n" />
</layout>
</appender>
<root>
<!-- minimum level to log -->
<level value="DEBUG" />
<appender-ref ref="RollingLogFileAppender" />
</root>
</log4net>

You can find more informations on this file here.

With this config, Log4Net will create a new “Banana.log” file in a “LOGS” folder, relative to the execute working directory. When this log file will exceeds 25MB, a new one will be create and the old one will be keeped on its side. A maximum of 10 files will be saved. And the minimum log level to write in the file is “DEBUG”, the lowest one.

Feel free to adapt this file to your taste. Note that you can also use an absolute path instead of the relative one.

One important step than can be overlooked is to specify to Visual Studio that we need to copy this file with the binaries when we build our solution.

Right click on log4net.config, click on Properties and be sure to select Copy if newer (or copy always) for the Copy to Output Directory option.

Log4Net configuration

In Logger.cs, add a new method that will read the config file, call it GetConfigFile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static FileInfo GetConfigFile()
{
FileInfo configFile = null;

// Search config file
var configFileNames = new[] { "Config/log4net.config", "log4net.config" };

foreach (var configFileName in configFileNames)
{
configFile = new FileInfo(configFileName);

if (configFile.Exists) break;
}

if (configFile == null || !configFile.Exists) throw new NullReferenceException("Log4net config file not found.");

return configFile;
}

This method try to read “Config/log4net.config” then “log4net.config” (all relative to the current working directory of the executed project) and will throw an exception if nothing has been found.

In EnsureLogger, save the config file in a new variable:

1
var configFile = GetConfigFile();

To configure Log4Net, we also need a “Repository”. To get one, simply write the following:

1
var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());

LogManager is a class provided by Log4Net and GetRepository will provides us a Log4Net repository from our executing project.

Now we only need to call XmlConfigurator that provides the needed configuration to Log4Net:

1
2
// Configure Log4Net
XmlConfigurator.Configure(logRepository, configFile);

Your Logger.cs file should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static void EnsureLogger()
{
if (_log != null) return;

var assembly = Assembly.GetEntryAssembly();
var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
var configFile = GetConfigFile();

// Configure Log4Net
XmlConfigurator.Configure(logRepository, configFile);
_log = LogManager.GetLogger(assembly, assembly.ManifestModule.Name.Replace(".dll", "").Replace(".", " "));
}

private static FileInfo GetConfigFile()
{
FileInfo configFile = null;

// Search config file
var configFileNames = new[] { "Config/log4net.config", "log4net.config" };

foreach (var configFileName in configFileNames)
{
configFile = new FileInfo(configFileName);

if (configFile.Exists) break;
}

if (configFile == null || !configFile.Exists) throw new NullReferenceException("Log4net config file not found.");

return configFile;
}

The configuration is now completed but we still can’t log anything. Let’s add some logging methods!

Add logging methods

In Logger.cs, add a new Log method with a string parameter that will be the message to log:

1
2
3
4
5
6
public static void Log(string message)
{
EnsureLogger();

_log.Debug($"Test log: {message}");
}

We call EnsureLogger to be sure to have the logger ready to use then we call the “Debug” method (from Log4Net) that will write the given message to the log file.

In the Program.cs file in the Banana project, call this method with a message that you choose:

1
2
3
4
5
6
7
8
9
10
using Banana.Toolbox.Logger;

// [...]

static void Main(string[] args)
{
Console.WriteLine("Hello World!");

Logger.Log("Message from the main program");
}

Don’t forget to add a reference to the logger from the Toolbox project with an using as shown above

Now, execute the project and check that you have a new line in your log file (Banana\bin\Debug\netcoreapp2.1\LOGS in my case):

1
DEBUG 2018-10-23 15:34:40,689   435ms Banana                 - Test log: Message from the main program

You now have everything you need to start to log everything you want! 😊

Download the example solution here: https://github.com/MayakoLyyn/Log4Net-dotnetcore