Skip to content

Minimal API binds a variant-typed parameter as null for a valid literal and answers 200 #116

Closed

Description

@CaffeinatedCoder

A minimal-API parameter typed as a sealed variant — Int32Range.Finite, DateRange.UnboundedEnd, … — binds null for a valid literal of that shape and answers 200, including when the parameter is not nullable. Measured on main (7f4dc47) with WebApplication on .NET 10 and no registration:

app.MapGet("/union",  (Int32Range seats)         => ...);   // ?seats=[1,5]  → 200, seats = [1,5]
app.MapGet("/finite", (Int32Range.Finite seats)  => ...);   // ?seats=[1,5]  → 200, seats = null
                                                            // ?seats=(,5]   → 200, seats = null
                                                            // ?seats=garbage → 400
app.MapGet("/finite-nullable", (Int32Range.Finite? seats) => ...); // ?seats=[1,5] → 200, null

Cause. The variants have no TryParse of their own; minimal APIs find the inherited static Int32Range.TryParse(string?, IFormatProvider?, out Int32Range), call it, and then can't assign the union-typed result to the variant-typed parameter, so the parameter stays null while the parse "succeeded". Garbage still fails the inherited TryParse, which is why it gets a 400. MVC is unaffected: it binds through the variant's own RangeVariantTypeConverter, which TypeConverterContractTests holds, and refuses another shape.

Authority. docs/serialization.md says minimal APIs and MVC bind query, route and form values through the static TryParse, and that a property typed as one shape rejects the others. Here a valid literal of the right shape is dropped and the handler runs with null. HostBindingTests has Minimal_BindsTheQueryStringThroughTryParse for the union only.

Fix: give every sealed variant its own static Parse/TryParse returning the variant (the literal must have that shape; otherwise false), mirroring RangeVariantTypeConverter. Additive, not breaking — but the binding answers wrongly without throwing today, so under the release rule it ships in 10.0.0 rather than waiting.

Tests: a minimal-API case in HostBindingTests with a variant-typed parameter: right shape binds, wrong shape and garbage give 400. Seed: remove one variant's TryParse.

Found in the 10.0.0 bug hunt (code-analyst reading, reproduced here).

Activity

  1. added this to the 10.0.0 milestone on Oct 17, 2026
  2. added
    bugSomething isn't working
    silentAnswers wrongly without throwing — fixes go in the next major, never deferred
    area/literalsParse, TryParse and formatting
    package/coreAffects CodoMetis.ValueRanges
    on Oct 17, 2026
  3. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Measured wider by HostCultureProbeTests (#124), the host half of route B step 2. The defect is not confined to a query parameter:

    • Every variant of every range type (all 11 range types × EmptyRange, Finite, UnboundedStart, UnboundedEnd, Infinity).
    • Every minimal-API source that binds through TryParse: query, nullable query, route value and form field. The JSON body is correct (it goes through the variant's [JsonConverter]), and so is MVC on all five sources (it uses the variant's TypeConverter).
    • Right shape and wrong shape alike answer 200 with null. A literal of another shape should be 400, as MVC answers it.
    • Under every request culture (invariant, de-DE, fa-IR, th-TH via RequestLocalization). The culture plays no part.

    That makes 160 failing requests per range type, all bound to the same cause. They're registered as candidates in test/oracles/candidates.json, one per range type, with rows like Int32Range variant minimal TryParse . Each one fails as candidate fixed when the fix lands, so the fix PR removes the 11 entries. The test the fix needs already exists: the sweep is that test.

  4. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Would catch: HostCultureProbeTests (#125), row Int32Range. For example: Int32Range variant minimal TryParse query as Int32Range.Finite: answered 200 having bound null for [-5,10) under the default culture. The same failure appears for every variant × {query, nullable query, route value, form field} × {default, de-DE, fa-IR, th-TH}, and with a literal of another shape, in all 11 range rows.
    Verified by running the sweep on main at 3ecfa10 (the defect is unfixed), before the candidates were registered: 11 rows failed with 160 such lines each, and nothing else failed. The 11 candidates.json rows (<Type> variant minimal TryParse ) are the standing proof. Each fails as candidate fixed once the fix lands.

  5. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Oracle check (main at 3ecfa10): no oracle on main catches this yet (pending). Verified by a run: every class in oracles.json passes on 3ecfa10 with the defect present. TypeConverterContractTests reaches only the MVC path, which is correct. The minimal-API path is held only by HostBindingTests (J15), a worked example for the union type, not an oracle.
    The pair is host × variant-typed parameter in axes.md. Route B step 2's HostCultureProbeTests (#125, see the comment above) catches it, so this becomes would catch when #125 merges.

  6. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fixed in #177 (stacked on #175 → #173 → #171). Every shape of the 11 range types now has its own Parse and TryParse(string?, IFormatProvider?, out TShape?) through RangeFormat.ParseVariant, which the shapes' TypeConverter also uses. A minimal-API parameter typed as a shape binds a literal of that shape and answers 400 for another shape or garbage, on every source and request culture. The 11 candidates are gone, claim J38 covers the new surface, and the seed variant-without-tryparse.patch fails HostCultureProbeTests. Added package/nodatime: the NodaTime shapes got the same members.

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/literalsParse, TryParse and formattingbugSomething isn't workingfound-by/oracleFound by the generated-oracle machinery (claims × API surfaces)package/coreAffects CodoMetis.ValueRangespackage/nodatimeAffects CodoMetis.ValueRanges.NodaTimeseverity/lowCosmetic, message or documentation-level differencesilentAnswers wrongly without throwing — fixes go in the next major, never deferred

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions