From d00c7ef59723fa2d7d5a544b9a831b73eb397c12 Mon Sep 17 00:00:00 2001 From: Harsh Gupta Date: Mon, 27 Oct 2014 12:33:30 -0700 Subject: [PATCH] Adding support for property level binding using IBinderMetadata and enabling FromXXX attributes to be decorated on properties. --- .../ControllerActionArgumentBinder.cs | 46 +- .../ModelBinders/BodyModelBinder.cs | 6 +- .../ParameterBinding/ModelBindingHelper.cs | 12 +- .../Properties/Resources.Designer.cs | 16 - src/Microsoft.AspNet.Mvc.Core/Resources.resx | 3 - .../BinderMetadata/FromBodyAttribute.cs | 2 +- .../BinderMetadata/FromFormAttribute.cs | 2 +- .../BinderMetadata/FromQueryAttribute.cs | 2 +- .../BinderMetadata/FromRouteAttribute.cs | 2 +- .../Binders/CancellationTokenModelBinder.cs | 2 +- .../Binders/CollectionModelBinder.cs | 20 +- .../Binders/ComplexModelDtoModelBinder.cs | 14 +- .../Binders/CompositeModelBinder.cs | 53 +- .../Binders/GenericModelBinder.cs | 10 +- .../Binders/KeyValuePairModelBinder.cs | 15 +- .../Binders/MutableObjectModelBinder.cs | 163 ++++-- .../MutableObjectModelBinderContext.cs | 14 + .../BodyBindingState.cs | 29 ++ .../Metadata/AssociatedMetadataProvider.cs | 24 +- .../ModelBindingContext.cs | 52 +- .../OperationBindingContext.cs | 46 ++ .../Properties/Resources.Designer.cs | 16 + .../Resources.resx | 3 + .../Validation/ModelValidationContext.cs | 4 +- .../HttpRequestMessageModelBinder.cs | 2 +- .../BodyModelBinderTests.cs | 17 +- .../ControllerActionArgumentBinderTests.cs | 54 +- .../ModelBindingTests.cs | 431 +++++++++++++++- .../Binders/ArrayModelBinderTest.cs | 7 +- .../Binders/ByteArrayModelBinderTests.cs | 5 +- .../CancellationTokenModelBinderTests.cs | 11 +- .../Binders/CollectionModelBinderTest.cs | 9 +- .../Binders/CompositeModelBinderTest.cs | 22 +- .../Binders/DictionaryModelBinderTest.cs | 7 +- .../Binders/KeyValuePairModelBinderTest.cs | 9 +- .../Binders/ModelBindingContextTest.cs | 6 +- .../Binders/MutableObjectModelBinderTest.cs | 462 ++++++++++++++++-- .../AssociatedMetadataProviderTest.cs | 46 ++ .../Controllers/ActionFilterController.cs | 4 + .../Controllers/FromAttributesController.cs | 70 +++ .../Controllers/HomeController.cs | 20 + ...r.cs => PropertiesGetCreatedController.cs} | 6 +- .../WithBinderMetadataController.cs | 7 + ...e.cs => FromNonExistantBinderAttribute.cs} | 5 +- .../ModelBindingWebSite/FromTestAttribute.cs | 13 + .../ModelBindingWebSite/Models/Address.cs | 12 + .../ModelBindingWebSite/Models/Company.cs | 17 + .../ModelBindingWebSite/Models/Customer.cs | 15 + .../ModelBindingWebSite/Models/Department.cs | 12 + .../ModelBindingWebSite/Models/Document.cs | 14 + .../{Model => Models}/Employee.cs | 0 .../EmployeeWithBinderMetadata.cs | 0 .../Models/MixedUser_FromBody.cs | 23 + .../Models/MixedUser_FromForm.cs | 19 + .../{Model => Models}/Person.cs | 0 test/WebSites/ModelBindingWebSite/Startup.cs | 1 + .../TestMetadataAwareBinder.cs | 37 ++ 57 files changed, 1640 insertions(+), 279 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinderContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BodyBindingState.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/OperationBindingContext.cs create mode 100644 test/WebSites/ModelBindingWebSite/Controllers/FromAttributesController.cs rename test/WebSites/ModelBindingWebSite/Controllers/{MultipleParametersFromBodyController.cs => PropertiesGetCreatedController.cs} (64%) rename test/WebSites/ModelBindingWebSite/{Model/ExternalType.cs => FromNonExistantBinderAttribute.cs} (63%) create mode 100644 test/WebSites/ModelBindingWebSite/FromTestAttribute.cs create mode 100644 test/WebSites/ModelBindingWebSite/Models/Address.cs create mode 100644 test/WebSites/ModelBindingWebSite/Models/Company.cs create mode 100644 test/WebSites/ModelBindingWebSite/Models/Customer.cs create mode 100644 test/WebSites/ModelBindingWebSite/Models/Department.cs create mode 100644 test/WebSites/ModelBindingWebSite/Models/Document.cs rename test/WebSites/ModelBindingWebSite/{Model => Models}/Employee.cs (100%) rename test/WebSites/ModelBindingWebSite/{Model => Models}/EmployeeWithBinderMetadata.cs (100%) create mode 100644 test/WebSites/ModelBindingWebSite/Models/MixedUser_FromBody.cs create mode 100644 test/WebSites/ModelBindingWebSite/Models/MixedUser_FromForm.cs rename test/WebSites/ModelBindingWebSite/{Model => Models}/Person.cs (100%) create mode 100644 test/WebSites/ModelBindingWebSite/TestMetadataAwareBinder.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionArgumentBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionArgumentBinder.cs index 63a31c4c1b..0b8771682d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ControllerActionArgumentBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionArgumentBinder.cs @@ -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(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 arguments, - ModelMetadata modelMetadata) + IEnumerable 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 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; diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs index 5142ec5d6c..f8e89ecee8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs @@ -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, diff --git a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs index ee97ebfee6..1846f7dc20 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs @@ -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)) diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 1f8dbdbdd2..b7a013f594 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -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); - /// - /// More than one parameter is bound to the HTTP request's content. - /// - internal static string MultipleBodyParametersAreNotAllowed - { - get { return GetString("MultipleBodyParametersAreNotAllowed"); } - } - - /// - /// More than one parameter is bound to the HTTP request's content. - /// - internal static string FormatMultipleBodyParametersAreNotAllowed() - { - return GetString("MultipleBodyParametersAreNotAllowed"); - } - /// /// The provided anti-forgery token failed a custom data check. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 5e00916e47..6d2c64bbf7 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -117,9 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - More than one parameter is bound to the HTTP request's content. - The provided anti-forgery token failed a custom data check. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromBodyAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromBodyAttribute.cs index 59f073975f..bd7c644df3 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromBodyAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromBodyAttribute.cs @@ -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. /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class FromBodyAttribute : Attribute, IFormatterBinderMetadata { } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs index 75c26f43ba..79191054fb 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs @@ -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. /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class FromFormAttribute : Attribute, IFormDataValueProviderMetadata { } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs index dce4d4c54b..8268a74ec2 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs @@ -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. /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class FromQueryAttribute : Attribute, IQueryValueProviderMetadata { } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs index eb0dec2132..613667dc1e 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs @@ -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. /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class FromRouteAttribute : Attribute, IRouteDataValueProviderMetadata { } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CancellationTokenModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CancellationTokenModelBinder.cs index a9671a9e9b..9313dc17ab 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CancellationTokenModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CancellationTokenModelBinder.cs @@ -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); } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs index 1f0fd2e6ac..f64a0175e3 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs @@ -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; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs index cde6d458aa..8cfe74c323 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs @@ -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); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs index ffbf9fb2c3..e94fd3a0b7 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs @@ -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; + } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/GenericModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/GenericModelBinder.cs index 0c53344599..51a02c69b5 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/GenericModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/GenericModelBinder.cs @@ -22,16 +22,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding _activator = activator; } - public Task BindModelAsync(ModelBindingContext bindingContext) + public async Task 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) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs index c73e4dd88b..431cb3ef22 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs @@ -28,14 +28,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding internal async Task> TryBindStrongModel(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(untypedModel); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 9a1cdd91ba..5d405bf1c4 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -16,35 +16,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public virtual async Task 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 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 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 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 CreateAndPopulateDto(ModelBindingContext bindingContext, IEnumerable 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 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 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) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinderContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinderContext.cs new file mode 100644 index 0000000000..2f894c51e4 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinderContext.cs @@ -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 PropertyMetadata { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BodyBindingState.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BodyBindingState.cs new file mode 100644 index 0000000000..f621c8ebbd --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BodyBindingState.cs @@ -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 +{ + /// + /// Represents state of models which are bound using body. + /// + public enum BodyBindingState + { + /// + /// Represents if there has been no metadata found which needs to read the body during the current + /// model binding process. + /// + NotBodyBased, + + /// + /// Represents if there is a that + /// has been found during the current model binding process. + /// + FormatterBased, + + /// + /// Represents if there is a that + /// has been found during the current model binding process. + /// + FormBased + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs index 8ff812929e..5f6f89e0c7 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs @@ -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 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; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs index e67f35fbc1..14aaaef6ea 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs @@ -29,25 +29,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// Initializes a new instance of the class using the - /// . - // + /// . + /// + /// Existing . + /// Model name of associated with the new . + /// Model metadata of associated with the new . + /// /// /// This constructor copies certain values that won't change between parent and child objects, /// e.g. ValueProvider, ModelState /// - 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; } + /// + /// Represents the associated with this context. + /// + public OperationBindingContext OperationBindingContext { get; set; } + /// /// Gets or sets the model associated with this context. /// @@ -129,32 +136,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public bool FallbackToEmptyPrefix { get; set; } - /// - /// Gets or sets the for the current request. - /// - public HttpContext HttpContext { get; set; } - /// /// Gets or sets the associated with this context. /// public IValueProvider ValueProvider { get; set; } - /// - /// Gets or sets the associated with this context. - /// - public IModelBinder ModelBinder { get; set; } - - /// - /// Gets or sets the associated with this context. - /// - public IModelMetadataProvider MetadataProvider { get; set; } - - /// - /// Gets or sets the instance used for model validation with this - /// context. - /// - public IModelValidatorProvider ValidatorProvider { get; set; } - /// /// Gets a dictionary of property name to instances for /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/OperationBindingContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/OperationBindingContext.cs new file mode 100644 index 0000000000..49b8196abe --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/OperationBindingContext.cs @@ -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 +{ + /// + /// A context that contains information specific to the current request and the action whose parameters + /// are being model bound. + /// + public class OperationBindingContext + { + /// + /// Represents if there has been a body bound model found during the current model binding process. + /// + public BodyBindingState BodyBindingState { get; set; } = BodyBindingState.NotBodyBased; + + /// + /// Gets or sets the for the current request. + /// + public HttpContext HttpContext { get; set; } + + /// + /// Gets unaltered value provider collection. + /// Value providers can be filtered by specific model binders. + /// + public IValueProvider ValueProvider { get; set; } + + /// + /// Gets or sets the associated with this context. + /// + public IModelBinder ModelBinder { get; set; } + + /// + /// Gets or sets the associated with this context. + /// + public IModelMetadataProvider MetadataProvider { get; set; } + + /// + /// Gets or sets the instance used for model validation with this + /// context. + /// + public IModelValidatorProvider ValidatorProvider { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs index 430756e319..925624c0aa 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs @@ -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); + /// + /// More than one parameter and/or property is bound to the HTTP request's content. + /// + internal static string MultipleBodyParametersOrPropertiesAreNotAllowed + { + get { return GetString("MultipleBodyParametersOrPropertiesAreNotAllowed"); } + } + + /// + /// More than one parameter and/or property is bound to the HTTP request's content. + /// + internal static string FormatMultipleBodyParametersOrPropertiesAreNotAllowed() + { + return GetString("MultipleBodyParametersOrPropertiesAreNotAllowed"); + } + /// /// Value cannot be null or empty. /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx index f8dbd03c98..81bc569163 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + More than one parameter and/or property is bound to the HTTP request's content. + Value cannot be null or empty. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs index cbcb4916d6..cda6ddb4d5 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs @@ -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) diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs index a7761b795e..e9b7f7f212 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs @@ -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); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/BodyModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/BodyModelBinderTests.cs index 4c497179fc..2ae8587c81 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/BodyModelBinderTests.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc // Arrange var bindingContext = GetBindingContext(typeof(Person), inputFormatter: null); bindingContext.ModelMetadata.BinderMetadata = Mock.Of(); - 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() : Mock.Of(); - 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(), - ModelBinder = GetBodyBinder(inputFormatter, null), - MetadataProvider = metadataProvider, - HttpContext = new DefaultHttpContext(), - ModelState = new ModelStateDictionary() + ModelState = new ModelStateDictionary(), + OperationBindingContext = operationBindingContext, }; return bindingContext; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs index d66b3081ed..febc538292 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs @@ -71,7 +71,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test Mock.Of()); // Act var context = DefaultControllerActionArgumentBinder - .GetModelBindingContext(modelMetadata, actionBindingContext); + .GetModelBindingContext(modelMetadata, actionBindingContext, Mock.Of()); // Assert Assert.Equal(expectedFallToEmptyPrefix, context.FallbackToEmptyPrefix); @@ -106,63 +106,13 @@ namespace Microsoft.AspNet.Mvc.Core.Test Mock.Of()); // Act var context = DefaultControllerActionArgumentBinder - .GetModelBindingContext(modelMetadata, actionBindingContext); + .GetModelBindingContext(modelMetadata, actionBindingContext, Mock.Of()); // 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 - { - new ParameterDescriptor - { - Name = "bodyParam", - ParameterType = typeof(Person), - }, - new ParameterDescriptor - { - Name = "bodyParam1", - ParameterType = typeof(Person), - } - } - }; - - var binder = new Mock(); - var metadataProvider = new DataAnnotationsModelMetadataProvider(); - var actionContext = new ActionContext(new RouteContext(Mock.Of()), - actionDescriptor); - actionContext.Controller = Mock.Of(); - var bindingContext = new ActionBindingContext(actionContext, - metadataProvider, - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of()); - - var actionBindingContextProvider = new Mock(); - actionBindingContextProvider.Setup(p => p.GetActionBindingContextAsync(It.IsAny())) - .Returns(Task.FromResult(bindingContext)); - - var invoker = new DefaultControllerActionArgumentBinder( - actionBindingContextProvider.Object); - - // Act - var ex = await Assert.ThrowsAsync( - () => 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() { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs index fb9af37b56..e1c10ae313 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs @@ -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( + 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(() => - 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(() => + 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(() => + 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(() => + 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> + { + new KeyValuePair("homeAddress.Street", "1"), + new KeyValuePair("homeAddress.State", "WA_Form_Home"), + new KeyValuePair("homeAddress.Zip", "2"), + new KeyValuePair("officeAddress.Street", "3"), + new KeyValuePair("officeAddress.State", "WA_Form_Office"), + new KeyValuePair("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( + 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> + { + new KeyValuePair("Street", "1"), + new KeyValuePair("State", "WA_Form_Home"), + new KeyValuePair("Zip", "2"), + new KeyValuePair("officeAddress.Street", "3"), + new KeyValuePair("officeAddress.State", "WA_Form_Office"), + new KeyValuePair("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( + 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> + { + new KeyValuePair("Street", "1"), + new KeyValuePair("State", "WA_Form"), + new KeyValuePair("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( + 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> + { + new KeyValuePair("OfficeAddress.Street", "1"), + new KeyValuePair("OfficeAddress.State", "WA_Form"), + new KeyValuePair("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( + 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( + 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 + (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( + 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( + 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( + 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( + 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( + 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( + 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")] diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ArrayModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ArrayModelBinderTest.cs index 00f834a2ea..938d6b5397 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ArrayModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ArrayModelBinderTest.cs @@ -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; } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ByteArrayModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ByteArrayModelBinderTests.cs index 44305725d8..14cd56fbfe 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ByteArrayModelBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ByteArrayModelBinderTests.cs @@ -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; } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CancellationTokenModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CancellationTokenModelBinderTests.cs index b2322de320..f877826859 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CancellationTokenModelBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CancellationTokenModelBinderTests.cs @@ -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; diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CollectionModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CollectionModelBinderTest.cs index 446ab01113..bc50dda7fd 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CollectionModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CollectionModelBinderTest.cs @@ -135,7 +135,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider()); ModelValidationNode childValidationNode = null; - Mock.Get(bindingContext.ModelBinder) + Mock.Get(bindingContext.OperationBindingContext.ModelBinder) .Setup(o => o.BindModelAsync(It.IsAny())) .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; } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs index f6ef99b713..ebc14c4571 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs @@ -31,7 +31,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { { "someName", "dummyValue" } }, - ValidatorProvider = GetValidatorProvider() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = GetValidatorProvider() + } }; var mockIntBinder = new Mock(); @@ -78,7 +81,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { { "someOtherName", "dummyValue" } }, - ValidatorProvider = GetValidatorProvider() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = GetValidatorProvider() + } }; var mockIntBinder = new Mock(); @@ -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(), }; // 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; } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/DictionaryModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/DictionaryModelBinderTest.cs index 303bb5665d..34b0dda257 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/DictionaryModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/DictionaryModelBinderTest.cs @@ -25,8 +25,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { "someName[0]", new KeyValuePair(42, "forty-two") }, { "someName[1]", new KeyValuePair(84, "eighty-four") } }, - ModelBinder = CreateKvpBinder(), - MetadataProvider = metadataProvider + OperationBindingContext = new OperationBindingContext + { + ModelBinder = CreateKvpBinder(), + MetadataProvider = metadataProvider + } }; var binder = new DictionaryModelBinder(); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/KeyValuePairModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/KeyValuePairModelBinderTest.cs index 41f9fa32b8..65e805964c 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/KeyValuePairModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/KeyValuePairModelBinderTest.cs @@ -120,9 +120,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test ModelMetadata = metataProvider.GetMetadataForType(null, typeof(KeyValuePair)), ModelName = "someName", ValueProvider = valueProvider, - ModelBinder = innerBinder ?? CreateIntBinder(), - MetadataProvider = metataProvider, - ValidatorProvider = Mock.Of() + OperationBindingContext = new OperationBindingContext + { + ModelBinder = innerBinder ?? CreateIntBinder(), + MetadataProvider = metataProvider, + ValidatorProvider = Mock.Of() + } }; return bindingContext; } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ModelBindingContextTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ModelBindingContextTest.cs index 6c82bd881f..925f9fed1f 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ModelBindingContextTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ModelBindingContextTest.cs @@ -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); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs index afb18287af..c545e37801 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs @@ -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(); + mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .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(), + }, + + // 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(); + mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .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(), + 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(), + } + } + }; + + 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(), + } + } + }; + + 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(); + mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .Returns(Task.FromResult(false)); + + var bindingContext = new MutableObjectBinderContext + { + ModelBindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForType(typeof(BinderMetadataPocoType)), + ValueProvider = mockValueProvider.Object, + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + 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(); + mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .Returns(Task.FromResult(valueProviderProvidesValue)); + + var bindingContext = new MutableObjectBinderContext + { + ModelBindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForType(modelType), + ValueProvider = mockValueProvider.Object, + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + 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(); + mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .Returns(Task.FromResult(false)); + + var mockOriginalValueProvider = new Mock(); + mockOriginalValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .Returns(Task.FromResult(originalValueProviderProvidesValue)); + mockOriginalValueProvider.Setup(o => o.Filter(It.IsAny())) + .Returns( + 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(), + }, + + // 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(); + mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .Returns(Task.FromResult(valueProviderProvidesValue)); + + var mockOriginalValueProvider = new Mock(); + mockOriginalValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .Returns(Task.FromResult(false)); + + var bindingContext = new MutableObjectBinderContext + { + ModelBindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForType(modelType), + ValueProvider = mockValueProvider.Object, + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + 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() + OperationBindingContext = new OperationBindingContext + { + ModelBinder = mockDtoBinder.Object, + MetadataProvider = new DataAnnotationsModelMetadataProvider(), + ValidatorProvider = Mock.Of() + } }; mockDtoBinder @@ -46,8 +359,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var testableBinder = new Mock { 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() + OperationBindingContext = new OperationBindingContext + { + ModelBinder = mockDtoBinder.Object, + MetadataProvider = new DataAnnotationsModelMetadataProvider(), + ValidatorProvider = Mock.Of() + } }; mockDtoBinder @@ -88,8 +404,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var testableBinder = new Mock { 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(), - MetadataProvider = new DataAnnotationsModelMetadataProvider() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + 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(), - MetadataProvider = new DataAnnotationsModelMetadataProvider() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + 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(), - MetadataProvider = new DataAnnotationsModelMetadataProvider() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + 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(), - MetadataProvider = new DataAnnotationsModelMetadataProvider() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + 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() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of() + } }; // Act @@ -429,7 +760,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { ModelMetadata = containerMetadata, ModelName = "theModel", - ValidatorProvider = Mock.Of() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of() + } }; 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() + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of() + } }; 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 GetMetadataForPropertiesPublic(ModelBindingContext bindingContext) + public virtual new IEnumerable GetMetadataForProperties(ModelBindingContext bindingContext) { return base.GetMetadataForProperties(bindingContext); } - protected override IEnumerable GetMetadataForProperties(ModelBindingContext bindingContext) - { - return GetMetadataForPropertiesPublic(bindingContext); - } - public virtual void SetPropertyPublic(ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult, diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/AssociatedMetadataProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/AssociatedMetadataProviderTest.cs index 459fc42335..000c3048d5 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/AssociatedMetadataProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/AssociatedMetadataProviderTest.cs @@ -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(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(parameterMetadata.BinderMetadata); + } +#endif + public class TestBinderMetadataAttribute : Attribute, IBinderMetadata + { + } + + [TestBinderMetadata] + public class Person + { + public Person Parent { get; set; } + + public void Update(Person person) + { + } + } + // GetMetadataForProperty [Fact] diff --git a/test/WebSites/FiltersWebSite/Controllers/ActionFilterController.cs b/test/WebSites/FiltersWebSite/Controllers/ActionFilterController.cs index ece954192e..59b90b73b9 100644 --- a/test/WebSites/FiltersWebSite/Controllers/ActionFilterController.cs +++ b/test/WebSites/FiltersWebSite/Controllers/ActionFilterController.cs @@ -30,6 +30,10 @@ namespace FiltersWebSite public override void OnActionExecuting(ActionExecutingContext context) { + if (context.ActionArguments["fromGlobalActionFilter"] == null) + { + context.ActionArguments["fromGlobalActionFilter"] = new List(); + } (context.ActionArguments["fromGlobalActionFilter"] as List) .Add(Helpers.GetContentResult(context.Result, "Controller override - OnActionExecuting")); } diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromAttributesController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromAttributesController.cs new file mode 100644 index 0000000000..3d67be77be --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Controllers/FromAttributesController.cs @@ -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) + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs b/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs index a4407e9592..2ff33600fa 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs @@ -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 CreateValidationDictionary() { var result = new Dictionary(); diff --git a/test/WebSites/ModelBindingWebSite/Controllers/MultipleParametersFromBodyController.cs b/test/WebSites/ModelBindingWebSite/Controllers/PropertiesGetCreatedController.cs similarity index 64% rename from test/WebSites/ModelBindingWebSite/Controllers/MultipleParametersFromBodyController.cs rename to test/WebSites/ModelBindingWebSite/Controllers/PropertiesGetCreatedController.cs index 828ac950f9..d24aa42110 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/MultipleParametersFromBodyController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/PropertiesGetCreatedController.cs @@ -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; } } } \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Controllers/WithBinderMetadataController.cs b/test/WebSites/ModelBindingWebSite/Controllers/WithBinderMetadataController.cs index 059b0c6988..17da259fe8 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/WithBinderMetadataController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/WithBinderMetadataController.cs @@ -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; + } } } \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Model/ExternalType.cs b/test/WebSites/ModelBindingWebSite/FromNonExistantBinderAttribute.cs similarity index 63% rename from test/WebSites/ModelBindingWebSite/Model/ExternalType.cs rename to test/WebSites/ModelBindingWebSite/FromNonExistantBinderAttribute.cs index 3762870cfe..63357fa801 100644 --- a/test/WebSites/ModelBindingWebSite/Model/ExternalType.cs +++ b/test/WebSites/ModelBindingWebSite/FromNonExistantBinderAttribute.cs @@ -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; } } } \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/FromTestAttribute.cs b/test/WebSites/ModelBindingWebSite/FromTestAttribute.cs new file mode 100644 index 0000000000..5cf413fd77 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/FromTestAttribute.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/Address.cs b/test/WebSites/ModelBindingWebSite/Models/Address.cs new file mode 100644 index 0000000000..f1917c5bfa --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/Address.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/Company.cs b/test/WebSites/ModelBindingWebSite/Models/Company.cs new file mode 100644 index 0000000000..e0b435463a --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/Company.cs @@ -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 Employees { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/Customer.cs b/test/WebSites/ModelBindingWebSite/Models/Customer.cs new file mode 100644 index 0000000000..15da4ec197 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/Customer.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/Department.cs b/test/WebSites/ModelBindingWebSite/Models/Department.cs new file mode 100644 index 0000000000..238e6f7894 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/Department.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/Document.cs b/test/WebSites/ModelBindingWebSite/Models/Document.cs new file mode 100644 index 0000000000..354164571e --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/Document.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Model/Employee.cs b/test/WebSites/ModelBindingWebSite/Models/Employee.cs similarity index 100% rename from test/WebSites/ModelBindingWebSite/Model/Employee.cs rename to test/WebSites/ModelBindingWebSite/Models/Employee.cs diff --git a/test/WebSites/ModelBindingWebSite/Model/EmployeeWithBinderMetadata.cs b/test/WebSites/ModelBindingWebSite/Models/EmployeeWithBinderMetadata.cs similarity index 100% rename from test/WebSites/ModelBindingWebSite/Model/EmployeeWithBinderMetadata.cs rename to test/WebSites/ModelBindingWebSite/Models/EmployeeWithBinderMetadata.cs diff --git a/test/WebSites/ModelBindingWebSite/Models/MixedUser_FromBody.cs b/test/WebSites/ModelBindingWebSite/Models/MixedUser_FromBody.cs new file mode 100644 index 0000000000..8538419cd2 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/MixedUser_FromBody.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/MixedUser_FromForm.cs b/test/WebSites/ModelBindingWebSite/Models/MixedUser_FromForm.cs new file mode 100644 index 0000000000..950dbc2237 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/MixedUser_FromForm.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Model/Person.cs b/test/WebSites/ModelBindingWebSite/Models/Person.cs similarity index 100% rename from test/WebSites/ModelBindingWebSite/Model/Person.cs rename to test/WebSites/ModelBindingWebSite/Models/Person.cs diff --git a/test/WebSites/ModelBindingWebSite/Startup.cs b/test/WebSites/ModelBindingWebSite/Startup.cs index 61940ae87e..4649eb800b 100644 --- a/test/WebSites/ModelBindingWebSite/Startup.cs +++ b/test/WebSites/ModelBindingWebSite/Startup.cs @@ -22,6 +22,7 @@ namespace ModelBindingWebSite .Configure(m => { m.MaxModelValidationErrors = 8; + m.ModelBinders.Insert(0, typeof(TestMetadataAwareBinder)); }); }); diff --git a/test/WebSites/ModelBindingWebSite/TestMetadataAwareBinder.cs b/test/WebSites/ModelBindingWebSite/TestMetadataAwareBinder.cs new file mode 100644 index 0000000000..c87a39fc3e --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/TestMetadataAwareBinder.cs @@ -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 + { + protected override Task 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)); + } + } +} \ No newline at end of file