Updated on
Install Serilog.AspNetCore and register Serilog on the host. Every ILogger<T> the framework already injects then writes through Serilog: no controller changes, no new interface, no Log.Information calls scattered through the code.
That is the whole integration, and it works because these two are not alternatives. ILogger<T> is the abstraction .NET code logs against; Serilog is one implementation of what happens next. Choosing Serilog does not mean abandoning ILogger<T>: it means giving it somewhere better to write, per our guide to logging best practices with Serilog.
Serilog vs ILogger: What Is the Difference?
They are not competitors. ILogger<T> is an interface; Serilog is something that implements what sits behind it.
.NET ships a logging abstraction (ILogger<T>, injected into anything that asks) and a provider model that decides where those messages actually go. Out of the box, the providers are modest: console, debug, event source.
Serilog replaces the provider layer. Our classes keep injecting ILogger<T> and keep calling LogInformation, and Serilog takes over from that point, adding sinks for files, databases, Seq, and dozens more, plus enrichers that attach context to every event.
So “Serilog or ILogger” is the wrong question. The real choice is which provider stands behind the interface, and the answer for most ASP.NET Core applications is Serilog, kept behind ILogger<T> rather than in front of it, so no application code depends on the logging library.

Microsoft’s logging-providers guidance draws the same line: “Logging providers persist logs, except for the Console provider, which only displays logs as standard output.” Four providers ship in the runtime libraries (Console, Debug, EventSource and, on Windows, EventLog), and Serilog appears on that page’s own list of third-party providers, not as an alternative to ILogger<T>.
ILogger<T> (Microsoft.Extensions.Logging) | Serilog | |
|---|---|---|
| What it is | The logging abstraction in .NET | A logging implementation |
| Who writes against it | Our application code | Nothing, usually: it sits behind ILogger<T> |
| Ships with | The .NET SDK | A NuGet package |
| Decides where logs go | No, providers do | Yes, through sinks |
| Structured logging | Message templates with named arguments; whether they survive as data is up to the provider, and the built-in console provider formats them into text | The same message templates, with the named arguments kept as properties on the event and written as data by any sink that supports it |
| Configuration | The Logging section of appsettings.json, or ILoggingBuilder in code | LoggerConfiguration, appsettings.json, or both |
| Replaces the other | Not applicable | No. It plugs in underneath |
| Use both | This is the normal setup | This is the normal setup |
Using Microsoft’s ILogger<T>
Let’s start by creating a .NET 10 Web API project.
Navigate to the WeatherForecastController class within the project and open it. Let’s take a look at the predefined code:
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
Here, we observe that the ILogger<WeatherForecastController> is being injected into the constructor of the WeatherForecastController class. This dependency injection utilizes Microsoft’s ILogger API.
The interface and the two types that sit around it have their own article on this site: ILogger, ILoggerFactory, and ILoggerProvider.
Now let’s add some logging to the Get() method:
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
_logger.LogInformation("Processing GetWeatherForecast...");
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
Let’s run the application, call /WeatherForecast, and observe the output in the console:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5066
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: CodeMaze\CodeMazeGuides\dotnet-logging\...
info: UsingSerilogWithMicrosoftILogger.Controllers.WeatherForecastController[0]
Processing GetWeatherForecast...
This output confirms that the default logging is functioning as expected, and that our Processing GetWeatherForecast... message reaches the console through the built-in console provider.
How Do We Use Serilog With ILogger<T> in ASP.NET Core?
Install Serilog.AspNetCore, then register Serilog once during startup. Nothing else in the application has to change.
The registration replaces the default logging providers with Serilog and tells it where to write. A minimal configuration writes to the console; adding a file sink, a filter, or an enricher happens in the same call.
From that point, every ILogger<T> resolved from dependency injection is a Serilog logger. The WeatherForecastController above does not know it: its constructor, its field, and its LogInformation call are all unchanged, and the message simply appears in Serilog’s format instead of the default one.
That is the reason to integrate this way rather than calling Serilog’s static Log class directly. Application code keeps depending on the abstraction, so swapping the logging library later is a startup change rather than a search-and-replace across every file.
Let’s install the Serilog.AspNetCore NuGet package. The samples here use version 10.0.0, published under the Apache-2.0 licence.
To redirect the ASP.NET Core logs through Serilog, we need to configure it. Let’s open the Program.cs file and insert the highlighted lines:
using Serilog;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, loggerConfiguration) =>
{
loggerConfiguration.WriteTo.Console();
});
builder.Services.AddControllers();
// .. removed for brevity
Serilog.AspNetCore also documents a service-collection form, builder.Services.AddSerilog((services, loggerConfiguration) => ...). Both are supported on version 10.0.0, and the host-based call compiles without an obsolescence warning, so which one we reach for is a style decision rather than a correctness one.
A minimal configuration writes to the console, and everything else about the logger is configured in the same lambda: Serilog’s log levels and minimum-level control, or a swap to rolling file logging with Serilog.
Now, we can run the application, call /WeatherForecast again, and verify that the console output has changed:
[00:21:30 INF] Now listening on: http://localhost:5066
[00:21:30 INF] Application started. Press Ctrl+C to shut down.
[00:21:30 INF] Hosting environment: Development
[00:21:30 INF] Content root path: CodeMaze\CodeMazeGuides\dotnet-logging\...
[00:21:35 INF] Request starting HTTP/1.1 GET http://localhost:5066/WeatherForecast - null null
[00:21:35 INF] Executing endpoint 'UsingSerilogWithMicrosoftILogger.Controllers.WeatherForecastController.Get (UsingSerilogWithMicrosoftILogger)'
[00:21:35 INF] Route matched with {action = "Get", controller = "WeatherForecast"}. Executing controller action with signature System.Collections.Generic.IEnumerable`1[UsingSerilogWithMicrosoftILogger.WeatherForecast] Get() on controller UsingSerilogWithMicrosoftILogger.Controllers.WeatherForecastController (UsingSerilogWithMicrosoftILogger).
[00:21:35 INF] Processing GetWeatherForecast...
[00:21:35 INF] Executing ObjectResult, writing value of type 'UsingSerilogWithMicrosoftILogger.WeatherForecast[]'.
[00:21:35 INF] Executed action UsingSerilogWithMicrosoftILogger.Controllers.WeatherForecastController.Get (UsingSerilogWithMicrosoftILogger) in 209.5771ms
[00:21:35 INF] Executed endpoint 'UsingSerilogWithMicrosoftILogger.Controllers.WeatherForecastController.Get (UsingSerilogWithMicrosoftILogger)'
[00:21:35 INF] Request finished HTTP/1.1 GET http://localhost:5066/WeatherForecast - 200 null application/json; charset=utf-8 436.6306ms
Serilog is now the provider behind ILogger<T>, and our highlighted message arrives through it unchanged.
How Do We Log Requests With Serilog?
Add UseSerilogRequestLogging() to the middleware pipeline, and one summary event stands in for the framework’s per-request chatter.
Serilog.AspNetCore’s readme names the problem: “The default request logging implemented by ASP.NET Core is noisy, with multiple events emitted per request.” The output above shows it. A single call to /WeatherForecast produced seven framework messages, with our own log line buried in the middle of them.
The middleware writes one event per request instead, carrying the method, path, status code, and elapsed time as structured properties rather than as text inside a sentence. That is what makes them queryable later.
On its own it adds that summary rather than removing the rest, so the registration also raises the minimum level for Microsoft.AspNetCore to warning. The framework chatter goes, and the summary and our own messages stay.
It also gives us a hook for context: the middleware accepts a callback for enriching the request event, which is where a user id, a tenant, or a correlation id belongs.
Both changes live in Program.cs:
using Serilog;
using Serilog.Events;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, loggerConfiguration) =>
{
loggerConfiguration
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.WriteTo.Console();
});
// .. removed for brevity
app.UseSerilogRequestLogging();
The summary line carries its values as named properties rather than as prose, which is the same reason we care about writing message templates properly in our own code. Let’s run the application and call /WeatherForecast once more:
[00:24:23 INF] Now listening on: http://localhost:5066 [00:24:23 INF] Application started. Press Ctrl+C to shut down. [00:24:23 INF] Hosting environment: Development [00:24:23 INF] Content root path: CodeMaze\CodeMazeGuides\dotnet-logging\... [00:24:27 INF] Processing GetWeatherForecast... [00:24:27 INF] HTTP GET /WeatherForecast responded 200 in 203.2499 ms
Eight lines become two: our own message, and one summary event per request that names the method, the path, the status code, and the elapsed time.
Conclusion
Using Serilog with Microsoft’s ILogger takes one package and one registration call. Dependency injection does the rest: our controllers keep asking for ILogger<T>, and Serilog quietly becomes what answers.
That is the shape worth remembering. Serilog is not a replacement for ILogger<T> but the provider standing behind it, which is why adopting it costs no changes in application code, and why swapping it out later would cost none either.
From here, the sinks are the interesting part: a file, a database, Seq, and many more, each configured in the same call that registered the logger.
Tested with .NET 10.0.10.
