4

I'm currently converting a ASP.NET library to ASP.NET Core that's heavy on Model Binding, and one issue I'm unable to resolve is that for cases where a model binder wants to try and get values via Value Providers both from (From)Query and (From)Route (as in, they have different BindingSource) it can only access one, resulting in the binding to fail since it fails to find the target value.

While debugging the application I can see that the ModelBindingContext does have both value providers in its private OriginalValueProvider, but when trying to access the ValueProvider it only returns one of the providers - in this case, only the QueryStringValueProvider and not the RouteValueProvider).

This seems not to have been the behavior back in regular ASP.NET, as there the existing application happily uses both value providers to find its value - and does so successfully. Is this some breaking change between System.Web.Http.ModelBinding -> Microsoft.AspNetCore.Mvc.ModelBinding? Is there some option that must be enabled? Some guidance would be appreciated.

Here follows some screen caps from my debugging:

System.Web.Http original: System.Web.Http original

Microsoft.AspNetCore.Mvc rework: Microsoft.AspNetCore.Mvc rework

Microsoft.AspNetCore.Mvc OriginalValueProvider(s) proof: Microsoft.AspNetCore.Mvc OriginalValue proof

Edit: And here follows some code snippets, as requested:

Controller:

[HttpGet]
[Route("accounts/{accountId}/catalogs/{catalogId}/medias", Name = "GetCatalogMedias")]
[ProducesResponseType(typeof(IList<MediaResponse>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCatalogMedias(
    [FromRoute] string accountId,
    [FromRoute] string catalogId,
    [FromQuery, SortingFieldDefaultValue("created", SortingDirection.Desc)] IEnumerable<SortingField> sort,
    [FromQuery] QueryTree<MediaResponse> q = null,
    [FromQuery, Range(0, int.MaxValue)] int offset = 0,
    [FromQuery, Range(1, 200)] int limit = 200)
{
    if (!ModelState.IsValid)
    {
        return await _responseFactory.CreateBadRequest(ModelState);
    }

Binder:

public class CatalogMediasQueryModelBinder<T> : BaseBinder, IModelBinder
{
    private readonly QueryModelBinder _queryModelBinder;

    public CatalogMediasQueryModelBinder(QueryModelBinder queryModelBinder)
    {
        _queryModelBinder = queryModelBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var success = TryGetValueProviderResult(bindingContext, out var catalogId, "catalogId");

        if (!success)
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Missing catalogId");
            return;
        }
        //...

Base Binder:

public class BaseBinder
{
    protected bool TryGetValueProviderResult(ModelBindingContext bindingContext, out string result, string findValue = null)
    {
        if (bindingContext?.ValueProvider == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }
        result = null;
        var modelName = findValue ?? bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
        if (valueProviderResult == ValueProviderResult.None)
        {
            return false;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        result = value;
        return true;
    }

    protected Task SetModelBindingResult<T>(ModelBindingContext bindingContext, T value)
    {
        bindingContext.Result = ModelBindingResult.Success(value);
        return Task.CompletedTask;
    }
}
  • `the ModelBindingContext does have both value providers in its private OriginalValueProvider, but when trying to access the ValueProvider it only returns one of the providers - in this case, only the QueryStringValueProvider and not the RouteValueProvider)` Please share the code of your custom model binder and corresponding controller action that you want passing data both from route and querystring. – Fei Han Mar 03 '20 at 06:07
  • @Feihan, I've edited the post and included the necessary code snippets to correlate my statements and my screen caps. – Julius Mikkelä Mar 03 '20 at 09:10
  • 1
    `only the QueryStringValueProvider and not the RouteValueProvider` can not reproduce same issue, as you did in code, specified '***[FromRoute] string catalogId***' for parameter, which would [get value from RouteValueProvider](https://i.stack.imgur.com/X12g5.png). – Fei Han Mar 04 '20 at 10:02
  • Thank you for trying @FeiHan; do you have any idea what could cause this or if there's some possible solution? I've been thinking about trying to make a custom ValueProvider and see if that could fix it, but since this is "supposed" to work then I'm also hesitant to throw more digital duct tape at it. – Julius Mikkelä Mar 05 '20 at 11:16

0 Answers0