Chasing symptoms when debugging can be an inefficient path. Instead of relying on intuition and making chaotic changes, a systematic routine can reduce the problem space and help pinpoint the root cause. These guidelines are useful for addressing performance issues, data races, network timeouts, and UI freezes. While the examples use .NET 8 and Windows, the core ideas are applicable across different platforms.
1: Freeze the Environment and Record the Scene
To begin, establish a controlled environment. Prevent any further changes by stopping deployments and isolating the issue to a specific machine or container to act as a lab. It’s important to record the exact versions of software and commands being used.
# Environment snapshot
dotnet --info
git rev-parse --short HEAD
set | findstr /R ASPNETCORE_ENVIRONMENT|DOTNET_
If the error involves time or random numbers, set a fixed seed and control the clock. For instance, in testing scenarios:
// .NET 8 xUnit example
[Fact]
public void ParseInvoice_IsStable()
{
var rng = new Random(42); // fixed seed
var now = new DateTime(2025, 10, 09, 2, 11, 00, DateTimeKind.Utc);
using var clock = new FixedClock(now); // your simple IClock
var sut = new InvoiceParser(clock, rng);
Assert.Equal(123.45m, sut.Parse(total:123.45));
}
This step is crucial for stable testing, which allows you to accurately bisect and benchmark.
2: Create a Minimal Reproduction
Reduce the code to the smallest amount required to cause the problem. Copy just enough code into a basic console app or test case that replicates the error. Avoid including the whole dependency injection setup and middleware. A smaller reproduction makes it easier to identify the signal.
dotnet new console -n Repro.LoginHang
cd Repro.LoginHang
// Program.cs
using System.Net.Http;
var client = new HttpClient(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
});
for (int i = 0; i < 100; i++)
{
var t = client.GetAsync(https://example.test/login).GetAwaiter().GetResult(); // sync-over-async trap
Console.WriteLine((int)t.StatusCode);
}
This short code may reveal deadlocks or problems of running out of the pool. Then you can start the debugging easier.
3: Use Specific Logging and Counters
Rather than adding too many logs, focus on adding useful ones. Place logs near the code that might be slow or risky, and include correlation IDs and timing information.
using var activity = MyActivitySource.StartActivity(Auth.ExchangeCode);
var sw = ValueStopwatch.StartNew();
// call the dependency
_logger.LogInformation(auth.exchange started {CorrelationId}, cid);
// ...
_logger.LogInformation(auth.exchange done {CorrelationId} {ElapsedMs}, cid, sw.GetElapsedTime().TotalMilliseconds);
Track live counters to detect leaks or starvation by streaming:
# CPU, GC, threadpool, exceptions in real time
dotnet-counters monitor System.Runtime --process-id <pid>
Look for patterns such as:
- Thread pool queue length increasing without recovery
- GC heap size growing continuously
- Exception rates peaking around the time of the error
4: Use git bisect
to Find the Problematic Commit
Instead of guessing, use the repository's history to find the change that introduced the fault.
git bisect start
git bisect bad HEAD
git bisect good v1.18.0 # pick a known-good tag or commit
# For each step:
# 1) build and run the minimal repro script
# 2) mark good/bad
git bisect bad # or git bisect good
This procedure typically takes seven to ten steps. Once it's complete, the offending commit can be inspected. Analyze the changes and confirm the result.
5: Write a Test That Fails Before Fixing
Create a test that captures the problematic behavior. This ensures that the problem won't occur again.
[Fact]
public async Task Login_Timeouts_WhenTokenSwallowed()
{
using var server = TestServerFactory.Create();
var client = server.CreateClient();
var sw = Stopwatch.StartNew();
await Assert.ThrowsAsync<timeoutexception>(() => client.GetAsync(/login));
Assert.True(sw.Elapsed < TimeSpan.FromSeconds(5)); // fails today
}
Now fix the code. Resolve sync-over-async issues, handle exceptions correctly, and set appropriate timeouts.
// Before
var token = _provider.Get().Result; // blocks UI or threadpool
// After
var token = await _provider.GetAsync(); // flows correctly with await
Run the test to see if it passes. Add a brief comment explaining the edge case.
Top comments (0)