Support C# unions types in parameter binding
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
Track the design and implementation of C# union type support in Minimal APIs **non-body** parameter binding sources: query string, route values, headers, and form fields. Body parameters are covered in related issues (#66542, #66543) because STJ does the heavy lifting; non-body bindings go through a completely different pipeline (`TryParse` / `IParsable` / form complex-type binding) and need their own design.
### Scenarios
****Union from query string (simple cases):**
```csharp
union Sort(Ascending, Descending);
app.MapGet("/items", ([FromQuery] Sort sort) => /* ... */);
// GET /items?sort=ascending
```
**Union from route value:**
```csharp
union Id(int Numeric, Guid Guid);
app.MapGet("/items/{id}", (Id id) => /* ... */);
// /items/42 → Numeric(42)
// /items/3fa85f64-5717-4562-b3fc-2c963f66afa6 → Guid(...)
```
**Union from form (complex):**
```csharp
union Command(CreateOrder, UpdateOrder);
// CreateOrder { int CustomerId, string[] Items }
// UpdateOrder { int OrderId, string Status }
app.MapPost("/orders", ([FromForm] Command command) => /* ... */);
// content-type: application/x-www-form-urlencoded
// body: customerId=42&items=apple&items=pear → binds CreateOrder
// body: orderId=7&status=shipped → binds UpdateOrder
```
**Union from header:**
```csharp
union AuthScheme(BearerToken, ApiKey);
app.MapGet("/secure", ([FromHeader] AuthScheme auth) => /* ... */);
```
### Areas to investigate / design questions
1. **Parameter classification** (`RequestDelegateFactory.CreateArgument`, `EndpointParameter` in RDG):
- Today: union types with no `TryParse` and no explicit `[FromQuery]`/`[FromRoute]`/`[FromForm]` are inferred as body parameters.
- We need to decide how an explicitly-attributed union (`[FromQuery] MyUnion x`) is bound.
2. **`TryParse` semantics for unions** — design choices:
- **Option A (compiler-synthesized):** If C# emits a `TryParse` on unions whose cases are all `IParsable`, RDF/RDG just uses it — no ASP.NET work beyond detection.
- **Option B (framework-side fallback):** ASP.NET tries each case's `TryParse` in declaration order, picks the first success. Ambiguity rules + classifier hooks needed.
- **Option C (require explicit format):** Force users to provide an `IParsable` implementation. No magic.
- Need to align with the C# language design — confirm whether unions get auto-generated `TryParse`/`IParsable` for parsable cases.
3. **Complex form binding** (`FormDataMapper` / RDG form emitter):
- `FormDataMapper` walks properties of complex form-bound types. For unions it needs to determine the active case and recurse into its properties.
- **Non-discriminator (structural) matching** must be supported as the default path, mirroring STJ's behavior for unambiguous unions: pick the case whose required-property set matches the posted form fields. Example — `union Command(CreateOrder { CustomerId, Items }, UpdateOrder { OrderId, Status })` should bind `customerId=...&items=...` to `CreateOrder` without any `kind=` field.
- **Discriminator-based matching** is an opt-in for ambiguous unions (e.g., two cases share the same property shape) and should follow the same classifier convention used by STJ — not a separate ASP.NET-only concept.
- **Ambiguity / no-match handling**: define clear errors when zero or multiple cases match.
4. **Unambiguous primitive unions** — should `union Sort(Ascending, Descending)` (case-class unions with no payload) bind from `?sort=ascending` automatically? This is a strong UX win and probably the most-requested scenario. No discriminator needed — the value itself names the case.
5. **Value-shape unions in query/route/header** — `union Id(int Numeric, Guid Guid)` should bind by trying each case's parser; first unambiguous success wins. No discriminator field — the input shape itself disambiguates. Only fall back to a discriminator/classifier when case parsers overlap (e.g., `union(int, long)`).
6. **Error reporting**:
- 400 responses should clearly indicate which union case failed to parse (or that no case matched / multiple cases matched), not a generic "invalid value".
- Decide on the `BadRequest` payload format (especially for forms with multiple invalid fields).
7. **OpenAPI implications**:
- Query/route parameters of union type need schema representation distinct from body schemas covered in #66544.
- OpenAPI 3.0 parameters don't support `anyOf` cleanly at the parameter level — likely need to expand to multiple parameter definitions or document a single `string` parameter with format hints. Coordinate with #66544.
8. **RDG parity** (#66543 sibling):
- Whatever runtime contract we land on must be detectable at compile time by the source generator.
- `IsParsable` / `IsBindable` checks in RDG need to recognize unions.
Contributor guide
Assessment
This issue has not been assessed yet.