dotnet / dotnet/aspnetcore

Results / TypedResults ignore null values

Open
#49,107 9 comments 2 reactions 0 assignees View on GitHub
area-minimal
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

When using Minimal APIs with nullable annotations enabled, it is possible to make a delegate that returns a null value for a nullable-annotated type, e.g.
```csharp
// Responds 200: "null"
app.MapGet("/nullable", T? () => null);
```

If the same value is instead wrapped in a `Results` or `TypedResults` container, the response body will be empty, e.g.
```csharp
// Responds 200: ""
app.MapGet("/iresult", IResult () => Results.Ok(null));
app.MapGet("/typed_result", Ok () => TypedResults.Ok(null));
```

This means changing an endpoint delegate to use these result wrappers causes a breakage in the API.

`T` can be both a value or reference type, the problem still occurs.

### Expected Behavior

Wrapping a return value in Results / TypedResults of the same status should respond with the exact same body as the value by itself.

Explicitly setting the status code in an idiomatic way should not cause a breaking change in the API.

```csharp
// Responds 200: "null"
app.MapGet("/nullable", T? () => null);
app.MapGet("/iresult", IResult () => Results.Ok(null));
app.MapGet("/typed_result", Ok () => TypedResults.Ok(null));
```

Current behavior is acceptable in case `null` is an invalid value for the type.
```csharp
// Programmer error, T is not nullable. Nasal demons may occur.
app.MapGet("/", Ok () => TypedResults.Ok(null!));
```

### Steps To Reproduce

```csharp
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/nullable", Data? () => Data.NullInstance);
app.MapGet("/iresult", IResult () => Results.Ok(Data.NullInstance));
app.MapGet("/typed_result", Ok () => TypedResults.Ok(Data.NullInstance));
// Semantically adjacent endpoints, for the sake of discussion
app.MapGet("/void", void () => {});
app.MapGet("/string", string () => "null");
await app.StartAsync();

var client = new HttpClient { BaseAddress = new Uri(app.Urls.First()), };
var endpoints = new[] { "/nullable", "/iresult", "/typed_result", "/void", "/string", };
foreach (var e in endpoints)
{
var r = await client.GetAsync(e);
var s = r.StatusCode;
var c = await r.Content.ReadAsStringAsync();
var contentType = r.Content.Headers.ContentType;
Console.WriteLine($"{e} -> {s} ({contentType}): `{c}`");
}

await app.StopAsync();

public record Data
{
public static Data? NullInstance => null;
}
```

Prints:
```
/nullable -> OK (application/json; charset=utf-8): `null`
/iresult -> OK (): ``
/typed_result -> OK (): ``
/void -> OK (): ``
/string -> OK (text/plain; charset=utf-8): `null`
```

### Exceptions (if any)

_No response_

### .NET Version

7.0.203

### Anything else?

```
.NET SDK:
Version: 7.0.203
Commit: 5b005c19f5

Runtime Environment:
OS Name: Windows
OS Version: 10.0.22621
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\7.0.203\

Host:
Version: 8.0.0-preview.5.23280.8
Architecture: x64
Commit: bc78804f5d

.NET SDKs installed:
6.0.410 [C:\Program Files\dotnet\sdk]
7.0.107 [C:\Program Files\dotnet\sdk]
7.0.203 [C:\Program Files\dotnet\sdk]
8.0.100-preview.5.23303.2 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
Microsoft.AspNetCore.App 6.0.18 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 7.0.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 8.0.0-preview.5.23302.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.15 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.18 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 7.0.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 8.0.0-preview.5.23280.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 6.0.18 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 7.0.7 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 8.0.0-preview.5.23302.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
None

Environment variables:
Not set

global.json file:
C:\Users\ThomasGravgaardHanse\RiderProjects\ProofOfConcept\global.json
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.