Shimming ReactiveUI's Log<T>() To Support ASPNet's ILogger<T>

Picking up Xamarin app development again after some time and decided to tackle the world of Reactive programming that I fell in love with from when I was building Angular2 web apps using reactive extensions.

Having recently developed server side with ASP.Net Core, I got use to using Ilogger<T> from Microsoft.Extensions.Logging.Abstractions. Was quite handy having a consistant ILogger interface everywhere that was immediately compatable with many 3rd party loggers (NLog, Serilog, et al) and even built some libraries of my own that use it that aren't solely intended for ASP.Net.

Whilst picking up ReactiveUI, they provide their own Service Locator inside of Splat and provide their own logging classes that aren't directly compatable with ILogger<T> (that I wasn't ready to let go of).

ReactiveUI's Handbook instructs developers to add IEnableLogging to their classes they wish to log from. And by doing so, they gain access to this.Log() thanks the extension method:

public static IFullLogger Log<T>(this T This) where T : IEnableLogger { /* ... */ }

This was done so that your class name (through accessing T) would be included in the logger's output to help keep track of where the log was issued.

Seeing how this was done similarly to ILogger<T> which is really just ILogger. I wanted to bridge the gap by implementing ASP.Net's ILogger and ReactiveUI's Splat logging. Which first involes implementing ILogger

public class ReactiveLogger<TClass> : ILogger<TClass>
{
    public IDisposable BeginScope<TState>(TState state)
    {
        throw new NotImplementedException();
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        throw new NotImplementedException();
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {        
        throw new NotImplementedException();   
    }
}

Taking a peek into how Microsoft.Extensions.Logging.Console processes public void Log<TState>( ... ) I started out by checking IsEnabled(), followed by null-checking formatter and then creating our error message by the supplied formatter, state and extenction parameters.

    if (!IsEnabled(logLevel))
        return;
    if(formatter == null)
        throw new ArgumentNullException(nameof(formatter));

    var message = formatter(state, exception);

    if (string.IsNullOrEmpty(message) && exception == null)
        return;

You may have noticed a lack of IEnableLogging interface on ReactiveLogger. That's because the logic behind the Log<T>() extension method uses the name of the invoking class. In this case, it'd end up always being ReactiveLogger.

Not very helpful if there's dozens of classes being provided with this logger...

Digging into Splat's ILogManager (which gets invoked on behalf of Log<T>()) it retreives a memory-cached IFullLogger initialised with the name of the invoking class.

So lets copy this bahaviour

    // Get Splat's ILogManager
    var factory = (Splat.ILogManager)Splat.Locator.Current.GetService(typeof(Splat.ILogManager))
        ?? throw new Exception($"{nameof(Splat.ILogManager)} was not found. Please ensure your dependency resolver is configured correctly.");

    // Get a logger to reprecent our TClass
    var logger = factory.GetLogger(typeof(TClass));

What's Splat.Locator.Current you ask? It's ReactiveUI's prefered method of getting services required throughout your application without tightly coupling everyhting.

Note: You're welcome to replace this, but make sure you're populating services requried by ReactiveUI

We're asking Splat's ILogManager to provide us with a IFullLogger (in the same way Log<T>() does), passing in the the target class's type provided through templating.

All that's left is to invoke the correct logginer method.

    switch (logLevel)
    {
        case LogLevel.Trace:
            // This is very verbose and can be considered equally annoying as LogLevel.Debug in this case.
        case LogLevel.Debug:
            if (exception != null)
                logger.DebugException($"[{eventId.Id}]: {message}", exception);
            else
                logger.Debug($"[{eventId.Id}]: {message}");
            break;
        case LogLevel.Information:
            if (exception != null)
                logger.InfoException($"[{eventId.Id}]: {message}", exception);
            else
                logger.Info($"[{eventId.Id}]: {message}");
            break;
        case LogLevel.Warning:
            if (exception != null)
                logger.WarnException($"[{eventId.Id}]: {message}", exception);
            else
                logger.Warn($"[{eventId.Id}]: {message}");
            break;
        case LogLevel.Error:
            if (exception != null)
                logger.ErrorException($"[{eventId.Id}]: {message}", exception);
            else
                logger.Error($"[{eventId.Id}]: {message}");
            break;
        case LogLevel.Critical:
            if (exception != null)
                logger.FatalException($"[{eventId.Id}]: {message}", exception);
            else
                logger.Fatal($"[{eventId.Id}]: {message}");
            break;
        case LogLevel.None:
            break;
    }

Where We call the appropiate logging methods in IFullLogger based on our logLevel, if our exception is null and the output of the formatted message.

The Completed ReactiveLogger

public class ReactiveLogger<TClass> : ILogger<TClass>
{
    public IDisposable BeginScope<TState>(TState state)
    {
        // Scopes haven't been considered yet and I'll update this article when I need it. 
        throw new NotImplementedException();
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true; // Up too you on how you decide to disable logging
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel))
            return;
        if(formatter == null)
            throw new ArgumentNullException(nameof(formatter));

        var message = formatter(state, exception);

        if (string.IsNullOrEmpty(message) && exception == null)
            return;

        // Get Splat's ILogManager
        var factory = (Splat.ILogManager)Splat.Locator.Current.GetService(typeof(Splat.ILogManager))
            ?? throw new Exception($"{nameof(Splat.ILogManager)} was not found. Please ensure your dependency resolver is configured correctly.");

        // Get a logger to reprecent our TClass
        var logger = factory.GetLogger(typeof(TClass));

        switch (logLevel)
        {
            case LogLevel.Trace:
            case LogLevel.Debug:
                if (exception != null)
                    logger.DebugException($"[{eventId.Id}]: {message}", exception);
                else
                    logger.Debug($"[{eventId.Id}]: {message}");
                break;
            case LogLevel.Information:
                if (exception != null)
                    logger.InfoException($"[{eventId.Id}]: {message}", exception);
                else
                    logger.Info($"[{eventId.Id}]: {message}");
                break;
            case LogLevel.Warning:
                if (exception != null)
                    logger.WarnException($"[{eventId.Id}]: {message}", exception);
                else
                    logger.Warn($"[{eventId.Id}]: {message}");
                break;
            case LogLevel.Error:
                if (exception != null)
                    logger.ErrorException($"[{eventId.Id}]: {message}", exception);
                else
                    logger.Error($"[{eventId.Id}]: {message}");
                break;
            case LogLevel.Critical:
                if (exception != null)
                    logger.FatalException($"[{eventId.Id}]: {message}", exception);
                else
                    logger.Fatal($"[{eventId.Id}]: {message}");
                break;
            case LogLevel.None:
                break;
        }
    }
}

Bootstrapping Splat's Service Locator

When it comes to bootstrapping Splat's service locator, it gets a little too verbose as you're forced to register multiple logger types:

    Locator.CurrentMutable.Register<Microsoft.Extensions.Logging.ILogger<MyService>>(() => new ReactiveLogger<MyService>());
    Locator.CurrentMutable.Register<Microsoft.Extensions.Logging.ILogger<MyotherService>>(() => new ReactiveLogger<MyOtherService>());
    // ...

Time to make life a little easier (and reading on your eyes) by using extension methods!

public static class SplatLocatorExtensions
{
    public static Splat.IMutableDependencyResolver RegisterLogger(this Splat.IMutableDependencyResolver services, Type type)
    {
        // Turn ReactiveLogger<> into ReactiveLogger<type>
        var genericReactiveLogger = typeof(ReactiveLogger<>).MakeGenericType(type);
        // Turn ILogger<> into ILogger<type>
        var genericLogger = typeof(ILogger<>).MakeGenericType(type);

        // Register it!
        services.Register(() => Activator.CreateInstance(genericReactiveLogger), genericLogger);
        return services;
    }

    public static Splat.IMutableDependencyResolver RegisterLogger<TType>(this Splat.IMutableDependencyResolver services)
    {
        // Much simpler as we're dealing with templated types rather than `Type` parameters.
        services.Register(() => new ReactiveLogger<TType>(), typeof(ILogger<TType>));
        return services;
    }
}

This allows us to quickly register logging for services that need ILogger like this:

    Locator.CurrentMutable.RegisterLogger<MyService>()
                          .RegisterLogger<MyOtherService>()
                          // ...

This allows for us to easily use ASP.Net's ILogger<T> in our Xamarin app when using ReactiveUI as our reactive framework.