Adding support for model binding specifically marked controller properties.

This commit is contained in:
Harsh Gupta 2015-03-02 18:12:16 -08:00
parent c082d4aa49
commit adeb1ba194
42 changed files with 969 additions and 291 deletions

View File

@ -30,6 +30,11 @@ namespace Microsoft.AspNet.Mvc
public IList<ParameterDescriptor> Parameters { get; set; }
/// <summary>
/// The set of properties which are model bound.
/// </summary>
public IList<ParameterDescriptor> BoundProperties { get; set; }
public IList<FilterDescriptor> FilterDescriptors { get; set; }
/// <summary>

View File

@ -26,6 +26,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
Filters = new List<IFilter>();
RouteConstraints = new List<IRouteConstraintProvider>();
Properties = new Dictionary<object, object>();
ControllerProperties = new List<PropertyModel>();
}
public ControllerModel([NotNull] ControllerModel other)
@ -48,6 +49,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
ApiExplorer = new ApiExplorerModel(other.ApiExplorer);
AttributeRoutes = new List<AttributeRouteModel>(
other.AttributeRoutes.Select(a => new AttributeRouteModel(a)));
ControllerProperties =
new List<PropertyModel>(other.ControllerProperties.Select(p => new PropertyModel(p)));
}
public IList<IActionConstraintMetadata> ActionConstraints { get; private set; }
@ -76,6 +79,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
public TypeInfo ControllerType { get; private set; }
public IList<PropertyModel> ControllerProperties { get; }
public IList<IFilter> Filters { get; private set; }
public IList<IRouteConstraintProvider> RouteConstraints { get; private set; }

View File

@ -10,6 +10,7 @@ using Microsoft.AspNet.Cors;
using Microsoft.AspNet.Cors.Core;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.Framework.Internal;
using Microsoft.Framework.Logging;
@ -44,8 +45,9 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
public ControllerModel BuildControllerModel([NotNull] TypeInfo typeInfo)
{
var controllerModel = CreateControllerModel(typeInfo);
var controllerType = typeInfo.AsType();
foreach (var methodInfo in typeInfo.AsType().GetMethods())
foreach (var methodInfo in controllerType.GetMethods())
{
var actionModels = _actionModelBuilder.BuildActionModels(typeInfo, methodInfo);
if (actionModels != null)
@ -58,6 +60,17 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
}
}
foreach (var propertyHelper in PropertyHelper.GetProperties(controllerType))
{
var propertyInfo = propertyHelper.Property;
var propertyModel = CreatePropertyModel(propertyInfo);
if (propertyModel != null)
{
propertyModel.Controller = controllerModel;
controllerModel.ControllerProperties.Add(propertyModel);
}
}
return controllerModel;
}
@ -134,6 +147,25 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
return controllerModel;
}
/// <summary>
/// Creates a <see cref="PropertyModel"/> for the given <see cref="PropertyInfo"/>.
/// </summary>
/// <param name="propertyInfo">The <see cref="PropertyInfo"/>.</param>
/// <returns>A <see cref="PropertyModel"/> for the given <see cref="PropertyInfo"/>.</returns>
protected virtual PropertyModel CreatePropertyModel([NotNull] PropertyInfo propertyInfo)
{
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToArray() is object
var attributes = propertyInfo.GetCustomAttributes(inherit: true).OfType<object>().ToArray();
var propertyModel = new PropertyModel(propertyInfo, attributes);
var bindingInfo = BindingInfo.GetBindingInfo(attributes);
propertyModel.BindingInfo = bindingInfo;
propertyModel.PropertyName = propertyInfo.Name;
return propertyModel;
}
private static void AddRange<T>(IList<T> list, IEnumerable<T> items)
{
foreach (var item in items)

View File

@ -0,0 +1,69 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ApplicationModels
{
/// <summary>
/// A type which is used to represent a property in a <see cref="ControllerModel"/>.
/// </summary>
[DebuggerDisplay("PropertyModel: Name={PropertyName}")]
public class PropertyModel
{
/// <summary>
/// Creates a new instance of <see cref="PropertyModel"/>.
/// </summary>
/// <param name="propertyInfo">The <see cref="PropertyInfo"/> for the underlying property.</param>
/// <param name="attributes">Any attributes which are annotated on the property.</param>
public PropertyModel([NotNull] PropertyInfo propertyInfo,
[NotNull] IReadOnlyList<object> attributes)
{
PropertyInfo = propertyInfo;
Attributes = new List<object>(attributes);
}
/// <summary>
/// Creats a new instance of <see cref="PropertyModel"/> from a given <see cref="PropertyModel"/>.
/// </summary>
/// <param name="other">The <see cref="PropertyModel"/> which needs to be copied.</param>
public PropertyModel([NotNull] PropertyModel other)
{
Controller = other.Controller;
Attributes = new List<object>(other.Attributes);
BindingInfo = other.BindingInfo;
PropertyInfo = other.PropertyInfo;
PropertyName = other.PropertyName;
}
/// <summary>
/// Gets or sets the <see cref="ControllerModel"/> this <see cref="PropertyModel"/> is associated with.
/// </summary>
public ControllerModel Controller { get; set; }
/// <summary>
/// Gets any attributes which are annotated on the property.
/// </summary>
public IReadOnlyList<object> Attributes { get; }
/// <summary>
/// Gets or sets the <see cref="BindingInfo"/> associated with this model.
/// </summary>
public BindingInfo BindingInfo { get; set; }
/// <summary>
/// Gets the underlying <see cref="PropertyInfo"/>.
/// </summary>
public PropertyInfo PropertyInfo { get; }
/// <summary>
/// Gets or sets the name of the property represented by this model.
/// </summary>
public string PropertyName { get; set; }
}
}

View File

@ -1293,9 +1293,10 @@ namespace Microsoft.AspNet.Mvc
var validationContext = new ModelValidationContext(
modelName,
BindingContext.ValidatorProvider,
ModelState,
modelExplorer);
bindingSource: null,
validatorProvider: BindingContext.ValidatorProvider,
modelState: ModelState,
modelExplorer: modelExplorer);
ObjectValidator.Validate(validationContext);
return ModelState.IsValid;

View File

@ -11,6 +11,7 @@ using Microsoft.AspNet.Mvc.ApplicationModels;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
@ -42,6 +43,12 @@ namespace Microsoft.AspNet.Mvc
foreach (var controller in application.Controllers)
{
// Only add properties which are explictly marked to bind.
// The attribute check is required for ModelBinder attribute.
var controllerPropertyDescriptors = controller.ControllerProperties
.Where(p => p.BindingInfo != null)
.Select(CreateParameterDescriptor)
.ToList();
foreach (var action in controller.Actions)
{
// Controllers with multiple [Route] attributes (or user defined implementation of
@ -60,6 +67,7 @@ namespace Microsoft.AspNet.Mvc
AddRouteConstraints(removalConstraints, actionDescriptor, controller, action);
AddProperties(actionDescriptor, action, controller, application);
actionDescriptor.BoundProperties = controllerPropertyDescriptors;
if (IsAttributeRoutedAction(actionDescriptor))
{
hasAttributeRoutes = true;
@ -272,13 +280,25 @@ namespace Microsoft.AspNet.Mvc
return actionDescriptor;
}
private static ParameterDescriptor CreateParameterDescriptor(ParameterModel parameter)
private static ParameterDescriptor CreateParameterDescriptor(ParameterModel parameterModel)
{
var parameterDescriptor = new ParameterDescriptor()
{
Name = parameter.ParameterName,
ParameterType = parameter.ParameterInfo.ParameterType,
BindingInfo = parameter.BindingInfo
Name = parameterModel.ParameterName,
ParameterType = parameterModel.ParameterInfo.ParameterType,
BindingInfo = parameterModel.BindingInfo
};
return parameterDescriptor;
}
private static ParameterDescriptor CreateParameterDescriptor(PropertyModel propertyModel)
{
var parameterDescriptor = new ParameterDescriptor()
{
BindingInfo = propertyModel.BindingInfo,
Name = propertyModel.PropertyName,
ParameterType = propertyModel.PropertyInfo.PropertyType,
};
return parameterDescriptor;

View File

@ -80,11 +80,11 @@ namespace Microsoft.AspNet.Mvc.Core
return actionResult;
}
protected override Task<IDictionary<string, object>> GetActionArgumentsAsync(
protected override Task<IDictionary<string, object>> BindActionArgumentsAsync(
ActionContext context,
ActionBindingContext bindingContext)
{
return _argumentBinder.GetActionArgumentsAsync(context, bindingContext);
return _argumentBinder.BindActionArgumentsAsync(context, bindingContext, Instance);
}
// Marking as internal for Unit Testing purposes.

View File

@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.Internal;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc
@ -34,9 +35,10 @@ namespace Microsoft.AspNet.Mvc
_validator = validator;
}
public async Task<IDictionary<string, object>> GetActionArgumentsAsync(
public async Task<IDictionary<string, object>> BindActionArgumentsAsync(
ActionContext actionContext,
ActionBindingContext actionBindingContext)
ActionBindingContext actionBindingContext,
object controller)
{
var actionDescriptor = actionContext.ActionDescriptor as ControllerActionDescriptor;
if (actionDescriptor == null)
@ -47,31 +49,48 @@ namespace Microsoft.AspNet.Mvc
nameof(actionContext));
}
var operationBindingContext = GetOperationBindingContext(actionContext, actionBindingContext);
var controllerProperties = new Dictionary<string, object>(StringComparer.Ordinal);
await PopulateArgumentsAsync(
operationBindingContext,
actionContext.ModelState,
controllerProperties,
actionDescriptor.BoundProperties);
var controllerType = actionDescriptor.ControllerTypeInfo.AsType();
ActivateProperties(controller, controllerType, controllerProperties);
var actionArguments = new Dictionary<string, object>(StringComparer.Ordinal);
await PopulateArgumentsAsync(
actionContext,
actionBindingContext,
operationBindingContext,
actionContext.ModelState,
actionArguments,
actionDescriptor.Parameters);
return actionArguments;
}
private void ActivateProperties(object controller, Type containerType, Dictionary<string, object> properties)
{
var propertyHelpers = PropertyHelper.GetProperties(controller);
foreach (var property in properties)
{
var propertyHelper = propertyHelpers.First(helper => helper.Name == property.Key);
if (propertyHelper.Property == null || !propertyHelper.Property.CanWrite)
{
// nothing to do
return;
}
var setter = PropertyHelper.MakeFastPropertySetter(propertyHelper.Property);
setter(controller, property.Value);
}
}
private async Task PopulateArgumentsAsync(
ActionContext actionContext,
ActionBindingContext bindingContext,
OperationBindingContext operationContext,
ModelStateDictionary modelState,
IDictionary<string, object> arguments,
IEnumerable<ParameterDescriptor> parameterMetadata)
{
var operationBindingContext = new OperationBindingContext
{
ModelBinder = bindingContext.ModelBinder,
ValidatorProvider = bindingContext.ValidatorProvider,
MetadataProvider = _modelMetadataProvider,
HttpContext = actionContext.HttpContext,
ValueProvider = bindingContext.ValueProvider,
};
var modelState = actionContext.ModelState;
modelState.MaxAllowedErrors = _options.MaxModelValidationErrors;
foreach (var parameter in parameterMetadata)
{
@ -82,9 +101,9 @@ namespace Microsoft.AspNet.Mvc
metadata,
parameter.BindingInfo,
modelState,
operationBindingContext);
operationContext);
var modelBindingResult = await bindingContext.ModelBinder.BindModelAsync(modelBindingContext);
var modelBindingResult = await operationContext.ModelBinder.BindModelAsync(modelBindingContext);
if (modelBindingResult != null && modelBindingResult.IsModelSet)
{
var modelExplorer = new ModelExplorer(
@ -95,8 +114,9 @@ namespace Microsoft.AspNet.Mvc
arguments[parameter.Name] = modelBindingResult.Model;
var validationContext = new ModelValidationContext(
modelBindingResult.Key,
bindingContext.ValidatorProvider,
actionContext.ModelState,
modelBindingContext.BindingSource,
operationContext.ValidatorProvider,
modelState,
modelExplorer);
_validator.Validate(validationContext);
}
@ -121,5 +141,19 @@ namespace Microsoft.AspNet.Mvc
return modelBindingContext;
}
private OperationBindingContext GetOperationBindingContext(
ActionContext actionContext,
ActionBindingContext bindingContext)
{
return new OperationBindingContext
{
ModelBinder = bindingContext.ModelBinder,
ValidatorProvider = bindingContext.ValidatorProvider,
MetadataProvider = _modelMetadataProvider,
HttpContext = actionContext.HttpContext,
ValueProvider = bindingContext.ValueProvider,
};
}
}
}

View File

@ -149,13 +149,7 @@ namespace Microsoft.AspNet.Mvc
type,
typeof(ActivateAttribute),
CreateActivateInfo);
var activatorsForFromServiceProperties = PropertyActivator<ActionContext>.GetPropertiesToActivate(
type,
typeof(FromServicesAttribute),
CreateFromServicesInfo);
return Enumerable.Concat(activatorsForActivateProperties, activatorsForFromServiceProperties)
.ToArray();
return activatorsForActivateProperties;
}
private PropertyActivator<ActionContext> CreateActivateInfo(

View File

@ -147,6 +147,24 @@ namespace Microsoft.AspNet.Mvc.Description
}
}
if (context.ActionDescriptor.BoundProperties != null)
{
foreach (var actionParameter in context.ActionDescriptor.BoundProperties)
{
var visitor = new PseudoModelBindingVisitor(context, actionParameter);
var modelMetadata = context.MetadataProvider.GetMetadataForProperty(
containerType: context.ActionDescriptor.ControllerTypeInfo.AsType(),
propertyName: actionParameter.Name);
var bindingContext = ApiParameterDescriptionContext.GetContext(
modelMetadata,
actionParameter.BindingInfo,
propertyName: actionParameter.Name);
visitor.WalkParameter(bindingContext);
}
}
for (var i = context.Results.Count - 1; i >= 0; i--)
{
// Remove any 'hidden' parameters. These are things that can't come from user input,

View File

@ -88,7 +88,7 @@ namespace Microsoft.AspNet.Mvc.Core
protected abstract Task<IActionResult> InvokeActionAsync(ActionExecutingContext actionExecutingContext);
protected abstract Task<IDictionary<string, object>> GetActionArgumentsAsync(
protected abstract Task<IDictionary<string, object>> BindActionArgumentsAsync(
[NotNull] ActionContext context,
[NotNull] ActionBindingContext bindingContext);
@ -434,7 +434,7 @@ namespace Microsoft.AspNet.Mvc.Core
Instance = CreateInstance();
var arguments = await GetActionArgumentsAsync(ActionContext, ActionBindingContext);
var arguments = await BindActionArgumentsAsync(ActionContext, ActionBindingContext);
_actionExecutingContext = new ActionExecutingContext(
ActionContext,

View File

@ -14,12 +14,15 @@ namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Returns a dictionary of representing the parameter-argument name-value pairs,
/// which can be used to invoke the action.
/// which can be used to invoke the action. Also binds properties explicitly marked properties on the
/// <paramref name="controller"/>.
/// </summary>
/// <param name="context">The action context assoicated with the current action.</param>
/// <param name="bindingContext">The <see cref="ActionBindingContext"/>.</param>
Task<IDictionary<string, object>> GetActionArgumentsAsync(
/// <param name="controller">The controller object which contains the action.</param>
Task<IDictionary<string, object>> BindActionArgumentsAsync(
[NotNull] ActionContext context,
[NotNull] ActionBindingContext bindingContext);
[NotNull] ActionBindingContext bindingContext,
[NotNull] object controller);
}
}

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNet.Mvc.Logging
Name = inner.Name;
DisplayName = inner.DisplayName;
Parameters = inner.Parameters.Select(p => new ParameterDescriptorValues(p)).ToList();
BoundProperties = inner.BoundProperties.Select(p => new ParameterDescriptorValues(p)).ToList();
FilterDescriptors = inner.FilterDescriptors.Select(f => new FilterDescriptorValues(f)).ToList();
RouteConstraints = inner.RouteConstraints.Select(r => new RouteDataActionConstraintValues(r)).ToList();
AttributeRouteInfo = new AttributeRouteInfoValues(inner.AttributeRouteInfo);
@ -54,6 +55,12 @@ namespace Microsoft.AspNet.Mvc.Logging
/// </summary>
public IList<ParameterDescriptorValues> Parameters { get; }
/// <summary>
/// The parameters of the action as <see cref="ParameterDescriptorValues"/>.
/// See <see cref="ActionDescriptor.BoundProperties"/>.
/// </summary>
public IList<ParameterDescriptorValues> BoundProperties { get; }
/// <summary>
/// The filters of the action as <see cref="FilterDescriptorValues"/>.
/// See <see cref="ActionDescriptor.FilterDescriptors"/>.

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNet.Mvc.Logging
ApiExplorer = new ApiExplorerModelValues(inner.ApiExplorer);
Actions = inner.Actions.Select(a => new ActionModelValues(a)).ToList();
Attributes = inner.Attributes.Select(a => a.GetType()).ToList();
ControllerProperties = inner.ControllerProperties.Select(p => new PropertyModelValues(p)).ToList();
Filters = inner.Filters.Select(f => new FilterValues(f)).ToList();
ActionConstraints = inner.ActionConstraints?.Select(a => new ActionConstraintValues(a))?.ToList();
RouteConstraints = inner.RouteConstraints.Select(
@ -43,6 +44,11 @@ namespace Microsoft.AspNet.Mvc.Logging
/// </summary>
public Type ControllerType { get; }
/// <summary>
/// The <see cref="Type"/> of the controller. See <see cref="ControllerModel.ControllerType"/>.
/// </summary>
public IList<PropertyModelValues> ControllerProperties { get; }
/// <summary>
/// See <see cref="ControllerModel.ApiExplorer"/>.
/// </summary>

View File

@ -0,0 +1,39 @@
// 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 Microsoft.AspNet.Mvc.ApplicationModels;
using Microsoft.Framework.Internal;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Mvc.Logging
{
/// <summary>
/// Logging representation of a <see cref="PropertyModelValues"/>. Logged as a substructure of
/// <see cref="ControllerModelValues"/>, this contains the name, type, and
/// binder metadata of the property.
/// </summary>
public class PropertyModelValues : ReflectionBasedLogValues
{
public PropertyModelValues([NotNull] PropertyModel inner)
{
PropertyName = inner.PropertyName;
PropertyType = inner.PropertyInfo.PropertyType;
}
/// <summary>
/// The name of the property. See <see cref="PropertyModel.PropertyName"/>.
/// </summary>
public string PropertyName { get; }
/// <summary>
/// The <see cref="Type"/> of the property.
/// </summary>
public Type PropertyType { get; }
public override string Format()
{
return LogFormatter.FormatLogValues(this);
}
}
}

View File

@ -41,10 +41,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public static BindingInfo GetBindingInfo(IEnumerable<object> attributes)
{
var bindingInfo = new BindingInfo();
var isBindingInfoPresent = false;
// BinderModelName
foreach (var binderModelNameAttribute in attributes.OfType<IModelNameProvider>())
{
isBindingInfoPresent = true;
if (binderModelNameAttribute?.Name != null)
{
bindingInfo.BinderModelName = binderModelNameAttribute.Name;
@ -55,6 +57,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// BinderType
foreach (var binderTypeAttribute in attributes.OfType<IBinderTypeProviderMetadata>())
{
isBindingInfoPresent = true;
if (binderTypeAttribute.BinderType != null)
{
bindingInfo.BinderType = binderTypeAttribute.BinderType;
@ -65,6 +68,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// BindingSource
foreach (var bindingSourceAttribute in attributes.OfType<IBindingSourceMetadata>())
{
isBindingInfoPresent = true;
if (bindingSourceAttribute.BindingSource != null)
{
bindingInfo.BindingSource = bindingSourceAttribute.BindingSource;
@ -76,11 +80,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var predicateProviders = attributes.OfType<IPropertyBindingPredicateProvider>().ToArray();
if (predicateProviders.Length > 0)
{
isBindingInfoPresent = true;
bindingInfo.PropertyBindingPredicateProvider = new CompositePredicateProvider(
predicateProviders);
}
return bindingInfo;
return isBindingInfoPresent ? bindingInfo : null;
}
private class CompositePredicateProvider : IPropertyBindingPredicateProvider

View File

@ -15,6 +15,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public static class ModelAttributes
{
/// <summary>
/// Gets the attributes for the given <paramref name="parameter"/>.
/// </summary>
/// <param name="parameter">A <see cref="ParameterInfo"/> for which attributes need to be resolved.
/// </param>
/// <returns>An <see cref="IEnumerable{object}"/> containing the attributes on the
/// <paramref name="parameter"/> before the attributes on the <paramref name="parameter"/> type.</returns>
public static IEnumerable<object> GetAttributesForParameter(ParameterInfo parameter)
{
// Return the parameter attributes first.
var parameterAttributes = parameter.GetCustomAttributes();
var typeAttributes = parameter.ParameterType.GetTypeInfo().GetCustomAttributes();
return parameterAttributes.Concat(typeAttributes);
}
/// <summary>
/// Gets the attributes for the given <paramref name="property"/>.
/// </summary>

View File

@ -64,18 +64,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <returns>A new instance of <see cref="ModelBindingContext"/>.</returns>
public static ModelBindingContext GetModelBindingContext(
[NotNull] ModelMetadata metadata,
[NotNull] BindingInfo bindingInfo,
BindingInfo bindingInfo,
string modelName)
{
var binderModelName = bindingInfo.BinderModelName ?? metadata.BinderModelName;
var binderModelName = bindingInfo?.BinderModelName ?? metadata.BinderModelName;
var propertyPredicateProvider =
bindingInfo.PropertyBindingPredicateProvider ?? metadata.PropertyBindingPredicateProvider;
bindingInfo?.PropertyBindingPredicateProvider ?? metadata.PropertyBindingPredicateProvider;
return new ModelBindingContext()
{
ModelMetadata = metadata,
BindingSource = bindingInfo.BindingSource ?? metadata.BindingSource,
BindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource,
PropertyFilter = propertyPredicateProvider?.PropertyFilter,
BinderType = bindingInfo.BinderType ?? metadata.BinderType,
BinderType = bindingInfo?.BinderType ?? metadata.BinderType,
BinderModelName = binderModelName,
ModelName = binderModelName ?? metadata.PropertyName ?? modelName,
FallbackToEmptyPrefix = binderModelName == null,

View File

@ -31,7 +31,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// <inheritdoc />
public void Validate([NotNull] ModelValidationContext modelValidationContext)
{
var modelExplorer = modelValidationContext.ModelExplorer;
var validationContext = new ValidationContext()
{
ModelValidationContext = modelValidationContext,
@ -40,23 +39,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
ValidateNonVisitedNodeAndChildren(
modelValidationContext.RootPrefix,
modelExplorer,
validationContext,
validators: null);
}
private bool ValidateNonVisitedNodeAndChildren(
string modelKey,
ModelExplorer modelExplorer,
ValidationContext validationContext,
IList<IModelValidator> validators)
{
var modelValidationContext = validationContext.ModelValidationContext;
var modelExplorer = modelValidationContext.ModelExplorer;
// Recursion guard to avoid stack overflows
RuntimeHelpers.EnsureSufficientExecutionStack();
var modelState = validationContext.ModelValidationContext.ModelState;
var modelState = modelValidationContext.ModelState;
var bindingSource = modelExplorer.Metadata.BindingSource;
var bindingSource = modelValidationContext.BindingSource;
if (bindingSource != null && !bindingSource.IsFromRequest)
{
// Short circuit if the metadata represents something that was not bound using request data.
@ -64,7 +64,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var validationState = modelState.GetFieldValidationState(modelKey);
if (validationState == ModelValidationState.Unvalidated)
{
validationContext.ModelValidationContext.ModelState.MarkFieldSkipped(modelKey);
modelValidationContext.ModelState.MarkFieldSkipped(modelKey);
}
// For validation purposes this model is valid.
@ -83,7 +83,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// The validators are not null in the case of validating an array. Since the validators are
// the same for all the elements of the array, we do not do GetValidators for each element,
// instead we just pass them over. See ValidateElements function.
var validatorProvider = validationContext.ModelValidationContext.ValidatorProvider;
var validatorProvider = modelValidationContext.ValidatorProvider;
var validatorProviderContext = new ModelValidatorProviderContext(modelExplorer.Metadata);
validatorProvider.GetValidators(validatorProviderContext);
@ -160,7 +160,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
}
}
private bool ValidateProperties(string currentModelKey, ModelExplorer modelExplorer, ValidationContext validationContext)
private bool ValidateProperties(
string currentModelKey,
ModelExplorer modelExplorer,
ValidationContext validationContext)
{
var isValid = true;
@ -168,13 +171,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
var propertyExplorer = modelExplorer.GetExplorerForProperty(property.PropertyName);
var propertyMetadata = propertyExplorer.Metadata;
var propertyValidationContext = new ValidationContext()
{
ModelValidationContext = ModelValidationContext.GetChildValidationContext(
validationContext.ModelValidationContext,
propertyExplorer),
Visited = validationContext.Visited
};
var propertyBindingName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName;
var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, propertyBindingName);
if (!ValidateNonVisitedNodeAndChildren(
childKey,
propertyExplorer,
validationContext,
propertyValidationContext,
validators: null))
{
isValid = false;
@ -209,7 +218,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
var elementExplorer = new ModelExplorer(_modelMetadataProvider, elementMetadata, element);
var elementKey = ModelBindingHelper.CreateIndexModelName(currentKey, index);
if (!ValidateNonVisitedNodeAndChildren(elementKey, elementExplorer, validationContext, validators))
var elementValidationContext = new ValidationContext()
{
ModelValidationContext = ModelValidationContext.GetChildValidationContext(
validationContext.ModelValidationContext,
elementExplorer),
Visited = validationContext.Visited
};
if (!ValidateNonVisitedNodeAndChildren(elementKey, elementValidationContext, validators))
{
isValid = false;
}
@ -245,9 +262,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// In a large array (tens of thousands or more) scenario it's very significant.
if (validators == null || validators.Count > 0)
{
var modelValidationContext =
new ModelValidationContext(validationContext.ModelValidationContext, modelExplorer);
var modelValidationContext = ModelValidationContext.GetChildValidationContext(
validationContext.ModelValidationContext,
modelExplorer);
var modelValidationState = modelState.GetValidationState(modelKey);
// If either the model or its properties are unvalidated, validate them now.

View File

@ -11,6 +11,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
[NotNull] ModelBindingContext bindingContext,
[NotNull] ModelExplorer modelExplorer)
: this(bindingContext.ModelName,
bindingContext.BindingSource,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
modelExplorer)
@ -19,6 +20,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ModelValidationContext(
string rootPrefix,
BindingSource bindingSource,
[NotNull] IModelValidatorProvider validatorProvider,
[NotNull] ModelStateDictionary modelState,
[NotNull] ModelExplorer modelExplorer)
@ -27,25 +29,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
RootPrefix = rootPrefix;
ValidatorProvider = validatorProvider;
ModelExplorer = modelExplorer;
BindingSource = bindingSource;
}
public ModelValidationContext(
/// <summary>
/// Constructs a new instance of the <see cref="ModelValidationContext"/> class using the
/// <paramref name="parentContext" /> and <paramref name="modelExplorer"/>.
/// </summary>
/// <param name="parentContext">Existing <see cref="ModelValidationContext"/>.</param>
/// <param name="modelExplorer"><see cref="ModelExplorer"/> associated with the new
/// <see cref="ModelValidationContext"/>.</param>
/// <returns></returns>
public static ModelValidationContext GetChildValidationContext(
[NotNull] ModelValidationContext parentContext,
[NotNull] ModelExplorer modelExplorer)
{
ModelExplorer = modelExplorer;
ModelState = parentContext.ModelState;
RootPrefix = parentContext.RootPrefix;
ValidatorProvider = parentContext.ValidatorProvider;
return new ModelValidationContext(
parentContext.RootPrefix,
modelExplorer.Metadata.BindingSource,
parentContext.ValidatorProvider,
parentContext.ModelState,
modelExplorer);
}
public ModelExplorer ModelExplorer { get; }
public ModelStateDictionary ModelState { get; }
public string RootPrefix { get; set; }
public BindingSource BindingSource { get; set; }
public IModelValidatorProvider ValidatorProvider { get; }
}
}

View File

@ -424,9 +424,10 @@ namespace System.Web.Http
var modelValidationContext = new ModelValidationContext(
keyPrefix,
BindingContext.ValidatorProvider,
ModelState,
modelExplorer);
bindingSource: null,
validatorProvider: BindingContext.ValidatorProvider,
modelState: ModelState,
modelExplorer: modelExplorer);
ObjectValidator.Validate(modelValidationContext);
}

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
// Some IBindingSourceMetadata attributes like ModelBinder attribute return null
// as their binding source. Special case to ensure we do not ignore them.
if (parameter.BindingInfo.BindingSource != null ||
if (parameter.BindingInfo?.BindingSource != null ||
parameter.Attributes.OfType<IBindingSourceMetadata>().Any())
{
// This has a binding behavior configured, just leave it alone.
@ -30,12 +30,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
else if (ValueProviderResult.CanConvertFromString(parameter.ParameterInfo.ParameterType))
{
// Simple types are by-default from the URI.
parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
parameter.BindingInfo.BindingSource = uriBindingSource;
}
else
{
// Complex types are by-default from the body.
parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
parameter.BindingInfo.BindingSource = BindingSource.Body;
}

View File

@ -93,7 +93,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
foreach (var parameter in candidate.Action.Parameters)
{
// We only consider parameters that are marked as bound from the URL.
var source = parameter.BindingInfo.BindingSource;
var source = parameter.BindingInfo?.BindingSource;
if (source == null)
{
continue;
@ -115,7 +115,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
}
}
var prefix = parameter.BindingInfo.BinderModelName ?? parameter.Name;
var prefix = parameter.BindingInfo?.BinderModelName ?? parameter.Name;
parameters.Add(new OverloadedParameter()
{

View File

@ -58,6 +58,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
controller.Filters.Add(new AuthorizeFilter(new AuthorizationPolicyBuilder().Build()));
controller.RouteConstraints.Add(new AreaAttribute("Admin"));
controller.Properties.Add(new KeyValuePair<object, object>("test key", "test value"));
controller.ControllerProperties.Add(
new PropertyModel(typeof(TestController).GetProperty("TestProperty"), new List<object>()));
// Act
var controller2 = new ControllerModel(controller);
@ -67,7 +69,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
{
if (property.Name.Equals("Actions") ||
property.Name.Equals("AttributeRoutes") ||
property.Name.Equals("ApiExplorer"))
property.Name.Equals("ApiExplorer") ||
property.Name.Equals("ControllerProperties"))
{
// This test excludes other ApplicationModel objects on purpose because we deep copy them.
continue;
@ -110,6 +113,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
private class TestController
{
public string TestProperty { get; set; }
public void Edit()
{
}

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.Cors.Core;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.Internal;
using Microsoft.Framework.OptionsModel;
using Moq;
@ -71,6 +72,27 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
Assert.Single(model.Filters, f => f is CorsAuthorizationFilterFactory);
}
[Fact]
public void BuildControllerModel_AddsControllerProperties()
{
// Arrange
var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(null),
NullLoggerFactory.Instance,
null);
var typeInfo = typeof(ModelBinderController).GetTypeInfo();
// Act
var model = builder.BuildControllerModel(typeInfo);
// Assert
Assert.Equal(2, model.ControllerProperties.Count);
Assert.Equal("Bound", model.ControllerProperties[0].PropertyName);
Assert.Equal(BindingSource.Query, model.ControllerProperties[0].BindingInfo.BindingSource);
Assert.NotNull(model.ControllerProperties[0].Controller);
var attribute = Assert.Single(model.ControllerProperties[0].Attributes);
Assert.IsType<FromQueryAttribute>(attribute);
}
[Fact]
public void BuildControllerModel_DisableCorsAttributeAddsDisableCorsAuthorizationFilter()
{
@ -168,6 +190,14 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
{
}
public class ModelBinderController
{
[FromQuery]
public string Bound { get; set; }
public string Unbound { get; set; }
}
public class SomeFiltersController : IAsyncActionFilter, IResultFilter
{
public Task OnActionExecutionAsync(

View File

@ -0,0 +1,64 @@
// 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.Reflection;
using Microsoft.AspNet.Mvc.ModelBinding;
using Xunit;
namespace Microsoft.AspNet.Mvc.ApplicationModels
{
public class PropertyModelTest
{
[Fact]
public void CopyConstructor_CopiesAllProperties()
{
// Arrange
var propertyModel = new PropertyModel(typeof(TestController).GetProperty("Property"),
new List<object>() { new FromBodyAttribute() });
propertyModel.Controller = new ControllerModel(typeof(TestController).GetTypeInfo(), new List<object>());
propertyModel.BindingInfo = BindingInfo.GetBindingInfo(propertyModel.Attributes);
propertyModel.PropertyName = "Property";
// Act
var propertyModel2 = new PropertyModel(propertyModel);
// Assert
foreach (var property in typeof(PropertyModel).GetProperties())
{
var value1 = property.GetValue(propertyModel);
var value2 = property.GetValue(propertyModel2);
if (typeof(IEnumerable<object>).IsAssignableFrom(property.PropertyType))
{
Assert.Equal<object>((IEnumerable<object>)value1, (IEnumerable<object>)value2);
// Ensure non-default value
Assert.NotEmpty((IEnumerable<object>)value1);
}
else if (property.PropertyType.IsValueType ||
Nullable.GetUnderlyingType(property.PropertyType) != null)
{
Assert.Equal(value1, value2);
// Ensure non-default value
Assert.NotEqual(value1, Activator.CreateInstance(property.PropertyType));
}
else
{
Assert.Same(value1, value2);
// Ensure non-default value
Assert.NotNull(value1);
}
}
}
private class TestController
{
public string Property { get; set; }
}
}
}

View File

@ -5,12 +5,50 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.ApplicationModels;
using Microsoft.AspNet.Mvc.ModelBinding;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class ControllerActionDescriptorBuilderTest
{
[Fact]
public void Build_WithControllerPropertiesSet_AddsPropertiesWithBinderMetadataSet()
{
// Arrange
var applicationModel = new ApplicationModel();
var controller = new ControllerModel(typeof(TestController).GetTypeInfo(),
new List<object>() { });
controller.ControllerProperties.Add(
new PropertyModel(
controller.ControllerType.GetProperty("BoundProperty"),
new List<object>() { })
{
BindingInfo = BindingInfo.GetBindingInfo(new object[] { new FromQueryAttribute() }),
PropertyName = "BoundProperty"
});
controller.ControllerProperties.Add(
new PropertyModel(controller.ControllerType.GetProperty("UnboundProperty"), new List<object>() { }));
controller.Application = applicationModel;
applicationModel.Controllers.Add(controller);
var methodInfo = typeof(TestController).GetMethod("SomeAction");
var actionModel = new ActionModel(methodInfo, new List<object>() { });
actionModel.Controller = controller;
controller.Actions.Add(actionModel);
// Act
var descriptors = ControllerActionDescriptorBuilder.Build(applicationModel);
// Assert
var property = Assert.Single(descriptors.Single().BoundProperties);
Assert.Equal("BoundProperty", property.Name);
Assert.Equal(typeof(string), property.ParameterType);
Assert.Equal(BindingSource.Query, property.BindingInfo.BindingSource);
}
[Fact]
public void Build_WithPropertiesSet_FromApplicationModel()
{
@ -88,6 +126,11 @@ namespace Microsoft.AspNet.Mvc
private class TestController
{
[FromQuery]
public string BoundProperty { get; set; }
public string UnboundProperty { get; set; }
public void SomeAction() { }
}
}

View File

@ -111,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.Test
var id = Assert.Single(main.Parameters);
Assert.Equal("id", id.Name);
Assert.Null(id.BindingInfo.BindingSource);
Assert.Null(id.BindingInfo?.BindingSource);
Assert.Equal(typeof(int), id.ParameterType);
}
@ -130,7 +130,7 @@ namespace Microsoft.AspNet.Mvc.Test
var id = Assert.Single(main.Parameters, p => p.Name == "id");
Assert.Equal("id", id.Name);
Assert.Null(id.BindingInfo.BindingSource);
Assert.Null(id.BindingInfo?.BindingSource);
Assert.Equal(typeof(int), id.ParameterType);
var entity = Assert.Single(main.Parameters, p => p.Name == "entity");
@ -155,19 +155,19 @@ namespace Microsoft.AspNet.Mvc.Test
var id = Assert.Single(main.Parameters, p => p.Name == "id");
Assert.Equal("id", id.Name);
Assert.Null(id.BindingInfo.BindingSource);
Assert.Null(id.BindingInfo?.BindingSource);
Assert.Equal(typeof(int), id.ParameterType);
var upperCaseId = Assert.Single(main.Parameters, p => p.Name == "ID");
Assert.Equal("ID", upperCaseId.Name);
Assert.Null(upperCaseId.BindingInfo.BindingSource);
Assert.Null(upperCaseId.BindingInfo?.BindingSource);
Assert.Equal(typeof(int), upperCaseId.ParameterType);
var pascalCaseId = Assert.Single(main.Parameters, p => p.Name == "Id");
Assert.Equal("Id", pascalCaseId.Name);
Assert.Null(id.BindingInfo.BindingSource);
Assert.Null(id.BindingInfo?.BindingSource);
Assert.Equal(typeof(int), pascalCaseId.ParameterType);
}
@ -209,7 +209,7 @@ namespace Microsoft.AspNet.Mvc.Test
var entity = Assert.Single(notFromBody.Parameters);
Assert.Equal("entity", entity.Name);
Assert.Null(entity.BindingInfo.BindingSource);
Assert.Null(entity.BindingInfo?.BindingSource);
Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
}

View File

@ -2063,6 +2063,8 @@ namespace Microsoft.AspNet.Mvc
// Arrange
var actionDescriptor = new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(TestController).GetTypeInfo(),
BoundProperties = new List<ParameterDescriptor>(),
MethodInfo = typeof(TestController).GetTypeInfo()
.DeclaredMethods
.First(m => m.Name.Equals("ActionMethodWithDefaultValues", StringComparison.Ordinal)),

View File

@ -143,58 +143,6 @@ namespace Microsoft.AspNet.Mvc.Core
Assert.Same(bindingContext, controller.BindingContext);
}
[Fact]
public void CreateController_PopulatesServicesFromServiceContainer()
{
// Arrange
var actionDescriptor = new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(ControllerWithActivateAndFromServices).GetTypeInfo()
};
var services = GetServices();
var urlHelper = services.GetRequiredService<IUrlHelper>();
var httpContext = new DefaultHttpContext
{
RequestServices = services
};
var context = new ActionContext(httpContext, new RouteData(), actionDescriptor);
var factory = new DefaultControllerFactory(new DefaultControllerActivator(new DefaultTypeActivatorCache()));
// Act
var result = factory.CreateController(context);
// Assert
var controller = Assert.IsType<ControllerWithActivateAndFromServices>(result);
Assert.Same(urlHelper, controller.Helper);
}
[Fact]
public void CreateController_PopulatesUserServicesFromServiceContainer()
{
// Arrange
var actionDescriptor = new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(ControllerWithActivateAndFromServices).GetTypeInfo()
};
var services = GetServices();
var testService = services.GetService<TestService>();
var httpContext = new DefaultHttpContext
{
RequestServices = services
};
var context = new ActionContext(httpContext, new RouteData(), actionDescriptor);
var factory = new DefaultControllerFactory(new DefaultControllerActivator(new DefaultTypeActivatorCache()));
// Act
var result = factory.CreateController(context);
// Assert
var controller = Assert.IsType<ControllerWithActivateAndFromServices>(result);
Assert.Same(testService, controller.TestService);
}
[Fact]
public void CreateController_IgnoresPropertiesThatAreNotDecoratedWithActivateAttribute()
{

View File

@ -901,6 +901,41 @@ namespace Microsoft.AspNet.Mvc.Description
Assert.Equal(typeof(int), id.Type);
}
[Fact]
public void GetApiDescription_WithControllerProperties_Merges_ParameterDescription()
{
// Arrange
var action = CreateActionDescriptor("FromQueryName", typeof(TestController));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(5, description.ParameterDescriptions.Count);
var name = Assert.Single(description.ParameterDescriptions, p => p.Name == "name");
Assert.Same(BindingSource.Query, name.Source);
Assert.Equal(typeof(string), name.Type);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(BindingSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var product = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product");
Assert.Same(BindingSource.Body, product.Source);
Assert.Equal(typeof(Product), product.Type);
var userId = Assert.Single(description.ParameterDescriptions, p => p.Name == "UserId");
Assert.Same(BindingSource.Header, userId.Source);
Assert.Equal(typeof(string), userId.Type);
var comments = Assert.Single(description.ParameterDescriptions, p => p.Name == "Comments");
Assert.Same(BindingSource.ModelBinding, comments.Source);
Assert.Equal(typeof(string), comments.Type);
}
private IReadOnlyList<ApiDescription> GetApiDescriptions(ActionDescriptor action)
{
return GetApiDescriptions(action, CreateFormatters());
@ -950,17 +985,42 @@ namespace Microsoft.AspNet.Mvc.Description
return formatters;
}
private ControllerActionDescriptor CreateActionDescriptor(string methodName = null)
private ControllerActionDescriptor CreateActionDescriptor(string methodName = null, Type controllerType = null)
{
var action = new ControllerActionDescriptor();
action.SetProperty(new ApiDescriptionActionData());
action.MethodInfo = GetType().GetMethod(
methodName ?? "ReturnsObject",
BindingFlags.Instance | BindingFlags.NonPublic);
if (controllerType != null)
{
action.MethodInfo = controllerType.GetMethod(
methodName ?? "ReturnsObject",
BindingFlags.Instance | BindingFlags.Public);
action.ControllerTypeInfo = controllerType.GetTypeInfo();
action.BoundProperties = new List<ParameterDescriptor>();
foreach (var property in action.ControllerTypeInfo.GetProperties())
{
var bindingInfo = BindingInfo.GetBindingInfo(property.GetCustomAttributes().OfType<object>());
if (bindingInfo != null)
{
action.BoundProperties.Add(new ParameterDescriptor()
{
BindingInfo = bindingInfo,
Name = property.Name,
ParameterType = property.PropertyType,
});
}
}
}
else
{
action.MethodInfo = GetType().GetMethod(
methodName ?? "ReturnsObject",
BindingFlags.Instance | BindingFlags.NonPublic);
}
action.Parameters = new List<ParameterDescriptor>();
foreach (var parameter in action.MethodInfo.GetParameters())
{
action.Parameters.Add(new ParameterDescriptor()
@ -1118,6 +1178,27 @@ namespace Microsoft.AspNet.Mvc.Description
{
}
private class TestController
{
[FromRoute]
public int Id { get; set; }
[FromBody]
public Product Product { get; set; }
[FromHeader]
public string UserId { get; set; }
[ModelBinder]
public string Comments { get; set; }
public string NotBound { get; set; }
public void FromQueryName([FromQuery] string name)
{
}
}
private class Product
{
public int ProductId { get; set; }

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -54,48 +55,33 @@ namespace Microsoft.AspNet.Mvc.Core.Test
public async Task GetActionArgumentsAsync_DoesNotAddActionArguments_IfBinderReturnsFalse()
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
}
}
};
});
var actionContext = GetActionContext(actionDescriptor);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(result: null));
var actionContext = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
actionDescriptor);
var actionBindingContext = new ActionBindingContext()
{
ModelBinder = binder.Object,
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var inputFormattersProvider = new Mock<IInputFormattersProvider>();
inputFormattersProvider
.SetupGet(o => o.InputFormatters)
.Returns(new List<IInputFormatter>());
var invoker = new DefaultControllerActionArgumentBinder(
modelMetadataProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), modelMetadataProvider),
new MockMvcOptionsAccessor());
var argumentBinder = GetArgumentBinder();
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext);
var result = await argumentBinder
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
Assert.Empty(result);
@ -105,20 +91,14 @@ namespace Microsoft.AspNet.Mvc.Core.Test
public async Task GetActionArgumentsAsync_DoesNotAddActionArguments_IfBinderDoesNotSetModel()
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
}
}
};
});
var binder = new Mock<IModelBinder>();
binder
@ -135,19 +115,12 @@ namespace Microsoft.AspNet.Mvc.Core.Test
ModelBinder = binder.Object,
};
var inputFormattersProvider = new Mock<IInputFormattersProvider>();
inputFormattersProvider
.SetupGet(o => o.InputFormatters)
.Returns(new List<IInputFormatter>());
var argumentBinder = GetArgumentBinder();
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var invoker = new DefaultControllerActionArgumentBinder(
modelMetadataProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), modelMetadataProvider),
new MockMvcOptionsAccessor());
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext);
var result = await argumentBinder
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
Assert.Empty(result);
@ -158,19 +131,14 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(string),
Name = "foo",
ParameterType = typeof(string),
BindingInfo = new BindingInfo(),
}
},
};
});
var value = "Hello world";
var metadataProvider = new EmptyModelMetadataProvider();
@ -194,16 +162,11 @@ namespace Microsoft.AspNet.Mvc.Core.Test
ModelBinder = binder.Object,
};
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
var invoker = new DefaultControllerActionArgumentBinder(
metadataProvider,
mockValidatorProvider.Object,
new MockMvcOptionsAccessor());
var argumentBinder = GetArgumentBinder();
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext);
var result = await argumentBinder
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
Assert.Equal(1, result.Count);
@ -214,50 +177,26 @@ namespace Microsoft.AspNet.Mvc.Core.Test
public async Task GetActionArgumentsAsync_CallsValidator_IfModelBinderSucceeds()
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
}
}
};
Name = "foo",
ParameterType = typeof(object),
});
var actionContext = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
actionDescriptor);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(result: new ModelBindingResult(
model: null,
key: string.Empty,
isModelSet: true)));
var actionBindingContext = new ActionBindingContext()
{
ModelBinder = binder.Object,
};
var actionContext = GetActionContext(actionDescriptor);
var actionBindingContext = GetActionBindingContext();
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Verifiable();
var invoker = new DefaultControllerActionArgumentBinder(
TestModelMetadataProvider.CreateDefaultProvider(),
mockValidatorProvider.Object,
new MockMvcOptionsAccessor());
var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object);
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext);
var result = await argumentBinder
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
mockValidatorProvider.Verify(
@ -269,19 +208,14 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
}
}
};
});
var actionContext = new ActionContext(
new DefaultHttpContext(),
@ -301,13 +235,11 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Verifiable();
var invoker = new DefaultControllerActionArgumentBinder(
TestModelMetadataProvider.CreateDefaultProvider(),
mockValidatorProvider.Object,
new MockMvcOptionsAccessor());
var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object);
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext);
var result = await argumentBinder
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
mockValidatorProvider.Verify(o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Never());
@ -318,55 +250,179 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
}
}
};
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(
result: new ModelBindingResult(model: "Hello", key: string.Empty, isModelSet: true)));
});
var actionContext = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
actionDescriptor);
var actionBindingContext = GetActionBindingContext();
var argumentBinder = GetArgumentBinder();
// Act
var result = await argumentBinder
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
Assert.Equal(5, actionContext.ModelState.MaxAllowedErrors);
}
[Fact]
public async Task GetActionArgumentsAsync_CallsValidator_ForControllerProperties_IfModelBinderSucceeds()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = "ValueBinderMarkedProperty",
ParameterType = typeof(string),
});
var actionContext = GetActionContext(actionDescriptor);
var actionBindingContext = GetActionBindingContext();
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Verifiable();
var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object);
// Act
var result = await argumentBinder
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
mockValidatorProvider.Verify(
o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Once());
}
[Fact]
public async Task GetActionArgumentsAsync_DoesNotCallValidator_ForControllerProperties_IfModelBinderFails()
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = "ValueBinderMarkedProperty",
ParameterType = typeof(string),
});
var actionContext = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
actionDescriptor);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
var actionBindingContext = new ActionBindingContext()
{
ModelBinder = binder.Object,
};
var inputFormattersProvider = new Mock<IInputFormattersProvider>();
inputFormattersProvider
.SetupGet(o => o.InputFormatters)
.Returns(new List<IInputFormatter>());
var options = new MockMvcOptionsAccessor();
options.Options.MaxModelValidationErrors = 5;
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
var invoker = new DefaultControllerActionArgumentBinder(
TestModelMetadataProvider.CreateDefaultProvider(),
mockValidatorProvider.Object,
options);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Verifiable();
var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object);
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext);
var result = await argumentBinder
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
Assert.Equal(5, actionContext.ModelState.MaxAllowedErrors);
mockValidatorProvider.Verify(o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Never());
}
[Fact]
public async Task GetActionArgumentsAsync_SetsControllerProperties()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = "ValueBinderMarkedProperty",
BindingInfo = new BindingInfo(),
ParameterType = typeof(string)
});
var actionContext = GetActionContext(actionDescriptor);
var actionBindingContext = GetActionBindingContext();
var argumentBinder = GetArgumentBinder();
var controller = new TestController();
// Act
var result = await argumentBinder.BindActionArgumentsAsync(actionContext, actionBindingContext, controller);
// Assert
Assert.Equal("Hello", controller.ValueBinderMarkedProperty);
Assert.Null(controller.UnmarkedProperty);
}
private static ActionContext GetActionContext(ActionDescriptor descriptor = null)
{
return new ActionContext(
new DefaultHttpContext(),
new RouteData(),
descriptor ?? GetActionDescriptor());
}
private static ActionDescriptor GetActionDescriptor()
{
Func<object, int> method = foo => 1;
return new ControllerActionDescriptor
{
MethodInfo = method.Method,
ControllerTypeInfo = typeof(TestController).GetTypeInfo(),
BoundProperties = new List<ParameterDescriptor>(),
Parameters = new List<ParameterDescriptor>()
};
}
private static ActionBindingContext GetActionBindingContext()
{
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(
result: new ModelBindingResult(model: "Hello", key: string.Empty, isModelSet: true)));
return new ActionBindingContext()
{
ModelBinder = binder.Object,
};
}
private static DefaultControllerActionArgumentBinder GetArgumentBinder(IObjectModelValidator validator = null)
{
var options = new MockMvcOptionsAccessor();
options.Options.MaxModelValidationErrors = 5;
if (validator == null)
{
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
validator = mockValidator.Object;
}
return new DefaultControllerActionArgumentBinder(
TestModelMetadataProvider.CreateDefaultProvider(),
validator,
options);
}
private class TestController

View File

@ -25,6 +25,22 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
private readonly Action<IApplicationBuilder> _app = new ModelBindingWebSite.Startup().Configure;
private readonly Action<IServiceCollection> _configureServices = new ModelBindingWebSite.Startup().ConfigureServices;
[Fact]
public async Task DoNotValidate_ParametersOrControllerProperties_IfSourceNotFromRequest()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/Validation/DoNotValidateParameter");
// Assert
var stringValue = await response.Content.ReadAsStringAsync();
var isModelStateValid = JsonConvert.DeserializeObject<bool>(stringValue);
Assert.True(isModelStateValid);
}
[Fact]
public async Task TypeBasedExclusion_ForBodyAndNonBodyBoundModels()
{
@ -181,6 +197,74 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
exception.ExceptionMessage);
}
[Fact]
public async Task ControllerPropertyAndAnActionWithoutFromBody_InvokesWithoutErrors()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/FromBodyControllerProperty/GetSiteUser");
// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
[Fact]
public async Task ControllerPropertyAndAnActionParameterWithFromBody_Throws()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/FromBodyControllerProperty/AddUser");
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(
"More than one parameter and/or property is bound to the HTTP request's content.",
exception.ExceptionMessage);
}
[Fact]
public async Task ControllerPropertyAndAModelPropertyWithFromBody_Throws()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/FromBodyControllerProperty/AddUser");
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(
"More than one parameter and/or property is bound to the HTTP request's content.",
exception.ExceptionMessage);
}
[Fact]
public async Task MultipleControllerPropertiesMarkedWithFromBody_Throws()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/MultiplePropertiesFromBody/GetUser");
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(
"More than one parameter and/or property is bound to the HTTP request's content.",
exception.ExceptionMessage);
}
[Fact]
public async Task MultipleParameterAndPropertiesMarkedWithFromBody_Throws()
{

View File

@ -279,7 +279,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
private static ModelValidationContext CreateValidationContext(ModelExplorer modelExplorer)
{
return new ModelValidationContext(null, null, null, modelExplorer);
return new ModelValidationContext(
rootPrefix: null,
bindingSource: null,
modelState: null,
validatorProvider: null,
modelExplorer: modelExplorer);
}
private class DerivedRequiredAttribute : RequiredAttribute

View File

@ -509,6 +509,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
ModelValidationContext = new ModelValidationContext(
key,
null,
TestModelValidatorProvider.CreateDefaultProvider(),
modelStateDictionary,
modelExplorer),

View File

@ -23,6 +23,7 @@ namespace ApplicationModelWebSite
{
public void Apply(ParameterModel model)
{
model.BindingInfo = model.BindingInfo ?? new BindingInfo();
model.BindingInfo.BindingSource = BindingSource.Custom;
model.BindingInfo.BinderModelName = "CoolMetadata";
}

View File

@ -16,7 +16,7 @@ namespace FormatterWebSite
var bodyParameter = context.ActionDescriptor
.Parameters
.FirstOrDefault(parameter => IsBodyBindingSource(
parameter.BindingInfo.BindingSource));
parameter.BindingInfo?.BindingSource));
if (bodyParameter != null)
{
var parameterBindingErrors = context.ModelState[bodyParameter.Name].Errors;

View File

@ -0,0 +1,30 @@
// 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 Microsoft.AspNet.Mvc;
using ModelBindingWebSite.Models;
namespace ModelBindingWebSite.Controllers
{
public class FromBodyControllerProperty : Controller
{
[FromBody]
public User SiteUser { get; set; }
public User GetSiteUser(int id)
{
return SiteUser;
}
// Will throw as Customer reads body.
public Customer GetCustomer(Customer customer)
{
return customer;
}
// Will throw as a controller property and a parameter name are being read from body.
public void AddUser([FromBody] User user)
{
}
}
}

View File

@ -0,0 +1,22 @@
// 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 Microsoft.AspNet.Mvc;
using ModelBindingWebSite.Models;
namespace ModelBindingWebSite.Controllers
{
public class MultiplePropertiesFromBodyController : Controller
{
[FromBody]
public User SiteUser { get; set; }
[FromBody]
public Country Country { get; set; }
public User GetUser()
{
return SiteUser;
}
}
}

View File

@ -11,6 +11,9 @@ namespace ModelBindingWebSite.Controllers
[Route("Validation/[Action]")]
public class ValidationController : Controller
{
[FromServices]
public ITestService ControllerService { get; set; }
public bool SkipValidation(Resident resident)
{
return ModelState.IsValid;
@ -21,6 +24,11 @@ namespace ModelBindingWebSite.Controllers
return ModelState.IsValid;
}
public bool DoNotValidateParameter([FromServices] ITestService service)
{
return ModelState.IsValid;
}
public IActionResult CreateRectangle([FromBody] Rectangle rectangle)
{
if (!ModelState.IsValid)

View File

@ -1,10 +1,15 @@
// 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.ComponentModel.DataAnnotations;
namespace ModelBindingWebSite
{
public interface ITestService
{
[Required]
string NeverBound { get; set; }
bool Test();
}
}

View File

@ -1,10 +1,15 @@
// 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.ComponentModel.DataAnnotations;
namespace ModelBindingWebSite
{
public class TestService : ITestService
{
[Required]
public string NeverBound { get; set; }
public bool Test()
{
return true;