Round #283
08 May 2026, 12:20· csharp· openai/gpt-oss-120b:free· r13-clean-hints
- Complexity
- band 0
- ZP / UO / OBS
- 2 / 5 / 1
- Journey weight
- 504
- 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-08T12:20:02Z
Add global exception handling middleware that maps domain exceptions to HTTP status codes
ZERO POINT — 2 entries (structural invariants)
● hsl(314.84, 100%, 27%) IMiddleware [contract]
● hsl(280.69, 100%, 27%) 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, ready‑to‑copy** implementation of a global exception‑handling middleware that
* catches any exception that bubbles out of the request pipeline,
* recognises the *domain* exceptions you define,
* maps each of those exceptions to the appropriate HTTP status code (you can extend the map easily),
* returns a JSON error payload that is safe for production (no stack‑trace unless you are in Development).
The solution is split into three parts:
1. **Domain exception hierarchy** – the custom exceptions you will throw from your domain layer.
2. **Exception‑to‑status‑code mapper** – a single place where you declare which exception maps to which HTTP status.
3. **Middleware** – the component that intercepts the exception, looks it up in the mapper and writes the response.
4. **Registration** – how to plug the middleware into the ASP.NET Core pipeline (both the classic `Startup` class and the newer minimal‑API `Program.cs` style).
---
## 1️⃣ Domain Exceptions (example)
```csharp
// ---------------------------------------------------------------
// File: Domain/Exceptions/DomainException.cs
// ---------------------------------------------------------------
using System;
namespace MyApp.Domain.Exceptions
{
/// <summary>
/// Base class for all domain‑specific exceptions.
/// </summary>
public abstract class DomainException : Exception
{
protected DomainException(string message) : base(message) { }
protected DomainException(string message, Exception inner) : base(message, inner) { }
}
// Example concrete exceptions -------------------------------------------------
public sealed class NotFoundException : DomainException
{
public NotFoundException(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) { }
}
// Add any other domain‑specific exceptions you need …
}
```
> **Why a base `DomainException`?**
> It lets the mapper treat *all* domain errors uniformly while still allowing you to differentiate them for status‑code mapping.
---
## 2️⃣ Exception‑to‑Status‑Code Mapper
```csharp
// ---------------------------------------------------------------
// File: Infrastructure/ExceptionMapping/ExceptionStatusCodeMapper.cs
// ---------------------------------------------------------------
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using MyApp.Domain.Exceptions;
namespace MyApp.Infrastructure.ExceptionMapping
{
/// <summary>
/// Central place that knows which HTTP status code belongs to which domain exception.
/// </summary>
public static class ExceptionStatusCodeMapper
{
// You can fill this dictionary with as many mappings as you need.
private static readonly IReadOnlyDictionary<Type, int> _mappings = new Dictionary<Type, int>
{
{ typeof(NotFoundException), StatusCodes.Status404NotFound },
{ typeof(ValidationException), StatusCodes.Status400BadRequest },
{ typeof(ConflictException), StatusCodes.Status409Conflict },
// Example of mapping a generic domain exception to 422 (Unprocessable Entity)
// { typeof(DomainException), StatusCodes.Status422UnprocessableEntity }
};
/// <summary>
/// Returns the HTTP status code that best matches the supplied exception.
/// If the exception type is not registered, <c>500 Internal Server Error</c> is returned.
/// </summary>
public static int GetStatusCode(Exception ex)
{
// Walk the inheritance chain – this allows a derived exception to be
// matched by a base‑type entry (e.g. DomainException → 422).
var type = ex.GetType();
while (type != typeof(object))
{
if (_mappings.TryGetValue(type, out var code))
return code;
type = type.BaseType!;
}
// Fallback – unknown exception → 500
return StatusCodes.Status500InternalServerError;
}
}
}
```
*You can also expose a method that returns a *problem‑details* object if you want richer error bodies – see the middleware below.*
---
## 3️⃣ Global Exception‑Handling Middleware
```csharp
// ---------------------------------------------------------------
// File: Infrastructure/Middleware/ExceptionHandlingMiddleware.cs
// ---------------------------------------------------------------
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MyApp.Infrastructure.ExceptionMapping;
using MyApp.Domain.Exceptions;
namespace MyApp.Infrastructure.Middleware
{
/// <summary>
/// Catches all unhandled exceptions, maps known domain exceptions to proper
/// HTTP status codes and writes a JSON error response.
/// </summary>
public sealed class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
private readonly IHostEnvironment _env;
public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> logger,
IHostEnvironment env)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_env = env ?? throw new ArgumentNullException(nameof(env));
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
// Log – always log the exception (including stack trace) as error.
_logger.LogError(ex, "Unhandled exception processing request {Method} {Path}",
context.Request?.Method,
context.Request?.Path.Value);
await WriteErrorResponseAsync(context, ex);
}
}
private async Task WriteErrorResponseAsync(HttpContext context, Exception exception)
{
// Determine status code
var statusCode = ExceptionStatusCodeMapper.GetStatusCode(exception);
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
// Build a minimal error payload.
// In production we hide internal details; in Development we expose them.
var errorResponse = new
{
// RFC 7807 Problem Details fields (optional but nice)
type = $"https://httpstatuses.com/{statusCode}",
title = GetDefaultTitle(statusCode),
status = statusCode,
detail = _env.IsDevelopment()
? exception.Message
: GetDefaultTitle(statusCode),
// Optional: include a traceId for correlation
traceId = context.TraceIdentifier,
// Optional: only in Development – full exception info
exception = _env.IsDevelopment()
? new
{
message = exception.Message,
stackTrace = exception.StackTrace,
// You could also add inner exception details recursively
}
: null
};
var json = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
// Omit null values (so "exception" disappears in Production)
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
});
await context.Response.WriteAsync(json);
}
private static string GetDefaultTitle(int statusCode) =>
statusCode switch
{
StatusCodes.Status400BadRequest => "Bad Request",
StatusCodes.Status401Unauthorized => "Unauthorized",
StatusCodes.Status403Forbidden => "Forbidden",
StatusCodes.Status404NotFound => "Not Found",
StatusCodes.Status409Conflict => "Conflict",
StatusCodes.Status422UnprocessableEntity => "Unprocessable Entity",
StatusCodes.Status500InternalServerError => "Internal Server Error",
_ => "Error"
};
}
// Extension method for a clean registration in the pipeline
public static class ExceptionHandlingMiddlewareExtensions
{
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ExceptionHandlingMiddleware>();
}
}
}
```
### What the middleware does
| Step | Action |
|------|--------|
| **Catch** | `try / catch` around the rest of the pipeline. |
| **Log** | Always logs the exception with full stack‑trace (so you have diagnostics). |
| **Map** | Calls `ExceptionStatusCodeMapper.GetStatusCode` to obtain the HTTP status. |
| **Serialize** | Returns a JSON payload that follows the *Problem Details* (RFC 7807) shape. In Development the `detail` field contains the real exception message and an `exception` sub‑object with stack‑trace; in Production only a generic message is sent. |
| **Write** | Sets `Content‑Type: application/json` and writes the JSON to the response body. |
You can further customise the payload (add `errors` collection for validation failures, etc.) without touching the rest of the pipeline.
---
## 4️⃣ Register the Middleware
### 4.1 Classic `Startup.cs` (ASP.NET Core 3.1 / 5 / 6)
```csharp
// ---------------------------------------------------------------
// File: Startup.cs
// ---------------------------------------------------------------
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyApp.Infrastructure.Middleware;
public class Startup
{
public Startup(IConfiguration configuration) => Configuration = configuration;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(); // or AddEndpointsApiExplorer + AddSwaggerGen, etc.
// Register any other services (DI, DB, MediatR, etc.)
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// IMPORTANT: the exception handler must be the *first* middleware
// so it can catch exceptions from everything that follows.
app.UseGlobalExceptionHandler();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage(); // optional – still useful for non‑domain exceptions
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
```
### 4.2 Minimal‑API style (`Program.cs`) – .NET 6/7/8+
```csharp
// ---------------------------------------------------------------
// File: Program.cs
// ---------------------------------------------------------------
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using MyApp.Infrastructure.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers(); // or builder.Services.AddEndpointsApiExplorer() + Swagger, etc.
var app = builder.Build();
// Global exception handling – must be first
app.UseGlobalExceptionHandler();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage(); // optional
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers(); // or app.MapGet/MapPost for minimal APIs
app.Run();
```
---
## 5️⃣ How to Use the Domain Exceptions in Your Code
```csharp
// ---------------------------------------------------------------
// Example: Application/Services/OrderService.cs
// ---------------------------------------------------------------
using MyApp.Domain.Exceptions;
public class OrderService
{
public async Task<OrderDto> GetAsync(Guid orderId)
{
var order = await _dbContext.Orders.FindAsync(orderId);
if (order == null)
throw new NotFoundException(nameof(Order), orderId);
// ... other logic that may throw ValidationException, ConflictException, etc.
return MapToDto(order);
}
}
```
When `NotFoundException` bubbles out of the controller action, the middleware will:
* log the error,
* set **HTTP 404**,
* return a JSON body like:
```json
{
"type": "https://httpstatuses.com/404",
"title": "Not Found",
"status": 404,
"detail": "Order with key 'c56a4180-65aa-42ec-a945-5fd21dec0538' was not found.",
"traceId": "00-4c2e...-..."
}
```
(If you are **not** in Development, `detail` will be `"Not Found"` and the `exception` property will be omitted.)
---
## 6️⃣ Extending the Mapper
If you add a new domain exception, simply add a line to the dictionary:
```csharp
{ typeof(InsufficientFundsException), StatusCodes.Status402PaymentRequired },
```
Or, if you want a *fallback* for all `DomainException` types:
```csharp
{ typeof(DomainException), StatusCodes.Status422UnprocessableEntity }
```
Because the mapper walks the inheritance chain, a derived exception that isn’t explicitly listed will automatically inherit the base‑type mapping.
---
## 7️⃣ Testing the Middleware (optional but handy)
```csharp
// ---------------------------------------------------------------
// File: Tests/Integration/ExceptionMiddlewareTests.cs
// ---------------------------------------------------------------
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
using MyApp;
public class ExceptionMiddlewareTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ExceptionMiddlewareTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task Get_NonExistingOrder_Returns404_WithProblemDetails()
{
// Arrange – use a GUID that does not exist
var id = Guid.NewGuid();
// Act
var response = await _client.GetAsync($"/api/orders/{id}");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
Assert.Equal(404, problem?.Status);
Assert.Equal("Not Found", problem?.Title);
}
}
```
---
## TL;DR – What you need to copy/paste
1. **Domain exceptions** (`Domain/Exceptions/*.cs`)
2. **Mapper** (`Infrastructure/ExceptionMapping/ExceptionStatusCodeMapper.cs`)
3. **Middleware** (`Infrastructure/Middleware/ExceptionHandlingMiddleware.cs`)
4. **Extension method** (`ExceptionHandlingMiddlewareExtensions`)
5. **Register** (`app.UseGlobalExceptionHandler();`) as the **first** middleware in `Startup.Configure` or `Program.cs`.
That’s it – you now have a single place that turns your domain‑level errors into proper HTTP responses, logs them, and returns clean, standards‑compliant JSON payloads. Happy coding!
Enhanced response (with ZPI)
**Global exception‑handling middleware**
```csharp
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
/// <summary>
/// Middleware that catches exceptions thrown by downstream components,
/// translates known domain exceptions into appropriate HTTP status codes,
/// and writes a JSON error payload. All other exceptions are logged and
/// returned as 500 Internal Server Error.
/// </summary>
public sealed class DomainExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<DomainExceptionHandlingMiddleware> _logger;
private readonly IDictionary<Type, HttpStatusCode> _exceptionMap;
/// <summary>
/// Creates a new instance.
/// </summary>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="logger">Typed logger injected by DI.</param>
/// <param name="exceptionMap">
/// Optional map that tells the middleware which <see cref="HttpStatusCode"/>
/// to use for a given exception type. If not supplied a sensible default
/// set is used.
/// </param>
public DomainExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<DomainExceptionHandlingMiddleware> logger,
IDictionary<Type, HttpStatusCode>? exceptionMap = null)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// Default mapping – can be overridden by DI registration.
_exceptionMap = exceptionMap ?? new Dictionary<Type, HttpStatusCode>
{
// Example domain exceptions – replace with the real ones from your
// domain layer.
[typeof(EntityNotFoundException)] = HttpStatusCode.NotFound,
[typeof(ValidationException)] = HttpStatusCode.BadRequest,
[typeof(ConflictException)] = HttpStatusCode.Conflict,
[typeof(UnauthorizedAccessException)] = HttpStatusCode.Unauthorized,
};
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex) when (IsHandledDomainException(ex, out var status))
{
// Known domain exception – translate to the mapped status code.
_logger.LogWarning(ex, "Handled domain exception: {ExceptionType}", ex.GetType().Name);
await WriteErrorResponseAsync(context, status, ex.Message);
}
catch (Exception ex)
{
// Unexpected exception – log and return 500.
_logger.LogError(ex, "Unhandled exception");
await WriteErrorResponseAsync(context, HttpStatusCode.InternalServerError,
"An unexpected error occurred.");
}
}
private bool IsHandledDomainException(Exception ex, out HttpStatusCode status)
{
// Walk the inheritance chain – a derived exception can be mapped by its base type.
var type = ex.GetType();
while (type != typeof(object))
{
if (_exceptionMap.TryGetValue(type, out status))
return true;
type = type.BaseType!;
}
status = default;
return false;
}
private static async Task WriteErrorResponseAsync(
HttpContext context,
HttpStatusCode status,
string message)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)status;
var payload = System.Text.Json.JsonSerializer.Serialize(new
{
error = message,
status = (int)status
});
await context.Response.WriteAsync(payload);
}
}
```
**Domain‑exception stubs (replace with your real domain types)**
```csharp
public class EntityNotFoundException : Exception
{
public EntityNotFoundException(string message) : base(message) { }
}
public class ValidationException : Exception
{
public ValidationException(string message) : base(message) { }
}
public class ConflictException : Exception
{
public ConflictException(string message) : base(message) { }
}
```
**Extension method for clean registration**
```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
public static class DomainExceptionHandlingExtensions
{
/// <summary>
/// Adds the <see cref="DomainExceptionHandlingMiddleware"/> to the pipeline.
/// </summary>
public static IApplicationBuilder UseDomainExceptionHandling(this IApplicationBuilder app)
{
return app.UseMiddleware<DomainExceptionHandlingMiddleware>();
}
/// <summary>
/// Registers the middleware as a scoped service so a custom exception map
/// can be supplied via DI if required.
/// </summary>
public static IServiceCollection AddDomainExceptionHandling(
this IServiceCollection services,
IDictionary<Type, HttpStatusCode>? customMap = null)
{
// The middleware itself is registered as scoped because it may depend
// on scoped services (e.g. a per‑request logger or a tenant‑specific map).
services.AddScoped<DomainExceptionHandlingMiddleware>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DomainExceptionHandlingMiddleware>>();
return new DomainExceptionHandlingMiddleware(
next: sp.GetRequiredService<RequestDelegate>(),
logger: logger,
exceptionMap: customMap);
});
return services;
}
}
```
**Typical usage in `Program.cs` (ASP.NET Core 6+ minimal hosting model)**
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register logging, controllers, etc.
builder.Services.AddControllers();
// Optional: supply a custom map
var customMap = new Dictionary<Type, HttpStatusCode>
{
[typeof(MyCustomDomainException)] = HttpStatusCode.BadRequest
};
builder.Services.AddDomainExceptionHandling(customMap);
var app = builder.Build();
// Register the middleware as the very first component so it can
// catch exceptions from everything that follows.
app.UseDomainExceptionHandling();
app.MapControllers();
app.Run();
```
### How it works
1. **Pipeline placement** – `UseDomainExceptionHandling` should be called early (ideally first) so the middleware wraps the whole request processing pipeline.
2. **Exception capture** – The `try / catch` block around `_next(context)` intercepts any exception bubbling up from downstream middleware, MVC actions, or any other component.
3. **Mapping** – A dictionary (`_exceptionMap`) defines which `HttpStatusCode` corresponds to each domain exception type. The lookup walks the inheritance chain, allowing derived exceptions to be handled by a base‑type entry.
4. **Logging** – Known domain exceptions are logged at *Warning* level; unexpected ones at *Error* level, both with the typed logger (`ILogger<DomainExceptionHandlingMiddleware>`).
5. **Response** – A small JSON payload (`{ error: "...", status: 400 }`) is written with the appropriate status code and `application/json` content type.
6. **DI registration** – The middleware is registered as *scoped* (the default for middleware) so any scoped services it needs can be resolved per request. The extension method also lets callers inject a custom mapping without touching the middleware source.
This implementation gives you a single, global place to translate domain‑specific errors into the correct HTTP semantics while keeping the rest of the application clean and focused on business logic.
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-haiku-4-5-20251001
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.