1

We have existing services where interface for Logging is injected into Child constructors and passed on to its parents (base).

Existing:

Public class Child: Parent
    public Ctor(ILogger logger) : base(ILogger logger)

New: (Modified ILogger to IMyLogger which internally uses Serilog Logger APIs for logging)

Public class Child: Parent
    public Ctor(IMyLogger<Child> logger) : base(IMyLogger<Parent> logger)

The above will obviously not work. But I need to work around this because these constructors are mostly used only Unittesting purposes or to retain a certain hierarchy chain which will be used while registering with DI framework!

Why I need to introduce a generic Logger is to bind a type to the Logger implementation which I can use later as my Serilog Source Context.

2 ways I can fix the above refactoring:

Options

  1. Retain ILogger and not make in Generic. Use StackFrames to find the caller Type and assign it to SourceContext which will later be logged
  2. Refactor ILogger to ILogger<T> but not pass the logger to base constructor.
Public class Child: Parent
    public Ctor(ILogger<Child> logger) : base()

Option 1 uses reflection and we can never rely on the Stack Frames to find the correct method which made the Logging call. Also performance will be impacted.

I feel option 2 is a cleaner way where each Child will have its own Context based Logger<Myself> which can later be mocked in Unit Tests.

Loggers should be one each for each service I feel. But am open to get pounded by varying thoughts and other better ways to handle this design. :-)

Miroslav Glamuzina
  • 4,472
  • 2
  • 19
  • 33
  • If I got it right, all you need is an injection factory that will create a specific logger whenever you resolve one... Something similar to what [autofac-serilog-integration](https://github.com/nblumhardt/autofac-serilog-integration) does. – tukaef Dec 18 '19 at 19:51
  • Sorry. No. As I wrote, the existing code has ILogger injected. But I wanted to inject ILogger. But as pointed out in the "NEW" section of code, I cant inject ILogger into base ctor which expects "ILogger. I am confused if we have to inject ILogger into base at all. I feel each Child should have its own Logger and the Base should have its own logger. – CodeTruthSeeker Dec 19 '19 at 20:01
  • 1
    I highly recommend reading [Simple Injector: Register ILogger by using ILoggerFactory.CreateLogger()](https://stackoverflow.com/questions/41243485/simple-injector-register-iloggert-by-using-iloggerfactory-createloggert/41244169#41244169). You can reduces your dependencies to `ILogger`. – Erik Philips Dec 19 '19 at 20:59
  • Why does the base class have it's own logger type, why not just `ILogger`? – Erik Philips Dec 19 '19 at 21:05
  • @ErikPhilips - I will have a look at the link you suggested. However to answer your question, since am refactoring a huge no of existing services by injecting a New IMyLogger, I want to ensure least changes. Why I use ILogger is following MS recommendation of getting the type context the efficient/elegant way. – CodeTruthSeeker Dec 19 '19 at 22:45
  • @ErikPhilips- And by its own logger type I meant in general each class in a chain of Dependent classes will have its own logger injected via constructor but will not be passed to the Base it derives from. This way each class can have its own ILogger setup via DI. In Current implementation all Children just pass the ILogger to the Base which logs thereby losing context. – CodeTruthSeeker Dec 19 '19 at 22:49
  • @ErikPhilips - Read the article. Thanks it opened up my understanding. But my issue is IMyLogger is my own implementation and doesnt derive from ILogger. I have a class MyLogger. In its constructor I set up the actual Serilog Logger as "_logger = Log.ForContext(). So you can call the MyLogger as a Facade which hides the Serilog implementation. – CodeTruthSeeker Dec 19 '19 at 23:05

1 Answers1

1

Fody Anotar

Why I need to introduce a generic Logger is to bind a type to the Logger implementation which I can use later as my Serilog Source Context.

I've had a lot of success using the Anotar.Serilog add-in for Fody, so I don't have to worry about details like this.

Your Code

public class MyClass
{
    void MyMethod()
    {
        LogTo.Debug("TheMessage");
    }
}

What gets compiled

public class MyClass
{
    static ILogger logger = Log.ForContext<MyClass>();

    void MyMethod()
    {
        if (logger.IsEnabled(LogEventLevel.Debug))
        {
            logger
                .ForContext("MethodName", "Void MyMethod()")
                .ForContext("LineNumber", 8)
                .Debug("TheMessage");
        }
    }
}

The one potential draw-back is that you can't easily test the logging behavior of your class in unit tests, because you're not invoking an injected, mockable interface. But I've tried a lot of different approaches over the years, and this one has far-and-away the most advantages and the fewest disadvantages. Besides, I've come to feel that logging is a cross-cutting concern that's kind of set apart from the normal rules of software architecture.

See https://github.com/Fody/Anotar

Avoiding generics with injection

But if you really like using an injected ILogger, you should be able to avoid injecting a generic type. Inject a non-generic ILogger instead. The details will depend on your DI framework, but they pretty much all will allow you to define injection using a factory that is aware of the type that you're injecting into. You can pass that type into the CreateLogger(ILoggerFactory, Type) extension method.

Of course, that means when your Base class emits a log message it will appear to be coming from its child class, which seems weird to me. You could use Property Injection instead of Constructor Injection for your loggers to avoid this.

Or you could avoid the problem entirely by re-architecting your classes to favor composition over inheritance. Usually you can just inject the Parent class into the Child class as a dependency, and end up with a better design pattern in the long run.

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • Static Factory Method? Yikes. – Erik Philips Dec 19 '19 at 21:01
  • 1
    @ErikPhilips: Can you elaborate? As much as I prefer injection normally, I've found that its benefits mostly don't apply to loggers. Are there times when you've been bitten by using a static factory method to acquire a logger? Things you do with your loggers that you wouldn't be able to do if you were using static factory methods? I love hearing other people's experiences. – StriplingWarrior Dec 19 '19 at 21:11
  • How do you configure a Static Method Logger? Also this now requires every programmer to have [Tribal Knowledge](https://en.wikipedia.org/wiki/Tribal_knowledge) on how to create a Logger, which is why DI exists in the first place, so people don't need to know how to create interfaces. – Erik Philips Dec 19 '19 at 21:15
  • In a static way. `Log.Logger = loggerConfiguration.CreateLogger();` I haven't found a need to inject different logging configurations into different services. – StriplingWarrior Dec 19 '19 at 21:18
  • @ErikPhilips: Regarding tribal knowledge, I don't think you read my answer thoroughly. My actual code just says `LogTo.Debug("Some message")`, which requires no more tribal knowledge than injecting an `ILogger`. Fody Weavers do the magic to produce a logger and add a bunch of contextual information to it for me. – StriplingWarrior Dec 19 '19 at 21:30
  • I'm certainly not going to argue any more about the merits of DI over Static Factory Methods. – Erik Philips Dec 19 '19 at 21:35
  • I'm certainly not going to ask you to. [You'd be preaching to the choir.](https://stackoverflow.com/a/13714035/120955) [I've used](https://stackoverflow.com/q/3480332/120955) dependency injection to get my logger instances, so I've seen some pros and cons inherent to that approach. I'm here sharing lessons from my experience. If you have had experiences I haven't had, then I want to learn from you as well. – StriplingWarrior Dec 19 '19 at 21:53