Claude-skill-registry csharp-dotnet-debugging
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/csharp-dotnet-debugging" ~/.claude/skills/majiayu000-claude-skill-registry-csharp-dotnet-debugging && rm -rf "$T"
manifest:
skills/data/csharp-dotnet-debugging/SKILL.mdsource content
.NET 10 Debugging Strategy Guide
CLI-first debugging for .NET 10 applications. Provide root cause analysis, not just fixes.
Debugging Workflow
- Diagnose - Identify the problem type
- Analyze - Use appropriate CLI tools
- Fix - Apply the correct solution
- Guard - Prevent recurrence
Quick Diagnosis Matrix
| Symptom | Tool | Command |
|---|---|---|
| High CPU | dotnet-counters | |
| Memory Leak | dotnet-gcdump | |
| Crash | dotnet-dump | |
| Slow Response | dotnet-trace | |
| Deadlock | dotnet-stack | |
Common Exceptions & Fixes
NullReferenceException
Root Cause: Accessing member on null object.
// ❌ Problem var name = user.Profile.DisplayName; // Profile could be null // ✅ Fix: Null-conditional + null-coalescing var name = user?.Profile?.DisplayName ?? "Anonymous"; // ✅ Guard: Required properties public class User { public required Profile Profile { get; init; } }
ObjectDisposedException
Root Cause: Using disposed resource, often from closure capturing.
// ❌ Problem: Closure captures disposed DbContext public async Task<List<User>> GetUsersAsync() { using var db = new AppDbContext(); return await Task.Run(() => db.Users.ToList()); // db disposed before Task runs } // ✅ Fix: Await inside using scope public async Task<List<User>> GetUsersAsync() { await using var db = new AppDbContext(); return await db.Users.ToListAsync(); }
InvalidOperationException (Collection Modified)
Root Cause: Modifying collection during enumeration.
// ❌ Problem foreach (var item in items) { if (item.ShouldRemove) items.Remove(item); } // ✅ Fix: ToList() to create snapshot, or use RemoveAll items.RemoveAll(x => x.ShouldRemove); // Or filter to new list items = items.Where(x => !x.ShouldRemove).ToList();
TaskCanceledException
Root Cause: Operation cancelled or timed out.
// ✅ Proper handling try { await httpClient.GetAsync(url, ct); } catch (TaskCanceledException) when (ct.IsCancellationRequested) { logger.LogInformation("Operation cancelled by user"); throw; } catch (TaskCanceledException) { logger.LogWarning("Operation timed out"); throw new TimeoutException($"Request to {url} timed out"); }
Deadlock (async/await)
Root Cause: Blocking on async code in synchronization context.
// ❌ Problem: .Result blocks, waiting for context public string GetData() { return GetDataAsync().Result; // DEADLOCK in ASP.NET/WinForms } // ✅ Fix: Async all the way public async Task<string> GetDataAsync() { return await httpClient.GetStringAsync(url); } // ✅ If sync required: ConfigureAwait(false) public string GetData() { return GetDataAsync().ConfigureAwait(false).GetAwaiter().GetResult(); }
CLI Debugging Tools
dotnet-counters (Real-time Metrics)
# Install dotnet tool install -g dotnet-counters # Monitor runtime metrics dotnet-counters monitor -p <PID> --counters System.Runtime # Key metrics to watch: # - CPU Usage (%) # - Working Set (MB) # - GC Heap Size (MB) # - Gen 0/1/2 GC Count # - Exception Count # - ThreadPool Queue Length
dotnet-dump (Crash Analysis)
# Install dotnet tool install -g dotnet-dump # Collect dump dotnet-dump collect -p <PID> # Analyze dotnet-dump analyze <dump-file> # Useful SOS commands: > clrstack # Current thread stack > clrthreads # All managed threads > dumpheap -stat # Heap statistics > dumpheap -type MyClass # Find specific type instances > gcroot <address> # Find what's keeping object alive
dotnet-gcdump (Memory Analysis)
# Install dotnet tool install -g dotnet-gcdump # Collect GC dump (lighter than full dump) dotnet-gcdump collect -p <PID> # Analyze with VS or dotnet-gcdump report dotnet-gcdump report <gcdump-file>
dotnet-trace (Performance Tracing)
# Install dotnet tool install -g dotnet-trace # Collect trace with default providers dotnet-trace collect -p <PID> --duration 00:00:30 # Collect with specific providers dotnet-trace collect -p <PID> --providers Microsoft-DotNETCore-SampleProfiler # Convert to speedscope format dotnet-trace convert <trace-file> --format speedscope
dotnet-stack (Thread Stacks)
# Install dotnet tool install -g dotnet-stack # Report all thread stacks (great for deadlock detection) dotnet-stack report -p <PID>
Memory Leak Detection
Step 1: Identify Growing Memory
dotnet-counters monitor -p <PID> --counters System.Runtime[gc-heap-size]
Step 2: Capture GC Dumps
# Take baseline dotnet-gcdump collect -p <PID> -o baseline.gcdump # Wait for memory growth... # Take second dump dotnet-gcdump collect -p <PID> -o after.gcdump
Step 3: Compare Dumps
# In dotnet-dump analyze > dumpheap -stat # Look for types with unexpectedly high counts/sizes
Common Leak Patterns
// ❌ Event handler not unsubscribed button.Click += OnClick; // Memory leak if button outlives subscriber // ✅ Fix: Unsubscribe in Dispose public void Dispose() { button.Click -= OnClick; } // ❌ Static collection growing static List<Request> _requests = new(); // ✅ Fix: Use bounded collection or clear periodically static ConcurrentQueue<Request> _requests = new();
High CPU Diagnosis
Step 1: Identify CPU Usage
dotnet-counters monitor -p <PID> --counters System.Runtime[cpu-usage]
Step 2: Collect Trace
dotnet-trace collect -p <PID> --profile cpu-sampling --duration 00:00:30
Step 3: Analyze Hot Paths
Open trace file in Visual Studio, PerfView, or convert to speedscope:
dotnet-trace convert trace.nettrace --format speedscope # Open https://speedscope.app and load the JSON
Guard Patterns (Prevention)
Result Pattern (Avoid Exceptions for Flow Control)
public record Result<T> { public bool IsSuccess { get; } public T? Value { get; } public string? Error { get; } private Result(bool success, T? value, string? error) { IsSuccess = success; Value = value; Error = error; } public static Result<T> Success(T value) => new(true, value, null); public static Result<T> Failure(string error) => new(false, default, error); }
Guard Clauses
public class UserService(IUserRepository repo) { public async Task<User> GetUserAsync(int id, CancellationToken ct) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id); var user = await repo.GetByIdAsync(id, ct) ?? throw new NotFoundException($"User {id} not found"); return user; } }
Defensive Async
// Always pass CancellationToken public async Task ProcessAsync(CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); await using var connection = await OpenConnectionAsync(ct); // ... }
Resources
- Detailed diagnosis workflows: See
references/diagnosis-workflows.md - Exception patterns: See
references/exception-patterns.md