5.4 - Advanced Techniques
Beyond the basic debugging tools, C# developers can leverage advanced techniques to handle complex debugging scenarios. This chapter explores logging frameworks, structured logging, and other advanced debugging approaches.
5.4.1 - Logging Frameworks
Logging frameworks provide structured, configurable logging capabilities that are essential for effective debugging, especially in production environments.
5.4.1.1 - Popular Logging Frameworks
Several logging frameworks are available for C# applications:
- Serilog: A flexible, structured logging framework
- NLog: A mature, highly configurable logging platform
- log4net: A port of the popular log4j framework
- Microsoft.Extensions.Logging: The built-in logging framework for .NET Core/.NET 5+
5.4.1.2 - Setting Up Serilog
Serilog is a popular choice due to its structured logging capabilities:
// Install packages: Serilog, Serilog.Sinks.Console, Serilog.Sinks.File
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
// Use the logger
Log.Information("Application started");
try
{
// Application code
Log.Information("Processing order {OrderId}", order.Id);
}
catch (Exception ex)
{
Log.Error(ex, "Error processing order {OrderId}", order.Id);
}
finally
{
Log.CloseAndFlush();
}
5.4.1.3 - Dependency Injection with Logging
In modern C# applications, logging is typically integrated via dependency injection:
// In Startup.cs or Program.cs
services.AddLogging(builder =>
{
builder.AddSerilog(dispose: true);
});
// In a service or controller
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void ProcessOrder(Order order)
{
_logger.LogInformation("Processing order {OrderId}", order.Id);
// Process the order
}
}
5.4.2 - Structured Logging
Structured logging captures not just text messages but structured data that can be queried and analyzed.
5.4.2.1 - Structured Log Messages
Instead of concatenating strings, use message templates with named parameters:
// Instead of this:
_logger.LogInformation("Processing order " + order.Id + " with " + order.Items.Count + " items");
// Do this:
_logger.LogInformation("Processing order {OrderId} with {ItemCount} items",
order.Id, order.Items.Count);
5.4.2.2 - Logging Objects
Log entire objects for comprehensive debugging information:
// Log the entire order object
_logger.LogDebug("Order details: {@Order}", order);
// The @ symbol tells Serilog to serialize the object
5.4.2.3 - Contextual Information
Add contextual information to your logs:
// Add user context
using (LogContext.PushProperty("UserId", user.Id))
{
_logger.LogInformation("User {UserName} is processing order {OrderId}",
user.Name, order.Id);
// All logs within this scope will include the UserId property
ProcessOrder(order);
}
5.4.3 - Trace Listeners
Trace listeners capture debug and trace output from your application and direct it to various targets.
5.4.3.1 - System.Diagnostics.Trace
The System.Diagnostics.Trace
class provides tracing capabilities:
// Configure trace listeners in code
Trace.Listeners.Add(new TextWriterTraceListener("application.log"));
Trace.Listeners.Add(new EventLogTraceListener("MyApplication"));
Trace.AutoFlush = true;
// Use tracing
Trace.WriteLine("Application started");
Trace.TraceInformation("Processing order {0}", order.Id);
Trace.TraceWarning("Order total exceeds threshold: {0}", order.Total);
Trace.TraceError("Error processing order: {0}", ex.Message);
5.4.3.2 - Custom Trace Listeners
Create custom trace listeners for specialized logging needs:
public class DatabaseTraceListener : TraceListener
{
public override void Write(string message)
{
// Write to database
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var command = new SqlCommand("INSERT INTO Logs (Message) VALUES (@Message)", connection))
{
command.Parameters.AddWithValue("@Message", message);
command.ExecuteNonQuery();
}
}
}
public override void WriteLine(string message)
{
Write(message + Environment.NewLine);
}
}
5.4.3.3 - Configuring Trace Listeners
Configure trace listeners in the application configuration file:
<configuration>
<system.diagnostics>
<trace autoflush="true" indentsize="4">
<listeners>
<add name="fileListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="application.log" />
<add name="consoleListener" type="System.Diagnostics.ConsoleTraceListener" />
<remove name="Default" />
</listeners>
</trace>
</system.diagnostics>
</configuration>
5.4.4 - Log Levels and Filtering
Log levels and filtering help manage the volume and relevance of log data.
5.4.4.1 - Standard Log Levels
Most logging frameworks support these standard log levels:
- Trace/Verbose: Detailed debugging information
- Debug: Information useful for debugging
- Information: General information about application flow
- Warning: Potential issues that don't prevent the application from working
- Error: Errors that prevent a function from working
- Critical/Fatal: Errors that cause the application to crash
5.4.4.2 - Configuring Log Levels
Configure log levels based on the environment:
// In development
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
// In production
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.WriteTo.Console()
.WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
5.4.4.3 - Filtering Logs
Filter logs based on various criteria:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Filter.ByIncludingOnly(evt => evt.Properties.ContainsKey("UserId"))
.WriteTo.File("logs/user-activity.log")
.CreateLogger();
5.4.5 - Logging Best Practices
Follow these best practices to maximize the value of your logging.
5.4.5.1 - Log Actionable Information
Log information that helps diagnose and resolve issues:
// Instead of this:
_logger.LogError("An error occurred");
// Do this:
_logger.LogError(ex, "Failed to process order {OrderId}: {ErrorMessage}",
order.Id, ex.Message);
5.4.5.2 - Include Context
Include relevant context in your logs:
_logger.LogInformation(
"User {UserId} processed order {OrderId} with {ItemCount} items for {Total:C}",
user.Id, order.Id, order.Items.Count, order.Total);
5.4.5.3 - Performance Considerations
Be mindful of logging performance:
// Avoid expensive operations when the log won't be written
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Order details: {@Order}", order);
}
// Use LoggerMessage.Define for high-performance logging
private static readonly Action<ILogger, string, int, Exception> _orderProcessed =
LoggerMessage.Define<string, int>(
LogLevel.Information,
new EventId(1, "OrderProcessed"),
"Processed order {OrderId} with {ItemCount} items");
public void ProcessOrder(Order order)
{
// Process the order
_orderProcessed(_logger, order.Id, order.Items.Count, null);
}
In the next chapter, we'll explore unit testing as a preventive debugging technique that helps catch issues before they reach production.