The ILogger Abstraction
Logging in .NET Core is built around a provider-agnostic abstraction: application code takes an ILogger<T> via constructor injection and calls methods like LogInformation, LogWarning, or LogError, without knowing or caring whether those messages end up in the console, a file, Application Insights, or an ELK stack. The generic type parameter T stamps each log entry with a category name (typically the fully qualified class name), which lets you filter and configure verbosity per-namespace in appsettings.json without touching a single line of application code.
Cricket analogy: This is like a team's official scorer recording every ball into a standard scorebook format without needing to know whether TV broadcasters or the press will read it later, the same way ILogger<T> stays unchanged regardless of which provider reads the logs.
Log Levels and Structured Logging
ILogger defines six ordered levels — Trace, Debug, Information, Warning, Error, and Critical — and appsettings.json's Logging:LogLevel section controls the minimum level emitted per category, so you might run "Default": "Information" broadly but "Microsoft.EntityFrameworkCore": "Warning" to silence noisy SQL logging. Crucially, .NET's logging is structured, not string-concatenated: calling logger.LogInformation("Order {OrderId} shipped to {City}", orderId, city) preserves OrderId and City as separate, queryable fields in providers that support structured sinks like Seq or Application Insights, rather than baking them into an opaque message string the way string interpolation would.
Cricket analogy: This is like classifying commentary into tiers — routine ball updates (Information), a rain-delay alert (Warning), an injury stoppage (Error) — each escalated differently, while recording the exact over number and batsman name as separate structured fields mirrors structured logging with named placeholders.
public class OrderService(ILogger<OrderService> logger)
{
public void Ship(int orderId, string city)
{
logger.LogInformation("Order {OrderId} shipped to {City}", orderId, city);
if (city == "Unknown")
{
logger.LogWarning("Order {OrderId} shipped to an unresolved city", orderId);
}
}
}
Scopes and Third-Party Providers
BeginScope lets you attach contextual data, such as a request's CorrelationId, to every log entry written within a using block, so a single downstream ILogger call automatically carries context set much higher up the call stack without threading a parameter through every method signature. While the built-in Console and Debug providers are fine for local development, production systems typically swap in Serilog or NLog as the logging provider (via AddSerilog or AddNLog), gaining structured sinks, enrichers that automatically attach machine name or environment, and log-shipping to aggregation platforms like Seq, Elasticsearch, or Datadog, all while application code keeps calling the same unchanged ILogger<T> API.
Cricket analogy: This is like a broadcast director tagging an entire session's coverage with a persistent 'Day 3, Session 2' label that automatically stamps every subsequent camera cut without the commentator repeating it each time, mirroring how BeginScope attaches context to every log entry within its block automatically.
Serilog's message template syntax, logger.LogInformation("Order {OrderId} shipped", orderId), is the same syntax the built-in Microsoft.Extensions.Logging abstraction uses, which is why Serilog can be dropped in as a provider with almost no changes to existing application logging calls.
Never log entire request or response bodies, connection strings, or PII like raw email addresses at Information level in production — structured sinks make logs highly searchable, which also makes over-logged sensitive data a serious compliance and security liability. Redact or omit sensitive fields before logging.
- ILogger<T> is a provider-agnostic abstraction; application code doesn't know or care where log entries ultimately go.
- The generic type parameter T supplies the log category, usually the fully qualified class name, enabling per-namespace filtering.
- Log levels (Trace, Debug, Information, Warning, Error, Critical) are configured per category in appsettings.json's Logging:LogLevel section.
- Structured logging with message templates like "Order {OrderId} shipped" keeps values as queryable fields, not flattened strings.
- BeginScope attaches ambient context to every log entry written within its block, without threading parameters manually.
- Serilog and NLog are common production providers, adding structured sinks, enrichers, and log-shipping while keeping the same ILogger<T> API.
- Never log sensitive data like PII or secrets at broadly visible log levels in production.
Practice what you learned
1. What does the generic type parameter in ILogger<OrderService> provide?
2. Why is logger.LogInformation("Order {OrderId} shipped", orderId) preferred over string interpolation like $"Order {orderId} shipped"?
3. What does BeginScope let you do?
4. How do you typically swap the built-in console logging for Serilog in an ASP.NET Core app?
5. Why is over-logging sensitive data a particular risk with structured logging sinks?
Was this page helpful?
You May Also Like
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics