C
C#4mo ago
Mayka

ILogger Dependency Injection for Libraries

The below is what I came up with to inject a logger into a library, but something feels off. Can anyone advise if something in my approach needs tweaking?
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace LogInjection;

class Program
{
static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}"
)
.CreateLogger();

using ServiceProvider serviceProvider = new ServiceCollection()
.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true))
.AddSingleton<FooFactory>()
.BuildServiceProvider();

var fooFactory = serviceProvider.GetRequiredService<FooFactory>();
fooFactory.CreateFoo("red").DoSomething();
fooFactory.CreateFoo("blue").DoSomething();
}
}

// Library code here
public class FooService { }

public class FooFactory(ILogger<FooService>? logger = null)
{
private readonly ILogger _logger = logger ?? NullLogger<FooService>.Instance;
public IFoo CreateFoo(string type)
{
_logger.LogInformation("Creating a new {Type}", type);
return type switch
{
"red" => new FooRed(_logger),
"blue" => new FooBlue(_logger),
_ => throw new ArgumentException("Unknown type", nameof(type)),
};
}
}

public interface IFoo
{
void DoSomething();
}

internal class FooRed(ILogger logger) : IFoo
{
public void DoSomething()
{
logger.LogInformation("I'm red!");
}
}

internal class FooBlue(ILogger logger) : IFoo
{
public void DoSomething()
{
logger.LogInformation("I'm blue!");
}
}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace LogInjection;

class Program
{
static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}"
)
.CreateLogger();

using ServiceProvider serviceProvider = new ServiceCollection()
.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true))
.AddSingleton<FooFactory>()
.BuildServiceProvider();

var fooFactory = serviceProvider.GetRequiredService<FooFactory>();
fooFactory.CreateFoo("red").DoSomething();
fooFactory.CreateFoo("blue").DoSomething();
}
}

// Library code here
public class FooService { }

public class FooFactory(ILogger<FooService>? logger = null)
{
private readonly ILogger _logger = logger ?? NullLogger<FooService>.Instance;
public IFoo CreateFoo(string type)
{
_logger.LogInformation("Creating a new {Type}", type);
return type switch
{
"red" => new FooRed(_logger),
"blue" => new FooBlue(_logger),
_ => throw new ArgumentException("Unknown type", nameof(type)),
};
}
}

public interface IFoo
{
void DoSomething();
}

internal class FooRed(ILogger logger) : IFoo
{
public void DoSomething()
{
logger.LogInformation("I'm red!");
}
}

internal class FooBlue(ILogger logger) : IFoo
{
public void DoSomething()
{
logger.LogInformation("I'm blue!");
}
}
17 Replies
mtreit
mtreit4mo ago
I don't see an obvious issue. I mean, I would use things like an enum instead of a string to determine the type, but of course this just seems to be a throw-away example so...
Mayka
Mayka4mo ago
Yep, in my actual code I am using an enum but didn’t bother for the example. What I ideally wanted was to be able to show a context of FooFactory within FooFactory, FooRed within FooRed, and FooBlue within FooBlue, but since the only entry point to the class would be the factory, I couldn’t figure out how to do that. The below fails, understandably, so I wasn’t sure if libraries were just supposed to expose one “context”:
public class FooFactory(ILogger<FooFactory>? logger = null)
{
private readonly ILogger _logger = logger ?? NullLogger<FooFactory>.Instance;
public IFoo CreateFoo(string type)
{
_logger.LogInformation("Creating a new {Type}", type);
return type switch
{
"red" => new FooRed(_logger),
"blue" => new FooBlue(_logger),
_ => throw new ArgumentException("Unknown type", nameof(type)),
};
}
}

public interface IFoo
{
void DoSomething();
}

internal class FooRed(ILogger<FooRed> logger) : IFoo
{
public void DoSomething()
{
logger.LogInformation("I'm red!");
}
}

internal class FooBlue(ILogger<FooBlue> logger) : IFoo
{
public void DoSomething()
{
logger.LogInformation("I'm blue!");
}
}
public class FooFactory(ILogger<FooFactory>? logger = null)
{
private readonly ILogger _logger = logger ?? NullLogger<FooFactory>.Instance;
public IFoo CreateFoo(string type)
{
_logger.LogInformation("Creating a new {Type}", type);
return type switch
{
"red" => new FooRed(_logger),
"blue" => new FooBlue(_logger),
_ => throw new ArgumentException("Unknown type", nameof(type)),
};
}
}

public interface IFoo
{
void DoSomething();
}

internal class FooRed(ILogger<FooRed> logger) : IFoo
{
public void DoSomething()
{
logger.LogInformation("I'm red!");
}
}

internal class FooBlue(ILogger<FooBlue> logger) : IFoo
{
public void DoSomething()
{
logger.LogInformation("I'm blue!");
}
}
mtreit
mtreit4mo ago
All of my library code just takes an ILogger. I don't think the library code should care if it's an ILogger<T> behind the scenes...what is the library code supposed to do with that? Just ILogger reduces coupling to other types, which is usually what you want.
Mayka
Mayka4mo ago
Interesting! Do you happen to have any public examples of that? Been trying to find actual usage of ILogger in libraries, since I learn best from concrete rather than contrived examples.
mtreit
mtreit4mo ago
Most of my stuff is all internal, but just do something like grep the Roslyn source code for ILogger and you'll see it's usually just a plain ILogger that's passed in to library code. At least from a quick look it seems that way.
Unknown User
Unknown User4mo ago
Message Not Public
Sign In & Join Server To View
Mayka
Mayka4mo ago
Do you have an example of what you mean? I am using the Msft.Ext.Logging ILogger interface in my example above already so that I’m not tied to Serilog. Is there a different area I would need to change as well?
Unknown User
Unknown User4mo ago
Message Not Public
Sign In & Join Server To View
mtreit
mtreit4mo ago
@TeBeCo I don't understand this example at all.
Unknown User
Unknown User4mo ago
Message Not Public
Sign In & Join Server To View
Mayka
Mayka4mo ago
@TeBeCo this code will be in a library with the only point of entry being FooFactory, so I would not be able to register FooRed or FooBlue. In that case are you saying I would want to inject a factory all the way down like this?
using LogInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;

Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}"
)
.CreateLogger();

using ServiceProvider serviceProvider = new ServiceCollection()
.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true))
.AddSingleton<FooFactory>()
.BuildServiceProvider();

var fooFactory = serviceProvider.GetRequiredService<FooFactory>();
fooFactory.CreateFoo("red").DoSomething();
fooFactory.CreateFoo("blue").DoSomething();

Console.WriteLine("\nPress any key to continue...");
Console.ReadKey();

namespace LogInjection
{
public class FooFactory(ILoggerFactory? loggerFactory = null)
{
private readonly ILogger _logger = loggerFactory is not null
? loggerFactory.CreateLogger<FooFactory>()
: NullLogger<FooFactory>.Instance;

public IFoo CreateFoo(string type)
{
_logger.LogInformation("Creating a new {Type}", type);

return type switch
{
"red" => new FooRed(loggerFactory),
"blue" => new FooBlue(loggerFactory),
_ => throw new ArgumentException("Unknown type", nameof(type)),
};
}
}

public interface IFoo
{
void DoSomething();
}

internal class FooRed(ILoggerFactory? loggerFactory) : IFoo
{
private readonly ILogger _logger = loggerFactory is not null
? loggerFactory.CreateLogger<FooRed>()
: NullLogger<FooRed>.Instance;

public void DoSomething()
{
_logger.LogInformation("I'm red!");
}
}

internal class FooBlue(ILoggerFactory? loggerFactory) : IFoo
{
private readonly ILogger _logger = loggerFactory is not null
? loggerFactory.CreateLogger<FooBlue>()
: NullLogger<FooBlue>.Instance;

public void DoSomething()
{
_logger.LogInformation("I'm blue!");
}
}
}
using LogInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;

Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}"
)
.CreateLogger();

using ServiceProvider serviceProvider = new ServiceCollection()
.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true))
.AddSingleton<FooFactory>()
.BuildServiceProvider();

var fooFactory = serviceProvider.GetRequiredService<FooFactory>();
fooFactory.CreateFoo("red").DoSomething();
fooFactory.CreateFoo("blue").DoSomething();

Console.WriteLine("\nPress any key to continue...");
Console.ReadKey();

namespace LogInjection
{
public class FooFactory(ILoggerFactory? loggerFactory = null)
{
private readonly ILogger _logger = loggerFactory is not null
? loggerFactory.CreateLogger<FooFactory>()
: NullLogger<FooFactory>.Instance;

public IFoo CreateFoo(string type)
{
_logger.LogInformation("Creating a new {Type}", type);

return type switch
{
"red" => new FooRed(loggerFactory),
"blue" => new FooBlue(loggerFactory),
_ => throw new ArgumentException("Unknown type", nameof(type)),
};
}
}

public interface IFoo
{
void DoSomething();
}

internal class FooRed(ILoggerFactory? loggerFactory) : IFoo
{
private readonly ILogger _logger = loggerFactory is not null
? loggerFactory.CreateLogger<FooRed>()
: NullLogger<FooRed>.Instance;

public void DoSomething()
{
_logger.LogInformation("I'm red!");
}
}

internal class FooBlue(ILoggerFactory? loggerFactory) : IFoo
{
private readonly ILogger _logger = loggerFactory is not null
? loggerFactory.CreateLogger<FooBlue>()
: NullLogger<FooBlue>.Instance;

public void DoSomething()
{
_logger.LogInformation("I'm blue!");
}
}
}
mtreit
mtreit4mo ago
It seems the recommendation for library code is to indeed take an ILoggerFactory, at least for general purpose public facing libraries.
mtreit
mtreit4mo ago
Logging guidance for .NET library authors - .NET
Learn how to expose logging as a library author in .NET. Follow the guidance to ensure your library is correctly exposed to consumers.
mtreit
mtreit4mo ago
If the logger is not passed along to other classes, the recommendation is to use ILogger<T> where T is the type name. Apparently my practice of just acceping ILogger (non-generic) is not considered a best practice.
Unknown User
Unknown User4mo ago
Message Not Public
Sign In & Join Server To View
Mayka
Mayka4mo ago
@TeBeCo, I like that a lot, it’s very clean. For internal classes which are only instantiated inside the library and not available to callers, would the best practice be for those to take the non-generic ILogger and have whichever class instantiates them pass in its own logger, since its type is meant to truly be internal and shouldn’t be leaking its context into logs anyhow? e.g.,
public class FooRed(ILogger<FooRed> logger) : IFoo
{
    private readonly FooColor _fooColor = new("red", logger);
 
    public void DoSomething()
    {
        logger.LogInformation("I'm {fooColor}!", _fooColor.Color);
    }
}
 
internal class FooColor
{
    public FooColor(string color, ILogger logger)
    {
        Color = color;
        logger.LogTrace("Creating a new {Color} ColorFoo", color);
    }
 
    public string Color { get; }
}
public class FooRed(ILogger<FooRed> logger) : IFoo
{
    private readonly FooColor _fooColor = new("red", logger);
 
    public void DoSomething()
    {
        logger.LogInformation("I'm {fooColor}!", _fooColor.Color);
    }
}
 
internal class FooColor
{
    public FooColor(string color, ILogger logger)
    {
        Color = color;
        logger.LogTrace("Creating a new {Color} ColorFoo", color);
    }
 
    public string Color { get; }
}
Unknown User
Unknown User4mo ago
Message Not Public
Sign In & Join Server To View