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:
- 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;
// }
}
- 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:
- 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();
- 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:
- 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;
}
- 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:
- 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);
}
-
Identify the Root Cause:
- The customer might be null if not found in the repository
- The issue occurs only for certain customer IDs
-
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:
- 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()
}
-
Identify the Root Cause:
- Connections are not being properly closed or disposed
- The connection pool eventually exhausts
-
Fix the Issue:
- Use
using
statements to ensure proper disposal - Implement connection pooling
- Add error handling
- Use
// 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:
- Observe: Gather information about the issue
- Hypothesize: Form a theory about the cause
- Predict: Determine what would happen if your hypothesis is correct
- Test: Modify the code to test your hypothesis
- 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:
- Isolate: Create a minimal reproducible example
- Bisect: Use binary search to find the problematic code
- Simplify: Remove complexity until the issue becomes clear
5.6.3.3 - Collaborative Debugging
Leverage collaboration to solve difficult problems:
- Pair Debugging: Work with another developer to debug the issue
- Rubber Duck Debugging: Explain the problem to someone else (or an inanimate object)
- 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.