
Structured Logging in ASP.NET Web API with Built-in ILogger
Author - Abdul Rahman (Bhai)
Web API
29 Articles
Table of Contents
What we gonna do?
Every ASP.NET project seems to start with the same ritual: add Serilog, wire up five enrichers nobody queries, register a sink for a log platform you will configure "later." It is 2026 and .NET 10 is here — so let's ask the uncomfortable question: do you actually need Serilog?
ILogger<T> is Microsoft's first-class structured logging abstraction, built into ASP.NET Core since day one. In .NET 10, the built-in logging stack has closed virtually every gap that once made Serilog the obvious choice — structured JSON output, log scopes, request logging, log redaction, Application Insights integration, and Aspire + OpenTelemetry for the ultimate local development experience.
Note: If you have not done so already, I recommend you read the article on Structured Logging with Serilog in ASP.NET WEB API to understand what Serilog offers before deciding whether to replace it.
In this article, let's learn how to set up structured logging in an ASP.NET Web API using only the built-in ILogger — no Serilog packages — every dependency used stays within the Microsoft ecosystem.
Why we gonna do?
Serilog has become a cargo-cult addition to .NET projects. Developers reach for it out of habit, and before long the codebase carries Serilog.AspNetCore, Serilog.Enrichers.Environment, Serilog.Enrichers.Thread, Serilog.Enrichers.Process, and half a dozen more packages — most of which write data to every log event that nobody ever queries in your APM.
The hidden cost is real. WithThreadId and WithProcessId call OS-level APIs on every log event — on hot paths that emit thousands of log lines per second, this adds measurable CPU overhead. WithMachineName and WithEnvironmentName are less expensive but still write properties to every single event even when your query tool filters on neither. The enrichers you never query are pure overhead.
Beyond performance, every extra NuGet package is a potential CVE, a version conflict waiting to happen, and one more entry in your upgrade checklist when .NET 12 ships. Most teams accept this without ever asking whether the functionality was already available in the framework itself.
The built-in ILogger already supports everything you need — structured JSON output, log scopes, request context logging, log redaction, Application Insights, and OpenTelemetry — with no Serilog packages. The question is not whether Serilog works; it is whether you still need it.
How we gonna do?
Clear Built-in Providers First
Start by clearing the default providers — the same first step you take with Serilog. This ensures ILogger routes only to the sinks you explicitly register, preventing duplicate output from the default Console provider:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
Structured JSON Console for Local Development
AddJsonConsole gives you structured JSON output in the terminal — the same benefit Serilog's Console sink provides, without the extra package. Enable IncludeScopes so log scope properties (covered below) appear in each event:
// IsLocal() and IsOffline() are custom IHostEnvironment extension methods — replace with your own environment names
if (builder.Environment.IsLocal() || builder.Environment.IsOffline())
{
builder.Logging.AddJsonConsole(options =>
{
options.IncludeScopes = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff zzz ";
options.JsonWriterOptions = new System.Text.Json.JsonWriterOptions
{
Indented = true
};
});
}
The output is indented, timestamped, and fully structured — easy to read locally and parseable by any log aggregator. No Serilog sink required.
Application Insights for Cloud Environments
For staging and production, wire AddApplicationInsights directly from Microsoft.Extensions.Logging.ApplicationInsights — a first-party Microsoft NuGet package, so your entire dependency tree stays within the Microsoft ecosystem:
if (builder.Environment.IsDevelopment() || builder.Environment.IsProduction())
{
builder.Logging.AddApplicationInsights(
configureTelemetryConfiguration: config =>
config.ConnectionString = builder.Configuration.GetConnectionString("ApplicationInsights"),
configureApplicationInsightsLoggerOptions: options =>
options.IncludeScopes = true);
}
Log Scope with ILogger — Same as Serilog
ILogger.BeginScope works identically to Serilog's LogContext.PushProperty — it enriches all log events emitted within the scope with the provided properties. Create a middleware to push per-request context (user, tenant, correlation ID) once and have it appear in every log event for that request:
public sealed class LogScopeMiddleware(
ILogger<LogScopeMiddleware> logger,
ITenantService tenantService) : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.User.Identity is { IsAuthenticated: true })
{
var user = context.User;
var claimsToInclude = new List<string> { "name", "username", "role" };
var scopeProperties = new Dictionary<string, object>();
foreach (var claimType in claimsToInclude)
{
scopeProperties[claimType] = string.Join(", ",
user.Claims
.Where(c => c.Type == claimType)
.Select(c => c.Value));
}
scopeProperties["UserId"] = tenantService.GetCurrentUser();
using (logger.BeginScope(scopeProperties))
{
await next(context);
}
}
else
{
await next(context);
}
}
}
// Extension member syntax requires C# 14 (.NET 10+).
public static class LogScopeMiddlewareExtensions
{
extension(IApplicationBuilder builder)
{
public IApplicationBuilder UseLogScope()
{
return builder.UseMiddleware<LogScopeMiddleware>();
}
}
}
// Program.cs — register and use the middleware
builder.Services.AddTransient<LogScopeMiddleware>();
app.UseLogScope();
IIS-Hosted / Offline Apps — stdout Logging via web.config
For on-premises apps hosted in IIS, enable stdout log capture in web.config — no file sink package required:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*"
modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\YourApi.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess">
<environmentVariables />
</aspNetCore>
</system.webServer>
</configuration>
Set stdoutLogEnabled="true" and point stdoutLogFile to your preferred logs folder. ASP.NET Core Module captures all stdout output — including the structured JSON from AddJsonConsole — into rolling log files.
Aspire + OpenTelemetry — The Best Local Developer Experience
The best local development combo is .NET Aspire with OpenTelemetry. Call AddServiceDefaults() in your local environment and Aspire's dashboard gives you structured logs, distributed traces, and metrics — all in one UI, with zero extra configuration:
if (builder.Environment.IsLocal())
{
// Registers OpenTelemetry tracing + metrics + the Aspire dashboard exporter.
// The built-in ILogger automatically propagates to the OTEL log exporter.
builder.AddServiceDefaults();
}
With Aspire running, open the dashboard at http://localhost:18888 to see all structured log events, trace context, and per-service metrics — no Seq, no Serilog, no extra sink packages.
Complete Program.cs — No Serilog
Here is the full wiring in one place, combining everything above:
var isTest = args.Any(a => a.Equals("--environment=Test", StringComparison.OrdinalIgnoreCase));
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
if (builder.Environment.IsLocal() || builder.Environment.IsOffline() || isTest)
{
builder.Logging.AddJsonConsole(options =>
{
options.IncludeScopes = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff zzz ";
options.JsonWriterOptions = new System.Text.Json.JsonWriterOptions
{
Indented = true
};
});
}
if (builder.Environment.IsDevelopment() || builder.Environment.IsProduction())
{
builder.Logging.AddApplicationInsights(
configureTelemetryConfiguration: config =>
config.ConnectionString = builder.Configuration.GetConnectionString("ApplicationInsights"),
configureApplicationInsightsLoggerOptions: options =>
options.IncludeScopes = true);
}
if (builder.Environment.IsLocal())
{
builder.AddServiceDefaults(); // Aspire + OpenTelemetry
}
// ... rest of service registration
// includes: builder.Services.AddTransient<LogScopeMiddleware>();
var app = builder.Build();
app.UseLogScope();
app.Logger.LogWarning("Launching {AppName} on {MachineName}...",
builder.Environment.ApplicationName,
Environment.MachineName);
await app.RunAsync();
Log Redaction
Masking PII — email addresses, phone numbers, national IDs — before they reach any log sink is also handled within the Microsoft ecosystem via Microsoft.Extensions.Compliance.Redaction. It works with the source-generated [LoggerMessage] pattern and requires no Serilog dependency. See Improve Data Security by Redacting Logs in .NET for a complete walkthrough with custom redactors.
When Serilog Still Makes Sense
The built-in logger does not cover every scenario. Consider keeping Serilog when you need:
- Seq — the Seq sink has no first-party equivalent in the built-in stack
- A niche or on-premises APM — if your vendor only offers a Serilog sink and has no official OTEL exporter
- Advanced destructuring policies — custom object serialization that JsonConsole does not support
- File sinks with rolling — if you cannot use stdout capture and need rolling log files without web.config
If none of these apply to your project, the built-in stack is enough. If you do decide Serilog is the right fit, start with Getting Started with Serilog in .NET — From Installation to Production-Ready Logging.
Summary
In this article, we saw how to replace Serilog with the built-in ILogger in ASP.NET Web API — no Serilog packages, staying entirely within the Microsoft ecosystem. Key takeaways:
- Call ClearProviders() to silence the default console provider before adding your own sinks
- Use AddJsonConsole with IncludeScopes = true for structured JSON output in local and offline environments
- Use AddApplicationInsights from Microsoft.Extensions.Logging.ApplicationInsights for cloud environments — no Serilog sink needed
- Implement ILogger.BeginScope in middleware to push per-request properties (user, tenant, correlation ID) to every log event — the same as Serilog's LogContext
- Enable stdoutLogEnabled in web.config for IIS-hosted on-prem apps instead of a file sink
- Use AddServiceDefaults() with .NET Aspire for the best local developer experience — structured logs, traces, and metrics in one dashboard
- Keep Serilog only if you need Seq, a vendor-specific sink without an OTEL exporter, or advanced destructuring policies
What to read next: learn how the core logging abstractions work under the hood in Introduction to Logging in .NET — From Basics to Best Practices, go deeper on message templates and structured data in Mastering Modern .NET Logging — Structured Logging and Advanced Concepts, explore all built-in providers and how to build your own in Understanding .NET Logging Providers — From Console to Custom Implementations, protect sensitive data with Improve Data Security by Redacting Logs in .NET, or squeeze every last allocation out of hot logging paths in High Performance Logging in .NET.