Intermediate

Debugging and Error Handling in ASP.NET Core

Debugging tools, error-handling middleware, Problem Details, custom exceptions and remote debugging.

Created with: .NET 6 and C#, Visual Studio Community / Enterprise
Document enriched with advanced .NET 6/7/8+ concepts

Table of Contents

  1. Course Overview
  2. Understanding Debugging
  3. Server-Side Debugging Tools
  4. Client-Side Debugging Tools
  5. Error Handling in ASP.NET Core
  6. Error Handling Middleware
  7. Problem Details (RFC 7807)
  8. Custom Exception Hierarchy
  9. Exception Filters vs Middleware
  10. Status Code Pages
  11. Structured Logging and Correlation
  12. Advanced Visual Studio Debugging
  13. Remote Debugging — Azure App Service
  14. .NET Diagnostic CLI Tools
  15. Application Insights and Error Monitoring
  16. Testing Error Handling
  17. Advanced Scenarios
  18. Best Practices and Common Pitfalls
  19. Review Questions

1. Course Overview

This course covers finding problems in ASP.NET code and handling them through effective exception management strategies.

Main Objectives

  • Debug the server side of ASP.NET
  • Debug the client side (JavaScript, TypeScript, CSS)
  • Handle errors effectively, without masking them
  • Understand the dangers of poor error handling
  • Implement a global error handler
  • Integrate logging with error handling
  • Use .NET diagnostic CLI tools
  • Monitor errors in production with Application Insights

ASP.NET Core Error Handling Pipeline

graph TD
    A[HTTP Request] --> B[Middleware Pipeline]
    B --> C{Environment?}
    C -->|Development| D[Developer Exception Page\nFull stack trace]
    C -->|Production| E[Exception Handler Middleware\nUseExceptionHandler]
    E --> F[Custom Error Endpoint\n/error or /Error]
    F --> G[Log the exception]
    G --> H[Notify the team]
    H --> I[Return secure response\nProblem Details RFC 7807]
    D --> J[Display technical details\nStack trace, variables, etc.]

2. Understanding Debugging

2.1 What is Debugging?

Debugging is the verification of assumptions on which code relies.

Debugging is necessary because our assumptions about the environment or data are wrong. It is not an indication that the developer did a poor job — it is inherent to software development.

Key quote from astronaut Frank Borman: “A failure of imagination” — often, the bug comes from not imagining that a file might not exist, or that a nested property could be null.

2.2 Why Debugging is Necessary

graph TD
    A[Debugging needed because...] --> B[False premises\nThe developer thought they covered\nall possible cases]
    A --> C[Unexpected data\nUnexpected null value]
    A --> D[Environment change\nFile not found, service unavailable]
    
    B --> E[Debugging goal:\nFind which assumption\nwas wrong]
    C --> E
    D --> E

2.3 Occam’s Razor Applied to Debugging

William of Ockham: “Numquam ponenda est pluralitas sine necessitate” — Never propose multiple premises when one is sufficient.

Practical application: Structure code to minimize the number of assumptions to track at once. This is achieved through:

  • Encapsulation: isolate responsibilities in distinct functions
  • Decomposition: break large functions into small, focused functions
  • Unit tests: allow testing each assumption individually

2.4 Demo: Refactoring a Monolithic Function

Before — Code That is Difficult to Debug

// Everything is mixed: file loading, Word parsing, mapping
public IEnumerable<DocumentCard> GetDocuments(string folderPath)
{
    return Directory.GetFiles(folderPath, "*.docx")
        .Select(file =>
        {
            using var doc = WordprocessingDocument.Open(file, false);
            var body = doc.MainDocumentPart?.Document.Body;
            var title = body?.Descendants<Title>().FirstOrDefault()?.InnerText;
            var text = body?.InnerText;
            return new DocumentCard { Title = title, Contents = text, Length = text?.Length ?? 0 };
        });
}

Problems:

  • File handling + Word parsing + mapping all in one function
  • Hard to isolate a specific bug
  • New dependency (Open XML) in the web layer

After — Decomposed and Testable Code

// Function 1: Load file names
private IEnumerable<string> GetDocumentFiles(string folderPath)
{
    return Directory.GetFiles(folderPath, "*.docx");
}

// Function 2: Read a Word document
private WordDocumentCard ParseWordDocument(string filePath)
{
    return new WordDocumentCard(filePath);
}

// Main function: clear orchestration
public IEnumerable<DocumentCard> GetDocuments(string folderPath)
{
    var fileNames = GetDocumentFiles(folderPath);
    var cards = new List<DocumentCard>();
    foreach (var file in fileNames)
    {
        var wordDoc = ParseWordDocument(file);
        cards.Add(wordDoc);
    }
    return cards;
}

Golden rule of debugging: When debugging, always ask yourself: “What assumption am I verifying?” If you don’t have a clear answer, stop and think.


3. Server-Side Debugging Tools

3.1 The Interactive Debugger

Quote from Why Programs Fail by Andreas Zeller: “Interactive debuggers have a certain playful quality that can easily distract from solving the problem.”

This playful quality can lead to debugger hypnosis — you end up stepping through code without really knowing why.

Solution: Peripatetic debugging (get up, walk, think away from the screen).

3.2 Breakpoints

Placement Strategy

graph TD
    A["No idea\nabout the cause"] --> B["Place the breakpoint\nat the highest entry point"]
    B --> C["Inspect values\nat this level"]
    C --> D{"Bug found?"}
    D -->|No| E["Place a breakpoint\ndeeper in the stack"]
    D -->|Yes| F["Fix and remove\nthe breakpoints"]
    E --> C

Best Practices

  • Breakpoints should never go into source control
  • Visual Studio stores breakpoints in the .suo file (ignored by git)
  • Avoid having dozens of simultaneous breakpoints
  • Remove breakpoints once the bug is identified

Advanced Breakpoint Types

Conditional breakpoint:
  → Triggers only when a condition is true
  → Ex: trigger only if document.Id == 42

Action breakpoint (Tracepoint):
  → Prints a message to the output window without stopping
  → Useful for tracing execution without interrupting flow

Hit Count breakpoint:
  → Triggers after N passes

Data Breakpoint:
  → Triggers when a variable's value changes
  → Very useful for tracking unexpected mutations

Import / Export breakpoints:
  → Useful for sharing debugging configurations (rare)

3.3 The Relationship Between Debugging and Testing

graph LR
    Debugging["Debugging"] <-->|complement| Testing["Testing"]
    ErrorHandling["Error Handling"] <-->|complement| Logging["Logging"]

The problem of code entrancy:

To debug, you need to easily access the problematic function. This becomes difficult when:

  • A database must be configured on the local machine
  • Long operations precede the bug location
  • The state causing the problem is hard to reproduce
  • Authentication is required

Solution: Unit tests allow direct access to the problematic function with the exact state needed.

Demo: Test + Debugging

[Fact]
public void DocumentTitleIsFromProudDocument()
{
    var parser = new WordDocumentParser();
    var doc = parser.Parse("TestDocuments/ProudAchievement.docx");
    Assert.Equal("A Proud Achievement", doc.Title);
}

4. Client-Side Debugging Tools

4.1 ASP.NET Core and HTTP

sequenceDiagram
    participant Browser as Browser
    participant Kestrel as Kestrel Server
    participant Razor as ASP.NET Core Code

    Browser->>Kestrel: GET /home (HTTP verb)
    Note over Browser,Kestrel: TCP connection opened
    Kestrel->>Razor: HTTP context passed
    Razor->>Razor: Code execution\n(model, view, controller)
    Razor-->>Kestrel: HTML + CSS + JS stream
    Kestrel-->>Browser: HTTP response (200 OK + content)
    Note over Browser: Browser parses\nHTML and loads assets

4.2 Browser Network Tools (DevTools)

To access DevTools: F12 or right-click → Inspect

Network Tab — Key Points

Useful filters:
  Doc      → Full HTML pages
  JS       → JavaScript files
  CSS      → Stylesheets
  XHR/Fetch → API requests (AJAX calls)
  WS       → WebSockets

For each request:
  Headers  → Metadata (Content-Type, cookies, etc.)
  Preview  → Preview of parsed response
  Response → Raw response (HTML or JSON)
  Initiator → Which line of code triggered the request
  Timing   → Detailed loading time

Browser Link creates a SignalR channel between Visual Studio and the browser. It masks the real client-server communication and hides request and content transformation details that are often the source of problems.


4.3 JavaScript and TypeScript Debugging

Using the Console

// JavaScript logging levels (from least to most urgent)
console.log("General information");
console.debug("Debug information");
console.info("Standard information");
console.warn("Warning");
console.error("Critical error"); // Displays in red

// Displaying objects
console.log("Received data:", myObject);
console.table(arrayOfObjects); // Table display
console.group("Group name");
console.groupEnd();

JavaScript Debugging Strategy

$(document).ready(function() {
    console.info("Document ready — handler triggered correctly");
    
    const documentElements = document.querySelectorAll('.document-item');
    console.info(`Elements found: ${documentElements.length}`);
    
    const cards = [];
    documentElements.forEach(el => {
        const id = el.getAttribute('data-document-id');
        const contents = el.getAttribute('data-document-contents');
        console.debug(`Processing document ID=${id}, length=${contents?.length}`);
        cards.push(new DocumentCard(id, contents));
    });
    
    console.info("Cards created:", cards);
    displayCards(cards);
});

TypeScript and Source Maps

// tsconfig.json — Transpilation configuration
{
    "compilerOptions": {
        "sourceMap": true,
        "outDir": "wwwroot/js",
        "target": "es6"
    },
    "exclude": ["node_modules", "wwwroot"],
    "include": ["scripts"]
}

Important: Put .ts files in source control, not the generated .js files.


4.4 CSS Debugging

graph TD
    A[CSS Problem] --> B{Problem type?}
    B -->|Style not applied| C[Check selector\nspecificity]
    B -->|Style from wrong source| D[Check stylesheet\norder]
    B -->|Missing CSS file| E[Check 404 error\nin Network tab]
    
    C --> F[DevTools → Elements tab\nHover over the element]
    D --> F
    E --> G[Fix the path\nin the layout]

The asp-append-version attribute adds a hash to the filename to invalidate the browser cache on updates.


5. Error Handling in ASP.NET Core

5.1 The Common Approach is Wrong

Exception handling must be exceptional. Normal execution flow should not involve exceptions.

The Galactic Anti-Pattern

// BAD CODE — Never do this
try
{
    // All code here
}
catch (Exception ex)
{
    // Swallow everything, silently hide everything
}

Why this is dangerous: If an error occurs and is swallowed, the code continues executing in a potentially corrupt state without anyone knowing.

5.2 The Horror Story (True Story)

A metabolic screening system had a universal query engine. The data part and the filter part of the SQL query were built separately. A silent exception removed the WHERE clause, returning all patient data to a user who should only see their own.

The cause: An overly broad try/catch that hid the query construction error.


5.3 Choosing the Right Place to Handle Exceptions

graph TD
    A[Exception raised here] --> B{Can the problem\nbe resolved\nat this level?}
    B -->|Yes| C[Handle the exception\nat this level\nEx: retry if network error]
    B -->|No| D[Let the exception\nbubble up the stack]
    D --> E{Upper level?\nCan it resolve?}
    E -->|Yes| F[Handle here]
    E -->|No| G[Let it bubble up further]
    G --> H[Global handler\ntop-level]
    H --> I[Log + Notify + Respond cleanly]

Handle via Prior Check (Best Approach)

// BETTER: Check before acting, not catch the exception after
public IEnumerable<DocumentCard> GetDocuments(string folderPath, string userId)
{
    var files = GetDocumentFiles(folderPath);
    var permittedFiles = files.Where(f => DocumentIsPermittedForCurrentUser(f, userId));
    
    var cards = new List<DocumentCard>();
    foreach (var file in permittedFiles)
    {
        cards.Add(new WordDocumentCard(file));
    }
    return cards;
}

Handle a Specific Exception (Acceptable)

// ACCEPTABLE: Catch a SPECIFIC exception when expected
private bool DocumentIsPermittedForCurrentUser(string filePath, ILogger logger)
{
    try
    {
        return File.Exists(filePath);
    }
    catch (FileNotFoundException ex)
    {
        // Specific exception, not a general one
        logger.LogInformation("File not found: {FilePath}", filePath);
        logger.LogError(ex, "Error checking file");
        return false;
        // Note: no generic catch(Exception)!
    }
}

5.4 Effective Error Logging

graph LR
    Code["Application Code"] --> Manifold["Logging manifold\n(ILogger abstraction)"]
    Manifold --> Console["Console\n(development)"]
    Manifold --> File["File\n(production)"]
    Manifold --> DB["Database\n(production)"]
    Manifold --> Cloud["Cloud service\n(e.g. Application Insights)"]

Configuration in ASP.NET Core

// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
// appsettings.Development.json — More verbose in development
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft.AspNetCore": "Debug"
    }
  }
}

Injecting and Using the Logger

public class DocumentService
{
    private readonly ILogger<DocumentService> _logger;

    public DocumentService(ILogger<DocumentService> logger)
    {
        _logger = logger;
    }

    public IEnumerable<DocumentCard> GetDocuments(string folderPath, string userId)
    {
        // Structured message template — do NOT use string interpolation
        _logger.LogInformation(
            "Loading documents from {FolderPath} for {UserId}",
            folderPath, userId);

        foreach (var file in GetDocumentFiles(folderPath))
        {
            _logger.LogDebug("Processing file {FileName}", file);
        }
    }
}

Logging rule: Logging code lives forever in the codebase. Simply adjust the logging level to filter them.


5.5 Global Error Handler (top-level)

4 Qualities of a Good Global Error Handler

  1. Log the exception
  2. Notify (email, Slack, etc.)
  3. Inform the user securely
  4. Be simple and resilient — the handler itself must never crash

Configuration in Program.cs

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
}

Error Page (Error.cshtml.cs)

public class ErrorModel : PageModel
{
    private readonly ILogger<ErrorModel> _logger;

    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public ErrorModel(ILogger<ErrorModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        
        var exDetail = HttpContext.Features.Get<IExceptionHandlerFeature>()
                       as ExceptionHandlerFeature;

        if (exDetail?.Error != null)
        {
            // 1. Log the exception
            _logger.LogError(exDetail.Error, "Unhandled error");
            // 2. Notify the team
            NotifyTeam(exDetail.Error);
        }
    }

    private void NotifyTeam(Exception ex)
    {
        // Implement: email, Slack, PagerDuty, etc.
        // Keep this method SIMPLE so it can never crash
    }

    private bool ErrorIsIgnorable(int statusCode)
    {
        return statusCode == 404 || statusCode == 303;
    }
}

6. Error Handling Middleware

6.1 Developer Exception Page

The Developer Exception Page is automatically enabled in the development environment. It displays:

  • The complete stack trace
  • Local variables at the time of the exception
  • HTTP headers from the request
  • Cookies and request parameters
// Customizing the Developer Exception Page
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] =
            Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;
        context.ProblemDetails.Extensions["environment"] =
            context.HttpContext.RequestServices
                .GetRequiredService<IWebHostEnvironment>().EnvironmentName;
    };
});

6.2 UseExceptionHandler — Production Middleware

sequenceDiagram
    participant Client
    participant ExMiddleware as Exception Handler Middleware
    participant Pipeline as Rest of Pipeline
    participant ErrorPage as /error endpoint

    Client->>ExMiddleware: HTTP Request
    ExMiddleware->>Pipeline: Pass the request
    Pipeline-->>ExMiddleware: Exception raised
    ExMiddleware->>ExMiddleware: Capture the exception
    ExMiddleware->>ErrorPage: Re-execute with /error
    ErrorPage-->>ExMiddleware: Secure error response
    ExMiddleware-->>Client: HTTP 500 response

Pattern 1 — Redirect to a Razor Page

app.UseExceptionHandler("/Error");

Pattern 2 — Inline Lambda (Minimal APIs)

app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        var feature = context.Features.Get<IExceptionHandlerPathFeature>();
        var exception = feature?.Error;
        var logger = context.RequestServices
            .GetRequiredService<ILogger<Program>>();

        logger?.LogError(exception, "Unhandled exception on {Path}", feature?.Path);

        context.Response.StatusCode = exception switch
        {
            NotFoundException => StatusCodes.Status404NotFound,
            ValidationException => StatusCodes.Status400BadRequest,
            UnauthorizedException => StatusCodes.Status403Forbidden,
            _ => StatusCodes.Status500InternalServerError
        };

        context.Response.ContentType = "application/problem+json";

        var problemDetails = new ProblemDetails
        {
            Status = context.Response.StatusCode,
            Title = GetTitle(exception),
            Detail = exception?.Message,
            Instance = context.Request.Path
        };
        problemDetails.Extensions["traceId"] =
            Activity.Current?.Id ?? context.TraceIdentifier;

        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});

static string GetTitle(Exception? ex) => ex switch
{
    NotFoundException => "Resource not found",
    ValidationException => "Invalid data",
    UnauthorizedException => "Access denied",
    _ => "An internal error occurred"
};

Pattern 3 — Dedicated Endpoint with Controller

// In Program.cs
app.UseExceptionHandler("/api/error");

[ApiController]
[Route("api/error")]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorController : ControllerBase
{
    private readonly ILogger<ErrorController> _logger;

    public ErrorController(ILogger<ErrorController> logger)
    {
        _logger = logger;
    }

    [Route("")]
    public IActionResult HandleError()
    {
        var feature = HttpContext.Features.Get<IExceptionHandlerFeature>();
        var exception = feature?.Error;

        _logger.LogError(exception, "Unhandled error");

        return exception switch
        {
            NotFoundException ex => Problem(
                statusCode: 404, title: "Resource not found", detail: ex.Message),
            ValidationException ex => ValidationProblem(ex.Errors),
            _ => Problem(statusCode: 500, title: "Internal server error")
        };
    }
}

6.3 Global Handling in Minimal APIs (.NET 7+)

// IExceptionHandler interface — .NET 7+
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Exception: {Message}", exception.Message);

        var (statusCode, title) = exception switch
        {
            NotFoundException => (StatusCodes.Status404NotFound, "Not Found"),
            ValidationException => (StatusCodes.Status400BadRequest, "Bad Request"),
            UnauthorizedException => (StatusCodes.Status403Forbidden, "Forbidden"),
            _ => (StatusCodes.Status500InternalServerError, "Internal Server Error")
        };

        httpContext.Response.StatusCode = statusCode;

        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = exception.Message,
            Type = $"https://httpstatuses.com/{statusCode}"
        };

        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
        return true;
    }
}

// Registration in Program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();

7. Problem Details (RFC 7807)

The RFC 7807 standard defines a unified JSON format for HTTP error responses. It is now the recommended standard for .NET REST APIs.

7.1 Problem Details Structure

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Resource not found",
  "status": 404,
  "detail": "The document with ID 42 does not exist.",
  "instance": "/api/documents/42",
  "traceId": "00-a9c2d7e3f4b5c6a7-b8d9e0f1a2b3c4d5-00",
  "errors": {
    "title": ["Title is required"],
    "content": ["Content cannot exceed 10,000 characters"]
  }
}

7.2 Complete Implementation with IProblemDetailsService

// Program.cs — Configuration
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] =
            Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;
        context.ProblemDetails.Extensions["nodeId"] = Environment.MachineName;
        
        // Do not expose details in production
        if (!context.HttpContext.RequestServices
                .GetRequiredService<IWebHostEnvironment>().IsDevelopment())
        {
            context.ProblemDetails.Detail = null;
        }
    };
});
// Factory helper for creating consistent ProblemDetails
public static class ProblemDetailsFactory
{
    public static ProblemDetails CreateNotFound(string resourceType, object id) =>
        new ProblemDetails
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
            Title = "Resource not found",
            Status = 404,
            Detail = $"{resourceType} with identifier '{id}' was not found."
        };

    public static ValidationProblemDetails CreateValidationError(
        IDictionary<string, string[]> errors) =>
        new ValidationProblemDetails(errors)
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            Title = "One or more validation errors occurred.",
            Status = 400
        };

    public static ProblemDetails CreateConflict(string detail) =>
        new ProblemDetails
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
            Title = "Resource conflict",
            Status = 409,
            Detail = detail
        };
}

7.3 Usage in Controllers

[ApiController]
[Route("api/[controller]")]
public class DocumentsController : ControllerBase
{
    private readonly IDocumentService _documentService;

    public DocumentsController(IDocumentService documentService)
    {
        _documentService = documentService;
    }

    [HttpGet("{id}")]
    [ProducesResponseType<DocumentDto>(StatusCodes.Status200OK)]
    [ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetDocument(int id)
    {
        var document = await _documentService.GetByIdAsync(id);

        if (document is null)
        {
            return Problem(
                statusCode: StatusCodes.Status404NotFound,
                title: "Document not found",
                detail: $"The document with ID {id} does not exist.",
                instance: HttpContext.Request.Path);
        }

        return Ok(document);
    }

    [HttpPost]
    [ProducesResponseType<DocumentDto>(StatusCodes.Status201Created)]
    [ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> CreateDocument([FromBody] CreateDocumentDto dto)
    {
        // Invalid ModelState → ValidationProblemDetails automatically thanks to [ApiController]
        var created = await _documentService.CreateAsync(dto);
        return CreatedAtAction(nameof(GetDocument), new { id = created.Id }, created);
    }
}

8. Custom Exception Hierarchy

8.1 Hierarchy Design

classDiagram
    Exception <|-- AppException
    AppException <|-- BusinessException
    AppException <|-- InfrastructureException
    BusinessException <|-- ValidationException
    BusinessException <|-- NotFoundException
    BusinessException <|-- ConflictException
    BusinessException <|-- UnauthorizedException
    BusinessException <|-- ForbiddenException
    InfrastructureException <|-- DatabaseException
    InfrastructureException <|-- ExternalServiceException

    class AppException {
        +string ErrorCode
        +int HttpStatusCode
    }
    class ValidationException {
        +IDictionary Errors
        +HttpStatusCode 400
    }
    class NotFoundException {
        +string ResourceType
        +object ResourceId
        +HttpStatusCode 404
    }
    class ConflictException {
        +HttpStatusCode 409
    }
    class UnauthorizedException {
        +HttpStatusCode 401
    }
    class ForbiddenException {
        +HttpStatusCode 403
    }
    class ExternalServiceException {
        +string ServiceName
        +HttpStatusCode 502
    }

8.2 Exception Class Implementations

// Base application exception
public abstract class AppException : Exception
{
    public string ErrorCode { get; }
    public virtual int HttpStatusCode => 500;

    protected AppException(string message, string errorCode, Exception? innerException = null)
        : base(message, innerException)
    {
        ErrorCode = errorCode;
    }
}

// Generic business exception
public class BusinessException : AppException
{
    public override int HttpStatusCode => 400;

    public BusinessException(string message, string errorCode = "BUSINESS_ERROR")
        : base(message, errorCode) { }
}

// Validation exception with per-field detail
public class ValidationException : BusinessException
{
    public IDictionary<string, string[]> Errors { get; }

    public ValidationException(IDictionary<string, string[]> errors)
        : base("One or more validation errors occurred.", "VALIDATION_ERROR")
    {
        Errors = errors;
    }

    public ValidationException(string field, string message)
        : this(new Dictionary<string, string[]> { { field, [message] } }) { }
}

// Resource not found exception (404)
public class NotFoundException : BusinessException
{
    public string ResourceType { get; }
    public object? ResourceId { get; }
    public override int HttpStatusCode => 404;

    public NotFoundException(string resourceType, object? resourceId = null)
        : base($"{resourceType} '{resourceId}' was not found.", "NOT_FOUND")
    {
        ResourceType = resourceType;
        ResourceId = resourceId;
    }
}

// Conflict exception (409)
public class ConflictException : BusinessException
{
    public override int HttpStatusCode => 409;
    public ConflictException(string message) : base(message, "CONFLICT") { }
}

// Authentication exception (401)
public class UnauthorizedException : BusinessException
{
    public override int HttpStatusCode => 401;
    public UnauthorizedException(string message = "Authentication required.")
        : base(message, "UNAUTHORIZED") { }
}

// Authorization exception (403)
public class ForbiddenException : BusinessException
{
    public override int HttpStatusCode => 403;
    public ForbiddenException(string message = "Access denied.")
        : base(message, "FORBIDDEN") { }
}

// External service exception (502)
public class ExternalServiceException : AppException
{
    public string ServiceName { get; }
    public override int HttpStatusCode => 502;

    public ExternalServiceException(string serviceName, string message, Exception? inner = null)
        : base(message, "EXTERNAL_SERVICE_ERROR", inner)
    {
        ServiceName = serviceName;
    }
}

8.3 Usage in the Service Layer

public class DocumentService : IDocumentService
{
    private readonly IDocumentRepository _repository;

    public DocumentService(IDocumentRepository repository)
    {
        _repository = repository;
    }

    public async Task<DocumentDto> GetByIdAsync(int id)
    {
        var document = await _repository.GetByIdAsync(id);

        if (document is null)
            throw new NotFoundException(nameof(Document), id);

        return document.ToDto();
    }

    public async Task<DocumentDto> CreateAsync(CreateDocumentDto dto)
    {
        var errors = new Dictionary<string, string[]>();

        if (string.IsNullOrWhiteSpace(dto.Title))
            errors["title"] = ["Title is required."];

        if (dto.Content?.Length > 10_000)
            errors["content"] = ["Content cannot exceed 10,000 characters."];

        if (errors.Count > 0)
            throw new ValidationException(errors);

        var existing = await _repository.GetByTitleAsync(dto.Title);
        if (existing is not null)
            throw new ConflictException($"A document named '{dto.Title}' already exists.");

        var created = await _repository.CreateAsync(dto.ToEntity());
        return created.ToDto();
    }
}

9. Exception Filters vs Middleware

9.1 Comparison of Approaches

graph LR
    subgraph "Middleware Pipeline"
        MW1[UseExceptionHandler] --> MW2[UseRouting]
        MW2 --> MW3[UseAuthentication]
        MW3 --> MW4[UseAuthorization]
        MW4 --> MW5[UseEndpoints]
    end

    subgraph "MVC Pipeline"
        MW5 --> F1[Exception Filters]
        F1 --> F2[Action Filters]
        F2 --> F3[Action Method]
    end
FeatureException FilterMiddleware
ScopeMVC/Razor Pages onlyAll HTTP requests
MVC context accessYes (ActionContext)No
Minimal APIsNoYes
Execution orderAfter routing/authBefore everything else
Recommended forMVC-specific logicUniversal global handling

9.2 Exception Filter — Implementation

public class ApiExceptionFilter : IExceptionFilter
{
    private readonly ILogger<ApiExceptionFilter> _logger;

    public ApiExceptionFilter(ILogger<ApiExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        _logger.LogError(context.Exception, "Exception in {ActionName}",
            context.ActionDescriptor.DisplayName);

        var (statusCode, problemDetails) = context.Exception switch
        {
            ValidationException ex => (400, (ProblemDetails)new ValidationProblemDetails(ex.Errors)
            {
                Title = "Validation errors",
                Status = 400
            }),
            NotFoundException ex => (404, new ProblemDetails
            {
                Title = "Not found",
                Detail = ex.Message,
                Status = 404
            }),
            ForbiddenException ex => (403, new ProblemDetails
            {
                Title = "Access denied",
                Detail = ex.Message,
                Status = 403
            }),
            _ => (500, new ProblemDetails
            {
                Title = "Internal server error",
                Status = 500
            })
        };

        context.Result = new ObjectResult(problemDetails) { StatusCode = statusCode };
        context.ExceptionHandled = true;
    }
}

// Global registration in Program.cs
builder.Services.AddControllers(options =>
{
    options.Filters.Add<ApiExceptionFilter>();
});

9.3 When to Use Each Approach

// Use Middleware for:
// - Global handling across the entire application (including Minimal APIs)
// - Centralized logging
// - Uniform response transformation
app.UseExceptionHandler(builder => { /* ... */ });

// Use Exception Filter for:
// - MVC controller-specific logic
// - Access to ActionContext (action name, etc.)
// - Reuse via attribute on a specific controller
[ServiceFilter(typeof(ApiExceptionFilter))]
public class DocumentsController : ControllerBase { }

10. Status Code Pages

10.1 UseStatusCodePages

Intercepts responses with an error status code without a response body (4xx, 5xx).

// Simple text response — useful for debugging
app.UseStatusCodePages();
// Returns: "Status Code: 404; Not Found"

// Custom response with lambda
app.UseStatusCodePages(async statusCodeContext =>
{
    statusCodeContext.HttpContext.Response.ContentType = "text/plain";
    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Error {statusCodeContext.HttpContext.Response.StatusCode}");
});

10.2 UseStatusCodePagesWithRedirects

// Temporary redirect (302) to an error page
app.UseStatusCodePagesWithRedirects("/errors/{0}");
// WARNING: the final status code becomes 200 (OK)!
// Avoid for REST APIs
// Re-executes the pipeline WITHOUT changing the HTTP status code
app.UseStatusCodePagesWithReExecute("/errors/{0}");

app.MapGet("/errors/{statusCode}", (int statusCode, HttpContext context) =>
{
    var title = statusCode switch
    {
        400 => "Bad request",
        401 => "Not authenticated",
        403 => "Access denied",
        404 => "Page not found",
        429 => "Too many requests",
        500 => "Server error",
        _ => "Error"
    };

    return Results.Problem(
        title: title,
        statusCode: statusCode,
        instance: context.Request.Path);
});

10.4 Comparison of Three Methods

MethodFinal HTTP CodeResponse BodyUse
UseStatusCodePagesUnchangedSimple textDebug only
UseStatusCodePagesWithRedirects200 (!)HTML or JSONWeb apps (legacy)
UseStatusCodePagesWithReExecuteUnchangedHTML or JSONRecommended

11. Structured Logging and Correlation

11.1 Structured Logging with ILogger

// BAD: text logging — hard to query
_logger.LogInformation($"User {userId} accessed document {docId}");

// GOOD: structured logging — each property is indexed separately
_logger.LogInformation(
    "User {UserId} accessed document {DocumentId}",
    userId, docId);

Structured logging enables queries like:

  • WHERE UserId = 'user-42'
  • WHERE DocumentId = 123 AND Level = 'Error'

11.2 Serilog — Advanced Configuration

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.ApplicationInsights
// Program.cs — Serilog Configuration
using Serilog;
using Serilog.Events;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithMachineName()
    .Enrich.WithEnvironmentName()
    .WriteTo.Console(outputTemplate:
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} " +
        "{Properties:j}{NewLine}{Exception}")
    .WriteTo.File(
        path: "logs/app-.log",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30)
    .WriteTo.ApplicationInsights(TelemetryConverter.Traces)
    .CreateLogger();

builder.Host.UseSerilog();

11.3 Correlation IDs — Request Tracing

// Middleware to inject a Correlation ID in all logs
public class CorrelationIdMiddleware
{
    private const string CorrelationIdHeader = "X-Correlation-Id";
    private readonly RequestDelegate _next;
    private readonly ILogger<CorrelationIdMiddleware> _logger;

    public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers.TryGetValue(
            CorrelationIdHeader, out var existingId)
            ? existingId.ToString()
            : Guid.NewGuid().ToString("N");

        context.Response.Headers[CorrelationIdHeader] = correlationId;

        using (LogContext.PushProperty("CorrelationId", correlationId))
        {
            _logger.LogDebug("Request {Method} {Path} — CorrelationId: {CorrelationId}",
                context.Request.Method, context.Request.Path, correlationId);

            await _next(context);
        }
    }
}

// Registration in Program.cs
app.UseMiddleware<CorrelationIdMiddleware>();

11.4 Performance Logging with Stopwatch

public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            await _next(context);
        }
        finally
        {
            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;

            if (elapsed > 1000)
            {
                _logger.LogWarning(
                    "Slow request {Method} {Path} — {StatusCode} — {Elapsed}ms",
                    context.Request.Method, context.Request.Path,
                    context.Response.StatusCode, elapsed);
            }
            else
            {
                _logger.LogInformation(
                    "Request {Method} {Path} — {StatusCode} — {Elapsed}ms",
                    context.Request.Method, context.Request.Path,
                    context.Response.StatusCode, elapsed);
            }
        }
    }
}

12. Advanced Visual Studio Debugging

12.1 Watch Window and QuickWatch

Watch Window (Ctrl+Alt+W, 1):
  → Evaluate complex expressions during debugging
  → Ex: documents.Where(d => d.Id > 10).Count()
  → Modify values during debugging
  → Expressions persist between sessions

QuickWatch (Shift+F9):
  → Quick popup to inspect a selected variable
  → Can add to Watch Window

12.2 Immediate Window

// In the Immediate window (Debug → Windows → Immediate)
// Execute C# expressions in real time during debugging

// Call methods
? _documentService.GetByIdAsync(42).Result

// Modify variables
documents = new List<DocumentCard>()

// Inspect properties
? currentUser.Claims.Where(c => c.Type == "role").Select(c => c.Value)

12.3 Advanced Conditional Breakpoints

// Conditional breakpoint — stops only when condition is true
// Condition: index == 9542 || document.Title.Contains("issue")

// Breakpoint with pass counter
// Hit Count: = 9542 (stops exactly on the 9542nd pass)

// Tracepoint — logs without stopping execution
// Action: "Processing {document.Title} — index {index}"

12.4 Exception Settings (Ctrl+Alt+E)

Debug → Windows → Exception Settings
→ Configure which exceptions trigger a stop
→ "Break When Thrown" — stops before any catch
→ "Break When User-Unhandled" — only if unhandled
→ Useful: enable "Break When Thrown" for NullReferenceException
   to find the origin BEFORE the exception bubbles up

12.5 Multi-Thread Debugging

Parallel Stacks Window (Debug → Windows → Parallel Stacks):
  → Visualizes all threads and their stack traces simultaneously

Thread Window (Debug → Windows → Threads):
  → Lists all threads with their state and location

Tasks Window (Debug → Windows → Tasks):
  → For async/await: see Tasks in progress and their state

12.6 IntelliTrace (Visual Studio Enterprise)

Visual Studio Enterprise only:
→ "Step Back" during a debugging session
→ Returns to the previous state
→ Useful for inspecting state before a mutation
→ Note: if you need this, the code is not decomposed enough

13. Remote Debugging — Azure App Service

13.1 Enabling Remote Debugging

sequenceDiagram
    participant VS as Visual Studio
    participant Azure as Azure Portal
    participant AppService as App Service

    VS->>Azure: Connect via Azure Credentials
    Azure->>AppService: Enable remote debugging
    Note over AppService: Port 4022 opened\nMsvsmon.exe started
    VS->>AppService: Attach the debugger
    AppService-->>VS: Debug session established
    Note over VS: Breakpoints active in code

Steps in Azure Portal

1. App Service → Configuration → General Settings
2. Remote Debugging: ON
3. Remote Visual Studio Version: 2022
4. Save

In Visual Studio:
1. View → Cloud Explorer OR Server Explorer
2. Navigate to App Service
3. Right-click → Attach Debugger

Important Warnings

- Never leave remote debugging enabled in production for long
- Degrades application performance
- Disable immediately after use
- Prefer logs and Application Insights for production

13.2 Snapshot Debugger (Application Insights)

// Captures full state WITHOUT stopping execution
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddSnapshotCollector((configuration) =>
{
    configuration.IsEnabled = true;
    configuration.IsEnabledInDeveloperMode = false;
    configuration.MaximumSnapshotsRequired = 3;
});

14. .NET Diagnostic CLI Tools

14.1 Overview

graph TD
    A[Application in production] --> B{What problem?}
    B -->|High CPU / Performance| C[dotnet-trace\nCollect execution traces]
    B -->|High memory / Leak| D[dotnet-dump\nCapture a memory dump]
    B -->|Real-time metrics| E[dotnet-counters\nCounter monitoring]
    B -->|GC profiling| F[dotnet-gcdump\nCapture GC heap]
    
    C --> G[Analyze in PerfView or speedscope]
    D --> H[Analyze with windbg or lldb]
    E --> I[Real-time display in console]

14.2 dotnet-counters — Real-Time Metrics

# Installation
dotnet tool install --global dotnet-counters

# List active .NET processes
dotnet-counters ps

# Monitor a process
dotnet-counters monitor --process-id 12345

# Monitor with specific counters
dotnet-counters monitor --process-id 12345 \
    --counters System.Runtime,Microsoft.AspNetCore.Hosting

# Save metrics to a CSV file
dotnet-counters collect --process-id 12345 \
    --output metrics.csv \
    --duration 00:01:00

Key counters:

System.Runtime:
  cpu-usage                 → CPU % used
  gc-heap-size              → GC heap size (MB)
  exception-count           → Exceptions per second  ← watch this number
  threadpool-thread-count   → ThreadPool threads

Microsoft.AspNetCore.Hosting:
  requests-per-second       → Requests/second
  failed-requests           → Failed requests
  current-requests          → Current requests

14.3 dotnet-trace — Trace Collection

# Installation
dotnet tool install --global dotnet-trace

# Collect a CPU trace (30 seconds)
dotnet-trace collect --process-id 12345 \
    --duration 00:00:30 \
    --output trace.nettrace

# CPU-only profile
dotnet-trace collect --process-id 12345 \
    --profile cpu-sampling

# GC (garbage collection) profile
dotnet-trace collect --process-id 12345 \
    --profile gc-verbose

# Convert for browser analysis
dotnet-trace convert trace.nettrace --format speedscope
# Open speedscope.json at https://www.speedscope.app

14.4 dotnet-dump — Memory Dumps

# Installation
dotnet tool install --global dotnet-dump

# Capture a full dump
dotnet-dump collect --process-id 12345 \
    --output dump.dmp \
    --type Full

# Capture a minimal dump (faster)
dotnet-dump collect --process-id 12345 --type Mini

# Analyze the dump interactively
dotnet-dump analyze dump.dmp

Commands in the interactive analyzer:

dumpheap -stat         → Managed heap statistics — top types by memory
dumpheap -type MyClass → All instances of a type
gcroot <address>       → Why this object is not collected by GC
threads                → Thread list
clrstack               → Stack trace of current thread
clrthreads             → Stacks of all threads
dumpobj <address>      → Details of a specific object
eeheap -gc             → Detailed GC heap info
# Example of memory leak analysis
dotnet-dump analyze dump.dmp

> dumpheap -stat
# If DocumentCard has 50,000 instances, likely a leak

> dumpheap -type DocumentCard
# Lists all DocumentCard instances with their addresses

> gcroot 00007f1234abcd00
# Shows the reference chain preventing collection

14.5 dotnet-gcdump — Lightweight GC Heap

# Installation
dotnet tool install --global dotnet-gcdump

# Capture the GC heap (less invasive than a full dump)
dotnet-gcdump collect --process-id 12345 \
    --output heap.gcdump

# Analyze in Visual Studio:
# File → Open → heap.gcdump → Heap view with graph

15. Application Insights and Error Monitoring

15.1 Application Insights Integration

dotnet add package Microsoft.ApplicationInsights.AspNetCore
// Program.cs
builder.Services.AddApplicationInsightsTelemetry(options =>
{
    options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
    options.EnableAdaptiveSampling = true;
    options.EnableQuickPulseMetricStream = true; // Live Metrics
});

builder.Services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();
// Custom initializer to enrich all telemetry
public class CustomTelemetryInitializer : ITelemetryInitializer
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CustomTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        var context = _httpContextAccessor.HttpContext;
        if (context is null) return;

        if (context.User?.Identity?.IsAuthenticated == true)
        {
            telemetry.Context.User.AuthenticatedUserId =
                context.User.FindFirst("sub")?.Value;
        }

        if (context.Request.Headers.TryGetValue("X-Correlation-Id", out var correlationId))
        {
            telemetry.Context.Operation.Id = correlationId.ToString();
        }
    }
}

15.2 Manual Exception Tracking

public class DocumentService : IDocumentService
{
    private readonly TelemetryClient _telemetryClient;

    public DocumentService(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

    public async Task<DocumentDto> ProcessDocumentAsync(int id)
    {
        using var operation = _telemetryClient.StartOperation<RequestTelemetry>(
            $"ProcessDocument:{id}");

        try
        {
            var result = await DoProcessingAsync(id);

            _telemetryClient.TrackEvent("DocumentProcessed",
                properties: new Dictionary<string, string>
                {
                    ["DocumentId"] = id.ToString()
                });

            return result;
        }
        catch (Exception ex)
        {
            _telemetryClient.TrackException(ex,
                properties: new Dictionary<string, string>
                {
                    ["DocumentId"] = id.ToString(),
                    ["Operation"] = "ProcessDocument"
                });

            operation.Telemetry.Success = false;
            throw;
        }
    }
}

15.3 Sentry — External Error Monitoring

dotnet add package Sentry.AspNetCore
builder.WebHost.UseSentry(options =>
{
    options.Dsn = builder.Configuration["Sentry:Dsn"];
    options.Debug = builder.Environment.IsDevelopment();
    options.TracesSampleRate = 0.1; // 10% of requests traced
    options.Environment = builder.Environment.EnvironmentName;
    options.MinimumBreadcrumbLevel = LogLevel.Debug;
    options.MinimumEventLevel = LogLevel.Warning;

    // Filter sensitive information
    options.SetBeforeSend(sentryEvent =>
    {
        sentryEvent.Request.Cookies = null;
        return sentryEvent;
    });
});

15.4 Raygun

dotnet add package Raygun4Net.AspNetCore
// Program.cs
builder.Services.AddRaygun(builder.Configuration);
// appsettings.json
{
  "RaygunSettings": {
    "ApiKey": "YOUR_API_KEY",
    "IgnoredFormFieldNames": ["password", "creditCard"],
    "IsRawDataIgnored": true
  }
}

15.5 Monitoring Tools Comparison

ToolAdvantagesDisadvantages
Application InsightsNative Azure integration, trace correlationPotentially high cost, Azure vendor lock-in
SentryOpen source, multi-cloud, excellent UIRequires separate server (self-hosted)
RaygunCrash Reporting + APM combinedPaid only
ELK StackVery flexible, freeComplex to configure

16. Testing Error Handling

16.1 Unit Tests for Exceptions

public class DocumentServiceTests
{
    private readonly Mock<IDocumentRepository> _repositoryMock;
    private readonly DocumentService _service;

    public DocumentServiceTests()
    {
        _repositoryMock = new Mock<IDocumentRepository>();
        _service = new DocumentService(_repositoryMock.Object);
    }

    [Fact]
    public async Task GetByIdAsync_DocumentNotFound_ThrowsNotFoundException()
    {
        // Arrange
        _repositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
            .ReturnsAsync((Document?)null);

        // Act & Assert
        var exception = await Assert.ThrowsAsync<NotFoundException>(
            () => _service.GetByIdAsync(999));

        Assert.Equal("Document", exception.ResourceType);
        Assert.Equal(999, exception.ResourceId);
    }

    [Theory]
    [InlineData("", "Title is required.")]
    [InlineData(null, "Title is required.")]
    public async Task CreateAsync_EmptyTitle_ThrowsValidationException(
        string title, string expectedError)
    {
        var dto = new CreateDocumentDto { Title = title, Content = "Valid content" };

        var exception = await Assert.ThrowsAsync<ValidationException>(
            () => _service.CreateAsync(dto));

        Assert.True(exception.Errors.ContainsKey("title"));
        Assert.Contains(expectedError, exception.Errors["title"]);
    }

    [Fact]
    public async Task CreateAsync_DuplicateTitle_ThrowsConflictException()
    {
        var dto = new CreateDocumentDto { Title = "Existing title", Content = "..." };
        _repositoryMock.Setup(r => r.GetByTitleAsync("Existing title"))
            .ReturnsAsync(new Document { Title = "Existing title" });

        await Assert.ThrowsAsync<ConflictException>(
            () => _service.CreateAsync(dto));
    }

    [Fact]
    public async Task GetByIdAsync_RepositoryThrows_PropagatesException()
    {
        var dbException = new DatabaseException("Connection lost");
        _repositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
            .ThrowsAsync(dbException);

        var thrown = await Assert.ThrowsAsync<DatabaseException>(
            () => _service.GetByIdAsync(1));

        Assert.Same(dbException, thrown);
    }
}

16.2 Integration Tests for Error Middleware

public class ErrorHandlingIntegrationTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ErrorHandlingIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddScoped<IDocumentService, FaultingDocumentService>();
            });
        }).CreateClient();
    }

    [Fact]
    public async Task GetDocument_NotFound_Returns404ProblemDetails()
    {
        var response = await _client.GetAsync("/api/documents/999");

        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
        Assert.Equal("application/problem+json",
            response.Content.Headers.ContentType?.MediaType);

        var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
        Assert.NotNull(problem);
        Assert.Equal(404, problem!.Status);
    }

    [Fact]
    public async Task CreateDocument_InvalidData_Returns400WithErrors()
    {
        var dto = new CreateDocumentDto { Title = "", Content = "..." };
        var response = await _client.PostAsJsonAsync("/api/documents", dto);

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

        var problem = await response.Content
            .ReadFromJsonAsync<ValidationProblemDetails>();
        Assert.NotNull(problem?.Errors);
        Assert.True(problem!.Errors.ContainsKey("title"));
    }

    [Fact]
    public async Task GetDocument_ServerError_Returns500WithoutSensitiveData()
    {
        var response = await _client.GetAsync("/api/documents/trigger-error");

        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);

        var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
        // Verify that no internal details are exposed in production
        Assert.Null(problem?.Detail);
    }
}

16.3 Fault Injection Testing

// Service that simulates failures to test resilience
public class ChaoticDocumentRepository : IDocumentRepository
{
    private readonly IDocumentRepository _inner;
    private readonly Random _random = new();
    private readonly double _failureRate;

    public ChaoticDocumentRepository(IDocumentRepository inner, double failureRate = 0.3)
    {
        _inner = inner;
        _failureRate = failureRate;
    }

    public async Task<Document?> GetByIdAsync(int id)
    {
        if (_random.NextDouble() < _failureRate)
        {
            throw new ExternalServiceException("Database",
                "Simulating a database failure");
        }
        return await _inner.GetByIdAsync(id);
    }
}

// Resilience test with Polly
[Fact]
public async Task Service_WithRetryPolicy_HandlesTransientErrors()
{
    // Arrange — simulate 2 failures then success
    var callCount = 0;
    var repositoryMock = new Mock<IDocumentRepository>();
    repositoryMock.Setup(r => r.GetByIdAsync(1))
        .ReturnsAsync(() =>
        {
            callCount++;
            if (callCount < 3)
                throw new ExternalServiceException("DB", "Transient error");
            return new Document { Id = 1, Title = "Test" };
        });

    var retryPolicy = Policy
        .Handle<ExternalServiceException>()
        .RetryAsync(3);

    // Act
    var result = await retryPolicy.ExecuteAsync(
        () => _service.GetByIdAsync(1));

    // Assert
    Assert.NotNull(result);
    Assert.Equal(3, callCount); // 2 failures + 1 success
}

16.4 GlobalExceptionHandler Tests

public class GlobalExceptionHandlerTests
{
    [Fact]
    public async Task TryHandleAsync_NotFoundException_Returns404()
    {
        var logger = new NullLogger<GlobalExceptionHandler>();
        var handler = new GlobalExceptionHandler(logger);
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        var result = await handler.TryHandleAsync(
            context,
            new NotFoundException("Document", 42),
            CancellationToken.None);

        Assert.True(result);
        Assert.Equal(404, context.Response.StatusCode);
    }

    [Fact]
    public async Task TryHandleAsync_UnknownException_Returns500()
    {
        var logger = new NullLogger<GlobalExceptionHandler>();
        var handler = new GlobalExceptionHandler(logger);
        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        var result = await handler.TryHandleAsync(
            context,
            new InvalidOperationException("Oops"),
            CancellationToken.None);

        Assert.True(result);
        Assert.Equal(500, context.Response.StatusCode);
    }
}

17. Advanced Scenarios

17.1 Production Debugging

ABSOLUTE RULE: Never restore production data outside the production environment.

A developer copying the production database to their laptop creates a massive data breach risk.

Solution:

  1. Create a server within the production security zone
  2. Install Visual Studio and restore only there
  3. OR create an anonymized copy of the data
-- Example anonymization script
UPDATE PatientData 
SET FirstName = 'Test',
    LastName = 'Patient',
    DateOfBirth = '1990-01-01',
    SensitiveDataField = 'REDACTED';

17.2 IntelliTrace (Visual Studio Enterprise)

IntelliTrace allows stepping back in debugging — inspecting the program state before the breakpoint.

Note: If you need prior state, place a breakpoint where the state changes. If that’s difficult, the real problem is that the code is not decomposed and testable enough.

17.3 Debugging Non-Decomposable Content

Debugging by Diff

graph TD
    A[Problem in a document/file] --> B[Get 2 versions of the file\none that works, one that doesn't]
    B --> C[Diff the two versions]
    C --> D[The differences reveal the problem]

Recommended tool: OOXML Tools (browser extension) for inspecting Word/Excel files.

A Word file = a ZIP file renamed to .docx
Contents:
├── word/
│   ├── document.xml    ← Main content
│   ├── styles.xml      ← Styles
│   └── settings.xml
├── docProps/
└── [Content_Types].xml

Debugging via Source Control

# Find what changed between two commits
git diff commit-A commit-B

# Check when a specific file changed
git log --follow -p myfile.cs

# Find which commit introduced a bug (binary search)
git bisect start
git bisect bad HEAD
git bisect good old-commit

17.4 Memory Analysis and Leaks

// Correct IDisposable pattern to avoid resource leaks
public class DocumentProcessor : IDisposable
{
    private bool _disposed = false;
    private readonly FileStream _stream;

    public DocumentProcessor(string filePath)
    {
        _stream = new FileStream(filePath, FileMode.Open);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _stream.Dispose();
            }
            _disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

// Cache with WeakReference — allows GC to collect if needed
private static readonly ConditionalWeakTable<object, DocumentCard> _weakCache = new();

18. Best Practices and Common Pitfalls

18.1 Core Principles

mindmap
    root((Debugging & Error Handling))
        Premises
            Always know which assumption you're verifying
            One assumption at a time
            Stop if hypnotized by the code
        Code structure
            Decompose into small functions
            Unit tests to access the code
            Source control to track changes
        Debugging tools
            Targeted and temporary breakpoints
            JavaScript console on client side
            Source maps for TypeScript
            DevTools network for HTTP
            dotnet-trace dotnet-dump dotnet-counters
        Error handling
            Exception handling is exceptional
            No galactic catch
            Log complete exceptions
            Robust global handler
        Production monitoring
            Application Insights or Sentry
            Correlation IDs
            Error alerts
            Problem Details RFC 7807

18.2 Error Handling Checklist

CriteriaBad PracticeGood Practice
Catch scopeGeneric catch (Exception)Specific catch (FileNotFoundException)
ReactionSilently swallow the exceptionLog + Notify + Rethrow
TimingCatch after the problemCheck before (null check, File.Exists)
LevelIn the wrong layerAt the level that can truly resolve
GlobalNo last-resort handlerCustom error page + logger + notification
Exposed infoStack trace to userSecure message (Problem Details)
Loggingex.Message only_logger.LogError(ex, ...) for stack trace
CorrelationLogs without contextCorrelation ID in every log
TestsNo tests for errorsUnit + integration tests

18.3 Common Pitfalls to Avoid

Pitfall 1 — Catch and re-throw with throw ex

// BAD — Resets the original stack trace
catch (Exception ex)
{
    throw ex; // Stack trace lost!
}

// GOOD — Propagates the original stack trace
catch (Exception)
{
    // Log, etc.
    throw; // Without variable: stack trace preserved
}

Pitfall 2 — Logging Without Context

// BAD — No context
_logger.LogError("Error");

// GOOD — Full context + exception with stack trace
_logger.LogError(exception,
    "Error processing document {DocumentId} for user {UserId}",
    documentId, userId);

Pitfall 3 — Exceptions for Flow Control

// BAD — Exception used as goto
try
{
    var user = GetUser(id);
}
catch (UserNotFoundException)
{
    CreateNewUser(id); // Normal flow handled via exception
}

// GOOD — Prior check
var user = GetUser(id);
if (user is null)
{
    user = CreateNewUser(id);
}

Pitfall 4 — Sensitive Information in Logs

// BAD — Never log sensitive data
_logger.LogInformation("Login with password {Password}", password);

// GOOD — Log metadata, not the data
_logger.LogInformation("Login attempt for {Username}", username);

Pitfall 5 — Overly Complex Error Handler

// BAD — Complex handler that can itself crash
public async Task<IActionResult> HandleError()
{
    await _complexEmailService.SendDetailedReport(exception); // Can crash!
    await _databaseAuditService.LogToAuditTable(exception);  // Can crash!
    return View("Error");
}

// GOOD — Simple and resilient handler
public IActionResult HandleError()
{
    try { _logger.LogError(GetException(), "Unhandled error"); } catch { }
    try { NotifyAsync().FireAndForget(); } catch { }
    return View("Error");
}

18.4 Complete Error Handling Pipeline

graph TD
    A[HTTP Request] --> B[CorrelationId Middleware]
    B --> C[RequestTiming Middleware]
    C --> D{Environment?}
    D -->|Development| E[UseDeveloperExceptionPage\nFull stack trace]
    D -->|Production| F[UseExceptionHandler]
    F --> G[GlobalExceptionHandler\nIExceptionHandler]
    G --> H{Exception type?}
    H -->|NotFoundException| I[404 + ProblemDetails]
    H -->|ValidationException| J[400 + ValidationProblemDetails]
    H -->|ForbiddenException| K[403 + ProblemDetails]
    H -->|Other| L[500 + Sanitized ProblemDetails\nno internal details]
    I --> M[Log + Application Insights\n+ Notification]
    J --> M
    K --> M
    L --> M
    M --> N[Client response]

19. Review Questions

Question 1

Q: What is the difference between UseExceptionHandler and UseDeveloperExceptionPage?

A: UseDeveloperExceptionPage displays full exception details (stack trace, local variables, headers) — only in development. UseExceptionHandler redirects to a custom error endpoint or page — used in production to provide a secure response without exposing internal information.


Question 2

Q: What is RFC 7807 and why is it important for REST APIs?

A: RFC 7807 defines the “Problem Details for HTTP APIs” format — a JSON standard for error responses with the fields type, title, status, detail, and instance. Its adoption ensures that all API errors follow a consistent and predictable format, facilitating client-side handling and Swagger/OpenAPI documentation.


Question 3

Q: Why is it dangerous to use catch (Exception ex) { throw ex; }?

A: throw ex resets the stack trace, losing information about the real origin of the exception. Use throw; alone (without variable) to propagate the original exception with its stack trace intact.


Question 4

Q: What is the difference between UseStatusCodePagesWithRedirects and UseStatusCodePagesWithReExecute?

A: WithRedirects performs a real HTTP 302 redirect, so the client ultimately receives a 200 OK (status code lost). WithReExecute re-executes the ASP.NET Core pipeline internally, preserving the original status code (e.g. 404 stays 404 in the final response). WithReExecute is recommended.


Question 5

Q: How do you implement a Correlation ID and why is it important?

A: A Correlation ID is a unique identifier per request, injected via middleware into all logs for that request. Without it, it is impossible to reconstruct the path of a request in distributed logs. It must be propagated in HTTP headers (X-Correlation-Id) to traverse multiple microservices.


Question 6

Q: What is the difference between an Exception Filter and the UseExceptionHandler middleware?

A: Exception Filters apply only within the MVC pipeline (controllers and Razor Pages) and have access to the MVC context (ActionContext). The UseExceptionHandler middleware applies to all HTTP requests, including Minimal APIs. For global handling, middleware is preferred.


Question 7

Q: How do dotnet-trace and dotnet-dump differ in their use cases?

A: dotnet-trace collects CPU/GC profiling traces in real time without stopping the application, useful for analyzing performance hotspots. dotnet-dump captures a complete snapshot of memory at a point in time, useful for analyzing memory leaks and deadlocks. The dump can be analyzed offline with dotnet-dump analyze.


Question 8

Q: What is debugger hypnosis and how do you avoid it?

A: Debugger hypnosis occurs when you step through code without having a precise hypothesis in mind. To avoid it: always explicitly formulate the assumption you are verifying before placing a breakpoint. If you cannot answer “What premise am I verifying?”, stop and think (peripatetic debugging).


Question 9

Q: Why is structured logging preferred over string interpolation logging?

A: _logger.LogInformation("User {UserId} accessed {DocId}", userId, docId) stores UserId and DocId as separately indexed properties in the logging engine. This enables queries like WHERE UserId = 'abc'. Interpolation $"User {userId}" produces a flat non-queryable string.


Question 10

Q: What are the 4 qualities of a good global error handler?

A:

  1. Log the exception with all details (stack trace, context)
  2. Notify the team (email, Slack, PagerDuty, etc.)
  3. Inform the user securely (without internal technical details)
  4. Be simple and resilient — the handler must never crash itself

Question 11

Q: How do you create a custom exception hierarchy to automatically map HTTP codes?

A: Create an abstract AppException class with a virtual HttpStatusCode property, then derived classes (NotFoundException → 404, ValidationException → 400, etc.) that override this property. In the global handler, a switch on the exception type uses this property to set the HTTP response code.


Question 12

Q: What is the difference between the Application Insights Snapshot Debugger and classic remote debugging?

A: Classic remote debugging (Attach Debugger) pauses application execution at breakpoints, impacting users. The Snapshot Debugger automatically captures a snapshot of the memory state when an exception is raised in production, without interrupting execution. Snapshots can be analyzed afterwards in Visual Studio.


Question 13

Q: Why should you avoid catching a generic exception and only logging ex.Message?

A: ex.Message only contains the exception message without the stack trace. The stack trace is essential for diagnosing the origin of the problem. Use _logger.LogError(exception, "message") which serializes the complete exception including the stack trace and inner exceptions.



Search Terms

asp.net · debugging · error · handling · core · testing · patterns · architecture · c# · .net · development · question · exception · logging · pitfall · tools · middleware · application · approach · comparison · configuration · exceptions · global · handle

Interested in this course?

Contact us to book it or get a custom training plan for your team.