DEV Community

Matt Brailsford
Matt Brailsford

Posted on

Adding ILogger<T> support to Umbraco v8

As part of my recent works on getting Vendr .NET Core ready, and because we are using a multi-targeted approach, one of the bigger changes between Umbraco v8 and v9 is that of logging where we have moved from an Umbraco logger interface of

public interface ILogger {
    Info<T>(...);
    Debug<T>(...);
    Error<T>(...);
}
Enter fullscreen mode Exit fullscreen mode

To the .NET Core logger interface of

public interface ILogger<T> {
    Info(...);
    Debug(...);
    Error(...);
}
Enter fullscreen mode Exit fullscreen mode

Functionally it's not a very big change, but when it comes to a multi-targeted approach one of the biggest aims you have is to try and maintain a standard API across both implementations in order to minimize the number of differences you have to manage.

And because logging is a cross cutting concern, it get's used in a LOT of places so we really don't want to have to be using conditional directives all over the place.

#if NET
    private readonly ILogger<MyClass> _logger;

    public MyClass(ILogger<MyClass> logger) 
    {
        _logger = logger;
    }
#else
    private readonly ILogger _logger;

    public MyClass(ILogger logger) 
    {
        _logger = logger;
    }
#endif

    public void MyMethod() 
    {
        // Do some common logic and log a result
        var result = "Some value";
#if NET
        _logger.Debug(result);
#else
        _logger.Debug<MyClass>(result);
#endif
    }

Enter fullscreen mode Exit fullscreen mode

YUCK! 🤮

Introducing your own logger abstraction

The first part of the puzzle is that we will want to introduce our own logger abstraction and give it 2 implementations for the different frameworks. This means most of our code can simply depend on our interface and then the 2 implementations handle hiding away the differences.

A simplified example might look something like this.

// Our custom ILogger interface
public interface ILogger<T>
{
    Info(string message);
    Debug(string message);
    Error(Exception exception, string message);
}

#if NET

// The Umbravo v9 logger implementation
public class MicrosoftLogger<T> : ILogger<T>
{
    private global::Microsoft.Extensions.Logging.ILogger<T> _logger;

    public MicrosoftLogger(global::Microsoft.Extensions.Logging.ILogger<T> logger)
        => _logger = logger;

    public void Info(string message)
        => _logger.LogInfo(message);

    public void Debug(string message)
        => _logger.LogDebug(message);

    public void Error(Exception exception, , string message)
        => _logger.LogDebug(exception, message);
}

#else

// The Umbravo v8 logger implementation
public class UmbracoLogger<T> : ILogger<T>
{
    private global::Umbraco.Core.Logging.ILogger _logger;

    public UmbracoLogger(global::Umbraco.Core.Logging.ILogger logger)
        => _logger = logger;

    public void Info(string message)
        => _logger.Info(typeof(T), message);

    public void Debug(string message)
        => _logger.Debug(typeof(T), message);

    public void Error(Exception exception, , string message)
        => _logger.Error(typeof(T), exception, message);
}

#endif

Enter fullscreen mode Exit fullscreen mode

By introducing the abstraction, our code now only needs to depend on our own interface and doesn't need to be concerned about how it has been implemented.


private readonly ILogger<MyClass> _logger;

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

public void MyMethod() 
{
    // Do some common logic and log a result
    var result = "Some value";

    _logger.Debug(result);
}

Enter fullscreen mode Exit fullscreen mode

Much better 😻

Registering your implementations with the DI container

The last thing we need to do to be able to use our custom logger is to register our implementations with the DI container.

For Umbraco v9 and .NET Core this is pretty easy as MSDI supports registering generic interfaces and so we'd register it by calling the following code in our IUmbracoBuilder extension for adding our package.

public static class MyPackageExtensions
{
    public static IUmbracoBuilder AddMyPackage(this IUmbracoBuilder builder)
    {
        builder.Services.AddSingleton(typeof(ILogger<>), typeof(MicrosoftLogger<>));
        // Register your other services...
    }
}
Enter fullscreen mode Exit fullscreen mode

For Umbraco v8 however, we have a bit a problem. As you can see in the v9 snippet above, we don't register a typed logger like ILogger<MyClass> as this would require us to register an instance for every class we want to be able to log in, which would be a lot. Instead we need to be able to register the generic interface with a generic implementation and have the DI container automatically resolve the generic type based up the injected dependency type.

Unfortunately, out of the box Umbraco doesn't provide the functionally for this and so we'll need to add it ourselves. But thankfully they do have something pretty close that we can modify.

What we need to do is define a custom extension method for the Umbraco Composer with the following code.

public static class ComposerExtensions
{
    internal static void RegisterAuto(this Composer composer, Type serviceBaseType, Type implementingBaseType)
    {
        var container = composer.Concrete as ServiceContainer;
        if (container != null)
        {
            container.RegisterFallback((serviceType, serviceName) =>
            {
                if (serviceType.IsInterface && !implementingBaseType.IsInterface
                    && serviceType.IsGenericType && serviceType.GetGenericTypeDefinition() == serviceBaseType)
                {
                    var genericArgs = serviceType.GetGenericArguments();
                    var implementingType = implementingBaseType.MakeGenericType(genericArgs);

                    container.Register(serviceType, implementingType);
                }

                return false;
            }, null);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What this is doing is registering a fallback method to the LightInject container (which is what Umbraco v8 uses for DI under the hood) which gets called if a requested type couldn't be located.

In this function we check to see if the requested type is the interface we are interested in and if so, we extract the generic type arguments from that type and then construct our concrete type passing in those generic arguments and then registering that service with the container for future requests.

With that in place, inside our composer we can then register our v8 implementation like so.

public class MyComposer : IUserComposer
{
    public void Compose(Composition composition)
    {
        composition.RegisterAuto(typeof(ILogger<>), typeof(UmbracoLogger<>));
        // Register your other services...
    }
}
Enter fullscreen mode Exit fullscreen mode

And wa-lah! We can now register our custom generic logger in Umbraco v9 and v8 and maintain a consistent usage throughout our codebase 😎

I hope this comes in useful for some of the other multi-targeting package developers out there.

Oldest comments (2)

Collapse
 
dhymik profile image
Mikael Kleinwort

Hi Matt,

thank you for posting this. I have a kind of similar problem in Umbraco 8. For some of my .Net Standard libraries, I need to provide a Microsoft.Extensions.Logging.ILogger. There is a Serilog extension for this, see this StackOverflow answer, which creates an MS logger from the Serilog logger.

The only trouble I have in Umbraco 8 is: how to get an instance of the Serilog logger through dependency injection or otherwise? Do you know a way?

I also posted a question on Our about this.

Collapse
 
dhymik profile image
Mikael Kleinwort

Hi Matt,

it turned out that the solution to my question was really easy. Thanks to Bjarke Berg for the key hint:

Serilog has a static instance called Log.Logger, which is populated in Umbraco v9. I guess that also exists in v8.

This did the trick. I can use Microsoft.Extensions.Logging.Abstractions in my .Net Standard libraries, logging against its MS ILogger abstraction, and provide the actual logger in Umbraco directly from Serilog, so all my class libraries log directly to the Umbraco 8 log. The Serilog.Extensions.Logging package does the heavy lifting between MS ILogger and Serilog ILogger.

May be this is also interesting for you, in this way, your Umbraco 8 and 9 code can just log against the MS ILogger and all you need for Umbraco 8 is a small service to provide the MS Logger. In case you are interested: I updated my issue on Our with the solution.