Round #347

08 May 2026, 17:07· csharp· claude-sonnet-4-6· r13-clean-hints
Complexity
band 0
ZP / UO / OBS
2 / 5 / 1
Journey weight
500
Lightshift ΔL
+0.50

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-08T17:07:02Z Add global exception handling middleware that maps domain exceptions to HTTP status codes ZERO POINT — 2 entries (structural invariants) ● hsl(314.84, 100%, 29%) IMiddleware [contract] ● hsl(280.69, 100%, 29%) 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 in C# ## Project Structure ``` src/ ├── Middleware/ │ └── GlobalExceptionHandlingMiddleware.cs ├── Exceptions/ │ ├── DomainException.cs │ ├── NotFoundException.cs │ ├── ValidationException.cs │ ├── ConflictException.cs │ ├── UnauthorizedException.cs │ ├── ForbiddenException.cs │ └── BusinessRuleException.cs ├── Models/ │ └── ErrorResponse.cs ├── Extensions/ │ └── ExceptionMiddlewareExtensions.cs └── Program.cs ``` ## Domain Exceptions ```csharp // Exceptions/DomainException.cs namespace YourApp.Exceptions; /// <summary> /// Base class for all domain-specific exceptions /// </summary> public abstract class DomainException : Exception { public string ErrorCode { get; } public IDictionary<string, object>? Metadata { get; } protected DomainException( string message, string errorCode, IDictionary<string, object>? metadata = null, Exception? innerException = null) : base(message, innerException) { ErrorCode = errorCode; Metadata = metadata; } } ``` ```csharp // Exceptions/NotFoundException.cs namespace YourApp.Exceptions; public class NotFoundException : DomainException { public NotFoundException(string resourceName, object resourceId) : base( message: $"Resource '{resourceName}' with identifier '{resourceId}' was not found.", errorCode: "RESOURCE_NOT_FOUND", metadata: new Dictionary<string, object> { ["resourceName"] = resourceName, ["resourceId"] = resourceId }) { } public NotFoundException(string message) : base(message, errorCode: "NOT_FOUND") { } } ``` ```csharp // Exceptions/ValidationException.cs namespace YourApp.Exceptions; public class ValidationException : DomainException { public IReadOnlyDictionary<string, string[]> Errors { get; } public ValidationException(IDictionary<string, string[]> errors) : base( message: "One or more validation errors occurred.", errorCode: "VALIDATION_FAILED", metadata: new Dictionary<string, object> { ["errors"] = errors }) { Errors = new Dictionary<string, string[]>(errors); } public ValidationException(string field, string error) : this(new Dictionary<string, string[]> { [field] = [error] }) { } public ValidationException(string message) : base(message, errorCode: "VALIDATION_FAILED") { Errors = new Dictionary<string, string[]>(); } } ``` ```csharp // Exceptions/ConflictException.cs namespace YourApp.Exceptions; public class ConflictException : DomainException { public ConflictException(string message) : base(message, errorCode: "CONFLICT") { } public ConflictException(string resourceName, string conflictReason) : base( message: $"Conflict occurred for resource '{resourceName}': {conflictReason}", errorCode: "RESOURCE_CONFLICT", metadata: new Dictionary<string, object> { ["resourceName"] = resourceName, ["reason"] = conflictReason }) { } } ``` ```csharp // Exceptions/UnauthorizedException.cs namespace YourApp.Exceptions; public class UnauthorizedException : DomainException { public UnauthorizedException(string message = "Authentication is required to access this resource.") : base(message, errorCode: "UNAUTHORIZED") { } } ``` ```csharp // Exceptions/ForbiddenException.cs namespace YourApp.Exceptions; public class ForbiddenException : DomainException { public ForbiddenException(string message = "You do not have permission to perform this action.") : base(message, errorCode: "FORBIDDEN") { } public ForbiddenException(string action, string resource) : base( message: $"Action '{action}' is not permitted on resource '{resource}'.", errorCode: "FORBIDDEN", metadata: new Dictionary<string, object> { ["action"] = action, ["resource"] = resource }) { } } ``` ```csharp // Exceptions/BusinessRuleException.cs namespace YourApp.Exceptions; public class BusinessRuleException : DomainException { public BusinessRuleException(string rule, string message) : base( message: message, errorCode: "BUSINESS_RULE_VIOLATION", metadata: new Dictionary<string, object> { ["rule"] = rule }) { } } ``` ```csharp // Exceptions/ServiceUnavailableException.cs namespace YourApp.Exceptions; public class ServiceUnavailableException : DomainException { public string ServiceName { get; } public ServiceUnavailableException(string serviceName, string? details = null, Exception? innerException = null) : base( message: $"Service '{serviceName}' is currently unavailable. {details}".TrimEnd(), errorCode: "SERVICE_UNAVAILABLE", metadata: new Dictionary<string, object> { ["serviceName"] = serviceName }, innerException: innerException) { ServiceName = serviceName; } } ``` ## Error Response Model ```csharp // Models/ErrorResponse.cs using System.Text.Json.Serialization; namespace YourApp.Models; public sealed class ErrorResponse { /// <summary>Machine-readable error code</summary> public string ErrorCode { get; init; } = string.Empty; /// <summary>Human-readable error message</summary> public string Message { get; init; } = string.Empty; /// <summary>Correlation ID for tracing across services</summary> public string TraceId { get; init; } = string.Empty; /// <summary>UTC timestamp of when the error occurred</summary> public DateTime Timestamp { get; init; } = DateTime.UtcNow; /// <summary>Validation errors (only populated for 400 responses)</summary> [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary<string, string[]>? ValidationErrors { get; init; } /// <summary>Additional error metadata</summary> [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary<string, object>? Details { get; init; } /// <summary>Stack trace (only in development)</summary> [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? StackTrace { get; init; } } ``` ## Exception Mapping Strategy ```csharp // Middleware/ExceptionMapping/IExceptionMapper.cs namespace YourApp.Middleware.ExceptionMapping; public interface IExceptionMapper { bool CanMap(Exception exception); (int StatusCode, string ErrorCode, string Message) Map(Exception exception); } ``` ```csharp // Middleware/ExceptionMapping/ExceptionMapRegistry.cs using System.Net; using YourApp.Exceptions; namespace YourApp.Middleware.ExceptionMapping; /// <summary> /// Registry that maps exception types to HTTP status codes using a priority-ordered chain /// </summary> public sealed class ExceptionMapRegistry { private readonly List<ExceptionMapping> _mappings = []; public ExceptionMapRegistry() { RegisterDefaults(); } private void RegisterDefaults() { // Domain exceptions — most specific first Register<NotFoundException>(HttpStatusCode.NotFound, "NOT_FOUND"); Register<ValidationException>(HttpStatusCode.BadRequest, "

Enhanced response (with ZPI)

# Global Exception Handling Middleware ## Domain Exceptions ```csharp // Exceptions/DomainException.cs namespace MyApi.Exceptions; public abstract class DomainException : Exception { protected DomainException(string message) : base(message) { } protected DomainException(string message, Exception innerException) : base(message, innerException) { } } public sealed class NotFoundException : DomainException { public NotFoundException(string resourceName, object key) : base($"Resource '{resourceName}' with key '{key}' was not found.") { } public NotFoundException(string message) : base(message) { } } public sealed class ValidationException : DomainException { public IReadOnlyDictionary<string, string[]> Errors { get; } public ValidationException(string message) : base(message) { Errors = new Dictionary<string, string[]>(); } public ValidationException(IDictionary<string, string[]> errors) : base("One or more validation errors occurred.") { Errors = new ReadOnlyDictionary<string, string[]>(errors); } } public sealed class ConflictException : DomainException { public ConflictException(string message) : base(message) { } } public sealed class UnauthorizedException : DomainException { public UnauthorizedException(string message = "Authentication is required.") : base(message) { } } public sealed class ForbiddenException : DomainException { public ForbiddenException(string message = "You do not have permission to perform this action.") : base(message) { } } public sealed class BusinessRuleException : DomainException { public string RuleCode { get; } public BusinessRuleException(string ruleCode, string message) : base(message) { RuleCode = ruleCode; } } ``` ## Problem Details Models ```csharp // Models/ProblemDetails.cs namespace MyApi.Models; /// <summary> /// Extends RFC 7807 ProblemDetails with a correlation ID and structured errors. /// </summary> public sealed class ApiProblemDetails { public string Type { get; init; } = "about:blank"; public string Title { get; init; } = string.Empty; public int Status { get; init; } public string Detail { get; init; } = string.Empty; public string Instance { get; init; } = string.Empty; public string TraceId { get; init; } = string.Empty; public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow; [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyDictionary<string, string[]>? Errors { get; init; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? RuleCode { get; init; } } ``` ## Exception Map ```csharp // Middleware/ExceptionStatusMap.cs namespace MyApi.Middleware; internal static class ExceptionStatusMap { private static readonly IReadOnlyDictionary<Type, (int StatusCode, string Title, string Type)> Map = new Dictionary<Type, (int, string, string)> { [typeof(NotFoundException)] = ( StatusCodes.Status404NotFound, "Resource Not Found", "https://tools.ietf.org/html/rfc7231#section-6.5.4"), [typeof(ValidationException)] = ( StatusCodes.Status422UnprocessableEntity, "Validation Failed", "https://tools.ietf.org/html/rfc4918#section-11.2"), [typeof(ConflictException)] = ( StatusCodes.Status409Conflict, "Conflict", "https://tools.ietf.org/html/rfc7231#section-6.5.8"), [typeof(UnauthorizedException)] = ( StatusCodes.Status401Unauthorized, "Unauthorized", "https://tools.ietf.org/html/rfc7235#section-3.1"), [typeof(ForbiddenException)] = ( StatusCodes.Status403Forbidden, "Forbidden", "https://tools.ietf.org/html/rfc7231#section-6.5.3"), [typeof(BusinessRuleException)] = ( StatusCodes.Status422UnprocessableEntity, "Business Rule Violation", "https://tools.ietf.org/html/rfc4918#section-11.2"), }; public static bool TryGet(Type exceptionType, out (int StatusCode, string Title, string Type) entry) => Map.TryGetValue(exceptionType, out entry); } ``` ## Middleware ```csharp // Middleware/ExceptionHandlingMiddleware.cs namespace MyApi.Middleware; public sealed class ExceptionHandlingMiddleware : IMiddleware { private readonly ILogger<ExceptionHandlingMiddleware> _logger; public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger) { _logger = logger; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { try { await next(context); } catch (Exception ex) { await HandleExceptionAsync(context, ex); } } private async Task HandleExceptionAsync(HttpContext context, Exception exception) { var (statusCode, title, type, errors, ruleCode) = Classify(exception); // Log unhandled/unexpected exceptions at Error; known domain exceptions at lower severity if (statusCode >= StatusCodes.Status500InternalServerError) { _logger.LogError( exception, "Unhandled exception on {Method} {Path}. TraceId: {TraceId}", context.Request.Method, context.Request.Path, context.TraceIdentifier); } else { _logger.LogWarning( exception, "Domain exception {ExceptionType} on {Method} {Path}. TraceId: {TraceId}", exception.GetType().Name, context.Request.Method, context.Request.Path, context.TraceIdentifier); } var problem = new ApiProblemDetails { Type = type, Title = title, Status = statusCode, Detail = exception.Message, Instance = context.Request.Path, TraceId = context.TraceIdentifier, Errors = errors, RuleCode = ruleCode, }; context.Response.ContentType = "application/problem+json"; context.Response.StatusCode = statusCode; await context.Response.WriteAsJsonAsync(problem, JsonOptions.Default); } private static (int StatusCode, string Title, string Type, IReadOnlyDictionary<string, string[]>? Errors, string? RuleCode) Classify(Exception exception) { // Walk inner exceptions so wrapped domain exceptions are still caught var target = UnwrapException(exception); if (ExceptionStatusMap.TryGet(target.GetType(), out var entry)) { IReadOnlyDictionary<string, string[]>? errors = null; string? ruleCode = null; if (target is ValidationException ve) errors = ve.Errors; if (target is BusinessRuleException bre) ruleCode = bre.RuleCode; return (entry.StatusCode, entry.Title, entry.Type, errors, ruleCode); } // Fallback — treat as internal server error; hide implementation details return ( StatusCodes.Status500InternalServerError, "An unexpected error occurred.", "https://tools.ietf.org/html/rfc7231#section-6.6.1", null, null

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
Better
V4qwen2.5-coder-7b-instruct
Better

Comments

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.

← back to all rounds