Debugging .NET Applications
#dotnet #debugging #troubleshooting #performance #logging #profiling
Last Updated: May 18, 2025 Related: .NET vs Laravel Complete Developer Guide
Quick Navigation
- [[#Development Debugging]]
- [[#Production Diagnostics]]
- [[#Performance Debugging]]
- [[#Memory Issues]]
- [[#Async & Threading Issues]]
- [[#Network & HTTP Debugging]]
- [[#Advanced Debugging Techniques]]
Development Debugging
๐ Visual Studio Debugging Features
Essential Debugging Windows
Debug โ Windows โ
โโโ Locals # Current scope variables
โโโ Watch # Custom expressions
โโโ Call Stack # Execution path
โโโ Immediate # Execute code during debugging
โโโ Autos # Automatic variable tracking
โโโ Threads # Thread information
โโโ Modules # Loaded assemblies
โโโ Exception Settings # Exception configuration
Advanced Breakpoints
// Conditional breakpoints
// Right-click breakpoint โ Conditions
// Condition: user.Id == 123
// Hit count: Break when hit count is multiple of 10
// Tracepoints (logging without stopping)
// Right-click breakpoint โ Actions
// Log message: "User {user.Name} processed at {DateTime.Now}"
// Data breakpoints (break when value changes)
// Debug โ New Breakpoint โ Data Breakpoint
// Address: &variableName
Debugging Attributes
// Control stepping behavior
[DebuggerStepThrough] // Skip this method when stepping
public static void UtilityMethod()
{
// Implementation details debugger will skip
}
// Custom debugger display
[DebuggerDisplay("User: {Name} ({Id})")]
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// Hide from debugger
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string _internalState;
// Custom type proxy for complex objects
[DebuggerTypeProxy(typeof(DebugView))]
public class ComplexObject
{
internal class DebugView
{
private readonly ComplexObject _obj;
public DebugView(ComplexObject obj) => _obj = obj;
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public string Summary => $"Count: {_obj.Items.Count}";
}
}
๐ ๏ธ VS Code Debugging
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net8.0/MyApp.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:5001"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}
// .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/MyApp.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}
๐ Structured Logging for Debugging
// Serilog configuration for development
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "MyApp")
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.File("logs/debug-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7)
.WriteTo.Seq("http://localhost:5341") // Optional: Seq for structured logs
.CreateLogger();
// Usage in code
public class UserService
{
private readonly ILogger<UserService> _logger;
public async Task<User> CreateUserAsync(CreateUserRequest request)
{
using var activity = _logger.BeginScope("Creating user {Email}", request.Email);
_logger.LogDebug("Validating user data");
try
{
var user = new User { Email = request.Email };
await _repository.SaveAsync(user);
_logger.LogInformation("User created successfully with ID {UserId}", user.Id);
return user;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create user {Email}", request.Email);
throw;
}
}
}
// Custom log enrichers
public class RequestEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)
{
var httpContext = // Get from current context
if (httpContext != null)
{
logEvent.AddPropertyIfAbsent(factory.CreateProperty(
"RequestId", httpContext.TraceIdentifier));
logEvent.AddPropertyIfAbsent(factory.CreateProperty(
"UserAgent", httpContext.Request.Headers["User-Agent"].ToString()));
}
}
}
Production Diagnostics
๐ Application Insights Integration
// Startup configuration
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("ApplicationInsights");
options.DeveloperMode = builder.Environment.IsDevelopment();
});
// Custom telemetry
public class OrderService
{
private readonly TelemetryClient _telemetryClient;
public async Task ProcessOrderAsync(Order order)
{
using var operation = _telemetryClient.StartOperation<RequestTelemetry>("ProcessOrder");
operation.Telemetry.Properties["OrderId"] = order.Id.ToString();
operation.Telemetry.Properties["CustomerId"] = order.CustomerId.ToString();
try
{
// Process order
await _paymentService.ChargeAsync(order);
_telemetryClient.TrackEvent("OrderProcessed", new Dictionary<string, string>
{
["OrderId"] = order.Id.ToString(),
["Amount"] = order.Total.ToString("C")
});
}
catch (Exception ex)
{
_telemetryClient.TrackException(ex, new Dictionary<string, string>
{
["OrderId"] = order.Id.ToString(),
["Stage"] = "Payment"
});
throw;
}
}
}
// Custom metrics
_telemetryClient.TrackMetric("OrderProcessingTime", stopwatch.ElapsedMilliseconds);
_telemetryClient.TrackDependency("SQL", "GetUser", "SELECT * FROM Users", startTime, stopwatch.Elapsed, success);
๐ฅ Health Checks for Diagnostics
// Comprehensive health check setup
builder.Services.AddHealthChecks()
.AddDbContext<ApplicationDbContext>(name: "database")
.AddRedis(builder.Configuration.GetConnectionString("Redis"), name: "redis")
.AddCheck<ApiHealthCheck>("external-api")
.AddCheck<DiskSpaceHealthCheck>("disk-space")
.AddCheck<MemoryHealthCheck>("memory");
// Custom health checks
public class ApiHealthCheck : IHealthCheck
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _config;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient.GetAsync(
_config["ExternalApi:HealthEndpoint"],
cancellationToken);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy("External API is responsive")
: HealthCheckResult.Unhealthy($"External API returned {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("External API is unreachable", ex);
}
}
}
public class MemoryHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var process = Process.GetCurrentProcess();
var memoryUsedMB = process.WorkingSet64 / 1024 / 1024;
var data = new Dictionary<string, object>
{
{ "MemoryUsedMB", memoryUsedMB },
{ "Gen0Collections", GC.CollectionCount(0) },
{ "Gen1Collections", GC.CollectionCount(1) },
{ "Gen2Collections", GC.CollectionCount(2) }
};
return memoryUsedMB > 500
? Task.FromResult(HealthCheckResult.Degraded("High memory usage", null, data))
: Task.FromResult(HealthCheckResult.Healthy("Memory usage normal", data));
}
}
// Health check endpoints
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false
});
๐ dotnet-dump and dotnet-trace
# Install diagnostic tools
dotnet tool install --global dotnet-dump
dotnet tool install --global dotnet-trace
dotnet tool install --global dotnet-counters
dotnet tool install --global dotnet-gcdump
# List running .NET processes
dotnet-trace ps
# Capture memory dump
dotnet-dump collect -p <process-id>
dotnet-dump collect -n <process-name>
# Analyze dump file
dotnet-dump analyze <dump-file>
> dumpheap -stat # Show object statistics
> dumpheap -type System.String # Show all strings
> gcroot <object-address> # Find GC roots
> clrstack # Show managed call stack
# Performance tracing
dotnet-trace collect -p <process-id> --duration 00:00:30
dotnet-trace convert trace.nettrace # Convert to various formats
# Real-time performance counters
dotnet-counters monitor -p <process-id>
dotnet-counters monitor --counters Microsoft.AspNetCore.Hosting -p <process-id>
# GC dump analysis
dotnet-gcdump collect -p <process-id>
Performance Debugging
โก Profiling with BenchmarkDotNet
[MemoryDiagnoser]
[ThreadingDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class StringComparisonBenchmark
{
private readonly string[] _strings = Enumerable.Range(0, 1000)
.Select(i => $"TestString{i}")
.ToArray();
[Benchmark(Baseline = true)]
public int StringEquals()
{
int count = 0;
foreach (var str in _strings)
{
if (str.Equals("TestString500"))
count++;
}
return count;
}
[Benchmark]
public int StringComparison()
{
int count = 0;
foreach (var str in _strings)
{
if (string.Equals(str, "TestString500", StringComparison.Ordinal))
count++;
}
return count;
}
[Benchmark]
public int StringContains()
{
return _strings.Count(s => s.Contains("TestString500"));
}
}
// Run benchmarks
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<StringComparisonBenchmark>();
// Results show method performance, memory allocation, etc.
}
}
๐ Custom Performance Counters
// Performance counter creation
public class PerformanceTracker
{
private readonly Counter<int> _requestCounter;
private readonly Histogram<double> _requestDuration;
private readonly UpDownCounter<int> _activeUsers;
public PerformanceTracker(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApp.Performance");
_requestCounter = meter.CreateCounter<int>(
"requests_total",
"count",
"Total number of requests");
_requestDuration = meter.CreateHistogram<double>(
"request_duration_seconds",
"seconds",
"Request duration in seconds");
_activeUsers = meter.CreateUpDownCounter<int>(
"active_users",
"count",
"Number of active users");
}
public void RecordRequest(string endpoint, double durationSeconds)
{
_requestCounter.Add(1, new TagList { { "endpoint", endpoint } });
_requestDuration.Record(durationSeconds, new TagList { { "endpoint", endpoint } });
}
public void UserConnected() => _activeUsers.Add(1);
public void UserDisconnected() => _activeUsers.Add(-1);
}
// Middleware for automatic tracking
public class PerformanceMiddleware
{
private readonly RequestDelegate _next;
private readonly PerformanceTracker _tracker;
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
stopwatch.Stop();
_tracker.RecordRequest(
context.Request.Path,
stopwatch.Elapsed.TotalSeconds);
}
}
}
๐ Entity Framework Performance Debugging
// Enable sensitive data logging for development
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (_environment.IsDevelopment())
{
optionsBuilder
.EnableSensitiveDataLogging()
.EnableDetailedErrors()
.LogTo(Console.WriteLine, LogLevel.Information);
}
}
// Query analysis
public class UserRepository
{
public async Task<List<User>> GetActiveUsersWithPostsAsync()
{
// โ Bad: N+1 query problem
var users = await _context.Users.Where(u => u.IsActive).ToListAsync();
foreach (var user in users)
{
user.Posts = await _context.Posts.Where(p => p.UserId == user.Id).ToListAsync();
}
// โ
Good: Single query with Include
return await _context.Users
.Where(u => u.IsActive)
.Include(u => u.Posts)
.ToListAsync();
// โ
Better: Projection for better performance
return await _context.Users
.Where(u => u.IsActive)
.Select(u => new User
{
Id = u.Id,
Name = u.Name,
Posts = u.Posts.Where(p => p.IsPublished).ToList()
})
.ToListAsync();
}
}
// Query splitting for large includes
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId);
// Enable split queries globally
modelBuilder.SetDefaultSplitQueries(true);
}
// Or per query
var blogs = await context.Blogs
.AsSplitQuery()
.Include(blog => blog.Posts)
.ToListAsync();
Memory Issues
๐ง Memory Leak Detection
// Event handler memory leaks
public class BadEventSubscriber
{
public BadEventSubscriber(EventPublisher publisher)
{
// โ Bad: Creates strong reference, prevents GC
publisher.SomethingHappened += HandleEvent;
}
private void HandleEvent(object sender, EventArgs e)
{
// Handle event
}
}
public class GoodEventSubscriber : IDisposable
{
private readonly EventPublisher _publisher;
public GoodEventSubscriber(EventPublisher publisher)
{
_publisher = publisher;
// โ
Good: Remove subscription in Dispose
_publisher.SomethingHappened += HandleEvent;
}
private void HandleEvent(object sender, EventArgs e)
{
// Handle event
}
public void Dispose()
{
_publisher.SomethingHappened -= HandleEvent;
}
}
// WeakReference for caching
public class CacheManager
{
private readonly ConcurrentDictionary<string, WeakReference<object>> _cache = new();
public T Get<T>(string key) where T : class
{
if (_cache.TryGetValue(key, out var weakRef) &&
weakRef.TryGetTarget(out var target))
{
return (T)target;
}
return null;
}
public void Set<T>(string key, T value) where T : class
{
_cache[key] = new WeakReference<object>(value);
}
}
// Memory diagnostic helper
public static class MemoryDiagnostics
{
public static void LogMemoryUsage(ILogger logger)
{
var process = Process.GetCurrentProcess();
var memInfo = new
{
WorkingSetMB = process.WorkingSet64 / 1024 / 1024,
PrivateMemoryMB = process.PrivateMemorySize64 / 1024 / 1024,
VirtualMemoryMB = process.VirtualMemorySize64 / 1024 / 1024,
ManagedMemoryMB = GC.GetTotalMemory(false) / 1024 / 1024,
Gen0Collections = GC.CollectionCount(0),
Gen1Collections = GC.CollectionCount(1),
Gen2Collections = GC.CollectionCount(2)
};
logger.LogInformation("Memory usage: {@MemoryInfo}", memInfo);
}
public static void ForceGarbageCollection()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
๐ Memory Dump Analysis
# Analyze memory dump with dotnet-dump
dotnet-dump analyze memory.dmp
# Common commands in dump analysis
> help # Show all commands
> dumpheap -stat # Object statistics
> dumpheap -type System.String # All string objects
> dumpheap -mt <method-table> # Objects of specific type
> gcroot <address> # Find what's keeping object alive
> finalizequeue # Objects waiting for finalization
> syncblk # Synchronization blocks
> threadpool # Thread pool information
> clrthreads # Managed threads
> dumpmodule -mt <module> # Method tables in module
# Example investigation
> dumpheap -stat
# Look for types with high count/size
> dumpheap -type MyApp.Models.User
# Get addresses of User objects
> gcroot 0x000001a2b3c4d5e6
# See what's holding references to prevent GC
Async & Threading Issues
๐ Deadlock Detection and Prevention
// โ Bad: Deadlock prone code
public class DeadlockService
{
public string GetDataSync()
{
// This can deadlock in ASP.NET
return GetDataAsync().Result;
}
public async Task<string> GetDataAsync()
{
await Task.Delay(100);
return "data";
}
}
// โ
Good: Async all the way
public class AsyncService
{
public async Task<string> GetDataAsync()
{
// Use ConfigureAwait(false) in libraries
await Task.Delay(100).ConfigureAwait(false);
return "data";
}
}
// Deadlock detection utility
public static class DeadlockDetector
{
private static readonly ConcurrentDictionary<int, DateTime> _operations = new();
public static async Task<T> WithDeadlockDetection<T>(
Func<Task<T>> operation,
int timeoutMs = 30000,
[CallerMemberName] string operationName = "")
{
var threadId = Thread.CurrentThread.ManagedThreadId;
var startTime = DateTime.UtcNow;
_operations[threadId] = startTime;
try
{
using var cts = new CancellationTokenSource(timeoutMs);
var task = operation();
var completedTask = await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token));
if (completedTask == task)
{
return await task;
}
else
{
throw new TimeoutException(
$"Operation '{operationName}' timed out after {timeoutMs}ms. " +
$"Possible deadlock on thread {threadId}");
}
}
finally
{
_operations.TryRemove(threadId, out _);
}
}
public static void LogLongRunningOperations(ILogger logger)
{
var threshold = TimeSpan.FromSeconds(10);
var now = DateTime.UtcNow;
foreach (var kvp in _operations)
{
if (now - kvp.Value > threshold)
{
logger.LogWarning(
"Long-running operation detected on thread {ThreadId}, " +
"running for {Duration}ms",
kvp.Key, (now - kvp.Value).TotalMilliseconds);
}
}
}
}
๐งต Thread Pool Analysis
// Thread pool monitoring
public class ThreadPoolMonitor
{
private readonly ILogger<ThreadPoolMonitor> _logger;
private readonly Timer _timer;
public ThreadPoolMonitor(ILogger<ThreadPoolMonitor> logger)
{
_logger = logger;
_timer = new Timer(LogThreadPoolStats, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
}
private void LogThreadPoolStats(object state)
{
ThreadPool.GetAvailableThreads(out int availableWorkerThreads, out int availableCompletionPortThreads);
ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads);
var busyWorkerThreads = maxWorkerThreads - availableWorkerThreads;
var busyCompletionPortThreads = maxCompletionPortThreads - availableCompletionPortThreads;
_logger.LogInformation(
"ThreadPool Stats - Worker: {BusyWorkerThreads}/{MaxWorkerThreads}, " +
"IO: {BusyCompletionPortThreads}/{MaxCompletionPortThreads}",
busyWorkerThreads, maxWorkerThreads,
busyCompletionPortThreads, maxCompletionPortThreads);
// Alert if thread pool is under pressure
if (availableWorkerThreads < maxWorkerThreads * 0.1)
{
_logger.LogWarning("Thread pool pressure detected: Only {AvailableThreads} worker threads available",
availableWorkerThreads);
}
}
public void Dispose()
{
_timer?.Dispose();
}
}
// Custom SynchronizationContext for testing
public class TestSynchronizationContext : SynchronizationContext
{
private readonly Queue<(SendOrPostCallback callback, object state)> _queue = new();
private readonly object _lock = new();
public override void Post(SendOrPostCallback d, object state)
{
lock (_lock)
{
_queue.Enqueue((d, state));
}
}
public void ExecuteAll()
{
while (true)
{
(SendOrPostCallback callback, object state) item;
lock (_lock)
{
if (_queue.Count == 0) break;
item = _queue.Dequeue();
}
item.callback(item.state);
}
}
}
Network & HTTP Debugging
๐ HTTP Client Debugging
// HTTP request/response logging
public class LoggingHandler : DelegatingHandler
{
private readonly ILogger<LoggingHandler> _logger;
public LoggingHandler(ILogger<LoggingHandler> logger) : base()
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var requestId = Guid.NewGuid().ToString();
// Log request
_logger.LogInformation(
"Sending HTTP request {RequestId}: {Method} {Uri}",
requestId, request.Method, request.RequestUri);
if (request.Content != null)
{
var content = await request.Content.ReadAsStringAsync();
_logger.LogDebug("Request {RequestId} body: {Content}", requestId, content);
}
var stopwatch = Stopwatch.StartNew();
try
{
var response = await base.SendAsync(request, cancellationToken);
stopwatch.Stop();
_logger.LogInformation(
"Received HTTP response {RequestId}: {StatusCode} in {ElapsedMs}ms",
requestId, response.StatusCode, stopwatch.ElapsedMilliseconds);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogWarning(
"HTTP request {RequestId} failed with {StatusCode}: {Content}",
requestId, response.StatusCode, errorContent);
}
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex,
"HTTP request {RequestId} failed after {ElapsedMs}ms",
requestId, stopwatch.ElapsedMilliseconds);
throw;
}
}
}
// Register the handler
builder.Services.AddHttpClient<ApiService>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddHttpMessageHandler<LoggingHandler>();
// Circuit breaker pattern for resilience
public class CircuitBreakerHandler : DelegatingHandler
{
private readonly int _failureThreshold;
private readonly TimeSpan _timeout;
private int _failureCount;
private DateTime _lastFailureTime;
private CircuitState _state = CircuitState.Closed;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (_state == CircuitState.Open)
{
if (DateTime.UtcNow - _lastFailureTime > _timeout)
{
_state = CircuitState.HalfOpen;
}
else
{
throw new HttpRequestException("Circuit breaker is open");
}
}
try
{
var response = await base.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
_failureCount = 0;
_state = CircuitState.Closed;
}
return response;
}
catch
{
_failureCount++;
_lastFailureTime = DateTime.UtcNow;
if (_failureCount >= _failureThreshold)
{
_state = CircuitState.Open;
}
throw;
}
}
private enum CircuitState { Closed, Open, HalfOpen }
}
๐ Network Tracing
// Enable .NET network tracing
// App.config or through environment variables
<system.diagnostics>
<trace autoflush="true" />
<sources>
<source name="System.Net" switchValue="Information">
<listeners>
<add name="System.Net" type="System.Diagnostics.TextWriterTraceListener"
initializeData="network.log" />
</listeners>
</source>
<source name="System.Net.Sockets" switchValue="Information">
<listeners>
<add name="System.Net.Sockets" type="System.Diagnostics.TextWriterTraceListener"
initializeData="sockets.log" />
</listeners>
</source>
</sources>
</system.diagnostics>
// Environment variables for .NET Core
// DOTNET_System_Net_SocketsTrace=true
// DOTNET_System_Net_Http_SocketsHttpHandler_Debug=true
// Custom network diagnostics
public class NetworkDiagnostics
{
public static async Task<NetworkLatency> MeasureLatencyAsync(string host, int port = 80)
{
var ping = new Ping();
var pingReply = await ping.SendPingAsync(host);
var tcpStopwatch = Stopwatch.StartNew();
using var tcpClient = new TcpClient();
try
{
await tcpClient.ConnectAsync(host, port);
tcpStopwatch.Stop();
return new NetworkLatency
{
Host = host,
PingLatency = pingReply.RoundtripTime,
TcpConnectLatency = tcpStopwatch.ElapsedMilliseconds,
Status = pingReply.Status
};
}
catch (Exception ex)
{
tcpStopwatch.Stop();
return new NetworkLatency
{
Host = host,
PingLatency = pingReply.RoundtripTime,
TcpConnectLatency = -1,
Status = pingReply.Status,
Error = ex.Message
};
}
}
}
public class NetworkLatency
{
public string Host { get; set; }
public long PingLatency { get; set; }
public long TcpConnectLatency { get; set; }
public IPStatus Status { get; set; }
public string Error { get; set; }
}
Advanced Debugging Techniques
๐ฌ Source Generators Debug Support
// Enable source generator debugging
// .csproj configuration
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
// Debug source generator
[Generator]
public class ApiControllerGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Add debugger launch for development
#if DEBUG
if (!System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debugger.Launch();
}
#endif
// Generator implementation
var source = GenerateControllerCode();
context.AddSource("GeneratedController.g.cs", source);
}
public void Initialize(GeneratorInitializationContext context)
{
// Initialize
}
}
// Debugging generated code
public partial class ApiController
{
// This will be generated by source generator
// Use "Go to Definition" to see generated code
}
๐ญ Aspect-Oriented Debugging
// Using Castle DynamicProxy for debugging
public class DebugInterceptor : IInterceptor
{
private readonly ILogger<DebugInterceptor> _logger;
public void Intercept(IInvocation invocation)
{
var methodName = $"{invocation.TargetType.Name}.{invocation.Method.Name}";
var parameters = string.Join(", ",
invocation.Arguments.Select(a => a?.ToString() ?? "null"));
_logger.LogDebug("Entering {MethodName}({Parameters})", methodName, parameters);
var stopwatch = Stopwatch.StartNew();
try
{
invocation.Proceed();
stopwatch.Stop();
_logger.LogDebug("Exiting {MethodName} after {ElapsedMs}ms",
methodName, stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Exception in {MethodName} after {ElapsedMs}ms",
methodName, stopwatch.ElapsedMilliseconds);
throw;
}
}
}
// Registration with DI container
builder.Services.AddScoped<DebugInterceptor>();
builder.Services.AddScoped<IUserService>(provider =>
{
var proxyGenerator = new ProxyGenerator();
var target = new UserService(/* dependencies */);
var interceptor = provider.GetService<DebugInterceptor>();
return proxyGenerator.CreateInterfaceProxyWithTarget<IUserService>(target, interceptor);
});
๐ง Custom Diagnostics Tools
// Custom diagnostic command for dotnet-dump
public static class CustomDiagnostics
{
[Conditional("DEBUG")]
public static void DumpObjectGraph(object obj, string fileName = null)
{
fileName ??= $"object-graph-{DateTime.Now:yyyyMMdd-HHmmss}.json";
var graph = BuildObjectGraph(obj, new HashSet<object>());
var json = JsonSerializer.Serialize(graph, new JsonSerializerOptions
{
WriteIndented = true,
ReferenceHandler = ReferenceHandler.IgnoreCycles
});
File.WriteAllText(fileName, json);
}
private static object BuildObjectGraph(object obj, HashSet<object> visited)
{
if (obj == null || visited.Contains(obj))
return null;
visited.Add(obj);
var type = obj.GetType();
if (type.IsPrimitive || type == typeof(string))
return obj;
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead)
.ToDictionary(
p => p.Name,
p => {
try
{
var value = p.GetValue(obj);
return BuildObjectGraph(value, visited);
}
catch
{
return "[Error reading property]";
}
});
return new { Type = type.Name, Properties = properties };
}
}
// Performance boundary tracking
public class PerformanceBoundary : IDisposable
{
private readonly string _operationName;
private readonly ILogger _logger;
private readonly Stopwatch _stopwatch;
private readonly IDisposable _scope;
public PerformanceBoundary(string operationName, ILogger logger)
{
_operationName = operationName;
_logger = logger;
_stopwatch = Stopwatch.StartNew();
_scope = _logger.BeginScope("Operation: {OperationName}", operationName);
_logger.LogDebug("Started {OperationName}", operationName);
}
public void Dispose()
{
_stopwatch.Stop();
_logger.LogDebug("Completed {OperationName} in {ElapsedMs}ms",
_operationName, _stopwatch.ElapsedMilliseconds);
_scope?.Dispose();
}
}
// Usage
public async Task<User> GetUserAsync(int id)
{
using var perf = new PerformanceBoundary(nameof(GetUserAsync), _logger);
// Implementation
return await _repository.GetByIdAsync(id);
}
Debugging Checklists
โ Production Issue Investigation
## Production Issue Investigation Checklist
### Initial Assessment
- [ ] Gather error messages and stack traces
- [ ] Check application logs for patterns
- [ ] Review monitoring dashboards (CPU, memory, requests)
- [ ] Identify affected users/features
- [ ] Determine timeline of issue onset
### Data Collection
- [ ] Capture memory dump if memory-related
- [ ] Collect performance traces
- [ ] Export relevant log segments
- [ ] Screenshot error pages/UI issues
- [ ] Gather user reproduction steps
### Analysis
- [ ] Analyze memory dumps for leaks/exceptions
- [ ] Review performance traces for bottlenecks
- [ ] Search logs for error patterns
- [ ] Compare with baseline performance metrics
- [ ] Check recent deployments/changes
### Resolution
- [ ] Develop hypothesis based on findings
- [ ] Create minimal reproduction in dev environment
- [ ] Implement fix with tests
- [ ] Verify fix in staging environment
- [ ] Deploy with monitoring plan
### Follow-up
- [ ] Monitor metrics post-deployment
- [ ] Update alerts/monitoring if needed
- [ ] Document root cause and solution
- [ ] Consider preventive measures
- [ ] Share learnings with team
๐ Performance Investigation Checklist
## Performance Investigation Checklist
### Baseline Establishment
- [ ] Collect current performance metrics
- [ ] Identify performance SLAs/targets
- [ ] Establish test scenarios
- [ ] Document current user experience
### Profiling
- [ ] CPU profiling for hot paths
- [ ] Memory profiling for allocations
- [ ] I/O profiling for database/network calls
- [ ] GC pressure analysis
### Common Performance Issues
- [ ] Synchronous calls in async contexts
- [ ] Inefficient database queries (N+1 problems)
- [ ] Large object heap pressure
- [ ] Inefficient collections/LINQ usage
- [ ] Excessive string concatenations
- [ ] Unoptimized serialization
### Optimization
- [ ] Implement caching where appropriate
- [ ] Optimize database queries
- [ ] Use async patterns correctly
- [ ] Profile memory allocations
- [ ] Consider batch operations
- [ ] Optimize startup performance
### Validation
- [ ] Before/after performance comparison
- [ ] Load testing with realistic scenarios
- [ ] Memory usage verification
- [ ] User experience validation
- [ ] Monitor in production
Related Notes
- .NET vs Laravel Complete Developer Guide
- ASP.NET Core Best Practices
- Package Management Strategies
- Performance Optimization Patterns
Quick Reference Commands
Diagnostic Tools
# Memory analysis
dotnet-dump collect -p <pid>
dotnet-dump analyze dump.dmp
# Performance tracing
dotnet-trace collect -p <pid> --duration 00:00:30
dotnet-trace convert trace.nettrace
# Real-time monitoring
dotnet-counters monitor -p <pid>
dotnet-gcdump collect -p <pid>
# Process information
dotnet-trace ps
dotnet-dump ps
Visual Studio Commands
Debug โ Windows โ Immediate Window # Execute code during debugging
Debug โ Windows โ Call Stack # View execution path
Debug โ Windows โ Locals # Current scope variables
Tools โ Diagnostic Tools # Memory and CPU usage
Analyze โ Performance Profiler # Detailed profiling
Tags
#debugging #dotnet #performance #memory #profiling #diagnostics #troubleshooting
This debugging guide should be updated as new .NET versions and tooling become available.