Round #299
08 May 2026, 13:06· csharp· openai/gpt-oss-120b:free· r13-clean-hints
- Complexity
- band 0
- ZP / UO / OBS
- 2 / 5 / 1
- Journey weight
- 494
- 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-08T13:06:33Z
Add global exception handling middleware that maps domain exceptions to HTTP status codes
ZERO POINT — 2 entries (structural invariants)
● hsl(314.84, 100%, 32%) IMiddleware [contract]
● hsl(280.69, 100%, 32%) 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)
Below is a **complete, drop‑in example** that adds a global exception‑handling middleware to an ASP.NET Core Web API project and maps your *domain* exceptions to the appropriate HTTP status codes.
---
## 1. Define the domain‑exception hierarchy
First create a small hierarchy (or use the one you already have).
Each exception type knows which HTTP status it should be translated to.
```csharp
// File: src/YourApp.Domain/Exceptions/DomainException.cs
using System;
namespace YourApp.Domain.Exceptions
{
/// <summary>
/// Base class for all domain‑level errors.
/// </summary>
public abstract class DomainException : Exception
{
protected DomainException(string message) : base(message) { }
/// <summary>
/// The HTTP status code that should be returned for this exception.
/// </summary>
public abstract int StatusCode { get; }
}
}
```
```csharp
// File: src/YourApp.Domain/Exceptions/NotFoundException.cs
using System.Net;
namespace YourApp.Domain.Exceptions
{
public sealed class NotFoundException : DomainException
{
public NotFoundException(string message) : base(message) { }
public override int StatusCode => (int)HttpStatusCode.NotFound;
}
}
```
```csharp
// File: src/YourApp.Domain/Exceptions/ConflictException.cs
using System.Net;
namespace YourApp.Domain.Exceptions
{
public sealed class ConflictException : DomainException
{
public ConflictException(string message) : base(message) { }
public override int StatusCode => (int)HttpStatusCode.Conflict;
}
}
```
```csharp
// File: src/YourApp.Domain/Exceptions/ValidationException.cs
using System.Net;
namespace YourApp.Domain.Exceptions
{
public sealed class ValidationException : DomainException
{
public ValidationException(string message) : base(message) { }
public override int StatusCode => (int)HttpStatusCode.BadRequest;
}
}
```
Add any other domain exceptions you need (e.g., `UnauthorizedException`, `ForbiddenException`, etc.) following the same pattern.
---
## 2. Create the middleware
```csharp
// File: src/YourApp.Api/Middleware/ExceptionHandlingMiddleware.cs
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using YourApp.Domain.Exceptions;
namespace YourApp.Api.Middleware
{
/// <summary>
/// Catches all unhandled exceptions, logs them and converts known
/// <see cref="DomainException"/> types to proper HTTP responses.
/// </summary>
public sealed class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
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)
{
// Default to 500 – internal server error
var statusCode = (int)HttpStatusCode.InternalServerError;
var problemDetails = new ProblemDetails
{
Instance = context.Request.Path,
Title = "An unexpected error occurred."
};
// -------------------------------------------------
// 1️⃣ Domain exceptions – map to the status code they expose
// -------------------------------------------------
if (exception is DomainException domainEx)
{
statusCode = domainEx.StatusCode;
problemDetails.Title = domainEx.Message;
problemDetails.Status = statusCode;
problemDetails.Type = $"https://httpstatuses.com/{statusCode}";
}
// -------------------------------------------------
// 2️⃣ Known framework exceptions (optional)
// -------------------------------------------------
else if (exception is UnauthorizedAccessException)
{
statusCode = (int)HttpStatusCode.Unauthorized;
problemDetails.Title = "Unauthorized.";
problemDetails.Status = statusCode;
problemDetails.Type = $"https://httpstatuses.com/{statusCode}";
}
// -------------------------------------------------
// 3️⃣ Anything else – log and hide details
// -------------------------------------------------
else
{
_logger.LogError(exception,
"Unhandled exception processing request {Method} {Path}",
context.Request.Method,
context.Request.Path);
problemDetails.Title = "Internal Server Error";
problemDetails.Status = statusCode;
problemDetails.Type = $"https://httpstatuses.com/{statusCode}";
}
// -------------------------------------------------
// Write the response
// -------------------------------------------------
context.Response.ContentType = "application/problem+json";
context.Response.StatusCode = statusCode;
var json = JsonSerializer.Serialize(problemDetails,
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
});
await context.Response.WriteAsync(json);
}
}
// Extension method for a clean registration in the pipeline
public static class ExceptionHandlingMiddlewareExtensions
{
public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ExceptionHandlingMiddleware>();
}
}
}
```
### Why `ProblemDetails`?
* It follows the RFC 7807 standard.
* Clients (including browsers, Postman, Swagger UI, etc.) already know how to render it.
* You can later enrich it with `extensions` (e.g., validation errors) without breaking the contract.
---
## 3. Register the middleware in the request pipeline
Place it **as the first middleware** (right after the built‑in `UseRouting` if you use endpoint routing, or even before that) so it can
Enhanced response (with ZPI)
**1. Global exception‑handling middleware**
```csharp
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
/// <summary>
/// Middleware that catches all exceptions thrown by downstream components,
/// translates known domain exceptions into appropriate HTTP status codes,
/// and returns a JSON error payload.
/// </summary>
public sealed class GlobalExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlingMiddleware> _logger;
private readonly IDictionary<Type, HttpStatusCode> _exceptionStatusMap;
public GlobalExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionHandlingMiddleware> logger,
IDictionary<Type, HttpStatusCode>? exceptionStatusMap = null)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// Default mapping – can be overridden via DI registration
_exceptionStatusMap = exceptionStatusMap ??
new Dictionary<Type, HttpStatusCode>
{
// Domain‑specific mappings
{ typeof(EntityNotFoundException), HttpStatusCode.NotFound },
{ typeof(ValidationException), HttpStatusCode.BadRequest },
{ typeof(ConflictException), HttpStatusCode.Conflict },
// Fallback for any other known domain exception
{ typeof(DomainException), HttpStatusCode.BadRequest }
};
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex) when (IsDomainException(ex))
{
// Log with appropriate level
_logger.LogWarning(ex, "Handled domain exception: {Message}", ex.Message);
await WriteErrorResponseAsync(context, ex);
}
catch (Exception ex)
{
// Unexpected exception – log as error and return 500
_logger.LogError(ex, "Unhandled exception");
await WriteErrorResponseAsync(context, ex, HttpStatusCode.InternalServerError);
}
}
private bool IsDomainException(Exception ex) =>
ex is DomainException || _exceptionStatusMap.ContainsKey(ex.GetType());
private HttpStatusCode ResolveStatusCode(Exception ex) =>
_exceptionStatusMap.TryGetValue(ex.GetType(), out var code)
? code
: HttpStatusCode.BadRequest; // fallback for unmapped domain exceptions
private async Task WriteErrorResponseAsync(HttpContext context, Exception ex,
HttpStatusCode? forcedStatus = null)
{
var status = forcedStatus ?? ResolveStatusCode(ex);
var problem = new ProblemDetails
{
Status = (int)status,
Title = ex.Message,
Type = ex.GetType().FullName,
Instance = context.Request.Path
};
var payload = JsonSerializer.Serialize(problem, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
context.Response.ContentType = "application/problem+json";
context.Response.StatusCode = (int)status;
await context.Response.WriteAsync(payload);
}
// Simple Problem Details DTO – you can replace with Microsoft.AspNetCore.Mvc.ProblemDetails
private sealed class ProblemDetails
{
public int Status { get; set; }
public string? Title { get; set; }
public string? Type { get; set; }
public string? Instance { get; set; }
}
}
```
**2. Domain‑exception base types (example)**
These are the exceptions the middleware knows how to translate.
You can place them in a shared project/namespace.
```csharp
using System;
public abstract class DomainException : Exception
{
protected DomainException(string message) : base(message) { }
protected DomainException(string message, Exception inner) : base(message, inner) { }
}
public sealed class EntityNotFoundException : DomainException
{
public EntityNotFoundException(string entityName, object key)
: base($"{entityName} with key '{key}' was not found.") { }
}
public sealed class ValidationException : DomainException
{
public ValidationException(string message) : base(message) { }
}
public sealed class ConflictException : DomainException
{
public ConflictException(string message) : base(message) { }
}
```
**3. Extension method for easy registration**
```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Net;
public static class GlobalExceptionHandlingExtensions
{
/// <summary>
/// Adds the <see cref="GlobalExceptionHandlingMiddleware"/> to the pipeline.
/// </summary>
public static IApplicationBuilder UseGlobalExceptionHandling(this IApplicationBuilder app,
IDictionary<Type, HttpStatusCode>? customMap = null)
{
// The middleware itself is resolved from DI, so we can pass the map via options.
return app.UseMiddleware<GlobalExceptionHandlingMiddleware>(customMap);
}
/// <summary>
/// Registers the middleware as a scoped service (so ILogger<T> is injected correctly).
/// </summary>
public static IServiceCollection AddGlobalExceptionHandling(this IServiceCollection services,
IDictionary<Type, HttpStatusCode>? customMap = null)
{
// Register the map as a singleton – the middleware receives it via constructor injection.
if (customMap != null)
{
services.AddSingleton(customMap);
}
// The middleware itself does not need explicit registration; UseMiddleware
// will resolve it from the container. Registering as scoped makes ILogger<T>
// scoped to the request pipeline.
services.AddScoped<GlobalExceptionHandlingMiddleware>();
return services;
}
}
```
**4. Wire‑up in `Program.cs` (minimal API style)**
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register logging, controllers, etc.
builder.Services.AddControllers();
// Register the exception‑handling middleware (optional custom map)
builder.Services.AddGlobalExceptionHandling(new Dictionary<Type, HttpStatusCode>
{
// Example of overriding the default mapping
{ typeof(ValidationException), HttpStatusCode.UnprocessableEntity }
});
var app = builder.Build();
// Global exception handling must be the first middleware so it can catch everything.
app.UseGlobalExceptionHandling();
// Typical ASP.NET Core pipeline
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
```
**5. What the code does**
| Step | Behaviour |
|------|-----------|
| **Middleware construction** | Receives `RequestDelegate` (the next component), an `ILogger<GlobalExceptionHandlingMiddleware>` and an optional dictionary that maps exception types to `HttpStatusCode`. |
| **InvokeAsync** | Executes the downstream pipeline inside a `try`. If a known domain exception is thrown, it logs a warning, maps the exception to a status code, and returns a JSON *Problem Details* payload. Any other exception is logged as an error and results in a `500 Internal Server Error`. |
| **Mapping** | By default the middleware knows about `EntityNotFoundException → 404`, `ValidationException → 400`, `ConflictException → 409`, and a generic `DomainException → 400`. The map can be overridden or extended via DI registration. |
| **Response format** | Returns `application/problem+json` with fields `status`, `title`, `type`, and `instance` – a lightweight version of the RFC‑7807 Problem Details format. |
| **DI registration** | `AddGlobalExceptionHandling` registers the middleware as scoped (so the injected `ILogger<T>` is request‑scoped) and optionally registers a custom map. The extension `UseGlobalExceptionHandling` adds the middleware to the pipeline. |
| **Placement** | The middleware should be placed **before** any other component that could throw domain exceptions (typically right after routing is set up, but before authentication/authorization if you want to capture those as well). |
With this implementation you have a single, reusable piece of infrastructure that converts your domain‑level errors into the correct HTTP responses, logs them appropriately, and keeps the rest of your controllers/services clean from repetitive `try/catch` blocks.
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.