Post

How to Configure Microsoft.Extensions.Logging in Optimizely CMS11

You might be thinking that Microsoft.Extensions.Logging and related packages are useful only for .NET Core and above. That’s not entirely true. While Microsoft.Extensions.Logging is target .NET Standard it’s possible to use it even on .NET Framework 4.x. This is de-facto logging library that class libraries should use at very least. Where Optimizely (ex. Episerver) CMS11 (running on .NET Framework) would collide with anything from the “new world” (.NET5)?

ms.logging-in-cms11-1

We do have many shared libraries across our codebase. As by design and name states - class libraries contain business logic and other sweet stuff. Main goal is to reuse and share the same business login across many rutimes and platforms as possible (and applicable).

Few years ago we refactored our shared class libraries to use Common.Logging as logging abstractions. That would allow us to reduce dependency on log4net coming from CMS platform. The same class libraries are using in other projects in our solution. Some of them even on .NET 6. To get everything in order and no exception during runtime - we had to pull in also Common.Logging dependency to get things working properly.

Depending on runtime (ASP.NET Core or Azure Runtime, or whatever) you might get to work with different logging abstractions and therefore you have to make sure that all moving parts are working properly together and there are all required adapters registered.

It is time for us to say good-bye to Common.Logging.

However if we throw out Common.Logging from our shared libraries and use Microsoft.Extensions.Logging abstractions, how can we be sure that we are not losing log entries when library will be used in CMS11 context where we have log4net and friends?

Therefore we have to trick DI container to pretend that it’s possible to create ILogger<T> instances when anyone from shared libraries is requesting instance of it.

Train StructureMap About MS.Extensions.Logging

First thing we have to do is to tell StructureMap what to do about ILogger<T> open generic (when someone requests this dependency).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[InitializableModule]
public class DependencyResolverInitialization : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.ConfigurationComplete += (o, e) =>
        {
            context.StructureMap().Configure(cfg =>
            {
                cfg.For(typeof(ILogger<>)).Use(new LoggerAdapterFactory());
            });
        };
    }

    public void Initialize(InitializationEngine context) { }

    public void Uninitialize(InitializationEngine context) { }
}

Type LoggerAdapterFactory is special StructureMap type that will instruct DI library what to do next.

It is of type Instance that is used by the library when StructureMap will have to “close” open generic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LoggerAdapterFactory : Instance
{
    public override Instance CloseType(Type[] types)
    {
        var instanceType = typeof(LoggerInstance<>).MakeGenericType(types);
        return Activator.CreateInstance(instanceType) as Instance;
    }

    // ignore
    public override IDependencySource ToDependencySource(Type pluginType) { throw new NotImplementedException(); }

    // this is just for the debugging
    public override string Description => "Build ILogger<T> with LoggerAdapterFactory";

    // what types this instance handles
    public override Type ReturnedType => typeof(ILogger<>);
}

We are “outsorcing” actual creation of the logger classes to LoggerInstance. Let’s take a look at LoggerInstance.

1
2
3
4
5
6
7
8
9
public class LoggerInstance<TCategoryName> : LambdaInstance<ILogger<TCategoryName>>
{
    public LoggerInstance()
        : base(() => new ExtensionsLogger<TCategoryName>()) { }

    // just made debugging easier
    public override string Description =>
        "via LoggerInstance<" + typeof(TCategoryName).Name + ">";
}

Everytime StructureMap will see ILogger<T> dependency it will use these 2 types to construct instance of the requested dependency.

And ExtensionsLogger is actual bridge between MS.Extensions.Logging logger and ILogger from Optimizely CMS.

Implement the Bridge

Actual implementation of logging bridge is quite simple -> we just have to translate calls from Microsoft.Extensions.Logging to Optimizely logger API.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class ExtensionsLogger<T> : ILogger<T>
{
    private readonly EPiServer.Logging.ILogger _innerLogger;

    public ExtensionsLogger()
    {
        _innerLogger = LogManager.GetLogger(typeof(T));
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
        Func<TState, Exception, string> formatter)
    {
        switch (logLevel)
        {
            case LogLevel.Trace:
                _innerLogger.Trace(formatter(state, exception));
                break;
            case LogLevel.Debug:
                _innerLogger.Debug(formatter(state, exception));
                break;
            case LogLevel.Information:
                _innerLogger.Information(formatter(state, exception));
                break;
            case LogLevel.Warning:
                _innerLogger.Warning(formatter(state, exception));
                break;
            case LogLevel.Error:
                _innerLogger.Error(formatter(state, exception));
                break;
            case LogLevel.Critical:
                _innerLogger.Critical(formatter(state, exception));
                break;
        }
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        switch (logLevel)
        {
            case LogLevel.Trace:
                return _innerLogger.IsTraceEnabled();
            case LogLevel.Debug:
                return _innerLogger.IsDebugEnabled();
            case LogLevel.Information:
                return _innerLogger.IsInformationEnabled();
            case LogLevel.Warning:
                return _innerLogger.IsWarningEnabled();
            case LogLevel.Error:
                return _innerLogger.IsErrorEnabled();
            case LogLevel.Critical:
                return _innerLogger.IsCriticalEnabled();
        }

        return false;
    }

    // no support for scopes for now
    public IDisposable BeginScope<TState>(TState state) { throw new NotImplementedException(); }
}

As we know category name (or <T> parameter for ILogger) we can ask LogManager to give us logger of that type: LogManager.GetLogger(typeof(T));. This will ensure that log entries are decorated with correct category.

Sample Usage

After configuring this properly in your app, you can ask for ILogger<T> dependnecy directly in your controller constructor, or you use class library that does this. It doesn’t matter. Infrastructure is set up properly and logging is working as expected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StartPageController : PageControllerBase<StartPage>
{
    private readonly ILogger<StartPageController> _logger;

    public StartPageController(ILogger<StartPageController> logger)
    {
        _logger = logger;
    }

    public ActionResult Index(StartPage currentPage)
    {
        _logger.LogInformation("TEST FROM START PAGE");

        ...
    }
}

ILogger<T> interface is coming from Microsoft.Extensions.Logging namespace.

ms.logging-in-cms11-2

After couple of seconds log entries show up also in log files (you will need to configure different settings from default ones to see also Information severity entries).

ms.logging-in-cms11-3

Happy coding!

Smooth migrating to .NET5!

[eof]

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.