I'm using the ASP.NET Core 6 minimal APIs to build an API. After extracting anonymous/lambda handlers from MapGet
into their own methods, some endpoints act erroneously by always returning a 200 (OK) response with an empty body.
Not sure whether I've missed some gotcha regarding how async
works in C# or whether this is a bug with ASP.NET Core.
Reproducible example
Following is a minimal example where endpoints /a
and /b
execute similar methods, but one of them never responds as expected. To run the example, just copy the code into a new project created with dotnet new web
.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
async Task<int> longRunningValueGenerator() {
await Task.Delay(1000);
return 4; // chosen by a fair dice roll
// guaranteed to be random
}
async Task<IResult> getValueA(HttpContext http) {
if(http.Request.Query["secret"] != "test") {
return Results.Unauthorized();
}
var value = await longRunningValueGenerator();
return Results.Ok(new {
Value = value
});
}
app.MapGet("/", () => "get /a or /b with secret=test");
app.MapGet("/a", getValueA);
app.MapGet("/b", async (HttpContext http) => {
if(http.Request.Query["secret"] != "test") {
return Results.Unauthorized();
}
var value = await longRunningValueGenerator();
return Results.Ok(new {
Value = value
});
});
app.Run();
Some observations
- This issue starts occurring once the secret check is added. This is just for example, adding different code also triggers this behavior.
- It still takes a second (as determined by the delay) for the server to respond if secret is correct, even for
getValueA
. /a
will never respond with a different status code other than 200, even if secret is incorrect.- Non-async handlers that are separate methods work as expected.
CodePudding user response:
There's a couple of overloads for MapGet
:
The difference between these two signatures is in that last parameter, Delegate
vs RequestDelegate
. Delegate
represents any callable function, but RequestDelegate
represents a function with one parameter, HttpContext
, that returns a Task
.
In your example, the getValueA
function works for both of these delegates: it's any callable function and it's a function that takes HttpContext
. For reasons beyond my understanding, the compiler picks the RequestDelegate
version of MapGet
, and effectively throws away the IResult
value that it isn't expecting.
There might be a few options for fixing this, but the following solution should work well here. It changes your getValueA
signature and takes advantage of parameter-binding for secret
:
async Task<IResult> getValueA([FromQuery] string secret)
{
if (secret != "test")
{
return Results.Unauthorized();
}
var value = await longRunningValueGenerator();
return Results.Ok(new
{
Value = value
});
}
This change means that the version of MapGet
that takes a RequestDelegate
is no longer a candidate, and therefore the Delegate
version is called and processed correctly.
CodePudding user response:
This is a known issue with .NET 6 and minimal APIs and it has to do with overload resolution and what gets inferred by default https://github.com/dotnet/aspnetcore/issues/39956.
It has been fixed in .NET 7, we may backport to .NET 6 (you can request a back port on the issue).