ASP.NET Core Debugging Guide
This guide provides a systematic approach to debugging ASP.NET Core applications, covering common error patterns, diagnostic tools, and resolution strategies.
Common Error Patterns
- Dependency Injection (DI) Container Errors
Symptoms:
-
InvalidOperationException: Unable to resolve service for type 'X'
-
InvalidOperationException: A circular dependency was detected
-
ObjectDisposedException: Cannot access a disposed object
Common Causes:
// Missing service registration public class MyController { public MyController(IMyService service) { } // Not registered in DI }
// Circular dependency public class ServiceA { public ServiceA(ServiceB b) { } } public class ServiceB { public ServiceB(ServiceA a) { } // Circular! }
// Captive dependency (singleton holding scoped) services.AddSingleton<ISingletonService, SingletonService>(); services.AddScoped<IScopedService, ScopedService>(); // Captured by singleton!
Debugging Steps:
-
Check Program.cs or Startup.cs for service registration
-
Verify service lifetime compatibility (Singleton > Scoped > Transient)
-
Use IServiceProvider.GetRequiredService<T>() for explicit resolution
-
Enable detailed DI errors in development:
builder.Host.UseDefaultServiceProvider(options => { options.ValidateScopes = true; options.ValidateOnBuild = true; });
Resolution Patterns:
// Register missing service builder.Services.AddScoped<IMyService, MyService>();
// Break circular dependency with Lazy<T> or factory builder.Services.AddScoped<ServiceA>(); builder.Services.AddScoped<ServiceB>(sp => new ServiceB(() => sp.GetRequiredService<ServiceA>()));
// Fix captive dependency - make both singleton or use factory builder.Services.AddSingleton<IScopedService>(sp => sp.CreateScope().ServiceProvider.GetRequiredService<ScopedService>());
- Middleware Pipeline Issues
Symptoms:
-
Requests returning unexpected status codes
-
Authentication/authorization not working
-
CORS errors
-
Request body already read exceptions
-
Response already started exceptions
Common Causes:
// Wrong middleware order app.UseAuthorization(); // Must come AFTER UseAuthentication! app.UseAuthentication();
// Missing middleware app.UseRouting(); // Missing UseAuthentication() and UseAuthorization() app.MapControllers();
// Response already started app.Use(async (context, next) => { await context.Response.WriteAsync("Hello"); // Response started await next(); // Next middleware tries to modify headers - ERROR });
Debugging Steps:
- Add diagnostic middleware to trace pipeline:
app.Use(async (context, next) => { Console.WriteLine($"Request: {context.Request.Path}"); await next(); Console.WriteLine($"Response: {context.Response.StatusCode}"); });
-
Check middleware order matches recommended sequence
-
Enable developer exception page in development
-
Check for Response.HasStarted before modifying response
Correct Middleware Order:
app.UseExceptionHandler("/error"); app.UseHsts(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.UseSession(); app.MapControllers();
- Entity Framework Core Query Problems
Symptoms:
-
InvalidOperationException: The instance of entity type 'X' cannot be tracked
-
N+1 query problems (excessive database queries)
-
DbUpdateConcurrencyException
-
Lazy loading returning null
-
Query performance issues
Common Causes:
// N+1 problem var orders = context.Orders.ToList(); foreach (var order in orders) { Console.WriteLine(order.Customer.Name); // Each access = new query }
// Tracking conflict var entity = context.Products.Find(1); context.Products.Update(new Product { Id = 1 }); // Already tracked!
// Missing Include for related data var order = context.Orders.First(); // Customer is null!
Debugging Steps:
- Enable sensitive data logging:
optionsBuilder .EnableSensitiveDataLogging() .EnableDetailedErrors() .LogTo(Console.WriteLine, LogLevel.Information);
-
Use SQL Server Profiler or EF Core logging to inspect queries
-
Check for AsNoTracking() usage on read-only queries
-
Verify Include() statements for related entities
Resolution Patterns:
// Eager loading to prevent N+1 var orders = context.Orders .Include(o => o.Customer) .Include(o => o.OrderItems) .ToList();
// Use AsNoTracking for read-only queries var products = context.Products .AsNoTracking() .Where(p => p.Price > 100) .ToList();
// Handle concurrency with retry try { await context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); await entry.ReloadAsync(); // Retry or merge changes }
// Split queries for complex includes var orders = context.Orders .Include(o => o.OrderItems) .AsSplitQuery() .ToList();
- Configuration Binding Failures
Symptoms:
-
Configuration values are null or default
-
InvalidOperationException when binding to options
-
Environment-specific settings not loading
-
Secrets not being read
Common Causes:
// Mismatched property names public class MyOptions { public string ConnectionString { get; set; } // But appsettings has "connectionString" }
// Missing section services.Configure<MyOptions>(config.GetSection("NonExistent"));
// Wrong environment file // appsettings.Development.json not loading in Development
Debugging Steps:
- Log all configuration sources:
foreach (var source in builder.Configuration.Sources) { Console.WriteLine(source.GetType().Name); }
- Dump effective configuration:
Console.WriteLine(builder.Configuration.GetDebugView());
-
Verify ASPNETCORE_ENVIRONMENT is set correctly
-
Check file names match exactly (case-sensitive on Linux)
Resolution Patterns:
// Validate options on startup services.AddOptions<MyOptions>() .Bind(config.GetSection("MyOptions")) .ValidateDataAnnotations() .ValidateOnStart();
// Use strongly-typed configuration public class MyOptions { public const string SectionName = "MySettings";
[Required]
public string ApiKey { get; set; } = string.Empty;
[Range(1, 100)]
public int MaxRetries { get; set; } = 3;
}
// Load with validation builder.Services.AddOptions<MyOptions>() .BindConfiguration(MyOptions.SectionName) .ValidateDataAnnotations() .ValidateOnStart();
- Authentication/Authorization Issues
Symptoms:
-
401 Unauthorized when credentials are correct
-
403 Forbidden with correct roles
-
JWT token validation failures
-
Cookie authentication not persisting
-
Claims not available in controllers
Common Causes:
// Missing authentication scheme [Authorize] // No scheme specified, uses default public class MyController { }
// Wrong claim type for roles options.TokenValidationParameters = new TokenValidationParameters { RoleClaimType = "role", // But token has "roles" };
// Cookie not being sent (SameSite issues) options.Cookie.SameSite = SameSiteMode.Strict; // Blocks cross-site
Debugging Steps:
- Log authentication events:
services.AddAuthentication() .AddJwtBearer(options => { options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { Console.WriteLine($"Auth failed: {context.Exception}"); return Task.CompletedTask; }, OnTokenValidated = context => { Console.WriteLine($"Token validated for: {context.Principal?.Identity?.Name}"); return Task.CompletedTask; } }; });
-
Inspect JWT tokens at jwt.io
-
Check claims in controller: User.Claims.Select(c => $"{c.Type}: {c.Value}")
-
Verify HTTPS is configured correctly for secure cookies
Resolution Patterns:
// Configure JWT properly services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "your-issuer", ValidateAudience = true, ValidAudience = "your-audience", ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("your-secret-key-at-least-32-chars")), RoleClaimType = ClaimTypes.Role, NameClaimType = ClaimTypes.Name }; });
// Configure CORS for cookie auth services.AddCors(options => { options.AddPolicy("AllowFrontend", builder => builder.WithOrigins("https://frontend.com") .AllowCredentials() .AllowAnyHeader() .AllowAnyMethod()); });
- Startup and Hosting Failures
Symptoms:
-
502.5 Process Failure on IIS/Azure
-
Application fails to start with no clear error
-
HostAbortedException during startup
-
Port already in use errors
Common Causes:
// Missing required service var myService = app.Services.GetRequiredService<IMyService>(); // Throws on startup
// Database not available during startup await context.Database.MigrateAsync(); // DB not ready
// Incorrect program entry public static void Main(string[] args) { } // Missing async/await
Debugging Steps:
- Enable stdout logging for IIS:
<!-- web.config --> <aspNetCore processPath="dotnet" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" />
-
Check Windows Event Viewer for .NET Runtime errors
-
Run application directly from command line:
dotnet MyApp.dll --urls "http://localhost:5000"
- Use ASPNETCORE_DETAILEDERRORS=true environment variable
Resolution Patterns:
// Graceful startup with error handling try { var builder = WebApplication.CreateBuilder(args); // Configure services var app = builder.Build(); // Configure pipeline await app.RunAsync(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); } finally { Log.CloseAndFlush(); }
// Handle database not ready services.AddDbContext<AppDbContext>(options => { options.UseSqlServer(connectionString, sqlOptions => { sqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); }); });
Debugging Tools
Visual Studio Debugger
Essential Features:
-
Breakpoints: Conditional, hit count, filter by thread
-
Exception Settings: Break on specific exceptions (Debug > Windows > Exception Settings)
-
Immediate Window: Evaluate expressions during debugging
-
Diagnostic Tools: CPU, memory, events timeline
-
Hot Reload: Edit code during debugging (supported operations)
Advanced Techniques:
// Conditional breakpoint expression User.IsAuthenticated && Request.Path.StartsWithSegments("/api")
// Tracepoint (log without stopping) // Right-click breakpoint > Actions > Log a message "Request to {Request.Path} at {DateTime.Now}"
// DebuggerDisplay attribute for better visualization [DebuggerDisplay("Order {Id}: {Customer.Name} - ${Total}")] public class Order { }
Visual Studio Code Debugging
launch.json Configuration:
{ "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, "env": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:5001" } }, { "name": ".NET Core Attach", "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" } ] }
dotnet-trace
Performance tracing and diagnostics:
Install
dotnet tool install --global dotnet-trace
Collect trace from running process
dotnet-trace collect --process-id <PID>
Collect with specific providers
dotnet-trace collect -p <PID> --providers Microsoft-Extensions-Logging
Collect CPU profile
dotnet-trace collect -p <PID> --profile cpu-sampling
Convert to speedscope format for visualization
dotnet-trace convert trace.nettrace --format speedscope
dotnet-dump
Memory dump analysis:
Install
dotnet tool install --global dotnet-dump
Collect dump from running process
dotnet-dump collect -p <PID>
Analyze dump
dotnet-dump analyze dump.dmp
Common SOS commands in analyze mode
dumpheap -stat # Heap statistics dumpheap -type MyClass # Find specific type instances gcroot <address> # Find GC roots for object threadpool # Thread pool info eestack # Managed stack traces
dotnet-counters
Real-time performance monitoring:
Install
dotnet tool install --global dotnet-counters
Monitor default counters
dotnet-counters monitor -p <PID>
Monitor specific counters
dotnet-counters monitor -p <PID> --counters
System.Runtime,
Microsoft.AspNetCore.Hosting,
Microsoft-AspNetCore-Server-Kestrel
Export to CSV
dotnet-counters collect -p <PID> --format csv -o counters.csv
ILogger and Logging Frameworks
Built-in Logging Configuration:
// Program.cs builder.Logging .ClearProviders() .AddConsole() .AddDebug() .SetMinimumLevel(LogLevel.Debug);
// Filter by category builder.Logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning); builder.Logging.AddFilter("MyApp", LogLevel.Debug);
Structured Logging with Serilog:
// Install: Serilog.AspNetCore Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7) .WriteTo.Seq("http://localhost:5341") // Centralized logging .CreateLogger();
builder.Host.UseSerilog();
Logging Best Practices:
public class OrderService { private readonly ILogger<OrderService> _logger;
public async Task<Order> ProcessOrderAsync(int orderId)
{
// Use structured logging with meaningful context
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = orderId,
["CorrelationId"] = Activity.Current?.Id
}))
{
_logger.LogInformation("Processing order {OrderId}", orderId);
try
{
var order = await GetOrderAsync(orderId);
_logger.LogDebug("Order details: {@Order}", order);
return order;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}
}
}
}
Application Insights
Setup and Configuration:
// Install: Microsoft.ApplicationInsights.AspNetCore builder.Services.AddApplicationInsightsTelemetry();
// Configure in appsettings.json { "ApplicationInsights": { "ConnectionString": "InstrumentationKey=xxx;IngestionEndpoint=xxx" } }
Custom Telemetry:
public class PaymentService { private readonly TelemetryClient _telemetry;
public async Task ProcessPaymentAsync(Payment payment)
{
using var operation = _telemetry.StartOperation<RequestTelemetry>("ProcessPayment");
_telemetry.TrackEvent("PaymentStarted", new Dictionary<string, string>
{
["PaymentId"] = payment.Id.ToString(),
["Amount"] = payment.Amount.ToString("C")
});
try
{
// Process payment
_telemetry.TrackMetric("PaymentAmount", (double)payment.Amount);
}
catch (Exception ex)
{
_telemetry.TrackException(ex);
operation.Telemetry.Success = false;
throw;
}
}
}
The Four Phases of ASP.NET Core Debugging
Phase 1: Reproduce and Isolate
Goals: Consistently reproduce the issue and narrow down the scope.
Actions:
-
Capture exact steps to reproduce
-
Note environment details (OS, .NET version, configuration)
-
Check if issue occurs in Development vs Production
-
Isolate to specific endpoint, service, or component
Commands:
Check .NET version and environment
dotnet --info
Verify configuration
dotnet run --environment Development
Test specific endpoint
curl -v https://localhost:5001/api/health
Check if issue is environment-specific
ASPNETCORE_ENVIRONMENT=Development dotnet run ASPNETCORE_ENVIRONMENT=Production dotnet run
Phase 2: Gather Evidence
Goals: Collect logs, traces, and diagnostic data.
Actions:
-
Enable verbose logging
-
Capture request/response details
-
Monitor performance counters
-
Collect memory dumps if needed
Configuration for Maximum Diagnostics:
// appsettings.Development.json { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft.AspNetCore": "Debug", "Microsoft.EntityFrameworkCore": "Debug", "Microsoft.EntityFrameworkCore.Database.Command": "Information" } } }
Middleware for Request Logging:
app.Use(async (context, next) => { var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogDebug("Request: {Method} {Path} {Query}",
context.Request.Method,
context.Request.Path,
context.Request.QueryString);
// Capture request body (careful with large bodies)
context.Request.EnableBuffering();
var stopwatch = Stopwatch.StartNew();
await next();
stopwatch.Stop();
logger.LogDebug("Response: {StatusCode} in {ElapsedMs}ms",
context.Response.StatusCode,
stopwatch.ElapsedMilliseconds);
});
Phase 3: Analyze and Hypothesize
Goals: Form theories based on evidence and test them.
Common Analysis Patterns:
For DI Errors:
// List all registered services foreach (var service in builder.Services) { Console.WriteLine($"{service.ServiceType.Name} -> {service.ImplementationType?.Name} ({service.Lifetime})"); }
For EF Core Issues:
// Log generated SQL optionsBuilder.LogTo( message => Debug.WriteLine(message), new[] { DbLoggerCategory.Database.Command.Name }, LogLevel.Information);
For Authentication Issues:
// Dump all claims app.Use(async (context, next) => { if (context.User.Identity?.IsAuthenticated == true) { foreach (var claim in context.User.Claims) { Console.WriteLine($"Claim: {claim.Type} = {claim.Value}"); } } await next(); });
Phase 4: Fix and Verify
Goals: Implement fix with confidence and prevent regression.
Best Practices:
-
Write a failing test that reproduces the bug
-
Implement the minimal fix
-
Verify the test passes
-
Check for side effects
-
Add logging/monitoring for the fixed code path
Example Test-Driven Fix:
[Fact] public async Task ProcessOrder_WhenCustomerNotFound_ThrowsNotFoundException() { // Arrange - This test should fail before fix var mockRepo = new Mock<ICustomerRepository>(); mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>())) .ReturnsAsync((Customer)null);
var service = new OrderService(mockRepo.Object);
// Act & Assert
await Assert.ThrowsAsync<CustomerNotFoundException>(
() => service.ProcessOrderAsync(1, customerId: 999));
}
Quick Reference Commands
Build and Run
Build project
dotnet build
Build in Release mode
dotnet build -c Release
Run with hot reload
dotnet watch run
Run specific project
dotnet run --project src/MyApp/MyApp.csproj
Run with specific environment
ASPNETCORE_ENVIRONMENT=Development dotnet run
Publish for deployment
dotnet publish -c Release -o ./publish
Entity Framework Core
List migrations
dotnet ef migrations list
Add new migration
dotnet ef migrations add AddUserTable
Update database
dotnet ef database update
Generate SQL script
dotnet ef migrations script --idempotent
Remove last migration
dotnet ef migrations remove
Drop database
dotnet ef database drop --force
Scaffold from existing database
dotnet ef dbcontext scaffold "Server=.;Database=MyDb;Trusted_Connection=True" Microsoft.EntityFrameworkCore.SqlServer
Testing
Run all tests
dotnet test
Run with verbosity
dotnet test -v detailed
Run specific test
dotnet test --filter "FullyQualifiedName~OrderServiceTests"
Run with coverage
dotnet test --collect:"XPlat Code Coverage"
Generate coverage report
reportgenerator -reports:coverage.cobertura.xml -targetdir:coveragereport
Diagnostics
List running .NET processes
dotnet-trace ps
Collect trace
dotnet-trace collect -p <PID> --duration 00:00:30
Monitor counters
dotnet-counters monitor -p <PID>
Collect memory dump
dotnet-dump collect -p <PID>
Analyze dump
dotnet-dump analyze core_dump.dmp
GC dump
dotnet-gcdump collect -p <PID>
Package Management
Add package
dotnet add package Serilog.AspNetCore
Remove package
dotnet remove package OldPackage
List packages
dotnet list package
List outdated packages
dotnet list package --outdated
Restore packages
dotnet restore
Clear NuGet cache
dotnet nuget locals all --clear
Secrets Management
Initialize user secrets
dotnet user-secrets init
Set a secret
dotnet user-secrets set "ConnectionStrings:Default" "Server=..."
List secrets
dotnet user-secrets list
Remove a secret
dotnet user-secrets remove "ConnectionStrings:Default"
Clear all secrets
dotnet user-secrets clear
Troubleshooting Checklist
Before Debugging
-
Reproduce the issue consistently
-
Check if issue occurs in Development mode
-
Verify .NET SDK/runtime version matches
-
Confirm database is accessible
-
Check environment variables are set
During Debugging
-
Enable detailed error pages (app.UseDeveloperExceptionPage() )
-
Set logging to Debug level
-
Check for null reference exceptions
-
Verify DI service registrations
-
Inspect middleware pipeline order
-
Check EF Core generated SQL
-
Verify authentication claims
After Fixing
-
Write test to prevent regression
-
Update logging for the fixed code path
-
Document the issue and solution
-
Check for similar issues elsewhere
-
Consider adding health checks
Resources
-
ASP.NET Core Documentation
-
Entity Framework Core Documentation
-
.NET Diagnostics Documentation
-
Serilog Documentation
-
Application Insights Documentation