// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
///
/// Represents an that delegates to one of a collection of
/// instances.
///
///
/// If no binder is available and the allows it,
/// this class tries to find a binder using an empty prefix.
///
public class CompositeModelBinder : ICompositeModelBinder
{
///
/// Initializes a new instance of the CompositeModelBinder class.
///
/// A collection of instances.
public CompositeModelBinder([NotNull] IEnumerable modelBinders)
{
ModelBinders = new List(modelBinders);
}
///
public IReadOnlyList ModelBinders { get; }
public virtual async Task BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
var newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName);
var modelBindingResult = await TryBind(newBindingContext);
if (modelBindingResult == null &&
bindingContext.FallbackToEmptyPrefix &&
!string.IsNullOrEmpty(bindingContext.ModelName))
{
// Fall back to empty prefix.
newBindingContext = CreateNewBindingContext(bindingContext,
modelName: string.Empty);
modelBindingResult = await TryBind(newBindingContext);
}
if (modelBindingResult == null)
{
return null; // something went wrong
}
bindingContext.OperationBindingContext.BodyBindingState =
newBindingContext.OperationBindingContext.BodyBindingState;
if (modelBindingResult.IsModelSet)
{
// Update the model state key if we are bound using an empty prefix and it is a complex type.
// This is needed as validation uses the model state key to log errors. The client validation expects
// the errors with property names rather than the full name.
if (newBindingContext.ModelMetadata.IsComplexType && string.IsNullOrEmpty(modelBindingResult.Key))
{
// For non-complex types, if we fell back to the empty prefix, we should still be using the name
// of the parameter/property. Complex types have their own property names which acts as model
// state keys and do not need special treatment.
// For example :
//
// public class Model
// {
// public int SimpleType { get; set; }
// }
// public void Action(int id, Model model)
// {
// }
//
// In this case, for the model parameter the key would be SimpleType instead of model.SimpleType.
// (i.e here the prefix for the model key is empty).
// For the id parameter the key would be id.
return modelBindingResult;
}
}
// Fall through to update the ModelBindingResult's key.
return new ModelBindingResult(
modelBindingResult.Model,
bindingContext.ModelName,
modelBindingResult.IsModelSet);
}
private async Task TryBind(ModelBindingContext bindingContext)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
foreach (var binder in ModelBinders)
{
var result = await binder.BindModelAsync(bindingContext);
if (result != null)
{
// Use returned ModelBindingResult if it either indicates the model was set or is related to a
// ModelState entry. The second condition is necessary because the ModelState entry would never be
// validated if caller fell back to the empty prefix, leading to an possibly-incorrect !IsValid.
//
// In most (hopefully all) cases, the ModelState entry exists because some binders add errors
// before returning a result with !IsModelSet. Those binders often cannot run twice anyhow.
if (result.IsModelSet || bindingContext.ModelState.ContainsKey(bindingContext.ModelName))
{
return result;
}
// Current binder should have been able to bind value but found nothing. Exit loop in a way that
// tells caller to fall back to the empty prefix, if appropriate. Do not return result because it
// means only "other binders are not applicable".
break;
}
}
// Either we couldn't find a binder, or the binder couldn't bind. Distinction is not important.
return null;
}
private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext,
string modelName)
{
var newBindingContext = new ModelBindingContext
{
Model = oldBindingContext.Model,
ModelMetadata = oldBindingContext.ModelMetadata,
ModelName = modelName,
ModelState = oldBindingContext.ModelState,
ValueProvider = oldBindingContext.ValueProvider,
OperationBindingContext = oldBindingContext.OperationBindingContext,
PropertyFilter = oldBindingContext.PropertyFilter,
BinderModelName = oldBindingContext.BinderModelName,
BindingSource = oldBindingContext.BindingSource,
BinderType = oldBindingContext.BinderType,
};
newBindingContext.OperationBindingContext.BodyBindingState = GetBodyBindingState(oldBindingContext);
// If the property has a specified data binding sources, we need to filter the set of value providers
// to just those that match. We can skip filtering when IsGreedy == true, because that can't use
// value providers.
//
// We also want to base this filtering on the - top-level value provider in case the data source
// on this property doesn't intersect with the ambient data source.
//
// Ex:
//
// public class Person
// {
// [FromQuery]
// public int Id { get; set; }
// }
//
// public IActionResult UpdatePerson([FromForm] Person person) { }
//
// In this example, [FromQuery] overrides the ambient data source (form).
var bindingSource = oldBindingContext.BindingSource;
if (bindingSource != null && !bindingSource.IsGreedy)
{
var valueProvider =
oldBindingContext.OperationBindingContext.ValueProvider as IBindingSourceValueProvider;
if (valueProvider != null)
{
newBindingContext.ValueProvider = valueProvider.Filter(bindingSource);
}
}
return newBindingContext;
}
private static BodyBindingState GetBodyBindingState(ModelBindingContext oldBindingContext)
{
var bindingSource = oldBindingContext.BindingSource;
var willReadBodyWithFormatter = bindingSource == BindingSource.Body;
var willReadBodyAsFormData = bindingSource == BindingSource.Form;
var currentModelNeedsToReadBody = willReadBodyWithFormatter || willReadBodyAsFormData;
var oldState = oldBindingContext.OperationBindingContext.BodyBindingState;
// We need to throw if there are multiple models which can cause body to be read multiple times.
// Reading form data multiple times is ok since we cache form data. For the models marked to read using
// formatters, multiple reads are not allowed.
if (oldState == BodyBindingState.FormatterBased && currentModelNeedsToReadBody ||
oldState == BodyBindingState.FormBased && willReadBodyWithFormatter)
{
throw new InvalidOperationException(Resources.MultipleBodyParametersOrPropertiesAreNotAllowed);
}
var state = oldBindingContext.OperationBindingContext.BodyBindingState;
if (willReadBodyWithFormatter)
{
state = BodyBindingState.FormatterBased;
}
else if (willReadBodyAsFormData && oldState != BodyBindingState.FormatterBased)
{
// Only update the model binding state if we have not discovered formatter based state already.
state = BodyBindingState.FormBased;
}
return state;
}
}
}