Minimal APIs: a C# union of IResult cases is serialized as JSON with 200 instead of executing the active case

Open
#70,454 1 comment 0 reactions 0 assignees View on GitHub

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

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

area-minimal
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 an IResult.
  • OpenAPI shows one 200 application/json response whose schema is anyOf: [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.ExecuteReturnAsync and the generated ExecuteReturnAsync: 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

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/aspnetcore

All issues in dotnet/aspnetcore

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.