Logging with Enums in .NET Core. In this blog post, I extend the…
By: Sebahattin Çatal
Tags: Dotnet, Dotnet Core

In this blog post, I extend the built-in logging framework with the use of enums as log events, building on the out-of-the-box log event concept.

A Brief Introduction

With the Microsoft.Extensions.Logging package, flexible and powerful logging concepts are available. This is an integral part of dotnet core. The new logging framework has integrated concepts like log categories, familiar to users of the Log4j, Log4net and NLog logging libraries, and structured logging, familiar to users of Serilog.

Microsoft has a nice introduction to the logging concepts here, and here is a short snippet that give you an idea of the standard functionality in ASP.NET core:

public class HomeController : Controller

{

private readonly ILogger<HomeController> _logger;



public HomeController(ILogger<HomeController> logger)

{

_logger = logger;

}

public IActionResult Index()

{

_logger.LogInformation("Index read {user}",

Request.HttpContext.User);



return View();

}

}

An implementation if ILogger<T> is automatically injected into the constructor by the built-in dependency injection container. This type also has overloaded methods that take an EventId as the first parameter, for example void LogInformation(EventId eventId, string message, params object[] args) Basically, you pass in any integer as the first parameter as the EventId type has an implicit operator converting an int to EventId : public static implicit operator EventId(int) . So, the typical example is that you create a class containing constants that represent your application events:

public class LoggingEvents

{

public const int GenerateItems = 1000;

public const int ListItems = 1001;

public const int GetItem = 1002;

public const int InsertItem = 1003;

public const int UpdateItem = 1004;

public const int DeleteItem = 1005;



public const int GetItemNotFound = 4000;

public const int UpdateItemNotFound = 4001;

}

And then you use this like so:

public IActionResult Index()

{

_logger.LogInformation(LoggingEvents.ListItems, "Index read {user}",

Request.HttpContext.User);



return View();

}

Using Enums As Event Ids

So, the first improvement that I suggest, is to use enums instead of having a static class containing a list of integer constants containing my event ids. What I would like to have, a definition of event ids like this:

public enum LoggingEvents

{

GenerateItems = 1000,

ListItems,

GetItem,

InsertItem,

UpdateItem,

DeleteItem,



GetItemNotFound = 4000,

UpdateItemNotFound

}

So, let’s have a first shot at an overloaded extension method that will implement this:

public static class LoggerExtensions

{

public static void LogInformation<T>(this ILogger<T> @this, Enum e, string message, params object[] args)

{

var i = (int)Convert.ChangeType(e, e.GetTypeCode());

@this.LogInformation(i, message, args);

}

}

Notice that we use the Enum type for the parameter, which is the ancestor type for all enums in C#. Then we do some magic to get the integer representation of the passed enum instance. Then, the log statement looks similar to the one above, but uses an enum instead of an integer: _logger.LogInformation(LoggingEvents.ListItems, "Index read {user}", Request.HttpContest.User);

Let’s try to improve this a bit by leveraging the fact that we are using enums. As I mentioned before, the built in logging features so-called semantic or structured logging. So let’s add some more semantics to our extension method above:

public static void LogInformation<T>(this ILogger<T> @this, Enum e,

string message, params object[] args)

{

var i = (int)Convert.ChangeType(e, e.GetTypeCode());

var eventName = Enum.GetName(e.GetType(), e);

var eventScope = e.GetType().Name;

var parameters = args.Append(eventScope).Append(eventName).ToArray();

@this.LogInformation(i, message + " (Event scope: {eventScope}, name: {eventName})",

parameters);

}

There’s a lot of stuff going on here, but let’s try to sort it out:

The eventName variable would hold the enum name, like ListItems.

variable would hold the enum name, like ListItems. The eventScope variable will be the name of the enum type, like LoggingEvents.

variable will be the name of the enum type, like LoggingEvents. The variables above are added to the log message parameters, so that they automatically become a part of the log message structure and are available for filtering, sorting, etc. when making sense of the logs.

Using Several Enum Types

With having the name and scope parameters attached to the log messages, it is feasible to structure the log events using several enum types. You could, for instance, do something like this:

public enum CustomerEvents

{

CustomerCreated = 1000,

CustomerRead,

CustomerChanged,

CustomerDeleted

}



public enum OrderEvents

{

OrderCreated = 2000,

OrderLineAdded,

OrderLineRemoved,

OrderCancelled

}

The only thing you would need to make sure, is that the integer values of the different enum types do not overlap, as that would mess up the event ids.

Centralizing Log Levels

One thing that always puzzles me when coding, is to figure out the correct log level (information, warning, error, critical, etc.). To me, it is very difficult to comprehend this while writing code. So, what I would like to do, is to move the log level concern out of the application code. Instead of having it sprinkled around the code, let’s make a map that maps the events to log levels:

private static readonly Dictionary<Enum, LogLevel> LogLevelMap = new Dictionary<Enum, LogLevel>

{

{ AppEvents.GenerateItems, LogLevel.Information },

{ AppEvents.ListItems, LogLevel.Information },

{ AppEvents.GetItem, LogLevel.Information },

{ AppEvents.InsertItem, LogLevel.Information },

{ AppEvents.UpdateItem, LogLevel.Information },

{ AppEvents.DeleteItem, LogLevel.Information },

{ AppEvents.GetItemNotFound, LogLevel.Error },

{ AppEvents.UpdateItemNotFound, LogLevel.Error }

};

Now we don’t have to implement overloads for LogInformation , LogError , LogWarning , and so on. We need only one overloaded method, Emit :

private const LogLevel DefaultLogLevel = LogLevel.None;



public static void Emit<T>(this ILogger<T> @this, Enum e, string message = null,

params object[] args)

{

var i = (int)Convert.ChangeType(e, e.GetTypeCode());

var eventName = Enum.GetName(e.GetType(), e);

var eventScope = e.GetType().Name;

var level = LogLevelMap.ContainsKey(e) ? LogLevelMap[e] : DefaultLogLevel;

var parameters = args.Append(eventScope).Append(eventName).ToArray();

@this.Log(level, i, message + " (Event scope: {eventScope}, name: {eventName})", parameters);

}

Then we can write log statements like these:

var id = 1;

_logger.Emit(LoggingEvents.GetItem, "Item({id})", id);

_logger.Emit(LoggingEvents.ListItems);

Thank you for reading my article. See you :)