Skip to content

ASP.NET Core: a value object's fault for a rejected route or query value, through an endpoint filter (spike first; reopens decision 39) #260

Open

Description

@CaffeinatedCoder

Problem

Decision 39 left a rejected value-object input with no fault in the response. A refused route or query value is an empty 400: TryParse reports only false, so the fault is lost (spikes/ValidationFaults, finding 5). A refused minimal-API body reached a hook only with ThrowOnBadRequest for the whole host. The decision is to be reopened "if ASP.NET hands a binding failure to a hook without ThrowOnBadRequest".

ASP.NET Core now does that. When a minimal-API endpoint has a filter, the filter pipeline runs even if binding fails. The filter sees HttpContext.Response.StatusCode == 400 and may write its own response (What's new in ASP.NET Core 11, dotnet/aspnetcore#64539). The change was backported to release/10.0 (dotnet/aspnetcore#66221).

The filter doesn't get which parameter failed, the exception or the fault. A filter that only sees the 400 adds nothing over UseStatusCodePages() (finding 2).

Change

A spike first, under spikes/, with a README giving the question, the answer and the evidence. It decides:

  • Whether the fault can be recovered without changing the base package. The candidate is an opt-in endpoint filter, on a route or a group. On a 400 it reads the raw route and query value of each value-object parameter again, parses the wrapped type, runs the value object's Create, and reports the fault as problem details keyed by the parameter's name. It uses fault identifiers only, since no refusal quotes the input.
  • Which positions it reaches: a route, query and header value, a missing body, a malformed body, and a body whose value object refuses. Reading the source suggests that a malformed body returns before the filter runs; measure it.
  • The hosts: ThrowOnBadRequest defaults to true in Development, where the framework throws instead. Also the Request Delegate Generator and Native AOT. MVC is out of scope (finding 4).
  • The target: which 10.0.x servicing release ships the backport, so whether this ships on net10.0 or on net11.0 only (decision 120).
  • The cost: parsing again only on the failure path, never on a request that bound.

Then a decision record that supersedes decision 39 states the answer: the filter lands in 2.4.0, or the record says why not.

Done when

  • The spike's README answers the questions above with measurements, and the framework facts it turns on are design/knowledge/ entries (decision 93).
  • A decision record supersedes 0039, and the ADR index says so.
  • If it lands, it has:
    • a test per position on each target it ships on;
    • an UnreadableInputTests row;
    • the AspNetCore README section and the changelog line;
    • claims in test/oracles/claims.json.

Activity

  1. added this to the 2.4.0 milestone on Oct 31, 2026
  2. added
    enhancementNew feature or request
    package/aspnetcoreAffects CodoMetis.TypeKit.AspNetCore
    area/value-objectsValue-object contracts and what the aspects generate for them
    area/parsingParse, TryParse, formatting and the type converter
    on Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/parsingParse, TryParse, formatting and the type converterarea/value-objectsValue-object contracts and what the aspects generate for themenhancementNew feature or requestpackage/aspnetcoreAffects CodoMetis.TypeKit.AspNetCore

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions