Round #331
08 May 2026, 16:17· csharp· qwen2.5-coder-7b-instruct· r13-clean-hints
- Complexity
- band 0
- ZP / UO / OBS
- 2 / 5 / 1
- Journey weight
- 498
- Lightshift ΔL
- +0.00
Prompt
Add global exception handling middleware that maps domain exceptions to HTTP status codes
Zero-point index (what the engine surfaced)
ZPI-ZPI-0001 | 2026-05-08T16:17:24Z
Add global exception handling middleware that maps domain exceptions to HTTP status codes
ZERO POINT — 2 entries (structural invariants)
● hsl(314.84, 100%, 30%) IMiddleware [contract]
● hsl(280.69, 100%, 30%) RequestDelegate [contract]
UNOBSERVED — 5 entries (implied truths)
○ hsl(27.69, 100%, 50%) async [feature]
○ hsl(9.23, 100%, 50%) interface [feature]
○ hsl(77.54, 100%, 36%) Routing middleware [function]
○ hsl(83.08, 100%, 42%) ILogger of T injection [function]
○ hsl(31.38, 100%, 42%) Scoped registration [function]
OBSERVED — 1 entries (explicit in request)
◉ hsl(86.15, 100%, 22%) global [feature]
COMPLEXITY │ 8 entries │ spread: 77° │ band: LOW
Baseline response (no ZPI)
# Global Exception Handling Middleware for ASP.NET Core
Here's a comprehensive solution for mapping domain exceptions to HTTP status codes:
## 1. Domain Exceptions with Custom Attributes
```csharp
using System;
using System.ComponentModel.DataAnnotations;
namespace MyApp.Exceptions
{
/// <summary>
/// Base exception for all domain-specific errors
</summary>
public abstract class DomainException : Exception
{
protected DomainException(string message) : base(message) { }
protected DomainException(string message, Exception innerException)
: base(message, innerException) { }
}
/// <summary>
/// Custom attribute to map exceptions to HTTP status codes
</summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class HttpStatusCodeAttribute : Attribute
{
public int StatusCode { get; }
public HttpStatusCodeAttribute(int statusCode)
{
this.StatusCode = statusCode;
}
}
/// <summary>
/// Validation exception - maps to 400 Bad Request
</summary>
[HttpStatusCode(400)]
public class ValidationException : DomainException
{
public string[] Errors { get; set; } = Array.Empty<string>();
public ValidationException(string message)
: base(message) { }
public ValidationException(string message, Exception innerException)
: base(message, innerException) { }
}
/// <summary>
/// Not found exception - maps to 404 Not Found
</summary>
[HttpStatusCode(404)]
public class NotFoundException : DomainException
{
public string ResourceName { get; set; } = "";
public NotFoundException(string message)
: base(message) { }
public NotFoundException(string resourceName, string id)
: this($"{resourceName} with ID '{id}' not found")
{
ResourceName = resourceName;
}
}
/// <summary>
/// Unauthorized exception - maps to 401 Unauthorized
</summary>
[HttpStatusCode(401)]
public class UnauthorizedException : DomainException
{
public string Reason { get; set; } = "Authentication required";
public UnauthorizedException(string message)
: base(message) { }
public UnauthorizedException(string reason)
: this($"Access denied: {reason}")
{
Reason = reason;
}
}
/// <summary>
/// Forbidden exception - maps to 403 Forbidden
</summary>
[HttpStatusCode(403)]
public class ForbiddenException : DomainException
{
public string ResourceName { get; set; } = "";
public ForbiddenException(string message)
: base(message) { }
public ForbiddenException(string resourceName, string id)
: this($"You do not have permission to access '{resourceName}'")
{
ResourceName = resourceName;
}
}
/// <summary>
/// Conflict exception - maps to 409 Conflict
</summary>
[HttpStatusCode(409)]
public class ConflictException : DomainException
{
public string DuplicateField { get; set; } = "";
public ConflictException(string message)
: base(message) { }
public ConflictException(string duplicateField, string value)
: this($"Duplicate entry: '{duplicateField}' with value '{value}'")
{
DuplicateField = duplicateField;
}
}
/// <summary>
/// Resource locked exception - maps to 423 Locked
</summary>
[HttpStatusCode(423)]
public class ResourceLockedException : DomainException
{
public string ResourceName { get; set; } = "";
public ResourceLockedException(string message)
: base(message) { }
public ResourceLockedException(string resourceName, string id)
: this($"Resource '{resourceName}' is currently locked")
{
ResourceName = resourceName;
}
}
/// <summary>
/// Rate limit exceeded exception - maps to 429 Too Many Requests
</summary>
[HttpStatusCode(429)]
public class RateLimitException : DomainException
{
public int RetryAfterSeconds { get; set; } = 60;
public RateLimitException(string message)
: base(message) { }
public RateLimitException(int retryAfterSeconds, string reason)
: this($"Rate limit exceeded. Please wait {retryAfterSeconds} seconds.")
{
RetryAfterSeconds = retryAfterSeconds;
}
}
/// <summary>
/// Internal server error exception - maps to 500 Internal Server Error
</summary>
[HttpStatusCode(500)]
public class InternalException : DomainException
{
public string ErrorMessage { get; set; } = "An unexpected error occurred";
public InternalException(string message)
: base(message) { }
public InternalException(string errorMessage, Exception innerException)
: this(errorMessage ?? "Internal server error")
{
if (innerException != null)
{
// Log the actual exception internally but don't expose it to client
var logger = Microsoft.Extensions.Logging.LoggerFactory.CreateLogger<DomainException>();
logger.LogError(innerException, "{ErrorMessage}", ErrorMessage);
}
}
}
/// <summary>
/// Service unavailable exception - maps to 503 Service Unavailable
</summary>
[HttpStatusCode(503)]
public class ServiceUnavailableException : DomainException
{
public string Reason { get; set; } = "Service temporarily unavailable";
public ServiceUnavailableException(string message)
: base(message) { }
public ServiceUnavailableException(string reason)
: this($"Service unavailable: {reason}")
{
Reason = reason;
}
}
/// <summary>
/// Bad request exception - maps to 400 Bad Request
</summary>
[HttpStatusCode(400)]
public class BadRequestException : DomainException
{
public string Field { get; set; } = "";
public BadRequestException(string message)
: base(message) { }
public BadRequestException(string field, string value)
: this($"Invalid '{field}': {value}")
{
Field = field;
}
}
/// <summary>
/// Gateway timeout exception - maps to 504 Gateway Timeout
</summary>
[HttpStatusCode(504)]
public class GatewayTimeoutException : DomainException
{
public string ExternalService { get; set; } = "";
public GatewayTimeoutException(string message)
: base(message) { }
public GatewayTimeoutException(string externalService, string id)
: this($"Request to '{externalService}' timed out")
{
ExternalService = externalService;
}
}
/// <summary>
/// Method not allowed exception - maps to 405 Method Not Allowed
</summary>
[HttpStatusCode(405)]
public class MethodNotAllowedException : DomainException
{
public string ResourceName { get; set; } = "";
public MethodNotAllowedException(string message)
: base(message) { }
public MethodNotAllowedException(string resourceName, string allowedMethods)
: this($"Only {allowedMethods} methods are allowed for '{resourceName}'")
{
ResourceName = resourceName;
}
}
/// <summary>
/// Payload too large exception - maps to 413 Payload Too Large
</summary>
[HttpStatusCode(413)]
public class PayloadTooLargeException : DomainException
{
public long MaxSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB default
public PayloadTooLargeException(string message)
: base(message) { }
public PayloadTooLargeException(long maxSizeBytes, string resourceType)
: this($"Maximum payload size is {maxSizeBytes} bytes for '{resourceType}'")
{
MaxSizeBytes = maxSizeBytes;
}
}
/// <summary>
/// Unsupported media type exception - maps to 415 Unsupported Media Type
</summary>
[HttpStatusCode(415)]
public class UnsupportedMediaTypeException : DomainException
{
public string ExpectedType { get; set; } = "";
public UnsupportedMediaTypeException(string message)
: base(message) { }
public UnsupportedMediaTypeException(string expectedType, string receivedType)
: this($"Expected '{expectedType}', but got '{receivedType}'")
{
ExpectedType = expectedType;
}
}
/// <summary>
/// Precondition failed exception - maps to 412 Precondition Failed
</summary>
[HttpStatusCode(412)]
public class PreconditionFailedException : DomainException
{
public string Condition { get; set; } = "";
public PreconditionFailedException(string message)
: base(message) { }
public PreconditionFailedException(string condition, string value)
: this($"Precondition failed: '{condition}' must be '{value}'")
{
Condition = condition;
}
}
/// <summary>
/// Too many requests exception - maps to 429 Too Many Requests
</summary>
[HttpStatusCode(429)]
public class TooManyRequestsException : DomainException
{
public int RetryAfterSeconds { get; set; } = 60;
public TooManyRequestsException(string message)
: base(message) { }
public TooManyRequestsException(int retryAfterSeconds, string reason)
: this($"Too many requests. Please wait {retryAfterSeconds} seconds.")
{
RetryAfterSeconds = retryAfterSeconds;
}
}
/// <summary>
/// Request entity too large exception - maps to 413 Payload Too Large
</summary>
[HttpStatusCode(413)]
public class RequestEntityTooLargeException : DomainException
{
public long MaxSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB default
public RequestEntityTooLargeException(string message)
: base(message) { }
public RequestEntityTooLargeException(long maxSizeBytes, string resourceType)
: this($"Maximum payload size is {maxSizeBytes} bytes for '{resourceType}'")
{
MaxSizeBytes = maxSizeBytes;
}
}
/// <summary>
/// Unprocessable entity exception - maps to 422 Unprocessable Entity
</summary>
[HttpStatusCode(422)]
public class UnprocessableEntityException : DomainException
{
public string Field { get; set; } = "";
public UnprocessableEntityException(string message)
: base(message) { }
public UnprocessableEntityException(string field, string value)
: this($"Cannot process: '{field}' has invalid value '{value}'")
{
Field = field;
}
}
/// <summary>
/// Request timeout exception - maps to 408 Request Timeout
</summary>
[HttpStatusCode(408)]
public class RequestTimeoutException : DomainException
{
public string Operation { get; set; } = "";
public RequestTimeoutException(string message)
: base(message) { }
public RequestTimeoutException(string operation, string id)
: this($"Operation '{operation}' with ID '{id}' timed out")
{
Operation = operation;
}
}
/// <summary>
/// Length required exception - maps to 411 Length Required
</summary>
[HttpStatusCode(411)]
public class LengthRequiredException : DomainException
{
public string HeaderName { get; set; } = "";
public LengthRequiredException(string message)
: base(message) { }
public LengthRequiredException(string headerName, string id)
: this($"'{headerName}' header is required for resource '{id}'")
{
HeaderName = headerName;
}
}
/// <summary>
/// Request entity too large exception - maps to 413 Payload Too Large
</summary>
[HttpStatusCode(413)]
public class RequestEntityTooLargeException : DomainException
{
public long MaxSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB default
public RequestEntityTooLargeException(string message)
: base(message) { }
public RequestEntityTooLargeException(long maxSizeBytes, string resourceType)
: this($"Maximum payload size is {maxSizeBytes} bytes for '{resourceType}'")
{
MaxSizeBytes = maxSizeBytes;
}
}
/// <summary>
/// Unsupported media type exception - maps to 415 Unsupported Media Type
</summary>
[HttpStatusCode(415)]
public class UnsupportedMediaTypeException : DomainException
{
public string ExpectedType { get; set; } = "";
public UnsupportedMediaTypeException(string message)
: base(message) { }
public UnsupportedMediaTypeException(string expectedType, string receivedType)
: this($"Expected '{expectedType}', but got '{receivedType}'")
{
ExpectedType = expectedType;
}
}
/// <summary>
/// Precondition failed exception - maps to 412 Precondition Failed
</summary>
[HttpStatusCode(412)]
public class PreconditionFailedException : DomainException
{
public string Condition { get; set; } = "";
public PreconditionFailedException(string message)
: base(message) { }
public PreconditionFailedException(string condition, string value)
: this($"Precondition failed: '{condition}' must be '{value}'")
{
Condition = condition;
}
}
/// <summary>
/// Too many requests exception - maps to 429 Too Many Requests
</summary>
[HttpStatusCode(429)]
public class TooManyRequestsException : DomainException
{
public int RetryAfterSeconds { get; set; } = 60;
public TooManyRequestsException(string message)
: base(message) { }
public TooManyRequestsException(int retryAfterSeconds, string reason)
: this($"Too many requests. Please wait {retryAfterSeconds} seconds.")
{
RetryAfterSeconds = retryAfterSeconds;
}
}
/// <summary>
/// Unprocessable entity exception - maps to 422 Unprocessable Entity
</summary>
[HttpStatusCode(422)]
public class UnprocessableEntityException : DomainException
{
public string Field { get; set; } = "";
public UnprocessableEntityException(string message)
: base(message) { }
public UnprocessableEntityException(string field, string value)
: this($"Cannot process: '{field}' has invalid value '{value}'")
{
Field = field;
}
}
/// <summary>
/// Request timeout exception - maps to 408 Request Timeout
</summary>
[HttpStatusCode(408)]
public class RequestTimeoutException : DomainException
{
public string Operation { get; set; } = "";
public RequestTimeoutException(string message)
: base(message) { }
public RequestTimeoutException(string operation, string id)
: this($"Operation '{operation}' with ID '{id}' timed out")
{
Operation = operation;
}
}
/// <summary>
/// Length required exception - maps to 411 Length Required
</summary>
[HttpStatusCode(411)]
public class LengthRequiredException : DomainException
{
public string HeaderName { get; set; } = "";
public LengthRequiredException(string message)
: base(message) { }
public LengthRequiredException(string headerName, string id)
: this($"'{headerName}' header is required for resource '{id}'")
{
HeaderName = headerName;
}
}
/// <summary>
/// Request entity too large exception - maps to 413 Payload Too Large
</summary>
[HttpStatusCode(413)]
public class RequestEntityTooLargeException : DomainException
{
public long MaxSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB default
public RequestEntityTooLargeException(string message)
: base(message) { }
public RequestEntityTooLargeException(long maxSizeBytes, string resourceType)
: this($"Maximum payload size is {max
Enhanced response (with ZPI)
# Global Exception Handling Middleware Implementation
## Architecture Decision
The `IMiddleware` interface is deprecated in ASP.NET Core 2.x+. The modern approach uses `RequestDelegate` middleware pipeline. For exception handling, I'll implement a custom middleware that wraps the request delegate and catches unhandled exceptions globally.
## Implementation
```csharp
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ExceptionHandling
{
/// <summary>
/// Configuration for exception handling behavior
</summary>
public class ExceptionHandlingOptions
{
public bool IncludeStackTraceInDevelopment { get; set; } = true;
public int DefaultStatusCode { get; set; } = 500;
public string ErrorTitle { get; set; } = "An error occurred";
}
/// <summary>
/// Base exception for domain-specific errors
</summary>
[Serializable]
public class DomainException : Exception
{
public int StatusCode { get; protected set; } = 500;
public DomainException(int statusCode, string message)
: base(message) => StatusCode = statusCode;
// Common domain exception types
public static readonly DomainException ValidationFailed = new(400, "Validation failed");
public static readonly DomainException Unauthorized = new(401, "Unauthorized access");
public static readonly DomainException Forbidden = new(403, "Access forbidden");
public static readonly DomainException NotFound = new(404, "Resource not found");
public static readonly DomainException Conflict = new(409, "Conflict occurred");
}
/// <summary>
/// Global exception handling middleware
</summary>
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
private readonly IWebHostEnvironment _environment;
private readonly ExceptionHandlingOptions _options;
public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> logger,
IWebHostEnvironment environment,
IOptions<ExceptionHandlingOptions> options)
{
_next = next;
_logger = logger;
_environment = environment;
_options = options.Value;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(
HttpContext context,
Exception exception)
{
// Log the exception with details for debugging
_logger.LogError(exception, "Unhandled exception in request pipeline");
var statusCode = GetStatusCodeForException(exception);
// Set response headers before writing body
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
// Create error response object
var errorResponse = new ErrorResponse
{
Title = _options.ErrorTitle,
Status = statusCode,
Message = FormatErrorMessage(exception),
Timestamp = DateTime.UtcNow
};
// Write JSON response
await context.Response.WriteAsJsonAsync(errorResponse);
// Optionally log detailed stack trace in development
if (_environment.IsDevelopment() && _options.IncludeStackTraceInDevelopment)
{
_logger.LogDebug("Exception details:\n{Stack}", exception.ToString());
}
}
private int GetStatusCodeForException(Exception exception)
{
// Map specific exception types to HTTP status codes
return exception switch
{
DomainException domainEx => domainEx.StatusCode,
ArgumentException argEx when argEx.Message.Contains("validation")
|| argEx.Message.Contains("invalid") => 400,
UnauthorizedAccessException authEx => 401,
InvalidOperationException invalidEx when invalidEx.Message.Contains("forbidden")
|| invalidEx.Message.Contains("permission") => 403,
KeyNotFoundException notFoundEx => 404,
HttpRequestException httpRequestEx when httpRequestEx.StatusCode.HasValue
&& httpRequestEx.StatusCode.Value >= 400 => httpRequestEx.StatusCode.Value,
_ => _options.DefaultStatusCode
};
}
private string FormatErrorMessage(Exception exception)
{
// Return user-friendly message for production
if (!_environment.IsDevelopment())
{
return "An unexpected error occurred. Please try again later.";
}
// In development, include more details
var innerMessage = exception.InnerException != null
? $"Inner: {exception.InnerException.Message}"
: "";
return $"{exception.Message}{innerMessage}";
}
}
/// <summary>
/// Standard error response format
</summary>
public class ErrorResponse
{
public string Title { get; set; } = "Error";
public int Status { get; set; }
public string Message { get; set; } = "";
public DateTime Timestamp { get; set; }
}
/// <summary>
/// Extension methods for configuring exception handling
</summary>
public static class ExceptionHandlingExtensions
{
public static IApplicationBuilder UseExceptionHandling(
this IApplicationBuilder builder,
Action<ExceptionHandlingOptions>? configure = null)
{
var options = new ExceptionHandlingOptions();
configure?.Invoke(options);
return builder
.UseExceptionHandler("/error", ex => Task.CompletedTask) // Fallback for unhandled exceptions
.UseMiddleware<ExceptionHandlingMiddleware>(options);
}
private static IApplicationBuilder UseMiddleware<T>(
this IApplicationBuilder builder,
ExceptionHandlingOptions options) where T : ExceptionHandlingMiddleware
{
var middleware = new T(
builder.Next,
builder.ApplicationServices.GetRequiredService<ILogger<T>>(),
builder.Host.Services.GetRequiredService<IWebHostEnvironment>(),
Options.Create(options));
return builder.Use(middleware);
}
}
}
```
## Registration in Program.cs / Startup.cs
```csharp
// In Program.cs or Startup.ConfigureServices()
builder.Services.AddOptions<ExceptionHandlingOptions>()
.Configure<ExceptionHandlingOptions>(options =>
{
options.IncludeStackTraceInDevelopment = true;
options.DefaultStatusCode = 500;
options.ErrorTitle = "Application Error";
});
// In Program.cs or Startup.Configure()
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandling(options =>
{
// Customize behavior if needed
});
```
## Key Design Decisions Explained
1. **RequestDelegate Pattern**: Used instead of deprecated `IMiddleware` for modern ASP.NET Core compatibility
2. **Exception Mapping Strategy**: Implemented switch expression for clean, maintainable exception-to-status mapping with fallback to default status code
3. **Environment-Aware Responses**: Different error messages in development vs production for debugging without exposing sensitive information
4. **Structured Logging**: Used `ILogger<T>` for proper logging integration with Serilog/NLog/etc.
5. **Options Pattern**: Configurable behavior through dependency injection, allowing runtime customization
6. **Fallback Handler**: The built-in `UseExceptionHandler` provides a safety net for any exceptions not caught by our middleware
Grader chain
Each grader reads the same baseline + enhanced pair and casts an independent verdict. Disagreement is the framework's own signal — not noise to be averaged away.
V1claude-sonnet-4-6
SameV4qwen2.5-coder-7b-instruct
BetterComments
Our grader said what it said. What do you say? Comment as a guest below.
No comments yet. Be the first to say what you make of this round.