Skip to content

FromQueryAttribute with Dictionary incorrectly handles missing parameters #64222

@kennethac

Description

@kennethac

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

In .NET 8, when using [FromQuery] on a controller action method parameter, if the request does not contain a query parameter to bind to the parameter, the framework usually responds with a "400 Bad Request" response that contains a helpful application/problem+json response about the parameter being missing.

However, if the [FromQuery] is used on a parameter which is a Dictionary<,> and that parameter is missing from the query string but there are other query parameters in the request, then an exception is thrown, causing a "500 Internal Server Error" response to be returned. Worse, the exception logged in the server logs contains the wrong information, saying instead "System.FormatException: The input string 'a' was not in a correct format" where a is the first received query string parameter (even if it does not correspond to a parameter on the action), making it very difficult to debug.

In .NET 9 and above, the behavior has changed slightly but still contains a bug. In these versions, omitting the query parameter for the Dictionary correctly results in a 400 response. However, if there are any extra query parameters which do not correspond to action parameters, they are also reported in the problem response as unable to be parsed (which is unnecessary and does not occur when the missing parameter is not a Dictionary).

Expected Behavior

The behavior for the Dictionary parameter should match the behavior for other parameters.

That is, in .NET 8, when missing it should return a 400 response with a helpful error message to both the client and the server.

In .NET 9, it should not report other, unknown parameters as validation errors.

Steps To Reproduce

This is a single file program that can be used to reproduce the problem:

#:sdk Microsoft.NET.Sdk.Web
#:property TargetFramework=net8.0
#:property PublishAot=false

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();
builder.Services.AddMvc();
var app = builder.Build();
app.MapControllers();

await app.RunAsync();

[ApiController]
public class TestController : ControllerBase {
  // Sending a request with no query string at all results in an ok with an empty dictionary as the value for c
  // Sending a request with a query string but not a value for c results in a 500 response
  [Route("/alone")]
  public IActionResult Test([FromQuery]Dictionary<int, string> c) {
    return Ok(new {
      c
    });
  }

  // Sending a request with a query string including a & b but not c results in a 500 response
  // Sending a request without a or b results in a 400 response
  [Route("/test")]
  public IActionResult Test([FromQuery]string a, [FromQuery]string b, [FromQuery]Dictionary<int, string> c) {
    return Ok(new {
      a, b, c
    });
  }
}

Exceptions (if any)

fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HNGOK9T4K392", Request id "0HNGOK9T4K392:00000001": An unhandled exception was thrown by the application.
      System.FormatException: The input string 'zebra' was not in a correct format.
         at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
         at System.Int32.Parse(String s, NumberStyles style, IFormatProvider provider)
         at System.ComponentModel.Int32Converter.FromString(String value, NumberFormatInfo formatInfo)
         at System.ComponentModel.BaseNumberConverter.ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, Object value)
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingHelper.ConvertSimpleType(Object value, Type destinationType, CultureInfo culture)
         at Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingHelper.ConvertTo[T](Object value, CultureInfo culture)
         at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinder`2.BindModelAsync(ModelBindingContext bindingContext)
         at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync(ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor parameter, ModelMetadata metadata, Object value, Object container)
         at Microsoft.AspNetCore.Mvc.Controllers.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

.NET Version

10.0.100-preview.6.25358.103 (but I've seen it in production in a .NET 8 Docker image)

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    NativeAOTarea-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions