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:
Microsoft.AspNetCore.Mvc rework:
Microsoft.AspNetCore.Mvc OriginalValueProvider(s) 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;
}
}