Skip to main content

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.

Several logging frameworks are available for C# applications:

  1. Serilog: A flexible, structured logging framework
  2. NLog: A mature, highly configurable logging platform
  3. log4net: A port of the popular log4j framework
  4. 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:

  1. Trace/Verbose: Detailed debugging information
  2. Debug: Information useful for debugging
  3. Information: General information about application flow
  4. Warning: Potential issues that don't prevent the application from working
  5. Error: Errors that prevent a function from working
  6. 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.