What is a suitable pattern for injecting loggers within dynamically-discovered .NET Core class libraries called from ASP.NET Core web apps?
Asked Answered
P

1

13

Overview

I'm trying to port a number of projects based on the .NET Framework to .NET Core. This involves porting a number of class libraries as well as top-level console/web applications that consume these libraries.

Part of my requirements is that my top-level applications should support a plugin-based system where I can easily swap out functionality by referencing different subsets of these class libraries. I've used MEF to do this. As an example, one of my ASP.NET Core web applications involves communicating with devices through an ICommunicationService, and I have different Nuget packages that export different implementations of this service:

[Export(typeof(ICommunicationService))]
[Shared]
public sealed class UsbCommunicationService : ICommunicationService
{
}

Redesigning Class Libraries

At the moment, these class libraries reference Common.Logging and instantiate loggers as read-only static fields:

[Export(typeof(ICommunicationService))]
[Shared]
public sealed class UsbCommunicationService : ICommunicationService
{
    ...
    private static readonly ILog Log = LogManager.GetLogger<UsbCommunicationService>();
    ....
}

I used Log4Net within my top-level applications and facilitated logging from within my class libraries by referencing the Common.Logging.Log4Net adapter.

However, I know that ASP.NET Core relies on Microsoft's new logging abstraction framework Microsoft.Extensions.Logging and that ASP.NET Core applications should be designed to support logging via constructor dependency injection of loggers, like this:

public class HomeController : Controller
{
    private ILogger<HomeController> _logger;

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

    public IActionResult Index()
    {
        _logger.LogInformation("Index action requested at {requestTime}", DateTime.Now);
        return View();
    }
}

I'm not entirely sure which combination of logging frameworks to use within my new .NET Core libraries and applications (see this related post), but I'm leaning towards switching from using Common.Logging to Microsoft.Extensions.Logging within my class libraries. In that case, I'm wondering how I should handle instantiation of loggers. Would something like this be appropriate?

using Microsoft.Extensions.Logging;
...
[ImportingConstructor]
public UsbCommunicationService(
    [Import] IUsbMessageEncoder encoder,
    [Import] IUsbMessageDecoder decoder,
    [Import] ILogger<UsbCommunicationService> logger /* Add this new import */)
{
    ...
}

In other words, should I switch all my class libraries that require logging to having those loggers injected during construction?

Consuming Class Libraries

Based on this answer to a similar question, I feel like the approach detailed above is along the right lines. However, I'm not sure how I would consume and properly instantiate services defined within class libraries within, say, an ASP.NET Core application.

ASP.NET Core uses its own dependency injection service which is completely separate to MEF. I can't simply write something like services.AddSingleton<ICommunicationService, UsbCommunicationService>(); within my web application for two reasons:

  1. The idea is to support a plugin-based system where plugins are discovered dynamically and therefore can't be hard-referenced by the "core" application itself.

  2. The other dependencies for UsbCommunicationService - IUsbMessageEncoder and IUsbMessageDecoder - are not known by ASP.NET Core's service injector and wouldn't be resolved.

Likewise, I can't use MEF to get an instance of UsbCommunicationService either as it wouldn't be able to resolve the reference to ILogger<UsbCommunicationService>.

Summary

In short, I'm trying to find solutions for the following:

  • Facilitating logging within .NET Core libraries with maximum flexibility for logging providers.
  • Allowing loggers to be supplied to these class libraries using dependency injection.
  • Allowing top-level ASP.NET Core or .NET Core console applications to dynamically discover and load these .NET Core libraries at run-time and provide them all with loggers or logger factories so that the top-level application and all loaded plugins use a common logging provider (e.g. Serilog, NLog, Log4Net, etc.).
    • For instance, if I wanted to use Log4Net's ColoredConsoleAppender, I should see all ASP.NET logs and class library logs appearing within the same console.
Pavyer answered 7/10, 2016 at 11:50 Comment(0)
D
4

Microsoft.Extensions.Logging is not strictly a logging framework; it's a facade. There's built-in implementations for things like Debug, Trace, Console, etc., but that's just for ease. In actual use, you'd likely plug in something like Serilog. Serilog is what is actually handling the logging, while Microsoft.Extensions.Logging just provides an abstracted way of "logging" without having to actually make your app code depend explicitly on Serilog's API.

Common.Logging is also a facade, where in this case, you've chosen to plug in log4net as the actual logger being used. However, that can be changed. Given this, you have a few possible paths you can take.

  1. Switch out Microsoft.Extensions.Logging with Common.Logging in your ASP.NET Core app. Yep, you can do that. Personally, I think Microsoft.Extensions.Logging is better, but you can use whatever you like.

  2. Switch out Common.Logging with Microsoft.Extensions.Logging in your class libraries. If you're rewriting them anyways, this might make the most sense, but it's also involves the most friction, in terms of things that need to be changed in your code.

  3. Write an adapter for Common.Logging that uses Microsoft.Extensions.Logging. This is admittedly a bit meta, but there's technically nothing wrong with simply using one facade to work with another facade to eventually work with a particular logging framework. That's sort of the entire point of the adapter pattern. This also nets you the best of both worlds: you don't need to change much in your libraries or your ASP.NET Core app. However, it does add to the entropy of your code due to the multiple facades in play. While it's impossible to say, I actually don't see Common.Logging continuing to be maintained going forward. Now that Microsoft sort of has an almost built-in facade, I expect to see that pretty much dominate. It might be for the best to jump ship now, while you're already incurring some amount of rework.

Dilley answered 29/3, 2018 at 15:33 Comment(3)
It's been one and a half years since I asked, so I've pretty much already done this - Microsoft.Extensions.Logging.Abstractions used in all class libraries, with Serilog (i.e best logging framework ever) actually doing the logging. NullLogger (from Microsoft...Abstractions) or a mock logger injected into the SUT within test code. By the way, fantastic work in Guardians of the Galaxy.Pavyer
I honestly didn't even notice the date you posted this. Just working through the unanswered queue. I guess better late than never, right?Dilley
Yeah it's always nice to have confirmation, plus it can help others.Pavyer

© 2022 - 2024 — McMap. All rights reserved.