Search code examples
c#dependency-injectionsimple-injector

Implementing a Logger Class for Log4Net, using Simple Injector


I've scoured through Stack Overflow to get an idea on how I can have logging in my C# application and keep the usage requirements specific to my application. The following questions that have previously been answered have helped assist me:

These implementations seem to want me to pass log4net.ILog to the constructor of my implementation or to the base implementation of log4net's LogImpl. However, I was having trouble configuring my abstract logger using Simple Injector.

As I see it, my implementation is working really well, but I dont know what some draw backs might exist, or perhaps other ways of doing this.

What I've Got So Far

  • I have an ILogger interface that requires a void Log(LogEntry entry) method.
  • An Adapter (I adapted it from the above referenced sources) - public class Log4netAdapter<T> : ILogger
  • DI container Simple Injector with the following registration:

Source Code

Simple Injector DI Container:

private SimpleInjector.Container container;

[SetUp]
public void SetUp()
{            
    // init log4net
    XmlConfigurator.Configure();
    container = new SimpleInjector.Container();
    container.RegisterConditional(
        typeof(ILogger),
        c => typeof(Log4netAdapter<>).MakeGenericType(c.Consumer.ImplementationType),
        Lifestyle.Singleton,
        c => true);
}    

Logger interface:

public interface ILogger
{
    void Log(LogEntry entry);
}

public class Log4netAdapter<T> : ILogger
{
    private readonly log4net.ILog Logger;

    public Log4netAdapter()
    {
        this.Logger = LogManager.GetLogger(typeof(T));
    }

    public void Log(LogEntry entry)
    {
        if (entry.Severity == LoggingEventType.Debug)
            Logger.Debug(entry.Message, entry.Exception);
        else if (entry.Severity == LoggingEventType.Information)
            Logger.Info(entry.Message, entry.Exception);
        else if (entry.Severity == LoggingEventType.Warning)
            Logger.Warn(entry.Message, entry.Exception);
        else if (entry.Severity == LoggingEventType.Error)
            Logger.Error(entry.Message, entry.Exception);
        else
            Logger.Fatal(entry.Message, entry.Exception);
    }
}

Extensions for ILogger:

public static class LoggerExtensions
{
    public static void Log(this ILogger logger, string message)
    {
        logger.Log(new LogEntry(LoggingEventType.Information, message));
    }
            public static void Log(this ILogger logger, Exception exception)
    {
        logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception));
    }
}

What I want to know

I want to know if this follows SOLID principles and is there a better way? If there is a better way, can anyone provide me with the reason for it following an example via C#.

What I dont like

My Implementation does not allow me to just call a specific method on any class that I am passing Ilogger into the constructor. It requires me to have Extensions created on the ILogger interface, which then redirects to my Log4netAdapter.


Solution

  • I want to know if this follows SOLID principles and is there a better way?

    Whether or not this is SOLID, highly depends on the broader context of the application. For instance, when you are injecting your ILogger into a large number of classes in your system, you are likely violating the Single Responsibility Principle and Open/Closed Principle. For instance, see this q&a.

    Although analysis of SRP and OCP require more context, we can actually say something about the ISP and DIP here:

    • The ILogger interface defines a single member and, therefore, follows the Interface Segregation Principle, which states that abstractions should be narrow.
    • Instead of depending on an abstraction from an external party, your application code depends on the application-specific ILogger abstraction and, therefore, follows the Dependency Inversion Principle, which states that abstractions should be owned by the consumers of the abstraction.

    What I dont like ... It requires me to have Extensions created on the ILogger interface, which then redirects to my Log4netAdapter.

    When we apply the Dependency Inversion Principle (mostly through Dependency Injection) we separate dependencies into two distinct groups:

    Where a class can safely directly depend on and invoke any Stable Dependency, you wish to hide any Volatile Dependency behind an abstraction.

    In the context of your question, Log4Net's appenders are, from the context of your application's code, Volatile Dependencies. This is especially because they do I/O, or as DIPP&P puts it:

    • "The Dependency introduces a requirement to set up and configure a runtime environment for the application." This means you will have to configure Log4net to prevent breaking your application.
    • You "expect to have to replace, wrap, decorate, or Intercept the class or module." The whole idea behind Log4net is to be able to change the channel to which you want to write logging information, so we certainly want to be able to replace the behavior, also for testing purposes.

    These are two characteristics of Volatile Dependencies and to make our application maintainable and testable, we hide Volatile Dependencies behind an abstraction, hence your ILogger abstraction.

    The extension methods on ILogger, however, are not Volatile Dependencies; they are Stable Dependencies. This is because the behavior inside those extension methods:

    • Does not do any I/O
    • is completely deterministic
    • doesn't have to be replaced (for instance using a configuration switch)

    The volatile part of the logging behavior is completely hidden behind the ILogger interface and this allows you to replace, mock, and intercept all volatile behavior.

    Because the extension methods are Stable Dependencies, any consumer can safely depend on them, without causing any maintainability or testability issues. As a matter of fact, having this stable part of the logging behavior not hidden behind an abstraction has some interesting advantages:

    • It allows those extension methods to be tested with the consuming code
    • It allows the input data to be validated when the code or the tests run. With logging libraries you'll often see that some validation exists, but that validation exists inside the production code, which is mocked away when you are running tests.

    This does mean, though, that you should ensure that the extension methods stay stable; they should not start to contain nondeterministic or volatile behavior.

    It requires me to have Extensions created on the ILogger interface

    The extension methods are not part of the interface; they are part of the consuming code. This means you can place the extension methods everywhere you like, and could even have different extension methods for different part of the code base (although perhaps not that likely for logging).