Adding support for property level binding using IBinderMetadata and enabling FromXXX attributes to be decorated on properties.

This commit is contained in:
Harsh Gupta 2014-10-27 12:33:30 -07:00
parent 27432d9453
commit d00c7ef597
57 changed files with 1640 additions and 279 deletions

View File

@ -52,38 +52,39 @@ namespace Microsoft.AspNet.Mvc
}
}
var bodyBoundParameterCount = parameterMetadata.Count(
modelMetadata => modelMetadata.BinderMetadata is IFormatterBinderMetadata);
if (bodyBoundParameterCount > 1)
{
throw new InvalidOperationException(Resources.MultipleBodyParametersAreNotAllowed);
}
var actionArguments = new Dictionary<string, object>(StringComparer.Ordinal);
foreach (var parameter in parameterMetadata)
{
await PopulateArgumentAsync(actionBindingContext, actionArguments, parameter);
}
await PopulateArgumentAsync(actionBindingContext, actionArguments, parameterMetadata);
return actionArguments;
}
private async Task PopulateArgumentAsync(
ActionBindingContext actionBindingContext,
IDictionary<string, object> arguments,
ModelMetadata modelMetadata)
IEnumerable<ModelMetadata> parameterMetadata)
{
var parameterType = modelMetadata.ModelType;
var modelBindingContext = GetModelBindingContext(modelMetadata, actionBindingContext);
if (await actionBindingContext.ModelBinder.BindModelAsync(modelBindingContext))
var operationBindingContext = new OperationBindingContext
{
arguments[modelMetadata.PropertyName] = modelBindingContext.Model;
ModelBinder = actionBindingContext.ModelBinder,
ValidatorProvider = actionBindingContext.ValidatorProvider,
MetadataProvider = actionBindingContext.MetadataProvider,
HttpContext = actionBindingContext.ActionContext.HttpContext,
ValueProvider = actionBindingContext.ValueProvider,
};
foreach (var parameter in parameterMetadata)
{
var parameterType = parameter.ModelType;
var modelBindingContext = GetModelBindingContext(parameter, actionBindingContext, operationBindingContext);
if (await actionBindingContext.ModelBinder.BindModelAsync(modelBindingContext))
{
arguments[parameter.PropertyName] = modelBindingContext.Model;
}
}
}
internal static ModelBindingContext GetModelBindingContext(ModelMetadata modelMetadata, ActionBindingContext actionBindingContext)
internal static ModelBindingContext GetModelBindingContext(ModelMetadata modelMetadata,
ActionBindingContext actionBindingContext,
OperationBindingContext operationBindingContext)
{
Predicate<string> propertyFilter =
propertyName => BindAttribute.IsPropertyAllowed(propertyName,
@ -95,14 +96,11 @@ namespace Microsoft.AspNet.Mvc
ModelName = modelMetadata.ModelName ?? modelMetadata.PropertyName,
ModelMetadata = modelMetadata,
ModelState = actionBindingContext.ActionContext.ModelState,
ModelBinder = actionBindingContext.ModelBinder,
ValidatorProvider = actionBindingContext.ValidatorProvider,
MetadataProvider = actionBindingContext.MetadataProvider,
HttpContext = actionBindingContext.ActionContext.HttpContext,
PropertyFilter = propertyFilter,
// Fallback only if there is no explicit model name set.
FallbackToEmptyPrefix = modelMetadata.ModelName == null,
ValueProvider = actionBindingContext.ValueProvider,
OperationBindingContext = operationBindingContext,
};
return modelBindingContext;

View File

@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Mvc
if (formatter == null)
{
var unsupportedContentType = Resources.FormatUnsupportedContentType(
bindingContext.HttpContext.Request.ContentType);
bindingContext.OperationBindingContext.HttpContext.Request.ContentType);
bindingContext.ModelState.AddModelError(bindingContext.ModelName, unsupportedContentType);
// Should always return true so that the model binding process ends here.
@ -51,8 +51,8 @@ namespace Microsoft.AspNet.Mvc
// Validate the deserialized object
var validationContext = new ModelValidationContext(
bindingContext.MetadataProvider,
bindingContext.ValidatorProvider,
bindingContext.OperationBindingContext.MetadataProvider,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
bindingContext.ModelMetadata,
containerMetadata: null,

View File

@ -39,6 +39,13 @@ namespace Microsoft.AspNet.Mvc
var modelMetadata = metadataProvider.GetMetadataForType(
modelAccessor: null,
modelType: typeof(TModel));
var operationBindingContext = new OperationBindingContext
{
ModelBinder = modelBinder,
ValidatorProvider = validatorProvider,
MetadataProvider = metadataProvider,
HttpContext = httpContext
};
var modelBindingContext = new ModelBindingContext
{
@ -46,12 +53,9 @@ namespace Microsoft.AspNet.Mvc
ModelName = prefix,
Model = model,
ModelState = modelState,
ModelBinder = modelBinder,
ValueProvider = valueProvider,
ValidatorProvider = validatorProvider,
MetadataProvider = metadataProvider,
FallbackToEmptyPrefix = true,
HttpContext = httpContext
OperationBindingContext = operationBindingContext,
};
if (await modelBinder.BindModelAsync(modelBindingContext))

View File

@ -10,22 +10,6 @@ namespace Microsoft.AspNet.Mvc.Core
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNet.Mvc.Core.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// More than one parameter is bound to the HTTP request's content.
/// </summary>
internal static string MultipleBodyParametersAreNotAllowed
{
get { return GetString("MultipleBodyParametersAreNotAllowed"); }
}
/// <summary>
/// More than one parameter is bound to the HTTP request's content.
/// </summary>
internal static string FormatMultipleBodyParametersAreNotAllowed()
{
return GetString("MultipleBodyParametersAreNotAllowed");
}
/// <summary>
/// The provided anti-forgery token failed a custom data check.
/// </summary>

View File

@ -117,9 +117,6 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="MultipleBodyParametersAreNotAllowed" xml:space="preserve">
<value>More than one parameter is bound to the HTTP request's content.</value>
</data>
<data name="AntiForgeryToken_AdditionalDataCheckFailed" xml:space="preserve">
<value>The provided anti-forgery token failed a custom data check.</value>
</data>

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Mvc
/// This attribute is used on action parameters to indicate
/// they are bound from the body of the incoming request.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromBodyAttribute : Attribute, IFormatterBinderMetadata
{
}

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Mvc
/// This attribute is used on action parameters to indicate that
/// they will be bound using form data of the incoming request.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromFormAttribute : Attribute, IFormDataValueProviderMetadata
{
}

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Mvc
/// This attribute is used on action parameters to indicate that
/// they will be bound using query data of the incoming request.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromQueryAttribute : Attribute, IQueryValueProviderMetadata
{
}

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Mvc
/// This attribute is used on action parameters to indicate that
/// they will be bound using route data of the incoming request.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromRouteAttribute : Attribute, IRouteDataValueProviderMetadata
{
}

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
if (bindingContext.ModelType == typeof(CancellationToken))
{
bindingContext.Model = bindingContext.HttpContext.RequestAborted;
bindingContext.Model = bindingContext.OperationBindingContext.HttpContext.RequestAborted;
return Task.FromResult(true);
}

View File

@ -47,10 +47,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var rawValueArray = RawValueToObjectArray(rawValue);
foreach (var rawValueElement in rawValueArray)
{
var innerBindingContext = new ModelBindingContext(bindingContext)
var innerModelMetadata =
bindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(null, typeof(TElement));
var innerBindingContext = new ModelBindingContext(bindingContext,
bindingContext.ModelName,
innerModelMetadata)
{
ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(null, typeof(TElement)),
ModelName = bindingContext.ModelName,
ValueProvider = new CompositeValueProvider
{
// our temporary provider goes at the front of the list
@ -60,7 +62,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
object boundValue = null;
if (await bindingContext.ModelBinder.BindModelAsync(innerBindingContext))
if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext))
{
boundValue = innerBindingContext.Model;
bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode);
@ -99,18 +101,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
foreach (var indexName in indexNames)
{
var fullChildName = ModelBindingHelper.CreateIndexModelName(bindingContext.ModelName, indexName);
var childBindingContext = new ModelBindingContext(bindingContext)
{
ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(null, typeof(TElement)),
ModelName = fullChildName
};
var childModelMetadata =
bindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(null, typeof(TElement));
var childBindingContext = new ModelBindingContext(bindingContext, fullChildName, childModelMetadata);
var didBind = false;
object boundValue = null;
var modelType = bindingContext.ModelType;
if (await bindingContext.ModelBinder.BindModelAsync(childBindingContext))
if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext))
{
didBind = true;
boundValue = childBindingContext.Model;

View File

@ -19,16 +19,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dto = (ComplexModelDto)bindingContext.Model;
foreach (var propertyMetadata in dto.PropertyMetadata)
{
var propertyBindingContext = new ModelBindingContext(bindingContext)
{
ModelMetadata = propertyMetadata,
ModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName,
propertyMetadata.PropertyName)
};
var propertyModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName,
propertyMetadata.PropertyName);
var propertyBindingContext = new ModelBindingContext(bindingContext,
propertyModelName,
propertyMetadata);
// bind and propagate the values
// If we can't bind, then leave the result missing (don't add a null).
if (await bindingContext.ModelBinder.BindModelAsync(propertyBindingContext))
if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext))
{
var result = new ComplexModelDtoResult(propertyBindingContext.Model,
propertyBindingContext.ValidationNode);

View File

@ -1,6 +1,7 @@
// 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;
@ -79,8 +80,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelName);
}
var validationContext = new ModelValidationContext(bindingContext.MetadataProvider,
bindingContext.ValidatorProvider,
var validationContext = new ModelValidationContext(bindingContext.OperationBindingContext.MetadataProvider,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
bindingContext.ModelMetadata,
containerMetadata: null);
@ -88,6 +89,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
newBindingContext.ValidationNode.Validate(validationContext, parentNode: null);
}
bindingContext.OperationBindingContext.BodyBindingState =
newBindingContext.OperationBindingContext.BodyBindingState;
bindingContext.Model = newBindingContext.Model;
return true;
}
@ -128,10 +131,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelName = modelName,
ModelState = oldBindingContext.ModelState,
ValueProvider = oldBindingContext.ValueProvider,
ValidatorProvider = oldBindingContext.ValidatorProvider,
MetadataProvider = oldBindingContext.MetadataProvider,
ModelBinder = oldBindingContext.ModelBinder,
HttpContext = oldBindingContext.HttpContext,
OperationBindingContext = oldBindingContext.OperationBindingContext,
PropertyFilter = oldBindingContext.PropertyFilter,
};
@ -141,11 +141,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
newBindingContext.ValidationNode = oldBindingContext.ValidationNode;
}
newBindingContext.OperationBindingContext.BodyBindingState = GetBodyBindingState(oldBindingContext);
// look at the value providers and see if they need to be restricted.
var metadata = oldBindingContext.ModelMetadata.BinderMetadata as IValueProviderMetadata;
if (metadata != null)
{
var valueProvider = oldBindingContext.ValueProvider as IMetadataAwareValueProvider;
// ValueProvider property might contain a filtered list of value providers.
// While deciding to bind a particular property which is annotated with a IValueProviderMetadata,
// instead of refiltering an already filtered list, we need to filter value providers from a global list
// of all value providers. This is so that every artifact that is explicitly marked using an
// IValueProviderMetadata can restrict model binding to only use value providers which support this
// IValueProviderMetadata.
var valueProvider = oldBindingContext.OperationBindingContext.ValueProvider as IMetadataAwareValueProvider;
if (valueProvider != null)
{
newBindingContext.ValueProvider = valueProvider.Filter(metadata);
@ -154,5 +162,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return newBindingContext;
}
private static BodyBindingState GetBodyBindingState(ModelBindingContext oldBindingContext)
{
var binderMetadata = oldBindingContext.ModelMetadata.BinderMetadata;
var newIsFormatterBasedMetadataFound = binderMetadata is IFormatterBinderMetadata;
var newIsFormBasedMetadataFound = binderMetadata is IFormDataValueProviderMetadata;
var currentModelNeedsToReadBody = newIsFormatterBasedMetadataFound || newIsFormBasedMetadataFound;
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 && newIsFormatterBasedMetadataFound)
{
throw new InvalidOperationException(Resources.MultipleBodyParametersOrPropertiesAreNotAllowed);
}
var state = oldBindingContext.OperationBindingContext.BodyBindingState;
if (newIsFormatterBasedMetadataFound)
{
state = BodyBindingState.FormatterBased;
}
else if (newIsFormBasedMetadataFound && oldState != BodyBindingState.FormatterBased)
{
// Only update the model binding state if we have not discovered formatter based state already.
state = BodyBindingState.FormBased;
}
return state;
}
}
}

View File

@ -22,16 +22,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
_activator = activator;
}
public Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
{
var binderType = ResolveBinderType(bindingContext.ModelType);
if (binderType != null)
{
var binder = (IModelBinder)_activator.CreateInstance(_serviceProvider, binderType);
return binder.BindModelAsync(bindingContext);
await binder.BindModelAsync(bindingContext);
// Was able to resolve a binder type, hence we should tell the model binding system to return
// true so that none of the other model binders participate.
return true;
}
return Task.FromResult(false);
return false;
}
private static Type ResolveBinderType(Type modelType)

View File

@ -28,14 +28,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
internal async Task<BindResult<TModel>> TryBindStrongModel<TModel>(ModelBindingContext parentBindingContext,
string propertyName)
{
var propertyBindingContext = new ModelBindingContext(parentBindingContext)
{
ModelMetadata = parentBindingContext.MetadataProvider.GetMetadataForType(modelAccessor: null,
modelType: typeof(TModel)),
ModelName = ModelBindingHelper.CreatePropertyModelName(parentBindingContext.ModelName, propertyName)
};
var propertyModelMetadata =
parentBindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(modelAccessor: null,
modelType: typeof(TModel));
var propertyModelName =
ModelBindingHelper.CreatePropertyModelName(parentBindingContext.ModelName, propertyName);
var propertyBindingContext =
new ModelBindingContext(parentBindingContext, propertyModelName, propertyModelMetadata);
if (await propertyBindingContext.ModelBinder.BindModelAsync(propertyBindingContext))
if (await propertyBindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext))
{
var untypedModel = propertyBindingContext.Model;
var model = ModelBindingHelper.CastOrDefault<TModel>(untypedModel);

View File

@ -16,35 +16,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public virtual async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
{
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!CanBindType(bindingContext.ModelType))
{
return false;
}
var topLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var isThereAnExplicitAlias = bindingContext.ModelMetadata.ModelName != null;
var mutableObjectBinderContext = new MutableObjectBinderContext()
{
ModelBindingContext = bindingContext,
PropertyMetadata = GetMetadataForProperties(bindingContext),
};
// The first check is necessary because if we fallback to empty prefix, we do not want to depend
// on a value provider to provide a value for empty prefix.
var containsPrefix = (bindingContext.ModelName == string.Empty && topLevelObject) ||
await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName);
// Always create the model if
// 1. It is a top level object and the model name is empty.
// 2. There is a value provider which can provide value for the model name.
// 3. There is an explicit alias provided by the user and it is a top level object.
// The reson we depend on explicit alias is that otherwise we want the FallToEmptyPrefix codepath
// to kick in so that empty prefix values could be bound.
if (!containsPrefix && !(isThereAnExplicitAlias && topLevelObject))
if (!(await CanCreateModel(mutableObjectBinderContext)))
{
return false;
}
EnsureModel(bindingContext);
var propertyMetadatas = GetMetadataForProperties(bindingContext).ToArray();
var dto = CreateAndPopulateDto(bindingContext, propertyMetadatas);
var dto = await CreateAndPopulateDto(bindingContext, mutableObjectBinderContext.PropertyMetadata);
// post-processing, e.g. property setters and hooking up validation
ProcessDto(bindingContext, dto);
@ -58,6 +47,121 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return CanUpdatePropertyInternal(propertyMetadata);
}
internal async Task<bool> CanCreateModel(MutableObjectBinderContext context)
{
var bindingContext = context.ModelBindingContext;
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var isThereAnExplicitAlias = bindingContext.ModelMetadata.ModelName != null;
// The fact that this has reached here,
// it is a complex object which was not directly bound by any previous model binders.
// Check if this was supposed to be handled by a non value provider based binder.
// if it was then it should be not be bound using mutable object binder.
// This check would prevent it from recursing in if a model contains a property of its own type.
// We skip this check if it is a top level object because we want to always evaluate
// the creation of top level object (this is also required for ModelBinderAttribute to work.)
if (!isTopLevelObject &&
bindingContext.ModelMetadata.BinderMetadata != null &&
!(bindingContext.ModelMetadata.BinderMetadata is IValueProviderMetadata))
{
return false;
}
// Create the object if :
// 1. It is a top level model with an explicit user supplied prefix.
// In this case since it will never fallback to empty prefix, we need to create the model here.
if (isTopLevelObject && isThereAnExplicitAlias)
{
return true;
}
// 2. It is a top level object and there is no model name ( Fallback to empty prefix case ).
// This is necessary as we do not want to depend on a value provider to contain an empty prefix.
if (isTopLevelObject && bindingContext.ModelName == string.Empty)
{
return true;
}
// 3. The model name is not prefixed and a value provider can directly provide a value for the model name.
// The fact that it is not prefixed means that the containsPrefixAsync call checks for the exact model name
// instead of doing a prefix match.
if (!bindingContext.ModelName.Contains(".") &&
await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName))
{
return true;
}
// 4. Any of the model properties can be bound using a value provider.
if (await CanValueBindAnyModelProperties(context))
{
return true;
}
return false;
}
private async Task<bool> CanValueBindAnyModelProperties(MutableObjectBinderContext context)
{
// We need to enumerate the non marked properties and properties marked with IValueProviderMetadata
// instead of checking bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName)
// because there can be a case
// where a value provider might be willing to provide a marked property, which might never be bound.
// For example if person.Name is marked with FromQuery, and FormValueProvider has a key person.Name, and the
// QueryValueProvider does not, we do not want to create Person.
var isAnyPropertyEnabledForValueProviderBasedBinding = false;
foreach (var propertyMetadata in context.PropertyMetadata)
{
// This check will skip properties which are marked explicitly using a non value binder.
if (propertyMetadata.BinderMetadata == null ||
propertyMetadata.BinderMetadata is IValueProviderMetadata)
{
isAnyPropertyEnabledForValueProviderBasedBinding = true;
// If any property can return a true value.
if (await CanBindValue(context.ModelBindingContext, propertyMetadata))
{
return true;
}
}
}
if (!isAnyPropertyEnabledForValueProviderBasedBinding)
{
// Either there are no properties or all the properties are marked as
// a non value provider based marker.
// This would be the case when the model has all its properties annotated with
// a IBinderMetadata. We want to be able to create such a model.
return true;
}
return false;
}
private async Task<bool> CanBindValue(ModelBindingContext bindingContext, ModelMetadata metadata)
{
var valueProvider = bindingContext.ValueProvider;
var valueProviderMetadata = metadata.BinderMetadata as IValueProviderMetadata;
if (valueProviderMetadata != null)
{
// if there is a binder metadata and since the property can be bound using a value provider.
var metadataAwareValueProvider = bindingContext.OperationBindingContext.ValueProvider as IMetadataAwareValueProvider;
if (metadataAwareValueProvider != null)
{
valueProvider = metadataAwareValueProvider.Filter(valueProviderMetadata);
}
}
var propertyModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName,
metadata.PropertyName);
if (await valueProvider.ContainsPrefixAsync(propertyModelName))
{
return true;
}
return false;
}
private static bool CanBindType(Type modelType)
{
// Simple types cannot use this binder
@ -106,19 +210,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return true;
}
private ComplexModelDto CreateAndPopulateDto(ModelBindingContext bindingContext,
private async Task<ComplexModelDto> CreateAndPopulateDto(ModelBindingContext bindingContext,
IEnumerable<ModelMetadata> propertyMetadatas)
{
// create a DTO and call into the DTO binder
var originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas);
var dtoBindingContext = new ModelBindingContext(bindingContext)
{
ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(() => originalDto,
typeof(ComplexModelDto)),
ModelName = bindingContext.ModelName
};
var complexModelDtoMetadata =
bindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(() => originalDto,
typeof(ComplexModelDto));
var dtoBindingContext =
new ModelBindingContext(bindingContext, bindingContext.ModelName, complexModelDtoMetadata);
bindingContext.ModelBinder.BindModelAsync(dtoBindingContext);
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(dtoBindingContext);
return (ComplexModelDto)dtoBindingContext.Model;
}
@ -165,8 +268,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext)
{
var validationInfo = GetPropertyValidationInfo(bindingContext);
var propertyTypeMetadata = bindingContext.MetadataProvider
.GetMetadataForType(null, bindingContext.ModelType);
var propertyTypeMetadata = bindingContext.OperationBindingContext
.MetadataProvider
.GetMetadataForType(null, bindingContext.ModelType);
Predicate<string> newPropertyFilter =
propertyName => bindingContext.PropertyFilter(propertyName) &&
BindAttribute.IsPropertyAllowed(
@ -198,7 +302,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
var propertyName = property.Name;
var propertyMetadata = bindingContext.PropertyMetadata[propertyName];
var requiredValidator = bindingContext.ValidatorProvider
var requiredValidator = bindingContext.OperationBindingContext
.ValidatorProvider
.GetValidators(propertyMetadata)
.FirstOrDefault(v => v != null && v.IsRequired);
if (requiredValidator != null)

View File

@ -0,0 +1,14 @@
// 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;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class MutableObjectBinderContext
{
public ModelBindingContext ModelBindingContext { get; set; }
public IEnumerable<ModelMetadata> PropertyMetadata { get; set; }
}
}

View File

@ -0,0 +1,29 @@
// 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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Represents state of models which are bound using body.
/// </summary>
public enum BodyBindingState
{
/// <summary>
/// Represents if there has been no metadata found which needs to read the body during the current
/// model binding process.
/// </summary>
NotBodyBased,
/// <summary>
/// Represents if there is a <see cref="IFormatterBinderMetadata"/> that
/// has been found during the current model binding process.
/// </summary>
FormatterBased,
/// <summary>
/// Represents if there is a <see cref = "IFormDataValueProviderMetadata" /> that
/// has been found during the current model binding process.
/// </summary>
FormBased
}
}

View File

@ -83,7 +83,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
parameterName,
binderMetadata);
return CreateMetadataFromPrototype(parameterInfo.Prototype, modelAccessor);
var metadata = CreateMetadataFromPrototype(parameterInfo.Prototype, modelAccessor);
// If there is no metadata associated with the parameter itself get it from the type.
if (metadata != null && metadata.BinderMetadata == null)
{
var typeInfo = GetTypeInformation(parameter.ParameterType);
metadata.BinderMetadata = typeInfo.Prototype.BinderMetadata;
}
return metadata;
}
private IEnumerable<ModelMetadata> GetMetadataForPropertiesCore(object container, Type containerType)
@ -115,6 +125,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
metadata.IsReadOnly = true;
}
// We need to update the property after the prototype creation because otherwise
// if the property type is same as the containing type, it would cause infinite recursion.
// If there is no metadata associated with the property itself get it from the type.
if (metadata != null && metadata.BinderMetadata == null)
{
if (propertyInfo.Prototype != null)
{
var typeInfo = GetTypeInformation(propertyInfo.Prototype.ModelType);
metadata.BinderMetadata = typeInfo.Prototype.BinderMetadata;
}
}
return metadata;
}

View File

@ -29,25 +29,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingContext"/> class using the
/// <param name="bindingContext" />.
// </summary>
/// <paramref name="bindingContext" />.
/// </summary>
/// <param name="bindingContext">Existing <see cref="ModelBindingContext"/>.</param>
/// <param name="modelName">Model name of associated with the new <see cref="ModelBindingContext"/>.</param>
/// <param name="modelMetadata">Model metadata of associated with the new <see cref="ModelBindingContext"/>.
/// </param>
/// <remarks>
/// This constructor copies certain values that won't change between parent and child objects,
/// e.g. ValueProvider, ModelState
/// </remarks>
public ModelBindingContext(ModelBindingContext bindingContext)
public ModelBindingContext([NotNull] ModelBindingContext bindingContext,
[NotNull] string modelName,
[NotNull] ModelMetadata modelMetadata)
{
if (bindingContext != null)
{
ModelState = bindingContext.ModelState;
ValueProvider = bindingContext.ValueProvider;
MetadataProvider = bindingContext.MetadataProvider;
ModelBinder = bindingContext.ModelBinder;
ValidatorProvider = bindingContext.ValidatorProvider;
HttpContext = bindingContext.HttpContext;
}
ModelName = modelName;
ModelMetadata = modelMetadata;
ModelState = bindingContext.ModelState;
ValueProvider = bindingContext.ValueProvider;
OperationBindingContext = bindingContext.OperationBindingContext;
}
/// <summary>
/// Represents the <see cref="OperationBindingContext"/> associated with this context.
/// </summary>
public OperationBindingContext OperationBindingContext { get; set; }
/// <summary>
/// Gets or sets the model associated with this context.
/// </summary>
@ -129,32 +136,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public bool FallbackToEmptyPrefix { get; set; }
/// <summary>
/// Gets or sets the <see cref="HttpContext"/> for the current request.
/// </summary>
public HttpContext HttpContext { get; set; }
/// <summary>
/// Gets or sets the <see cref="IValueProvider"/> associated with this context.
/// </summary>
public IValueProvider ValueProvider { get; set; }
/// <summary>
/// Gets or sets the <see cref="IModelBinder"/> associated with this context.
/// </summary>
public IModelBinder ModelBinder { get; set; }
/// <summary>
/// Gets or sets the <see cref="IModelMetadataProvider"/> associated with this context.
/// </summary>
public IModelMetadataProvider MetadataProvider { get; set; }
/// <summary>
/// Gets or sets the <see cref="IModelValidatorProvider"/> instance used for model validation with this
/// context.
/// </summary>
public IModelValidatorProvider ValidatorProvider { get; set; }
/// <summary>
/// Gets a dictionary of property name to <see cref="ModelMetadata"/> instances for
/// <see cref="ModelMetadata.Properties"/>

View File

@ -0,0 +1,46 @@
// 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.Http;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A context that contains information specific to the current request and the action whose parameters
/// are being model bound.
/// </summary>
public class OperationBindingContext
{
/// <summary>
/// Represents if there has been a body bound model found during the current model binding process.
/// </summary>
public BodyBindingState BodyBindingState { get; set; } = BodyBindingState.NotBodyBased;
/// <summary>
/// Gets or sets the <see cref="HttpContext"/> for the current request.
/// </summary>
public HttpContext HttpContext { get; set; }
/// <summary>
/// Gets unaltered value provider collection.
/// Value providers can be filtered by specific model binders.
/// </summary>
public IValueProvider ValueProvider { get; set; }
/// <summary>
/// Gets or sets the <see cref="IModelBinder"/> associated with this context.
/// </summary>
public IModelBinder ModelBinder { get; set; }
/// <summary>
/// Gets or sets the <see cref="IModelMetadataProvider"/> associated with this context.
/// </summary>
public IModelMetadataProvider MetadataProvider { get; set; }
/// <summary>
/// Gets or sets the <see cref="IModelValidatorProvider"/> instance used for model validation with this
/// context.
/// </summary>
public IModelValidatorProvider ValidatorProvider { get; set; }
}
}

View File

@ -10,6 +10,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNet.Mvc.ModelBinding.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// More than one parameter and/or property is bound to the HTTP request's content.
/// </summary>
internal static string MultipleBodyParametersOrPropertiesAreNotAllowed
{
get { return GetString("MultipleBodyParametersOrPropertiesAreNotAllowed"); }
}
/// <summary>
/// More than one parameter and/or property is bound to the HTTP request's content.
/// </summary>
internal static string FormatMultipleBodyParametersOrPropertiesAreNotAllowed()
{
return GetString("MultipleBodyParametersOrPropertiesAreNotAllowed");
}
/// <summary>
/// Value cannot be null or empty.
/// </summary>

View File

@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="MultipleBodyParametersOrPropertiesAreNotAllowed" xml:space="preserve">
<value>More than one parameter and/or property is bound to the HTTP request's content.</value>
</data>
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or empty.</value>
</data>

View File

@ -9,8 +9,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public ModelValidationContext([NotNull] ModelBindingContext bindingContext,
[NotNull] ModelMetadata metadata)
: this(bindingContext.MetadataProvider,
bindingContext.ValidatorProvider,
: this(bindingContext.OperationBindingContext.MetadataProvider,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
metadata,
bindingContext.ModelMetadata)

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
if (bindingContext.ModelType == typeof(HttpRequestMessage))
{
bindingContext.Model = bindingContext.HttpContext.GetHttpRequestMessage();
bindingContext.Model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage();
return Task.FromResult(true);
}

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc
// Arrange
var bindingContext = GetBindingContext(typeof(Person), inputFormatter: null);
bindingContext.ModelMetadata.BinderMetadata = Mock.Of<IFormatterBinderMetadata>();
var binder = bindingContext.ModelBinder;
var binder = bindingContext.OperationBindingContext.ModelBinder;
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
@ -70,7 +70,7 @@ namespace Microsoft.AspNet.Mvc
var bindingContext = GetBindingContext(typeof(Person), inputFormatter: null);
bindingContext.ModelMetadata.BinderMetadata = useBody ? Mock.Of<IFormatterBinderMetadata>() :
Mock.Of<IBinderMetadata>();
var binder = bindingContext.ModelBinder;
var binder = bindingContext.OperationBindingContext.ModelBinder;
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
@ -82,15 +82,20 @@ namespace Microsoft.AspNet.Mvc
private static ModelBindingContext GetBindingContext(Type modelType, IInputFormatter inputFormatter)
{
var metadataProvider = new EmptyModelMetadataProvider();
var operationBindingContext = new OperationBindingContext
{
ModelBinder = GetBodyBinder(inputFormatter, null),
MetadataProvider = metadataProvider,
HttpContext = new DefaultHttpContext(),
};
ModelBindingContext bindingContext = new ModelBindingContext
{
ModelMetadata = metadataProvider.GetMetadataForType(null, modelType),
ModelName = "someName",
ValueProvider = Mock.Of<IValueProvider>(),
ModelBinder = GetBodyBinder(inputFormatter, null),
MetadataProvider = metadataProvider,
HttpContext = new DefaultHttpContext(),
ModelState = new ModelStateDictionary()
ModelState = new ModelStateDictionary(),
OperationBindingContext = operationBindingContext,
};
return bindingContext;

View File

@ -71,7 +71,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
Mock.Of<IModelValidatorProvider>());
// Act
var context = DefaultControllerActionArgumentBinder
.GetModelBindingContext(modelMetadata, actionBindingContext);
.GetModelBindingContext(modelMetadata, actionBindingContext, Mock.Of<OperationBindingContext>());
// Assert
Assert.Equal(expectedFallToEmptyPrefix, context.FallbackToEmptyPrefix);
@ -106,63 +106,13 @@ namespace Microsoft.AspNet.Mvc.Core.Test
Mock.Of<IModelValidatorProvider>());
// Act
var context = DefaultControllerActionArgumentBinder
.GetModelBindingContext(modelMetadata, actionBindingContext);
.GetModelBindingContext(modelMetadata, actionBindingContext, Mock.Of<OperationBindingContext>());
// Assert
Assert.Equal(expectedFallToEmptyPrefix, context.FallbackToEmptyPrefix);
Assert.Equal(expectedModelName, context.ModelName);
}
[Fact]
public async Task Parameters_WithMultipleFromBody_Throw()
{
// Arrange
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = typeof(TestController).GetMethod("ActionWithTwoBodyParam"),
Parameters = new List<ParameterDescriptor>
{
new ParameterDescriptor
{
Name = "bodyParam",
ParameterType = typeof(Person),
},
new ParameterDescriptor
{
Name = "bodyParam1",
ParameterType = typeof(Person),
}
}
};
var binder = new Mock<IModelBinder>();
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var actionContext = new ActionContext(new RouteContext(Mock.Of<HttpContext>()),
actionDescriptor);
actionContext.Controller = Mock.Of<object>();
var bindingContext = new ActionBindingContext(actionContext,
metadataProvider,
Mock.Of<IModelBinder>(),
Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterSelector>(),
Mock.Of<IModelValidatorProvider>());
var actionBindingContextProvider = new Mock<IActionBindingContextProvider>();
actionBindingContextProvider.Setup(p => p.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
.Returns(Task.FromResult(bindingContext));
var invoker = new DefaultControllerActionArgumentBinder(
actionBindingContextProvider.Object);
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => invoker.GetActionArgumentsAsync(actionContext));
// Assert
Assert.Equal("More than one parameter is bound to the HTTP request's content.",
ex.Message);
}
[Fact]
public async Task GetActionArgumentsAsync_DoesNotAddActionArguments_IfBinderReturnsFalse()
{

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
@ -48,6 +49,32 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expectedValue, await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task TryUpdateModel_WithAPropertyFromBody()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// the name would be of customer.Department.Name
// and not for the top level customer object.
var input = "{\"Name\":\"RandomDepartment\"}";
var content = new StringContent(input, Encoding.UTF8, "application/json");
// Act
var response = await client.PostAsync("http://localhost/Home/GetCustomer?Id=1234", content);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var customer = JsonConvert.DeserializeObject<Customer>(
await response.Content.ReadAsStringAsync());
Assert.NotNull(customer.Department);
Assert.Equal("RandomDepartment", customer.Department.Name);
Assert.Equal(1234, customer.Id);
Assert.Equal(25, customer.Age);
Assert.Equal("dummy", customer.Name);
}
[Fact]
public async Task MultipleParametersMarkedWithFromBody_Throws()
{
@ -57,12 +84,280 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/MultipleParametersFromBody/MultipleParametersFromBodyThrows"));
client.GetAsync("http://localhost/FromAttributes/FromBodyParametersThrows"));
Assert.Equal("More than one parameter is bound to the HTTP request's content.",
Assert.Equal("More than one parameter and/or property is bound to the HTTP request's content.",
ex.Message);
}
[Fact]
public async Task MultipleParameterAndPropertiesMarkedWithFromBody_Throws()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/FromAttributes/FromBodyParameterAndPropertyThrows"));
Assert.Equal("More than one parameter and/or property is bound to the HTTP request's content.",
ex.Message);
}
[Fact]
public async Task MultipleParametersMarkedWith_FromFormAndFromBody_Throws()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/FromAttributes/FormAndBody_AsParameters_Throws"));
Assert.Equal("More than one parameter and/or property is bound to the HTTP request's content.",
ex.Message);
}
[Fact]
public async Task MultipleParameterAndPropertiesMarkedWith_FromFormAndFromBody_Throws()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/FromAttributes/FormAndBody_Throws"));
Assert.Equal("More than one parameter and/or property is bound to the HTTP request's content.",
ex.Message);
}
[Fact]
public async Task CanBind_MultipleParameters_UsingFromForm()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post,
"http://localhost/FromAttributes/MultipleFromFormParameters");
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string,string>("homeAddress.Street", "1"),
new KeyValuePair<string,string>("homeAddress.State", "WA_Form_Home"),
new KeyValuePair<string,string>("homeAddress.Zip", "2"),
new KeyValuePair<string,string>("officeAddress.Street", "3"),
new KeyValuePair<string,string>("officeAddress.State", "WA_Form_Office"),
new KeyValuePair<string,string>("officeAddress.Zip", "4"),
};
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var user = JsonConvert.DeserializeObject<User_FromForm>(
await response.Content.ReadAsStringAsync());
Assert.Equal("WA_Form_Home", user.HomeAddress.State);
Assert.Equal(1, user.HomeAddress.Street);
Assert.Equal(2, user.HomeAddress.Zip);
Assert.Equal("WA_Form_Office", user.OfficeAddress.State);
Assert.Equal(3, user.OfficeAddress.Street);
Assert.Equal(4, user.OfficeAddress.Zip);
}
[Fact]
public async Task CanBind_MultipleProperties_UsingFromForm()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post,
"http://localhost/FromAttributes/MultipleFromFormParameterAndProperty");
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string,string>("Street", "1"),
new KeyValuePair<string,string>("State", "WA_Form_Home"),
new KeyValuePair<string,string>("Zip", "2"),
new KeyValuePair<string,string>("officeAddress.Street", "3"),
new KeyValuePair<string,string>("officeAddress.State", "WA_Form_Office"),
new KeyValuePair<string,string>("officeAddress.Zip", "4"),
};
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var user = JsonConvert.DeserializeObject<User_FromForm>(
await response.Content.ReadAsStringAsync());
Assert.Equal("WA_Form_Home", user.HomeAddress.State);
Assert.Equal(1, user.HomeAddress.Street);
Assert.Equal(2, user.HomeAddress.Zip);
Assert.Equal("WA_Form_Office", user.OfficeAddress.State);
Assert.Equal(3, user.OfficeAddress.Street);
Assert.Equal(4, user.OfficeAddress.Zip);
}
[Fact]
public async Task CanBind_ComplexData_OnParameters_UsingFromAttributes()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Provide all three values, it should bind based on the attribute on the action method.
var request = new HttpRequestMessage(HttpMethod.Post,
"http://localhost/FromAttributes/GetUser/5/WA_Route/6" +
"?Street=3&State=WA_Query&Zip=4");
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string,string>("Street", "1"),
new KeyValuePair<string,string>("State", "WA_Form"),
new KeyValuePair<string,string>("Zip", "2"),
};
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var user = JsonConvert.DeserializeObject<User_FromForm>(
await response.Content.ReadAsStringAsync());
// Assert FromRoute
Assert.Equal("WA_Route", user.HomeAddress.State);
Assert.Equal(5, user.HomeAddress.Street);
Assert.Equal(6, user.HomeAddress.Zip);
// Assert FromForm
Assert.Equal("WA_Form", user.OfficeAddress.State);
Assert.Equal(1, user.OfficeAddress.Street);
Assert.Equal(2, user.OfficeAddress.Zip);
// Assert FromQuery
Assert.Equal("WA_Query", user.ShippingAddress.State);
Assert.Equal(3, user.ShippingAddress.Street);
Assert.Equal(4, user.ShippingAddress.Zip);
}
[Fact]
public async Task CanBind_ComplexData_OnProperties_UsingFromAttributes()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Provide all three values, it should bind based on the attribute on the action method.
var request = new HttpRequestMessage(HttpMethod.Post,
"http://localhost/FromAttributes/GetUser_FromForm/5/WA_Route/6" +
"?ShippingAddress.Street=3&ShippingAddress.State=WA_Query&ShippingAddress.Zip=4");
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string,string>("OfficeAddress.Street", "1"),
new KeyValuePair<string,string>("OfficeAddress.State", "WA_Form"),
new KeyValuePair<string,string>("OfficeAddress.Zip", "2"),
};
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var user = JsonConvert.DeserializeObject<User_FromForm>(
await response.Content.ReadAsStringAsync());
// Assert FromRoute
Assert.Equal("WA_Route", user.HomeAddress.State);
Assert.Equal(5, user.HomeAddress.Street);
Assert.Equal(6, user.HomeAddress.Zip);
// Assert FromForm
Assert.Equal("WA_Form", user.OfficeAddress.State);
Assert.Equal(1, user.OfficeAddress.Street);
Assert.Equal(2, user.OfficeAddress.Zip);
// Assert FromQuery
Assert.Equal("WA_Query", user.ShippingAddress.State);
Assert.Equal(3, user.ShippingAddress.Street);
Assert.Equal(4, user.ShippingAddress.Zip);
}
[Fact]
public async Task CanBind_ComplexData_OnProperties_UsingFromAttributes_WithBody()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Provide all three values, it should bind based on the attribute on the action method.
var request = new HttpRequestMessage(HttpMethod.Post,
"http://localhost/FromAttributes/GetUser_FromBody/5/WA_Route/6" +
"?ShippingAddress.Street=3&ShippingAddress.State=WA_Query&ShippingAddress.Zip=4");
var input = "{\"State\":\"WA_Body\",\"Street\":1,\"Zip\":2}";
request.Content = new StringContent(input, Encoding.UTF8, "application/json");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var user = JsonConvert.DeserializeObject<User_FromBody>(
await response.Content.ReadAsStringAsync());
// Assert FromRoute
Assert.Equal("WA_Route", user.HomeAddress.State);
Assert.Equal(5, user.HomeAddress.Street);
Assert.Equal(6, user.HomeAddress.Zip);
// Assert FromBody
Assert.Equal("WA_Body", user.OfficeAddress.State);
Assert.Equal(1, user.OfficeAddress.Street);
Assert.Equal(2, user.OfficeAddress.Zip);
// Assert FromQuery
Assert.Equal("WA_Query", user.ShippingAddress.State);
Assert.Equal(3, user.ShippingAddress.Street);
Assert.Equal(4, user.ShippingAddress.Zip);
}
[Fact]
public async Task NonExistingModelBinder_ForABinderMetadata_DoesNotRecurseInfinitely()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
var response = await client.GetStringAsync("http://localhost/WithMetadata/EchoDocument");
var document = JsonConvert.DeserializeObject<Document>
(response);
Assert.NotNull(document);
Assert.Null(document.Version);
Assert.Null(document.SubDocument);
}
[Fact]
public async Task ParametersWithNoValueProviderMetadataUseTheAvailableValueProviders()
{
@ -86,7 +381,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
}
[Fact]
public async Task ParametersAreAlwaysCreated()
public async Task ParametersAreAlwaysCreated_IfValuesAreProvidedWithoutModelName()
{
// Arrange
var server = TestServer.Create(_services, _app);
@ -107,6 +402,136 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(12, person.Age);
}
[Fact]
public async Task ParametersAreAlwaysCreated_IfValueIsProvidedForModelName()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await
client.GetAsync("http://localhost/WithoutMetadata" +
"/GetPersonParameter?p="); // here p is the model name.
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var person = JsonConvert.DeserializeObject<Person>(
await response.Content.ReadAsStringAsync());
Assert.NotNull(person);
Assert.Null(person.Name);
Assert.Equal(0, person.Age);
}
[Fact]
public async Task ParametersAreAlwaysCreated_IfNoValuesAreProvided()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await
client.GetAsync("http://localhost/WithoutMetadata" +
"/GetPersonParameter");
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var person = JsonConvert.DeserializeObject<Person>(
await response.Content.ReadAsStringAsync());
Assert.NotNull(person);
Assert.Null(person.Name);
Assert.Equal(0, person.Age);
}
[Fact]
public async Task PropertiesAreBound_IfTheyAreProvidedByValueProviders()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await
client.GetAsync("http://localhost/Properties" +
"/GetCompany?Employees[0].Name=somename&Age=12");
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var company = JsonConvert.DeserializeObject<Company>(
await response.Content.ReadAsStringAsync());
Assert.NotNull(company);
Assert.NotNull(company.Employees);
Assert.Equal(1, company.Employees.Count);
Assert.NotNull(company.Employees[0].Name);
}
[Fact]
public async Task PropertiesAreBound_IfTheyAreMarkedExplicitly()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await
client.GetAsync("http://localhost/Properties" +
"/GetCompany");
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var company = JsonConvert.DeserializeObject<Company>(
await response.Content.ReadAsStringAsync());
Assert.NotNull(company);
Assert.NotNull(company.CEO);
Assert.Null(company.CEO.Name);
}
[Fact]
public async Task PropertiesAreBound_IfTheyArePocoMetadataMarkedTypes()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await
client.GetAsync("http://localhost/Properties" +
"/GetCompany");
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var company = JsonConvert.DeserializeObject<Company>(
await response.Content.ReadAsStringAsync());
Assert.NotNull(company);
// Department property is not null because it was a marker poco.
Assert.NotNull(company.Department);
// beacause no value is provided.
Assert.Null(company.Department.Name);
}
[Fact]
public async Task PropertiesAreNotBound_ByDefault()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await
client.GetAsync("http://localhost/Properties" +
"/GetCompany");
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var company = JsonConvert.DeserializeObject<Company>(
await response.Content.ReadAsStringAsync());
Assert.NotNull(company);
Assert.Null(company.Employees);
}
[Theory]
[InlineData("http://localhost/Home/ActionWithPersonFromUrlWithPrefix/Javier/26")]
[InlineData("http://localhost/Home/ActionWithPersonFromUrlWithoutPrefix/Javier/26")]

View File

@ -91,8 +91,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
ModelMetadata = metadataProvider.GetMetadataForType(null, typeof(int[])),
ModelName = "someName",
ValueProvider = valueProvider,
ModelBinder = CreateIntBinder(),
MetadataProvider = metadataProvider
OperationBindingContext = new OperationBindingContext
{
ModelBinder = CreateIntBinder(),
MetadataProvider = metadataProvider
},
};
return bindingContext;
}

View File

@ -123,7 +123,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
ModelMetadata = metadataProvider.GetMetadataForType(null, modelType),
ModelName = "foo",
ValueProvider = valueProvider,
MetadataProvider = metadataProvider
OperationBindingContext = new OperationBindingContext
{
MetadataProvider = metadataProvider
}
};
return bindingContext;
}

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert
Assert.True(bound);
Assert.Equal(bindingContext.HttpContext.RequestAborted, bindingContext.Model);
Assert.Equal(bindingContext.OperationBindingContext.HttpContext.RequestAborted, bindingContext.Model);
}
[Theory]
@ -52,9 +52,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
ModelMetadata = metadataProvider.GetMetadataForType(null, modelType),
ModelName = "someName",
ValueProvider = new SimpleHttpValueProvider(),
ModelBinder = new CancellationTokenModelBinder(),
MetadataProvider = metadataProvider,
HttpContext = new DefaultHttpContext(),
OperationBindingContext = new OperationBindingContext
{
ModelBinder = new CancellationTokenModelBinder(),
MetadataProvider = metadataProvider,
HttpContext = new DefaultHttpContext(),
}
};
return bindingContext;

View File

@ -135,7 +135,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider());
ModelValidationNode childValidationNode = null;
Mock.Get<IModelBinder>(bindingContext.ModelBinder)
Mock.Get<IModelBinder>(bindingContext.OperationBindingContext.ModelBinder)
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) =>
{
@ -162,8 +162,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
ModelMetadata = metadataProvider.GetMetadataForType(null, typeof(int)),
ModelName = "someName",
ValueProvider = valueProvider,
ModelBinder = CreateIntBinder(),
MetadataProvider = metadataProvider
OperationBindingContext = new OperationBindingContext
{
ModelBinder = CreateIntBinder(),
MetadataProvider = metadataProvider
}
};
return bindingContext;
}

View File

@ -31,7 +31,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
{ "someName", "dummyValue" }
},
ValidatorProvider = GetValidatorProvider()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = GetValidatorProvider()
}
};
var mockIntBinder = new Mock<IModelBinder>();
@ -78,7 +81,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
{ "someOtherName", "dummyValue" }
},
ValidatorProvider = GetValidatorProvider()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = GetValidatorProvider()
}
};
var mockIntBinder = new Mock<IModelBinder>();
@ -151,7 +157,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
ModelState = new ModelStateDictionary()
ModelState = new ModelStateDictionary(),
OperationBindingContext = Mock.Of<OperationBindingContext>(),
};
// Act
@ -306,13 +313,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var bindingContext = new ModelBindingContext
{
ModelBinder = binder,
FallbackToEmptyPrefix = true,
MetadataProvider = metadataProvider,
ModelMetadata = metadataProvider.GetMetadataForType(null, type),
ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider,
ValidatorProvider = validatorProvider
OperationBindingContext = new OperationBindingContext
{
MetadataProvider = metadataProvider,
ModelBinder = binder,
ValidatorProvider = validatorProvider
}
};
return bindingContext;
}

View File

@ -25,8 +25,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ "someName[0]", new KeyValuePair<int, string>(42, "forty-two") },
{ "someName[1]", new KeyValuePair<int, string>(84, "eighty-four") }
},
ModelBinder = CreateKvpBinder(),
MetadataProvider = metadataProvider
OperationBindingContext = new OperationBindingContext
{
ModelBinder = CreateKvpBinder(),
MetadataProvider = metadataProvider
}
};
var binder = new DictionaryModelBinder<int, string>();

View File

@ -120,9 +120,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
ModelMetadata = metataProvider.GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
ModelName = "someName",
ValueProvider = valueProvider,
ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider,
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
OperationBindingContext = new OperationBindingContext
{
ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider,
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
}
};
return bindingContext;
}

View File

@ -21,11 +21,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
ValueProvider = new SimpleHttpValueProvider()
};
var newModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object));
// Act
var newBindingContext = new ModelBindingContext(originalBindingContext);
var newBindingContext = new ModelBindingContext(originalBindingContext, string.Empty, newModelMetadata);
// Assert
Assert.Null(newBindingContext.ModelMetadata);
Assert.Same(newModelMetadata, newBindingContext.ModelMetadata);
Assert.Equal("", newBindingContext.ModelName);
Assert.Equal(originalBindingContext.ModelState, newBindingContext.ModelState);
Assert.Equal(originalBindingContext.ValueProvider, newBindingContext.ValueProvider);

View File

@ -17,6 +17,316 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class MutableObjectModelBinderTest
{
[Theory]
[InlineData(typeof(Person), true)]
[InlineData(typeof(Person), false)]
[InlineData(typeof(EmptyModel), true)]
[InlineData(typeof(EmptyModel), false)]
public async Task
CanCreateModel_CreatesModel_ForTopLevelObjectIfThereIsExplicitPrefix(Type modelType, bool isPrefixProvided)
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
// Random type.
ModelMetadata = GetMetadataForType(typeof(Person)),
ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext
{
ValueProvider = mockValueProvider.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
},
// Setting it to empty ensures that model does not get created becasue of no model name.
ModelName = "dummyModelName",
}
};
bindingContext.ModelBindingContext.ModelMetadata.ModelName = isPrefixProvided ? "prefix" : null;
var mutableBinder = new TestableMutableObjectModelBinder();
bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties(
bindingContext.ModelBindingContext);
// Act
var retModel = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.Equal(isPrefixProvided, retModel);
}
[Theory]
[InlineData(typeof(Person), true)]
[InlineData(typeof(Person), false)]
[InlineData(typeof(EmptyModel), true)]
[InlineData(typeof(EmptyModel), false)]
public async Task
CanCreateModel_CreatesModel_ForTopLevelObjectIfThereIsEmptyModelName(Type modelType, bool emptyModelName)
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
// Random type.
ModelMetadata = GetMetadataForType(typeof(Person)),
ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
ValueProvider = mockValueProvider.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider()
}
}
};
bindingContext.ModelBindingContext.ModelName = emptyModelName ? string.Empty : "dummyModelName";
var mutableBinder = new TestableMutableObjectModelBinder();
bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties(
bindingContext.ModelBindingContext);
// Act
var retModel = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.Equal(emptyModelName, retModel);
}
[Fact]
public async Task CanCreateModel_ReturnsFalse_ForNonTopLevelModel_IfModelIsMarkedWithBinderMetadata()
{
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
// Get the property metadata so that it is not a top level object.
ModelMetadata = GetMetadataForType(typeof(Document))
.Properties
.First(metadata => metadata.PropertyName == nameof(Document.SubDocument)),
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
}
}
};
var mutableBinder = new MutableObjectModelBinder();
// Act
var canCreate = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.False(canCreate);
}
[Fact]
public async Task CanCreateModel_ReturnsTrue_ForTopLevelModel_IfModelIsMarkedWithBinderMetadata()
{
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
// Here the metadata represents a top level object.
ModelMetadata = GetMetadataForType(typeof(Document)),
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
}
}
};
var mutableBinder = new MutableObjectModelBinder();
// Act
var canCreate = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.True(canCreate);
}
[Fact]
public async Task CanCreateModel_CreatesModel_IfTheModelIsBinderPoco()
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(BinderMetadataPocoType)),
ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
ValueProvider = mockValueProvider.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
},
// Setting it to empty ensures that model does not get created becasue of no model name.
ModelName = "dummyModelName",
},
};
var mutableBinder = new TestableMutableObjectModelBinder();
bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties(
bindingContext.ModelBindingContext);
// Act
var retModel = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.True(retModel);
}
[Theory]
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false)]
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true)]
public async Task
CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfAValueProviderProvidesValue
(Type modelType, bool valueProviderProvidesValue)
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(valueProviderProvidesValue));
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(modelType),
ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
ValueProvider = mockValueProvider.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
},
// Setting it to empty ensures that model does not get created becasue of no model name.
ModelName = "dummyName"
}
};
var mutableBinder = new TestableMutableObjectModelBinder();
bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties(
bindingContext.ModelBindingContext);
// Act
var retModel = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.Equal(valueProviderProvidesValue, retModel);
}
[Theory]
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
public async Task CanCreateModel_ForExplicitValueProviderMetadata_UsesOriginalValueProvider(Type modelType, bool originalValueProviderProvidesValue)
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var mockOriginalValueProvider = new Mock<IMetadataAwareValueProvider>();
mockOriginalValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(originalValueProviderProvidesValue));
mockOriginalValueProvider.Setup(o => o.Filter(It.IsAny<IValueProviderMetadata>()))
.Returns<IValueProviderMetadata>(
valueProviderMetadata =>
{
if(valueProviderMetadata is ValueBinderMetadataAttribute)
{
return mockOriginalValueProvider.Object;
}
return null;
});
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(modelType),
ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext
{
ValueProvider = mockOriginalValueProvider.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
},
// Setting it to empty ensures that model does not get created becasue of no model name.
ModelName = "dummyName"
}
};
var mutableBinder = new TestableMutableObjectModelBinder();
bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties(
bindingContext.ModelBindingContext);
// Act
var retModel = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.Equal(originalValueProviderProvidesValue, retModel);
}
[Theory]
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false)]
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true)]
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
public async Task CanCreateModel_UnmarkedProperties_UsesCurrentValueProvider(Type modelType, bool valueProviderProvidesValue)
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(valueProviderProvidesValue));
var mockOriginalValueProvider = new Mock<IValueProvider>();
mockOriginalValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(modelType),
ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
ValueProvider = mockOriginalValueProvider.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
},
// Setting it to empty ensures that model does not get created becasue of no model name.
ModelName = "dummyName"
}
};
var mutableBinder = new TestableMutableObjectModelBinder();
bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties(
bindingContext.ModelBindingContext);
// Act
var retModel = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.Equal(valueProviderProvidesValue, retModel);
}
[Fact]
public async Task BindModel_InitsInstance()
{
@ -31,9 +341,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelMetadata = GetMetadataForObject(new Person()),
ModelName = "someName",
ValueProvider = mockValueProvider.Object,
ModelBinder = mockDtoBinder.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
OperationBindingContext = new OperationBindingContext
{
ModelBinder = mockDtoBinder.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
}
};
mockDtoBinder
@ -46,8 +359,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
testableBinder.Setup(o => o.EnsureModelPublic(bindingContext)).Verifiable();
testableBinder.Setup(o => o.GetMetadataForPropertiesPublic(bindingContext))
.Returns(new ModelMetadata[0]).Verifiable();
testableBinder.Setup(o => o.GetMetadataForProperties(bindingContext))
.Returns(new ModelMetadata[0]);
// Act
var retValue = await testableBinder.Object.BindModelAsync(bindingContext);
@ -73,9 +386,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelMetadata = GetMetadataForObject(new Person()),
ModelName = "",
ValueProvider = mockValueProvider.Object,
ModelBinder = mockDtoBinder.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
OperationBindingContext = new OperationBindingContext
{
ModelBinder = mockDtoBinder.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
}
};
mockDtoBinder
@ -88,8 +404,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
testableBinder.Setup(o => o.EnsureModelPublic(bindingContext)).Verifiable();
testableBinder.Setup(o => o.GetMetadataForPropertiesPublic(bindingContext))
.Returns(new ModelMetadata[0]).Verifiable();
testableBinder.Setup(o => o.GetMetadataForProperties(bindingContext))
.Returns(new ModelMetadata[0]);
// Act
var retValue = await testableBinder.Object.BindModelAsync(bindingContext);
@ -237,14 +553,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
}
};
var testableBinder = new TestableMutableObjectModelBinder();
// Act
var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext);
var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext);
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
// Assert
@ -267,14 +586,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(Person)),
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
}
};
var testableBinder = new TestableMutableObjectModelBinder();
// Act
var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext);
var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext);
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
// Assert
@ -300,14 +622,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(TypeWithExcludedPropertiesUsingBindAttribute)),
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
}
};
var testableBinder = new TestableMutableObjectModelBinder();
// Act
var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext);
var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext);
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
// Assert
@ -334,14 +659,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(TypeWithIncludedPropertiesUsingBindAttribute)),
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
}
};
var testableBinder = new TestableMutableObjectModelBinder();
// Act
var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext);
var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext);
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
// Assert
@ -355,7 +683,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForObject(new ModelWithMixedBindingBehaviors()),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
}
};
// Act
@ -429,7 +760,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
ModelMetadata = containerMetadata,
ModelName = "theModel",
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
}
};
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
@ -472,10 +806,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelMetadata = containerMetadata,
ModelName = "theModel",
ModelState = new ModelStateDictionary(),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
}
};
var validationContext = new ModelValidationContext(new EmptyModelMetadataProvider(),
bindingContext.ValidatorProvider,
bindingContext.OperationBindingContext
.ValidatorProvider,
bindingContext.ModelState,
containerMetadata,
null);
@ -705,7 +1043,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties.First(o => o.PropertyName == "PropertyWithDefaultValue");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode);
var requiredValidator = bindingContext.ValidatorProvider
var requiredValidator = bindingContext.OperationBindingContext
.ValidatorProvider
.GetValidators(propertyMetadata)
.FirstOrDefault(v => v.IsRequired);
@ -748,7 +1087,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
var dtoResult = new ComplexModelDtoResult(new DateTime(2001, 1, 1), validationNode);
var requiredValidator = bindingContext.ValidatorProvider
var requiredValidator = bindingContext.OperationBindingContext
.ValidatorProvider
.GetValidators(propertyMetadata)
.FirstOrDefault(v => v.IsRequired);
var validationContext = new ModelValidationContext(bindingContext, propertyMetadata);
@ -895,13 +1235,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelState = new ModelStateDictionary(),
ModelMetadata = metadata,
ModelName = "theModel",
ValidatorProvider = new CompositeModelValidatorProvider(provider.Object)
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = new CompositeModelValidatorProvider(provider.Object)
}
};
}
private static IModelValidator GetRequiredValidator(ModelBindingContext bindingContext, ModelMetadata propertyMetadata)
{
return bindingContext.ValidatorProvider
return bindingContext.OperationBindingContext
.ValidatorProvider
.GetValidators(propertyMetadata)
.FirstOrDefault(v => v.IsRequired);
}
@ -934,6 +1278,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
binderMetadata: null);
}
private class EmptyModel
{
}
private class Person
{
private DateTime? _dateOfDeath;
@ -1033,6 +1381,53 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
private class TypeWithNoBinderMetadata
{
public int UnMarkedProperty { get; set; }
}
private class BinderMetadataPocoType
{
[NonValueBinderMetadata]
public string MarkedWithABinderMetadata { get; set; }
}
// Not a Metadata poco because there is a property with value binder Metadata.
private class TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata
{
[NonValueBinderMetadata]
public string MarkedWithABinderMetadata { get; set; }
[ValueBinderMetadata]
public string MarkedWithAValueBinderMetadata { get; set; }
}
// not a Metadata poco because there is an unmarked property.
private class TypeWithUnmarkedAndBinderMetadataMarkedProperties
{
public int UnmarkedProperty { get; set; }
[NonValueBinderMetadata]
public string MarkedWithABinderMetadata { get; set; }
}
public class Document
{
[NonValueBinderMetadata]
public string Version { get; set; }
[NonValueBinderMetadata]
public Document SubDocument { get; set; }
}
private class NonValueBinderMetadataAttribute : Attribute, IBinderMetadata
{
}
private class ValueBinderMetadataAttribute : Attribute, IValueProviderMetadata
{
}
public class TestableMutableObjectModelBinder : MutableObjectModelBinder
{
public virtual bool CanUpdatePropertyPublic(ModelMetadata propertyMetadata)
@ -1065,16 +1460,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
EnsureModelPublic(bindingContext);
}
public virtual IEnumerable<ModelMetadata> GetMetadataForPropertiesPublic(ModelBindingContext bindingContext)
public virtual new IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext)
{
return base.GetMetadataForProperties(bindingContext);
}
protected override IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext)
{
return GetMetadataForPropertiesPublic(bindingContext);
}
public virtual void SetPropertyPublic(ModelBindingContext bindingContext,
ModelMetadata propertyMetadata,
ComplexModelDtoResult dtoResult,

View File

@ -95,6 +95,52 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
[Fact]
public void GetMetadataForProperty_WithNoBinderMetadata_GetsItFromType()
{
// Arrange
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var propertyMetadata = provider.GetMetadataForProperty(null, typeof(Person), nameof(Person.Parent));
// Assert
Assert.NotNull(propertyMetadata.BinderMetadata);
Assert.IsType<TestBinderMetadataAttribute>(propertyMetadata.BinderMetadata);
}
#if ASPNET50
[Fact]
public void GetMetadataForParameter_WithNoBinderMetadata_GetsItFromType()
{
// Arrange
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var parameterMetadata = provider.GetMetadataForParameter(null,
typeof(Person).GetMethod("Update"),
"person",
null);
// Assert
Assert.NotNull(parameterMetadata.BinderMetadata);
Assert.IsType<TestBinderMetadataAttribute>(parameterMetadata.BinderMetadata);
}
#endif
public class TestBinderMetadataAttribute : Attribute, IBinderMetadata
{
}
[TestBinderMetadata]
public class Person
{
public Person Parent { get; set; }
public void Update(Person person)
{
}
}
// GetMetadataForProperty
[Fact]

View File

@ -30,6 +30,10 @@ namespace FiltersWebSite
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ActionArguments["fromGlobalActionFilter"] == null)
{
context.ActionArguments["fromGlobalActionFilter"] = new List<ContentResult>();
}
(context.ActionArguments["fromGlobalActionFilter"] as List<ContentResult>)
.Add(Helpers.GetContentResult(context.Result, "Controller override - OnActionExecuting"));
}

View File

@ -0,0 +1,70 @@
// 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;
namespace ModelBindingWebSite.Controllers
{
public class FromAttributesController : Controller
{
[Route("/FromAttributes/[action]/{HomeAddress.Street}/{HomeAddress.State}/{HomeAddress.Zip}")]
public User_FromBody GetUser_FromBody(User_FromBody user)
{
return user;
}
[Route("/FromAttributes/[action]/{HomeAddress.Street}/{HomeAddress.State}/{HomeAddress.Zip}")]
public User_FromForm GetUser_FromForm(User_FromForm user)
{
return user;
}
[Route("/FromAttributes/[action]/{HomeAddress.Street}/{HomeAddress.State}/{HomeAddress.Zip}")]
public User_FromForm GetUser([FromRoute] Address homeAddress,
[FromForm] Address officeAddress,
[FromQuery] Address shippingAddress)
{
return new User_FromForm
{
HomeAddress = homeAddress,
OfficeAddress = officeAddress,
ShippingAddress = shippingAddress
};
}
public User_FromForm MultipleFromFormParameters([FromForm] Address homeAddress,
[FromForm] Address officeAddress)
{
return new User_FromForm
{
HomeAddress = homeAddress,
OfficeAddress = officeAddress,
};
}
// User_FromForm has a FromForm property.
public User_FromForm MultipleFromFormParameterAndProperty(User_FromForm user,
[FromForm] Address defaultAddress)
{
user.HomeAddress = defaultAddress;
return user;
}
public void FromBodyParametersThrows([FromBody] int id, [FromBody] string emp)
{
}
// Customer has a FromBody Property.
public void FromBodyParameterAndPropertyThrows([FromBody] Person p, Customer customer)
{
}
public void FormAndBody_Throws([FromForm] Person p, Customer customer)
{
}
public void FormAndBody_AsParameters_Throws([FromBody] int id, [FromForm] string emp)
{
}
}
}

View File

@ -49,6 +49,26 @@ namespace ModelBindingWebSite.Controllers
return person;
}
public Customer GetCustomer(int id)
{
var customer = CreateCustomer(id);
// should update customer.Department from body.
TryUpdateModelAsync(customer);
return customer;
}
private Customer CreateCustomer(int id)
{
return new Customer()
{
Id = id,
Name = "dummy",
Age = 25,
};
}
private Dictionary<string, string> CreateValidationDictionary()
{
var result = new Dictionary<string, string>();

View File

@ -1,14 +1,16 @@
// 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;
namespace ModelBindingWebSite.Controllers
{
public class MultipleParametersFromBodyController : Controller
public class PropertiesController : Controller
{
public void MultipleParametersFromBodyThrows([FromBody] int i, [FromBody] string emp)
public Company GetCompany(Company c)
{
return c;
}
}
}

View File

@ -1,7 +1,9 @@
// 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;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace ModelBindingWebSite.Controllers
{
@ -25,5 +27,10 @@ namespace ModelBindingWebSite.Controllers
{
return emp;
}
public Document EchoDocument(Document poco)
{
return poco;
}
}
}

View File

@ -1,11 +1,12 @@
// 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.ModelBinding;
namespace ModelBindingWebSite
{
public class ExternalType
public class FromNonExistantBinderAttribute : Attribute, IBinderMetadata
{
public string Department { get; set; }
}
}

View File

@ -0,0 +1,13 @@
// 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.ModelBinding;
namespace ModelBindingWebSite
{
public class FromTestAttribute : Attribute, IBinderMetadata
{
public object Value { get; set; }
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace ModelBindingWebSite
{
public class Address
{
public int Street { get; set; }
public string State { get; set; }
public int Zip { get; set; }
}
}

View File

@ -0,0 +1,17 @@
// 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;
namespace ModelBindingWebSite
{
public class Company
{
public Department Department { get; set; }
[FromTest]
public Person CEO { get; set; }
public IList<Employee> Employees { get; set; }
}
}

View File

@ -0,0 +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 Microsoft.AspNet.Mvc;
namespace ModelBindingWebSite
{
public class Customer : Person
{
public int Id { get; set; }
[FromBody]
public Department Department { get; set; }
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace ModelBindingWebSite
{
public class Department
{
// A single property marked with a binder metadata attribute makes it a binder metadata poco.
[FromTest]
public string Name { get; set; }
}
}

View File

@ -0,0 +1,14 @@
// 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.
namespace ModelBindingWebSite
{
public class Document
{
[FromNonExistantBinder]
public string Version { get; set; }
[FromNonExistantBinder]
public Document SubDocument { get; set; }
}
}

View File

@ -0,0 +1,23 @@
// 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;
namespace ModelBindingWebSite
{
public class User_FromBody
{
[FromRoute]
public Address HomeAddress { get; set; }
[FromBody]
public Address OfficeAddress { get; set; }
[FromQuery]
public Address ShippingAddress { get; set; }
// Should get it from the first value provider which
// can provide values for this.
public Address DefaultAddress { get; set; }
}
}

View File

@ -0,0 +1,19 @@
// 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;
namespace ModelBindingWebSite
{
public class User_FromForm
{
[FromRoute]
public Address HomeAddress { get; set; }
[FromForm]
public Address OfficeAddress { get; set; }
[FromQuery]
public Address ShippingAddress { get; set; }
}
}

View File

@ -22,6 +22,7 @@ namespace ModelBindingWebSite
.Configure<MvcOptions>(m =>
{
m.MaxModelValidationErrors = 8;
m.ModelBinders.Insert(0, typeof(TestMetadataAwareBinder));
});
});

View File

@ -0,0 +1,37 @@
// 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.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace ModelBindingWebSite
{
public class TestMetadataAwareBinder : MetadataAwareBinder<FromTestAttribute>
{
protected override Task<bool> BindAsync(ModelBindingContext bindingContext, FromTestAttribute metadata)
{
bindingContext.Model = metadata.Value;
if (!IsSimpleType(bindingContext.ModelType))
{
bindingContext.Model = Activator.CreateInstance(bindingContext.ModelType);
}
return Task.FromResult(true);
}
private bool IsSimpleType(Type type)
{
return type.GetTypeInfo().IsPrimitive ||
type.Equals(typeof(decimal)) ||
type.Equals(typeof(string)) ||
type.Equals(typeof(DateTime)) ||
type.Equals(typeof(Guid)) ||
type.Equals(typeof(DateTimeOffset)) ||
type.Equals(typeof(TimeSpan));
}
}
}