Minimal APIs: a C# union of IResult cases is serialized as JSON with 200 instead of executing the active case
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- csharp
- Domain
- backend-api-design
Research direction
Read RequestDelegateFactory, ExecuteHandlerHelper.ExecuteReturnAsync, the generated ExecuteReturnAsync, and EndpointMetadataPopulator; the issue identifies these as the return handling and metadata paths to inspect. Compare how they handle Results<T1,T2> with how a C# union exposes its active value, then run or add coverage for the reflection and Request Delegate Generator paths, endpoint filters, and OpenAPI metadata. Done means active IResult cases execute with their status and content type, and each case's response metadata reaches OpenAPI.
Written by the indexing model from the issue text.
Description
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
When a Minimal API handler returns a C# union whose cases are IResult types, the framework does not execute the active case. It serializes the union as JSON with status 200. A 404 problem goes out as a 200, with no error and no warning.
It happens on both the reflection path and the Request Delegate Generator.
Expected Behavior
The same behavior as Results<Ok<UserDto>, ProblemHttpResult>: the active case is executed (here 404 application/problem+json), and each case's response metadata reaches OpenAPI.
If this shape is not meant to be supported, an analyzer warning or a startup error would be much better than a wrong status code at runtime.
Steps To Reproduce
using Microsoft.AspNetCore.Http.HttpResults;
var app = WebApplication.Create(args);
app.MapGet("/union/{ok:bool}", (bool ok) => ok
? (Outcome)TypedResults.Ok(new UserDto(1, "ann"))
: TypedResults.Problem(statusCode: 404));
app.MapGet("/results/{ok:bool}", Results<Ok<UserDto>, ProblemHttpResult> (bool ok) => ok
? TypedResults.Ok(new UserDto(1, "ann"))
: TypedResults.Problem(statusCode: 404));
app.Run();
public record UserDto(int Id, string Name);
public union Outcome(Ok<UserDto>, ProblemHttpResult);
GET /results/false (correct):
404 application/problem+json
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404}
GET /union/false (wrong):
200 application/json
{"problemDetails":{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404},"contentType":"application/problem+json","statusCode":404}
The success case is wrong too. GET /union/true returns the Ok<T> wrapper instead of the DTO:
200 application/json
{"value":{"id":1,"name":"ann"},"statusCode":200}
Also:
- Endpoint filters get the union (
IUnion) as the return value, not anIResult. - OpenAPI shows one
200 application/jsonresponse whose schema isanyOf: [OkOfUserDto, ProblemHttpResult].
Exceptions (if any)
None. The wrong response is sent silently.
.NET Version
SDK 11.0.100, runtime 11.0.0. Same on 11.0.0-rc.3, and the same with <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>.
Anything else?
Every check looks only at the declared return type, and a union type does not implement IResult:
RequestDelegateFactory:typeof(IResult).IsAssignableFrom(returnType), otherwise the JSON writer.ExecuteHandlerHelper.ExecuteReturnAsyncand the generatedExecuteReturnAsync:obj is IResult.EndpointMetadataPopulator:typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType).
None of them look at IUnion.Value.
#66951 verified unions of data types as return types, but has no case with IResult cases. In #66857 it was suggested that users should eventually move to C# unions instead of Results<>, which makes this shape a natural thing to try. Related: #64599.
I am happy to send a PR once the expected behavior is agreed.
- Dominant language
- C#
- Stars
- 38.5k
- Forks
- 11.6k
- Avg merge
- 3d 1h
- Merged PRs (30d)
- 289
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/aspnetcore
-
area-networking
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70545 · 1 comment ·
Maintainers usually reply within 1 day
-
area-infrastructure
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
dotnet/aspnetcore#70524 · 1 comment ·
Maintainers usually reply within 1 day
-
[OpenTelemetry] Hosting replaces an Activity.DisplayName that the app set, when a route matchesOpenarea-hosting Regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70516 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
[SSE] ServerSentEventsResult.WrapEvents is missing [EnumeratorCancellation], so RequestAborted never reaches the app's generator on the IAsyncEnumerable<T> overloadPossibly taken @bharatprajapati-coder claimed this 1 day ago. Openarea-minimal
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70419 · 2 comments ·
Maintainers usually reply within 1 day
-
[Blazor] DefaultInMemoryCircuitPersistenceProvider never disposes the retention CancellationTokenSource when a persist is rejected at capacityPossibly taken @Vinoth2562000 claimed this 1 day ago. Openarea-blazor
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
dotnet/aspnetcore#70418 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
All issues in dotnet/aspnetcore
Similar issues
-
bug emitter:client:csharp
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
microsoft/typespec#12572 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
needs-area-label
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledOpenbug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
newrelic/newrelic-dotnet-agent#3949 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 87/100
Maintainers usually reply within 1 day
-
priority:P2
Difficulty 2/5 1-3 hours Newbie friendliness 80/100