logging
.NET
ILogger
software-development
programming-best-practices

Should I take ILogger, ILoggerT, ILoggerFactory or ILoggerProvider for a library?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

When building a .NET library that needs logging, accept ILogger<T> through constructor injection. This is the recommended approach because it follows the standard dependency injection pattern, produces correctly categorized log output, and works seamlessly with the host application's logging configuration. ILoggerFactory is the second choice when your library needs to create loggers for multiple internal classes. Avoid depending on ILoggerProvider directly — it is an implementation detail of the logging infrastructure.

The Four Interfaces

csharp
1// ILogger — base interface, no category
2public interface ILogger
3{
4    void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);
5    bool IsEnabled(LogLevel logLevel);
6    IDisposable BeginScope<TState>(TState state);
7}
8
9// ILogger<T> — typed wrapper, adds category name from T
10public interface ILogger<out TCategoryName> : ILogger { }
11
12// ILoggerFactory — creates ILogger instances
13public interface ILoggerFactory
14{
15    ILogger CreateLogger(string categoryName);
16}
17
18// ILoggerProvider — creates logger instances for a specific output (console, file, etc.)
19public interface ILoggerProvider
20{
21    ILogger CreateLogger(string categoryName);
22}
csharp
1public class OrderService
2{
3    private readonly ILogger<OrderService> _logger;
4
5    public OrderService(ILogger<OrderService> logger)
6    {
7        _logger = logger;
8    }
9
10    public void ProcessOrder(Order order)
11    {
12        _logger.LogInformation("Processing order {OrderId}", order.Id);
13
14        try
15        {
16            // Business logic
17            _logger.LogDebug("Order {OrderId} validated", order.Id);
18        }
19        catch (Exception ex)
20        {
21            _logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
22            throw;
23        }
24    }
25}

The host application registers logging in DI, and ILogger<OrderService> is automatically resolved with the category name "YourLibrary.OrderService":

csharp
1// Host application setup
2var builder = Host.CreateDefaultBuilder(args);
3builder.ConfigureLogging(logging =>
4{
5    logging.AddConsole();
6    logging.AddFilter("YourLibrary", LogLevel.Debug);  // Filter by category
7});

Log output includes the category name, making it easy to filter:

 
info: YourLibrary.OrderService[0]
      Processing order ORD-123

When to Accept ILoggerFactory

Use ILoggerFactory when your library creates multiple internal classes that each need their own logger:

csharp
1public class DatabaseConnection
2{
3    private readonly ILogger _logger;
4    private readonly ILogger _queryLogger;
5
6    public DatabaseConnection(ILoggerFactory loggerFactory)
7    {
8        _logger = loggerFactory.CreateLogger<DatabaseConnection>();
9        _queryLogger = loggerFactory.CreateLogger("YourLibrary.SQL");
10    }
11
12    public void ExecuteQuery(string sql)
13    {
14        _queryLogger.LogDebug("Executing: {Sql}", sql);
15    }
16}

This is common in libraries like Entity Framework Core that create loggers for different subsystems (SQL, change tracking, migrations).

When to Accept Plain ILogger

Use non-generic ILogger when you want maximum flexibility and do not care about the category name:

csharp
1public class SimpleHelper
2{
3    private readonly ILogger _logger;
4
5    // Accepts any ILogger — caller controls the category
6    public SimpleHelper(ILogger logger)
7    {
8        _logger = logger;
9    }
10}

This works but the category name depends on what the caller passes. The host application may have trouble filtering logs because the category is not predictable.

Never Accept ILoggerProvider

csharp
1// DON'T do this — ILoggerProvider is an implementation detail
2public class BadService
3{
4    public BadService(ILoggerProvider provider)  // Wrong!
5    {
6        var logger = provider.CreateLogger("BadService");
7    }
8}

ILoggerProvider represents a specific logging output (console, file, Application Insights). A single ILogger aggregates all providers. If you inject ILoggerProvider, you bypass the aggregation and only log to one output, missing the others.

NullLogger for Optional Logging

If logging is optional in your library, accept a nullable ILogger or use NullLogger:

csharp
1public class CacheService
2{
3    private readonly ILogger<CacheService> _logger;
4
5    public CacheService(ILogger<CacheService> logger = null)
6    {
7        _logger = logger ?? NullLogger<CacheService>.Instance;
8    }
9
10    public void Set(string key, object value)
11    {
12        _logger.LogDebug("Cache set: {Key}", key);
13        // Even if no logger is configured, NullLogger safely no-ops
14    }
15}

NullLogger<T>.Instance implements ILogger<T> but discards all log messages. This avoids null checks throughout your code.

Decision Table

ScenarioAcceptWhy
Single class needs loggingILogger<T>Correct category, standard DI pattern
Library creates many internal loggersILoggerFactoryCreate loggers with custom categories
Utility method, category unimportantILoggerMaximum flexibility
Logging is optionalILogger<T> with NullLogger defaultNo null checks needed
NeverILoggerProviderImplementation detail, bypasses aggregation

Common Pitfalls

  • Injecting ILoggerProvider instead of ILoggerFactory: ILoggerProvider is a single log output (e.g., console only). ILoggerFactory aggregates all configured providers. Using ILoggerProvider means your library's logs only go to one destination, ignoring others.
  • Creating a new LoggerFactory inside the library: Calling LoggerFactory.Create() inside your library creates a separate logging pipeline disconnected from the host's configuration. Always accept the logger through DI so the host application controls filtering, formatting, and outputs.
  • Using ILogger without a category: Non-generic ILogger has no category name, making it impossible for the host to filter your library's logs separately. Use ILogger<T> so logs are categorized by class name.
  • Not using structured logging: Passing string interpolation ($"Order {id} processed") loses structured data. Use message templates ("Order {OrderId} processed", id) so log providers can index and query by OrderId.
  • Making logging a required dependency: If your library throws when no logger is provided, it forces all consumers to set up logging even if they do not want it. Use NullLogger<T>.Instance as a default so logging is opt-in.

Summary

  • Accept ILogger<T> for standard single-class logging with automatic category naming
  • Accept ILoggerFactory when creating loggers for multiple internal subsystems
  • Avoid ILoggerProvider — it is an implementation detail that bypasses log aggregation
  • Use NullLogger<T>.Instance as a default to make logging optional
  • Always use structured logging with message templates, not string interpolation
  • Let the host application control log filtering and output through DI configuration

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design