Skip to main content

5.6 - Case Studies

This chapter presents real-world debugging scenarios and demonstrates how to apply the techniques covered in previous chapters to solve complex debugging challenges.

5.6.1 - Common Debugging Scenarios

Let's explore some common debugging scenarios that C# developers encounter and strategies for addressing them.

5.6.1.1 - Memory Leaks

Scenario: An application's memory usage grows over time, eventually leading to performance degradation or crashes.

Debugging Approach:

  1. Identify the Leak:
    • Take memory snapshots at different points in time
    • Compare snapshots to identify objects that accumulate
// Example of a memory leak due to event handler not being unsubscribed
public class LeakyClass
{
public LeakyClass()
{
// Subscribe to an event
EventPublisher.SomeEvent += HandleEvent;
}

private void HandleEvent(object sender, EventArgs e)
{
// Handle the event
}

// Missing: Unsubscribe from the event when done
// public void Dispose()
// {
// EventPublisher.SomeEvent -= HandleEvent;
// }
}
  1. Fix the Leak:
    • Implement proper disposal of resources
    • Use weak references for event handlers
    • Review static collections that might grow indefinitely
// Fixed version with proper disposal
public class FixedClass : IDisposable
{
public FixedClass()
{
EventPublisher.SomeEvent += HandleEvent;
}

private void HandleEvent(object sender, EventArgs e)
{
// Handle the event
}

public void Dispose()
{
EventPublisher.SomeEvent -= HandleEvent;
}
}

5.6.1.2 - Deadlocks

Scenario: An application freezes because two or more threads are waiting for each other to release resources.

Debugging Approach:

  1. Identify the Deadlock:
    • Use the Threads window to examine thread states
    • Look for threads in a "Wait" state
    • Examine the call stack of each thread
// Example of code that might cause a deadlock
object lock1 = new object();
object lock2 = new object();

// Thread 1
new Thread(() =>
{
lock (lock1)
{
Thread.Sleep(100); // Simulate work
lock (lock2)
{
// Do something
}
}
}).Start();

// Thread 2
new Thread(() =>
{
lock (lock2)
{
Thread.Sleep(100); // Simulate work
lock (lock1)
{
// Do something
}
}
}).Start();
  1. Fix the Deadlock:
    • Ensure consistent lock ordering
    • Use timeouts with lock attempts
    • Consider higher-level synchronization primitives
// Fixed version with consistent lock ordering
object lock1 = new object();
object lock2 = new object();

// Both threads acquire locks in the same order
new Thread(() =>
{
lock (lock1)
{
Thread.Sleep(100);
lock (lock2)
{
// Do something
}
}
}).Start();

new Thread(() =>
{
lock (lock1)
{
Thread.Sleep(100);
lock (lock2)
{
// Do something
}
}
}).Start();

5.6.1.3 - Performance Issues

Scenario: An application runs slower than expected, particularly under load.

Debugging Approach:

  1. Identify Performance Bottlenecks:
    • Use the Performance Profiler to identify CPU and memory hotspots
    • Look for excessive database queries or I/O operations
    • Check for inefficient algorithms or data structures
// Example of inefficient code
public List<Customer> FindCustomersByName(string name)
{
var result = new List<Customer>();
foreach (var customer in _allCustomers) // Inefficient linear search
{
if (customer.Name.Contains(name))
{
result.Add(customer);
}
}
return result;
}
  1. Fix Performance Issues:
    • Optimize algorithms and data structures
    • Implement caching for expensive operations
    • Reduce database round-trips
// Improved version with indexing
private Dictionary<string, List<Customer>> _customerNameIndex;

public void BuildNameIndex()
{
_customerNameIndex = new Dictionary<string, List<Customer>>(StringComparer.OrdinalIgnoreCase);
foreach (var customer in _allCustomers)
{
string[] nameParts = customer.Name.Split(' ');
foreach (var part in nameParts)
{
if (!_customerNameIndex.TryGetValue(part, out var customers))
{
customers = new List<Customer>();
_customerNameIndex[part] = customers;
}
customers.Add(customer);
}
}
}

public List<Customer> FindCustomersByName(string name)
{
if (_customerNameIndex.TryGetValue(name, out var result))
{
return result;
}
return new List<Customer>();
}

5.6.2 - Real-world Examples

Let's examine some real-world debugging scenarios and their solutions.

5.6.2.1 - Case Study: Intermittent NullReferenceException

Scenario: An application occasionally throws a NullReferenceException, but the issue is difficult to reproduce consistently.

Debugging Approach:

  1. Gather Information:
    • Enable first-chance exception breaking
    • Add comprehensive logging around the suspected area
    • Analyze the exception stack trace
// Original code with potential null reference
public void ProcessOrder(Order order)
{
var customer = _customerRepository.GetCustomer(order.CustomerId);
var discount = customer.LoyaltyLevel > 2 ? 0.1m : 0.0m;
order.ApplyDiscount(discount);
}
  1. Identify the Root Cause:

    • The customer might be null if not found in the repository
    • The issue occurs only for certain customer IDs
  2. Fix the Issue:

    • Add null checks
    • Improve error handling
    • Add defensive programming techniques
// Fixed version with null checking and logging
public void ProcessOrder(Order order)
{
try
{
var customer = _customerRepository.GetCustomer(order.CustomerId);
if (customer == null)
{
_logger.LogWarning("Customer not found for order {OrderId}, CustomerId: {CustomerId}",
order.Id, order.CustomerId);
return;
}

var discount = customer.LoyaltyLevel > 2 ? 0.1m : 0.0m;
order.ApplyDiscount(discount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing order {OrderId}", order.Id);
throw;
}
}

5.6.2.2 - Case Study: Database Connection Leaks

Scenario: An application experiences database connection timeouts after running for some time.

Debugging Approach:

  1. Gather Information:
    • Monitor database connection counts
    • Review connection handling code
    • Add logging for connection open/close operations
// Original code with connection leak
public Customer GetCustomer(int customerId)
{
var connection = new SqlConnection(_connectionString);
connection.Open();

var command = new SqlCommand("SELECT * FROM Customers WHERE Id = @Id", connection);
command.Parameters.AddWithValue("@Id", customerId);

var reader = command.ExecuteReader();
if (reader.Read())
{
return new Customer
{
Id = (int)reader["Id"],
Name = (string)reader["Name"],
// Other properties
};
}

return null;
// Missing: connection.Close()
}
  1. Identify the Root Cause:

    • Connections are not being properly closed or disposed
    • The connection pool eventually exhausts
  2. Fix the Issue:

    • Use using statements to ensure proper disposal
    • Implement connection pooling
    • Add error handling
// Fixed version with proper connection handling
public Customer GetCustomer(int customerId)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();

using (var command = new SqlCommand("SELECT * FROM Customers WHERE Id = @Id", connection))
{
command.Parameters.AddWithValue("@Id", customerId);

using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
return new Customer
{
Id = (int)reader["Id"],
Name = (string)reader["Name"],
// Other properties
};
}
}
}
}

return null;
}

5.6.3 - Problem-solving Approaches

Effective debugging requires a systematic approach to problem-solving.

5.6.3.1 - Scientific Method

Apply the scientific method to debugging:

  1. Observe: Gather information about the issue
  2. Hypothesize: Form a theory about the cause
  3. Predict: Determine what would happen if your hypothesis is correct
  4. Test: Modify the code to test your hypothesis
  5. Analyze: Evaluate the results and refine your hypothesis

5.6.3.2 - Divide and Conquer

Use a divide-and-conquer approach to narrow down the source of the issue:

  1. Isolate: Create a minimal reproducible example
  2. Bisect: Use binary search to find the problematic code
  3. Simplify: Remove complexity until the issue becomes clear

5.6.3.3 - Collaborative Debugging

Leverage collaboration to solve difficult problems:

  1. Pair Debugging: Work with another developer to debug the issue
  2. Rubber Duck Debugging: Explain the problem to someone else (or an inanimate object)
  3. Code Reviews: Have others review your code for potential issues

By applying these problem-solving approaches and learning from real-world examples, you can become more effective at debugging C# applications and resolving complex issues.

In the next section, we'll explore the Framework Class Library (FCL) and how to work with its various components effectively.