Logging in C# Applications: Best Practices
Logging is not just about writing messages to a file. In modern .NET applications, it is a critically important tool for monitoring, debugging, and auditing. Properly configured logging allows you to quickly find the causes of failures, analyze performance, and ensure compliance with security requirements. In this article, we will cover the best practices for logging in C# that will help you build a reliable diagnostic information collection system.
Choosing a Logging Library: Serilog vs NLog vs Built-in ILogger
In .NET, there are several popular solutions. Let's look at their strengths and weaknesses.
- Microsoft.Extensions.Logging (ILogger) — a built-in abstract interface. Suitable for simple projects. Does not support structured logging out of the box.
- Serilog — the market leader. It stands out for its ease of setup, powerful support for structured logs, and an extensive set of sinks.
- NLog — a mature framework with flexible configuration via XML or code. Well-suited for legacy systems and complex routing scenarios.
For a new project, Serilog is recommended. It integrates easily with ILogger and provides a concise API. Example setup:
// Installation: dotnet add package Serilog.AspNetCoreusing Serilog;
Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.Console() .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day) .CreateLogger();
var builder = WebApplication.CreateBuilder(args);builder.Host.UseSerilog(); // Replace the default loggerProper Use of Logging Levels
Do not mix all messages into one level. Use the following gradation:
- Trace (0) — the most detailed information for debugging. Includes execution details of every function. Enable only in the dev environment.
- Debug (1) — useful data for the developer: variable values, method execution results.
- Information (2) — business events: "User logged in", "Order created". Should be understandable not only to programmers.
- Warning (3) — anomalies that do not interrupt operation: exceeded execution time, retry connection attempts.
- Error (4) — exceptions and failures requiring intervention. Always log the full call stack.
- Critical (5) — fatal errors: lost database connection, memory overflow. Require immediate notification.
Example of proper logging in a controller:
[ApiController][Route("api/orders")]public class OrdersController : ControllerBase{ private readonly ILogger<OrdersController> _logger;
public OrdersController(ILogger<OrdersController> logger) { _logger = logger; }
[HttpPost] public IActionResult CreateOrder(OrderDto order) { _logger.LogInformation("Creating order {OrderId} for user {UserId}", order.Id, order.UserId); try { // business logic _logger.LogInformation("Order {OrderId} successfully created", order.Id); return Ok(); } catch (Exception ex) { _logger.LogError(ex, "Error creating order {OrderId}", order.Id); return StatusCode(500); } }}Structured Logging and Context
Avoid string concatenation in logs. Instead of Log.Information("User: " + user.Name), use templates with parameters:
_logger.LogInformation("User {UserName} performed