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
BetterV4qwen2.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.