Logging guidance for .NET library authors
As a library author, exposing logging is a great way to provide consumers with insight into the inner workings of your library. This guidance helps you expose logging in a way that is consistent with other .NET libraries and frameworks. It also helps you avoid common performance bottlenecks that may not be otherwise obvious.
When to use the ILoggerFactory
interface
When writing a library that emits logs, you need an ILogger object to record the logs. To get that object, your API can either accept an ILogger<TCategoryName> parameter, or it can accept an ILoggerFactory after which you call ILoggerFactory.CreateLogger. Which approach should be preferred?
When you need a logging object that can be passed along to multiple classes so that all of them can emit logs, use
ILoggerFactory
. It's recommended that each class creates logs with a separate category, named the same as the class. To do this, you need the factory to create uniqueILogger<TCategoryName>
objects for each class that emits logs. Common examples include public entry point APIs for a library or public constructors of types that might create helper classes internally.When you need a logging object that's only used inside one class and never shared, use
ILogger<TCategoryName>
, whereTCategoryName
is the type that produces the logs. A common example of this is a constructor for a class created by dependency injection.
If you're designing a public API that must remain stable over time, keep in mind that you might desire to refactor your internal implementation in the future. Even if a class doesn't create any internal helper types initially, that might change as the code evolves. Using ILoggerFactory
accommodates creating new ILogger<TCategoryName>
objects for any new classes without changing the public API.
For more information, see How filtering rules are applied.
Prefer source-generated logging
The ILogger
API supports two approaches to using the API. You can either call methods such as LoggerExtensions.LogError and LoggerExtensions.LogInformation, or you can use the logging source generator to define strongly typed logging methods. For most situations, the source generator is recommended because it offers superior performance and stronger typing. It also isolates logging-specific concerns such as message templates, IDs, and log levels from the calling code. The non-source-generated approach is primarily useful for scenarios where you are willing to give up those advantages to make the code more concise.
using Microsoft.Extensions.Logging;
namespace Logging.LibraryAuthors;
internal static partial class LogMessages
{
[LoggerMessage(
Message = "Sold {Quantity} of {Description}",
Level = LogLevel.Information)]
internal static partial void LogProductSaleDetails(
this ILogger logger,
int quantity,
string description);
}
The preceding code:
- Defines a
partial class
namedLogMessages
, which isstatic
so that it can be used to define extension methods on theILogger
type. - Decorates a
LogProductSaleDetails
extension method with theLoggerMessage
attribute andMessage
template. - Declares
LogProductSaleDetails
, which extends theILogger
and accepts aquantity
anddescription
.
Tip
You can step into the source-generated code during debugging, because it's part of the same assembly as the code that calls it.
Use IsEnabled
to avoid expensive parameter evaluation
There may be situations where evaluating parameters is expensive. Expanding upon the previous example, imagine the description
parameter is a string
that is expensive to compute. Perhaps the product being sold gets a friendly product description and relies on a database query, or reading from a file. In these situations, you can instruct the source generator to skip the IsEnabled
guard and manually add the IsEnabled
guard at the call site. This allows the user to determine where the guard is called and ensures that parameters that might be expensive to compute are only evaluated when truly needed. Consider the following code:
using Microsoft.Extensions.Logging;
namespace Logging.LibraryAuthors;
internal static partial class LogMessages
{
[LoggerMessage(
Message = "Sold {Quantity} of {Description}",
Level = LogLevel.Information,
SkipEnabledCheck = true)]
internal static partial void LogProductSaleDetails(
this ILogger logger,
int quantity,
string description);
}
When the LogProductSaleDetails
extension method is called, the IsEnabled
guard is manually invoked and the expensive parameter evaluation is limited to when it's needed. Consider the following code:
if (_logger.IsEnabled(LogLevel.Information))
{
// Expensive parameter evaluation
var description = product.GetFriendlyProductDescription();
_logger.LogProductSaleDetails(
quantity,
description);
}
For more information, see Compile-time logging source generation and High-performance logging in .NET.
Avoid string interpolation in logging
A common mistake is to use string interpolation to build log messages. String interpolation in logging is problematic for performance, as the string is evaluated even if the corresponding LogLevel
isn't enabled. Instead of string interpolation, use the log message template, formatting, and argument list. For more information, see Logging in .NET: Log message template.
Use no-op logging defaults
There may be times, when consuming a library that exposes logging APIs that expect either an ILogger
or ILoggerFactory
, that you don't want to provide a logger. In these cases, the Microsoft.Extensions.Logging.Abstractions NuGet package provides no-op logging defaults.
Library consumers can default to null logging if no ILoggerFactory
is provided. The use of null logging differs from defining types as nullable (ILoggerFactory?
), as the types are non-null. These convenience-based types don't log anything and are essentially no-ops. Consider using any of the available abstraction types where applicable: