Results / TypedResults ignore null values
- 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
Assessment
This issue has not been assessed yet.