rana404
rana404

Reputation: 21

How to switch from log4net to Microsoft.Extensions.Logging over multiple libraries

I have a c# project which references multiple libraries (core, standard). Every library uses log4net in the following way:

public class LibOne
{
    private static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    public LibOne()
    {
        Log.Debug("ctor called");
    }
}

The main project configures the Logger:

XmlConfigurator.Configure(LogManager.GetRepository(Assembly.GetEntryAssembly()),
                new FileInfo($"{AppDomain.CurrentDomain.BaseDirectory}\\log4net.config"));

The hierarchy looks something like this:

         MainProject
             |
   +---------+---------+
LibOne    LibTwo    LibThree
   |                   |
   |                   +---------+
   +---------+      LibSix    LibSeven
LibFour   LibFive

Okay, so with the above setup I can see log entries of every single library.


Now the libraries should be modernized and become more universal so that the consumer can decide his favorite logger (e.g. NLog, Console, whatever). Therefore I want to use Microsoft.Logging.Extensions. I've read a lot of documentation, guides, tips etc. and found out, that an ILoggerFactory may be the best way to start, but

  1. nobody explained, how to use the ILogger over multiple layers and how to configure the logger in the consuming project,
  2. nearly every example I found added an ILogger/ILogger<T> parameter to the constructor for DI, but I can't change the signature of every library's constructors. Is there a way to do it differently?
  3. I've read something about autofac but didn't understand it completely. Maybe I can use it to define which types/libraries I want to see in the logfile?

UPD 1 Is there any possibility to change the libraries code and handle it the same way, log4net does, but with an unspecified logprovider, e.g.

public class LibOne
{
    private static readonly ILogger<LibOne> Log = new LoggerFactory().CreateLogger<LibOne>(); //does this make sense?

    public LibOne()
    {
        Log.LogDebug("ctor called");
    }
}

Then how to configure the logger (e.g. log4net or debug console) in my main project? Am I able to add a provider later?

Sorry for these questions, but Microsoft's documentation about the Logging Extensions out of the asp.net scope (and without DependencyService) is really poor.

Upvotes: 2

Views: 2895

Answers (1)

Xerillio
Xerillio

Reputation: 5261

nobody explained, how to use the ILogger over multiple layers and how to configure the logger in the consuming project

I think you'll have to elaborate what you mean my "over multiple layers". The general convention is that every class that needs to do some logging requires it's own logger instance:

class MyController
{
    private readonly ILogger _logger;
    public MyController(ILogger<MyController> logger)
    {
        _logger = logger;
    }
}

// ...

class MyRepository
{
    private readonly ILogger _logger;
    public MyController(ILogger<MyRepository> logger)
    {
        _logger = logger;
    }
}

Alternatively, if you want to be able to "group" you logs by some more common categories rather that categorizing logs by the class, you could use the ILoggerFactory:

class MyController
{
    private readonly ILogger _logger;
    public MyController(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger("Controller");
    }
}

// ...

class MyRepository
{
    private readonly ILogger _logger;
    public MyController(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger("Repository");
    }
}

nearly every example I found added an ILogger/ILogger<T> parameter to the constructor for DI, but I can't change the signature of every library's constructors. Is there a way to do it differently?

I think you and your team should really think about why you have this requirement about not modifying the constructor. If you don't inject the dependency of the logger in the constructor or the methods then you'll have to resolve the logger service explicitly through the DI container, which means you are adding a dependency to the dependency injection (DI) framework which consumers of your libraries will then need to use. Of course you can make a custom solution to wrap around the DI framework to avoid a hard dependency of a specific DI framework, but I think you're giving yourself more trouble that way.

In other words: unless you want to complicate things for yourself, you will not avoid breaking changes with this refactoring. So, I would suggest you reconsider the requirement of not changing constructor signatures. If you really want, you could extend the constructors with an optional ILogger parameter which would make it less of a breaking change, but now your classes need to be prepared for not having a logger passed in.

To give you an example of not injecting the dependency through the constructor:

class MyStartupClass
{
    public static SomeDiLibrary.ContainerType Container;

    public SetupApplication()
    {
        Container = new SomeDiLibrary.ContainerType();
        Container.ConfigureServices(...);
    }
}

class MyController
{
    private readonly ILogger _logger;
    public MyController()
    {
        // Explicitly resolve an ILogger using the DI framework
        _logger = MyStartupClass.Container.Resolve<ILogger>();
    }
}

As you can see, MyController is now dependent on using SomeDiLibrary as the DI framework. Projects using your project therefore also needs to use SomeDiLibrary in order to "inject" their own logger types. If a consumer project wanted to use a different DI framework it would not play together with yours. Furthermore - in my very simplified example - you are left with the problem of how to make the DI container accessible to MyController. I'm guessing you are building some class libraries, which means you do not have a setup class like MyStartupClass, so where does MyController get hold of the container from?

Perhaps there are libraries out there to help you overcome this problem, but I'm not aware of such.

I've read something about autofac but didn't understand it completely. Maybe I can use it to define which types/libraries I want to see in the logfile?

Autofac is just one of the more common dependency injection frameworks. Similarly you have Castle Windsor, Spring.NET, Ninject to name a few I've heard of. This is what constitutes SomeDiLibrary above.

To summarize what a DI is:

With DI you typically aim to define dependencies in your classes using interfaces. The class conventionally takes all dependencies (i.e. instances of other classes/services) as input in the constructor.

The above lets you dynamically choose the implementation of an interface that you want a class use. Imagine you have a UserRepository and a controller that uses this repository to look up users directly from the database. If your controller looks up the same users very often, you might want to cache some of the results instead of querying the database every time. So your controller wants to use UserRepositoryCache instead. This requires to modify the controller class. But instead you could depend on a common interface IUserRepository and then it is up to the DI framework to configure which implementation (UserRepository or UserRepositoryCache) to supply to the controller whenever it's instantiated.

You can probably find a much better explanation of DI and DI frameworks but hopefully it gives you an idea.

Upvotes: 2

Related Questions