`string[]` bound to querystring in minimal API should return `400` when not present
See original GitHub issueIs there an existing issue for this?
- I have searched the existing issues
Describe the bug
When you have a minimal API that takes a string[]
(or StringValues
) parameter which binds to the querystring, for example:
app.MapGet("/", (string[] q) => q);
calling the API without a querystring (e.g. /
) should give a 400 response saying BadHttpRequestException: Required parameter "string[] q" was not provided from query string.
But this does not happen - instead q
is an empty array. This differs from both of the following other examples:
app.MapGet("/", (string q) => q); //
app.MapGet("/", (StringValues q) => q);
which both return a 400 response as expected.
Expected Behavior
When binding a required parameter in a minimal API, a value must be present in the binding source (typically the querystring when binding arrays), otherwise the API returns a 400. This is the documented behaviour which works for other parameter types.
Steps To Reproduce
To reproduce, create an api like this: app.MapGet("/", (string[] q) => q);
and call the URL /
(without a querystring). The response is []
, where it should be a 400 Bad Request.
Exceptions (if any)
No response
.NET Version
7.0.101
Anything else?
I found this behaviour while reading some of the Expression
generation code in RequestDelegateFactory
. In particular CreateArgument()
calls BindParameterFromValue
which calls BindParameterFromExpression
.
The argument expression generated by this code when binding a string[]
parameter (called q
in this example) to the querystring is similar to the following (some artisitic license):
Task Invoke(HttpContext httpContext)
{
bool wasParamCheckFailure = false; // Added by RequestDelegateFactory.Create()
string[] q_local = httpContext.Request.Query["q"] // 👈 This is the problem
if (q_local == null) // 👈 Because this is never true.
{
wasParamCheckFailure = true;
Log.RequiredParameterNotProvided(httpContext, "string[]", "q", "query");
}
if(wasParamCheckFailure) // Added by RequestDelegateFactory.Create()
{
httpContext.Response.StatusCode = 400;
return Task.CompletedTask;
}
// .. call handler, handle response
}
I think the problem lies in the line above. Query["q"]
returns StringValues.Empty
when the item is not present, and is implicitly converted to a string[]
. (string[])StringValues.Empty
always returns an empty array, so is never null
, so the check never succeeds.
Issue Analytics
- State:
- Created 8 months ago
- Comments:7 (7 by maintainers)
I think you’re right that arrays are currently the only type of parameter that can never be null, but it’s also the only supported parameter type other than
StringValues
that can already uniquely represent missing parameters without using null. If you were binding an array from the body, there’s a difference between[]
and an empty body that needs to be represented.To me this is similar to the following in DI where
dependency
will never be null:I completely agree this is a weird disconnect. In a world where more people were aware of the existence of
StringValues.Empty
and redundancy ofNullable<StringValues>
, I would have liked to useStringValues.Empty
instead of null here as well. I don’t think empty arrays are nearly as obscure.I’m not as worried about passing null into a
string[]?
parameter. If someone went out of their way to make the parameter nullable, passing in null seems reasonable to me. After all, the original proposal from @davidfowl in https://github.com/dotnet/aspnetcore/issues/32516#issuecomment-1020275918 was, “If there are no values we pass an empty array unless the user makes the array nullable.”However, going from calling the route handler with an empty array to short-circuiting with a 400 in the non-nullable
string[]
/int[]
/whatever[]
case does seem very risky. And then if we’re going to pass in an empty array in the non-nullable case, passing in null for the exact same request in the nullable case also seems inconsistent.Agreed. We should update that section to call out the behavior of both
StringValues
andstring
/tryparsable arrays.@halter73 what was the actual action here? From my reading we won’t be changing any implementation here - but maybe a docs update?