This guide helps you migrate from the legacy static Logger class to Microsoft's modern ILogger<T> interface, which is the standard logging abstraction in .NET.
| Aspect | Legacy Logger | Modern ILogger |
|---|---|---|
| Access Pattern | Static class (Logger.LogError(...)) |
Dependency injection (_logger.LogError(...)) |
| Verbosity Control | Custom integer levels (0-3) | Standard LogLevel enum |
| Testability | Difficult (static dependencies) | Easy (inject mock logger) |
| Configuration | Manual property setting | Configured via appsettings.json |
| Providers | Console only | Console, File, Event Log, Application Insights, etc. |
| Type Safety | None | Generic type parameter identifies log source |
| Structured Logging | No | Yes (with parameters) |
| Scoping | No | Yes (with BeginScope()) |
| Performance | Basic | Optimized with source generators |
// Old: 0=silent, 1=errors, 2=warnings, 3=debug
Logger.VerboseLevel = 2; // Show errors and warnings only// New: Configure in appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Bring": "Information"
}
}
}Mapping Table:
| Legacy VerboseLevel | Modern LogLevel | What Gets Logged |
|---|---|---|
| 0 (silent) | None |
Nothing |
| 1 (errors only) | Error |
Errors only |
| 2 (warnings+) | Warning |
Errors and warnings |
| 3 (debug+) | Debug or Trace |
Everything including debug info |
Before (Legacy):
using Bring.SPODataQuality;
try
{
// ... some operation
}
catch (Exception ex)
{
Logger.LogError("Failed to connect to SharePoint", ex);
}After (Modern):
using Microsoft.Extensions.Logging;
public class MyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
public void DoWork()
{
try
{
// ... some operation
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to connect to SharePoint");
}
}
}Key Improvements:
- Exception is the first parameter (better IntelliSense)
- Automatic stack trace inclusion
- Type-safe:
ILogger<MyService>identifies log source - Dependency injection enables testing
Before (Legacy):
Logger.LogWarning("No items found in list 'Documents'");After (Modern):
_logger.LogWarning("No items found in list '{ListName}'", "Documents");Key Improvements:
- Structured logging with named parameters
- Parameters extracted for log aggregation
- Better searchability in log management systems
Before (Legacy):
Logger.LogDebug($"Processing item {itemId} with status {status}");After (Modern):
_logger.LogDebug("Processing item {ItemId} with status {Status}", itemId, status);Key Improvements:
- No string interpolation needed
- Parameters logged separately (structured logging)
- Better performance (no allocation if debug logging disabled)
- Searchable by ItemId or Status in log analytics
The legacy Logger didn't have an info level. Use this for important operational messages:
_logger.LogInformation("SharePoint sync started at {StartTime}", DateTime.Now);
_logger.LogInformation("Processed {ItemCount} items in {Duration}ms", count, elapsed);Before:
public class SharePointService
{
public void DownloadFiles()
{
Logger.LogDebug("Starting file download");
try
{
// ... download logic
Logger.LogDebug("Downloaded 5 files successfully");
}
catch (Exception ex)
{
Logger.LogError("Download failed", ex);
}
}
}After:
public class SharePointService
{
private readonly ILogger<SharePointService> _logger;
public SharePointService(ILogger<SharePointService> logger)
{
_logger = logger;
}
public void DownloadFiles()
{
_logger.LogDebug("Starting file download");
try
{
// ... download logic
_logger.LogInformation("Downloaded {FileCount} files successfully", 5);
}
catch (Exception ex)
{
_logger.LogError(ex, "Download failed");
}
}
}Before:
if (Logger.VerboseLevel >= 3)
{
Logger.LogDebug($"Details: {expensiveOperation()}");
}After:
// Option 1: Let ILogger handle it (recommended)
_logger.LogDebug("Details: {Details}", expensiveOperation());
// Option 2: Explicit check for expensive operations
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Details: {Details}", expensiveOperation());
}Note: The second approach only necessary if expensiveOperation() is truly expensive, as ILogger optimizes away disabled log levels.
Before:
Logger.Log(2, "Custom warning message");After:
_logger.Log(LogLevel.Warning, "Custom warning message");
// Or use the specific helper:
_logger.LogWarning("Custom warning message");If you have a large codebase and can't migrate everything at once, use the LoggerAdapter:
using Bring.Adapters;
using Microsoft.Extensions.Logging;
public class LegacyService
{
public LegacyService(ILogger<LegacyService> modernLogger)
{
// Wrap modern logger to support legacy Logger API
var adapter = new LoggerAdapter(modernLogger);
// Configure legacy verbosity level
adapter.VerboseLevel = 3;
// Now legacy code can work unchanged
adapter.LogError("Legacy error message");
adapter.LogWarning("Legacy warning message");
adapter.LogDebug("Legacy debug message");
}
}When to use the adapter:
- Large codebase with many Logger references
- Gradual migration strategy
- Testing migration incrementally
- Supporting legacy code that can't be changed immediately
When NOT to use the adapter:
- New code (use ILogger directly)
- Small codebases (just migrate directly)
- When you need structured logging features
// Somewhere in Program.cs or startup
Logger.VerboseLevel = args.Contains("--verbose") ? 3 : 1;appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Bring": "Debug"
},
"Console": {
"FormatterName": "simple",
"FormatterOptions": {
"TimestampFormat": "yyyy-MM-dd HH:mm:ss.fff ",
"SingleLine": true,
"IncludeScopes": false
}
}
}
}appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Bring": "Trace"
}
}
}Command-line override:
dotnet run --Logging:LogLevel:Default=Debug-
Use structured logging:
_logger.LogInformation("User {UserId} logged in from {IpAddress}", userId, ip);
-
Pass exceptions as first parameter:
_logger.LogError(ex, "Failed to process {FileName}", fileName);
-
Use appropriate log levels:
Trace: Very detailed (e.g., entering/exiting methods)Debug: Developer information during debuggingInformation: Important business eventsWarning: Recoverable errors or unexpected situationsError: Failures that stop current operationCritical: Catastrophic failures requiring immediate attention
-
Check log level for expensive operations:
if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug("Complex data: {Data}", SerializeComplexObject()); }
-
Don't use string interpolation:
// BAD - loses structured logging _logger.LogInformation($"Processing {itemId}"); // GOOD - structured logging _logger.LogInformation("Processing {ItemId}", itemId);
-
Don't catch exceptions just to log them:
// BAD try { DoWork(); } catch (Exception ex) { _logger.LogError(ex, "Error"); throw; } // GOOD - let exceptions bubble, log at appropriate handler level
-
Don't log sensitive data:
// BAD _logger.LogDebug("Password: {Password}", password); // GOOD _logger.LogDebug("Authentication attempted for user {Username}", username);
public void TestMethod()
{
// No way to verify logging without capturing Console.Out
var service = new SharePointService();
service.DoWork();
// Can't assert what was logged
}using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
public class SharePointServiceTests
{
[Fact]
public void DoWork_LogsInformation()
{
// Arrange
var logger = new FakeLogger<SharePointService>();
var service = new SharePointService(logger);
// Act
service.DoWork();
// Assert
Assert.Contains(logger.LoggedMessages,
m => m.Level == LogLevel.Information && m.Message.Contains("Downloaded"));
}
[Fact]
public void DoWork_WithNullLogger_StillWorks()
{
// NullLogger for tests where logging doesn't matter
var service = new SharePointService(NullLogger<SharePointService>.Instance);
service.DoWork(); // No exceptions
}
}- Update all service constructors to accept
ILogger<T> - Replace
Logger.LogError()with_logger.LogError() - Replace
Logger.LogWarning()with_logger.LogWarning() - Replace
Logger.LogDebug()with_logger.LogDebug() - Replace
Logger.Log()with_logger.Log() - Convert string interpolation to structured logging parameters
- Update exception logging to pass exception as first parameter
- Configure logging in
appsettings.jsoninstead ofLogger.VerboseLevel - Update unit tests to inject logger mocks
- Remove
using Bring.SPODataQuality;(legacy namespace) - Add
using Microsoft.Extensions.Logging; - Consider using
LoggerAdapterfor gradual migration
- Microsoft Logging Documentation
- High-performance logging
- Logging Best Practices
- Project:
Application.cs- Example of modern ILogger usage
If you encounter issues during migration, check:
- Is the service registered in DI? (see
Program.cs) - Is logging configured in
appsettings.json? - Are you using
ILogger<T>where T is your class name? - Have you removed static
Loggerreferences?
For gradual migration, use the LoggerAdapter class in ConsoleApp1Net8/Adapters/LoggerAdapter.cs.