From e434918337271d4ae7c51a5035a36206f91d86aa Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 27 Feb 2014 23:20:28 -0800 Subject: [PATCH] Reintroduce model validation --- samples/MvcSample/Models/User.cs | 6 +- samples/MvcSample/project.json | 56 +-- .../ActionBindingContextExtensions.cs | 1 + .../ParameterBinding/ActionBindingContext.cs | 9 +- .../DefaultActionBindingContextProvider.cs | 12 +- .../ReflectedActionInvoker.cs | 1 - .../Binders/CollectionModelBinder.cs | 6 +- .../Binders/ComplexModelDtoModelBinder.cs | 2 +- .../Binders/ComplexModelDtoResult.cs | 17 +- .../Binders/CompositeModelBinder.cs | 54 +-- .../Binders/KeyValuePairModelBinder.cs | 3 +- .../Binders/MutableObjectModelBinder.cs | 284 ++++++++------- .../Binders/TypeMatchModelBinder.cs | 16 +- .../Internal/DictionaryHelper.cs | 50 +++ .../Internal/EfficientTypePropertyKey.cs | 2 +- .../Internal/ModelBindingContextExtensions.cs | 32 ++ .../Metadata/AssociatedMetadataProvider.cs | 68 ++-- .../ModelBindingContext.cs | 38 +- .../ModelStateDictionary.cs | 18 +- .../Properties/Resources.Designer.cs | 96 ++++++ .../Resources.resx | 18 + .../Validation/AssociatedValidatorProvider.cs | 54 +++ .../Validation/CompositeModelValidator.cs | 56 +++ .../DataAnnotationsModelValidator.cs | 61 ++++ .../DataAnnotationsModelValidatorProvider.cs | 82 +++++ .../DataMemberModelValidatorProvider.cs | 50 +++ .../Validation/ErrorModelValidator.cs | 27 ++ .../Validation/IModelValidator.cs | 11 + .../Validation/IModelValidatorProvider.cs | 9 + .../InvalidModelValidatorProvider.cs | 52 +++ .../Validation/ModelValidatedEventArgs.cs | 18 + .../Validation/ModelValidatingEventArgs.cs | 18 + .../Validation/ModelValidationContext.cs | 38 ++ .../Validation/ModelValidationNode.cs | 209 +++++++++++ .../Validation/ModelValidationResult.cs | 16 + .../RequiredMemberModelValidator.cs | 18 + .../Validation/ValidatableObjectAdapter.cs | 60 ++++ .../project.json | 14 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 5 +- .../Binders/CollectionModelBinderTest.cs | 24 +- .../Binders/ComplexModelDtoResultTest.cs | 52 ++- .../Binders/CompositeModelBinderTest.cs | 67 ++-- .../Binders/KeyValuePairModelBinderTest.cs | 43 ++- .../Binders/ModelBindingContextTest.cs | 55 ++- .../AssociatedMetadataProviderTest.cs | 60 ++-- .../AssociatedValidatorProviderTest.cs | 111 ++++++ ...taAnnotationsModelValidatorProviderTest.cs | 108 ++++++ .../DataAnnotationsModelValidatorTest.cs | 231 +++++++++++++ .../DataMemberModelValidatorProviderTest.cs | 92 +++++ .../Validation/ErrorModelValidatorTest.cs | 29 ++ .../InvalidModelValidatorProviderTest.cs | 94 +++++ .../Validation/ModelValidationNodeTest.cs | 326 ++++++++++++++++++ .../project.json | 12 +- test/TestCommon/CultureReplacer.cs | 4 +- test/TestCommon/ExceptionAssert.cs | 4 +- test/TestCommon/ReplaceCulture.cs | 56 +++ 56 files changed, 2498 insertions(+), 457 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Internal/DictionaryHelper.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingContextExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/AssociatedValidatorProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompositeModelValidator.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ErrorModelValidator.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IModelValidator.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IModelValidatorProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/InvalidModelValidatorProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidatedEventArgs.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidatingEventArgs.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationNode.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationResult.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/RequiredMemberModelValidator.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ValidatableObjectAdapter.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/AssociatedValidatorProviderTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataMemberModelValidatorProviderTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ErrorModelValidatorTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/InvalidModelValidatorProviderTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelValidationNodeTest.cs create mode 100644 test/TestCommon/ReplaceCulture.cs diff --git a/samples/MvcSample/Models/User.cs b/samples/MvcSample/Models/User.cs index 4ce4ebc24f..2ec732a48d 100644 --- a/samples/MvcSample/Models/User.cs +++ b/samples/MvcSample/Models/User.cs @@ -1,7 +1,11 @@ -namespace MvcSample.Models +using System.ComponentModel.DataAnnotations; + +namespace MvcSample.Models { public class User { + [Required] + [MinLength(4)] public string Name { get; set; } public string Address { get; set; } public int Age { get; set; } diff --git a/samples/MvcSample/project.json b/samples/MvcSample/project.json index e695307774..61fdb5ab57 100644 --- a/samples/MvcSample/project.json +++ b/samples/MvcSample/project.json @@ -1,40 +1,42 @@ { - "version" : "0.1-alpha-*", + "version": "0.1-alpha-*", "dependencies": { "Microsoft.AspNet.Abstractions": "0.1-alpha-*", "Microsoft.AspNet.ConfigurationModel": "0.1-alpha-*", - "Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*", - "Microsoft.AspNet.Routing" : "0.1-alpha-*", - "Microsoft.AspNet.Mvc.ModelBinding" : "", - "Microsoft.AspNet.Mvc.Core" : "", - "Microsoft.AspNet.Mvc" : "", + "Microsoft.AspNet.DependencyInjection": "0.1-alpha-*", + "Microsoft.AspNet.Routing": "0.1-alpha-*", + "Microsoft.AspNet.Mvc.ModelBinding": "", + "Microsoft.AspNet.Mvc.Core": "", + "Microsoft.AspNet.Mvc": "", "Microsoft.AspNet.Mvc.Razor": "", - "Microsoft.AspNet.Mvc.Rendering" : "" + "Microsoft.AspNet.Mvc.Rendering": "" }, "configurations": { "net45": { "dependencies": { - "Autofac": "3.3.0", - "Owin": "1.0", - "Microsoft.AspNet.DependencyInjection.Autofac": "0.1-alpha-*", - "Microsoft.Owin": "2.1.0", - "Microsoft.Owin.Diagnostics": "2.1.0", - "Microsoft.Owin.Hosting": "2.1.0", - "Microsoft.Owin.Host.HttpListener": "2.1.0", - "Microsoft.AspNet.AppBuilderSupport": "0.1-alpha-*" - } + "Autofac": "3.3.0", + "Owin": "1.0", + "Microsoft.AspNet.DependencyInjection.Autofac": "0.1-alpha-*", + "Microsoft.Owin": "2.1.0", + "Microsoft.Owin.Diagnostics": "2.1.0", + "Microsoft.Owin.Hosting": "2.1.0", + "Microsoft.Owin.Host.HttpListener": "2.1.0", + "Microsoft.AspNet.AppBuilderSupport": "0.1-alpha-*", + "System.ComponentModel.DataAnnotations": "" + } }, - "k10" : { - "dependencies": { - "System.ComponentModel": "4.0.0.0", - "System.Console": "4.0.0.0", - "System.Diagnostics.Debug": "4.0.10.0", - "System.Diagnostics.Tools": "4.0.0.0", - "System.Dynamic.Runtime": "4.0.0.0", - "System.Runtime": "4.0.20.0", - "System.Runtime.InteropServices": "4.0.10.0", - "System.Threading.Tasks": "4.0.0.0" - } + "k10": { + "dependencies": { + "System.ComponentModel": "4.0.0.0", + "System.Console": "4.0.0.0", + "System.Diagnostics.Debug": "4.0.10.0", + "System.Diagnostics.Tools": "4.0.0.0", + "System.Dynamic.Runtime": "4.0.0.0", + "System.Runtime": "4.0.20.0", + "System.Runtime.InteropServices": "4.0.10.0", + "System.Threading.Tasks": "4.0.0.0", + "Microsoft.ComponentModel.DataAnnotations": "0.1-alpha-*" + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/ActionBindingContextExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/ActionBindingContextExtensions.cs index 9e311f0039..2f5a68ab9b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/ActionBindingContextExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/ActionBindingContextExtensions.cs @@ -29,6 +29,7 @@ namespace Microsoft.AspNet.Mvc.Internal ModelMetadata = modelMetadata, ModelBinder = actionBindingContext.ModelBinder, ValueProvider = actionBindingContext.ValueProvider, + ValidatorProviders = actionBindingContext.ValidatorProviders, MetadataProvider = metadataProvider, HttpContext = actionBindingContext.ActionContext.HttpContext, FallbackToEmptyPrefix = true diff --git a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ActionBindingContext.cs b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ActionBindingContext.cs index debc2a555e..0902c0cf4e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ActionBindingContext.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ActionBindingContext.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNet.Mvc.ModelBinding; +using System.Collections.Generic; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { @@ -8,13 +9,15 @@ namespace Microsoft.AspNet.Mvc IModelMetadataProvider metadataProvider, IModelBinder modelBinder, IValueProvider valueProvider, - IInputFormatter inputFormatter) + IInputFormatter inputFormatter, + IEnumerable validatorProviders) { ActionContext = context; MetadataProvider = metadataProvider; ModelBinder = modelBinder; ValueProvider = valueProvider; InputFormatter = inputFormatter; + ValidatorProviders = validatorProviders; } public ActionContext ActionContext { get; private set; } @@ -26,5 +29,7 @@ namespace Microsoft.AspNet.Mvc public IValueProvider ValueProvider { get; private set; } public IInputFormatter InputFormatter { get; private set; } + + public IEnumerable ValidatorProviders { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/DefaultActionBindingContextProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/DefaultActionBindingContextProvider.cs index 9b515aec14..56a8d487e5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/DefaultActionBindingContextProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/DefaultActionBindingContextProvider.cs @@ -10,17 +10,20 @@ namespace Microsoft.AspNet.Mvc private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IEnumerable _modelBinders; private readonly IEnumerable _valueProviderFactories; - private readonly IEnumerable _bodyReaders; + private readonly IEnumerable _inputFormatters; + private readonly IEnumerable _validatorProviders; public DefaultActionBindingContextProvider(IModelMetadataProvider modelMetadataProvider, IEnumerable modelBinders, IEnumerable valueProviderFactories, - IEnumerable bodyReaders) + IEnumerable inputFormatters, + IEnumerable validatorProviders) { _modelMetadataProvider = modelMetadataProvider; _modelBinders = modelBinders.OrderBy(binder => binder.GetType() == typeof(ComplexModelDtoModelBinder) ? 1 : 0); _valueProviderFactories = valueProviderFactories; - _bodyReaders = bodyReaders; + _inputFormatters = inputFormatters; + _validatorProviders = validatorProviders; } public async Task GetActionBindingContextAsync(ActionContext actionContext) @@ -35,7 +38,8 @@ namespace Microsoft.AspNet.Mvc _modelMetadataProvider, new CompositeModelBinder(_modelBinders), new CompositeValueProvider(valueProviders), - new CompositeInputFormatter(_bodyReaders) + new CompositeInputFormatter(_inputFormatters), + _validatorProviders ); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs index ef4f3f88f8..88f21d6ba5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs @@ -148,7 +148,6 @@ namespace Microsoft.AspNet.Mvc actionBindingContext.ModelBinder.BindModel(modelBindingContext); parameterValues[parameter.Name] = modelBindingContext.Model; } - } return parameterValues; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs index 853eb2baf5..4e09660bff 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs @@ -60,8 +60,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding if (bindingContext.ModelBinder.BindModel(innerBindingContext)) { boundValue = innerBindingContext.Model; - // TODO: validation - // bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode); + bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode); } boundCollection.Add(ModelBindingHelper.CastOrDefault(boundValue)); } @@ -114,9 +113,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding didBind = true; boundValue = childBindingContext.Model; - // TODO: Validation // merge validation up - // bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode); + bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode); } // infinite size collection stops on first bind failure diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs index d3206c0b40..b2aa242998 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding if (bindingContext.ModelBinder.BindModel(propertyBindingContext)) { - dto.Results[propertyMetadata] = new ComplexModelDtoResult(propertyBindingContext.Model/*, propertyBindingContext.ValidationNode*/); + dto.Results[propertyMetadata] = new ComplexModelDtoResult(propertyBindingContext.Model, propertyBindingContext.ValidationNode); } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoResult.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoResult.cs index 52b0febea1..76d1c83542 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoResult.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoResult.cs @@ -1,21 +1,18 @@ -namespace Microsoft.AspNet.Mvc.ModelBinding +using Microsoft.AspNet.Mvc.ModelBinding.Internal; + +namespace Microsoft.AspNet.Mvc.ModelBinding { public sealed class ComplexModelDtoResult { - public ComplexModelDtoResult(object model/*, ModelValidationNode validationNode*/) + public ComplexModelDtoResult(object model, + [NotNull] ModelValidationNode validationNode) { - // TODO: Validation - //if (validationNode == null) - //{ - // throw Error.ArgumentNull("validationNode"); - //} - Model = model; - //ValidationNode = validationNode; + ValidationNode = validationNode; } public object Model { get; private set; } - //public ModelValidationNode ValidationNode { get; private set; } + public ModelValidationNode ValidationNode { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs index 85ac2dc38b..37db668822 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -29,14 +28,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public virtual bool BindModel(ModelBindingContext bindingContext) { - ModelBindingContext newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName); + var newBindingContext = CreateNewBindingContext(bindingContext, + bindingContext.ModelName, + reuseValidationNode: true); bool boundSuccessfully = TryBind(newBindingContext); - if (!boundSuccessfully && !String.IsNullOrEmpty(bindingContext.ModelName) + if (!boundSuccessfully && !string.IsNullOrEmpty(bindingContext.ModelName) && bindingContext.FallbackToEmptyPrefix) { // fallback to empty prefix? - newBindingContext = CreateNewBindingContext(bindingContext, modelName: String.Empty); + newBindingContext = CreateNewBindingContext(bindingContext, + modelName: string.Empty, + reuseValidationNode: false); boundSuccessfully = TryBind(newBindingContext); } @@ -49,31 +52,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // If we fell back to an empty prefix above and are dealing with simple types, // propagate the non-blank model name through for user clarity in validation errors. // Complex types will reveal their individual properties as model names and do not require this. - // TODO: Validation - //if (!newBindingContext.ModelMetadata.IsComplexType && String.IsNullOrEmpty(newBindingContext.ModelName)) - //{ - // newBindingContext.ValidationNode = new Validation.ModelValidationNode(newBindingContext.ModelMetadata, bindingContext.ModelName); - //} + if (!newBindingContext.ModelMetadata.IsComplexType && String.IsNullOrEmpty(newBindingContext.ModelName)) + { + newBindingContext.ValidationNode = new ModelValidationNode(newBindingContext.ModelMetadata, bindingContext.ModelName); + } - //newBindingContext.ValidationNode.Validate(context, null /* parentNode */); + var validationContext = new ModelValidationContext(bindingContext.ModelMetadata, + bindingContext.ModelState, + bindingContext.MetadataProvider, + bindingContext.ValidatorProviders); + + newBindingContext.ValidationNode.Validate(validationContext, parentNode: null); bindingContext.Model = newBindingContext.Model; return true; } - private bool TryBind(ModelBindingContext bindingContext) + private bool TryBind([NotNull] ModelBindingContext bindingContext) { - // TODO: The body of this method existed as HttpActionContextExtensions.Bind. We might have to refactor it into - // something that is shared. - if (bindingContext == null) - { - throw Error.ArgumentNull("bindingContext"); - } - // TODO: RuntimeHelpers.EnsureSufficientExecutionStack does not exist in the CoreCLR. // Protects against stack overflow for deeply nested model binding // RuntimeHelpers.EnsureSufficientExecutionStack(); - foreach (IModelBinder binder in Binders) + foreach (var binder in Binders) { if (binder.BindModel(bindingContext)) { @@ -85,7 +85,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return false; } - private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext, string modelName) + private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext, + string modelName, + bool reuseValidationNode) { var newBindingContext = new ModelBindingContext { @@ -93,17 +95,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding ModelName = modelName, ModelState = oldBindingContext.ModelState, ValueProvider = oldBindingContext.ValueProvider, + ValidatorProviders = oldBindingContext.ValidatorProviders, MetadataProvider = oldBindingContext.MetadataProvider, ModelBinder = oldBindingContext.ModelBinder, HttpContext = oldBindingContext.HttpContext }; - // TODO: Validation - //// validation is expensive to create, so copy it over if we can - //if (Object.ReferenceEquals(modelName, oldBindingContext.ModelName)) - //{ - // newBindingContext.ValidationNode = oldBindingContext.ValidationNode; - //} + // validation is expensive to create, so copy it over if we can + if (reuseValidationNode) + { + newBindingContext.ValidationNode = oldBindingContext.ValidationNode; + } return newBindingContext; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs index d602ab933e..34c581e08b 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs @@ -37,8 +37,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { object untypedModel = propertyBindingContext.Model; model = ModelBindingHelper.CastOrDefault(untypedModel); - // TODO: Revive once we get validation - // parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode); + parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode); return true; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 4c1b64f1ea..307a5a5d6e 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -21,13 +21,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } EnsureModel(bindingContext); - IEnumerable propertyMetadatas = GetMetadataForProperties(bindingContext); - ComplexModelDto dto = CreateAndPopulateDto(bindingContext, propertyMetadatas); + var propertyMetadatas = GetMetadataForProperties(bindingContext); + var dto = CreateAndPopulateDto(bindingContext, propertyMetadatas); // post-processing, e.g. property setters and hooking up validation ProcessDto(bindingContext, dto); - // TODO: Validation - // bindingContext.ValidationNode.ValidateAllProperties = true; // complex models require full validation + // complex models require full validation + bindingContext.ValidationNode.ValidateAllProperties = true; return true; } @@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private static bool CanBindType(Type modelType) { // Simple types cannot use this binder - bool isComplexType = !modelType.HasStringConverter(); + var isComplexType = !modelType.HasStringConverter(); if (!isComplexType) { return false; @@ -87,8 +87,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private ComplexModelDto CreateAndPopulateDto(ModelBindingContext bindingContext, IEnumerable propertyMetadatas) { // create a DTO and call into the DTO binder - ComplexModelDto originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas); - ModelBindingContext dtoBindingContext = new ModelBindingContext(bindingContext) + var originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas); + var dtoBindingContext = new ModelBindingContext(bindingContext) { ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(() => originalDto, typeof(ComplexModelDto)), ModelName = bindingContext.ModelName @@ -105,24 +105,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return Activator.CreateInstance(bindingContext.ModelType); } - //// Called when the property setter null check failed, allows us to add our own error message to ModelState. - //internal static EventHandler CreateNullCheckFailedHandler(ModelMetadata modelMetadata, object incomingValue) - //{ - // return (sender, e) => - // { - // ModelValidationNode validationNode = (ModelValidationNode)sender; - // ModelStateDictionary modelState = e.ActionContext.ModelState; + // Called when the property setter null check failed, allows us to add our own error message to ModelState. + internal static EventHandler CreateNullCheckFailedHandler(ModelMetadata modelMetadata, object incomingValue) + { + return (sender, e) => + { + var validationNode = (ModelValidationNode)sender; + var modelState = e.ValidationContext.ModelState; - // if (modelState.IsValidField(validationNode.ModelStateKey)) - // { - // string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ActionContext, modelMetadata, incomingValue); - // if (errorMessage != null) - // { - // modelState.AddModelError(validationNode.ModelStateKey, errorMessage); - // } - // } - // }; - //} + if (modelState.IsValidField(validationNode.ModelStateKey)) + { + // TODO: Revive ModelBinderConfig + // string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ValidationContext, modelMetadata, incomingValue); + var errorMessage = e.ValidationContext.ModelMetadata.PropertyName + " is required"; + if (errorMessage != null) + { + modelState.AddModelError(validationNode.ModelStateKey, errorMessage); + } + } + }; + } protected virtual void EnsureModel(ModelBindingContext bindingContext) { @@ -134,12 +136,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding protected virtual IEnumerable GetMetadataForProperties(ModelBindingContext bindingContext) { - // TODO: Revive required properties. This has a dependency on HttpBindingAttribute and DataAnnotations // keep a set of the required properties so that we can cross-reference bound properties later - HashSet requiredProperties = new HashSet(); - // Dictionary requiredValidators; - HashSet skipProperties = new HashSet(); - // GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out skipProperties); + HashSet requiredProperties; + Dictionary requiredValidators; + HashSet skipProperties; + GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out requiredValidators, out skipProperties); return from propertyMetadata in bindingContext.ModelMetadata.Properties let propertyName = propertyMetadata.PropertyName @@ -150,102 +151,84 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private static object GetPropertyDefaultValue(PropertyInfo propertyInfo) { - DefaultValueAttribute attr = propertyInfo.GetCustomAttribute(); + var attr = propertyInfo.GetCustomAttribute(); return (attr != null) ? attr.Value : null; } - //internal static void GetRequiredPropertiesCollection(ModelBindingContext bindingContext, out HashSet requiredProperties, out HashSet skipProperties) - //{ - // requiredProperties = new HashSet(StringComparer.OrdinalIgnoreCase); - // // requiredValidators = new Dictionary(StringComparer.OrdinalIgnoreCase); - // skipProperties = new HashSet(StringComparer.OrdinalIgnoreCase); + internal static void GetRequiredPropertiesCollection(ModelBindingContext bindingContext, + out HashSet requiredProperties, + out Dictionary requiredValidators, + out HashSet skipProperties) + { + requiredProperties = new HashSet(StringComparer.OrdinalIgnoreCase); + requiredValidators = new Dictionary(StringComparer.OrdinalIgnoreCase); + skipProperties = new HashSet(StringComparer.OrdinalIgnoreCase); - // // Use attributes on the property before attributes on the type. - // Type modelType = bindingContext.ModelType; - // ICustomTypeDescriptor modelDescriptor = new AssociatedMetadataTypeTypeDescriptionProvider(modelType) - // .GetTypeDescriptor(modelType); + // TODO: HttpBindingBehaviorAttribute + var modelTypeInfo = bindingContext.ModelType.GetTypeInfo(); + foreach (var propertyMetadata in bindingContext.ModelMetadata.Properties) + { + var propertyName = propertyMetadata.PropertyName; + var requiredValidator = bindingContext.GetValidators(propertyMetadata) + .FirstOrDefault(v => v.IsRequired); + // TODO: Revive HttpBindingBehaviorAttribute - // PropertyDescriptorCollection propertyDescriptors = modelDescriptor.GetProperties(); - - // // TODO: Revive HttpBindingBehavior - // // HttpBindingBehaviorAttribute typeAttr = modelDescriptor.GetAttributes().OfType().SingleOrDefault(); - - // foreach (PropertyDescriptor propertyDescriptor in propertyDescriptors) - // { - // string propertyName = propertyDescriptor.Name; - // ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyName]; - // // ModelValidator requiredValidator = context.GetValidators(propertyMetadata).Where(v => v.IsRequired).FirstOrDefault(); - // // requiredValidators[propertyName] = requiredValidator; - - // HttpBindingBehaviorAttribute propAttr = propertyDescriptor.Attributes.OfType().SingleOrDefault(); - // HttpBindingBehaviorAttribute workingAttr = propAttr ?? typeAttr; - // if (workingAttr != null) - // { - // switch (workingAttr.Behavior) - // { - // case HttpBindingBehavior.Required: - // requiredProperties.Add(propertyName); - // break; - - // case HttpBindingBehavior.Never: - // skipProperties.Add(propertyName); - // break; - // } - // } - // else if (requiredValidator != null) - // { - // requiredProperties.Add(propertyName); - // } - // } - //} + if (requiredValidator != null) + { + requiredValidators[propertyName] = requiredValidator; + requiredProperties.Add(propertyName); + } + } + } internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto) { - // TODO: Uncomment this once we revive validation + HashSet requiredProperties; + Dictionary requiredValidators; + HashSet skipProperties; + GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out requiredValidators, out skipProperties); - //HashSet requiredProperties; - //// Dictionary requiredValidators; - //HashSet skipProperties; - //GetRequiredPropertiesCollection(context, bindingContext, out requiredProperties, out requiredValidators, out skipProperties); + // Eliminate provided properties from requiredProperties; leaving just *missing* required properties. + requiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName)); - //// Eliminate provided properties from requiredProperties; leaving just *missing* required properties. - //requiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName)); + foreach (var missingRequiredProperty in requiredProperties) + { + var addedError = false; + var modelStateKey = ModelBindingHelper.CreatePropertyModelName( + bindingContext.ValidationNode.ModelStateKey, missingRequiredProperty); - //foreach (string missingRequiredProperty in requiredProperties) - //{ - // string modelStateKey = ModelBindingHelper.CreatePropertyModelName( - // bindingContext.ValidationNode.ModelStateKey, missingRequiredProperty); + // Update Model as SetProperty() would: Place null value where validator will check for non-null. This + // ensures a failure result from a required validator (if any) even for a non-nullable property. + // (Otherwise, propertyMetadata.Model is likely already null.) + var propertyMetadata = bindingContext.PropertyMetadata[missingRequiredProperty]; + propertyMetadata.Model = null; - // // Update Model as SetProperty() would: Place null value where validator will check for non-null. This - // // ensures a failure result from a required validator (if any) even for a non-nullable property. - // // (Otherwise, propertyMetadata.Model is likely already null.) - // ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[missingRequiredProperty]; - // propertyMetadata.Model = null; + // Execute validator (if any) to get custom error message. + IModelValidator validator; + if (requiredValidators.TryGetValue(missingRequiredProperty, out validator)) + { + addedError = RunValidator(validator, bindingContext, propertyMetadata, modelStateKey); + } - // // Execute validator (if any) to get custom error message. - // ModelValidator validator = requiredValidators[missingRequiredProperty]; - // bool addedError = RunValidator(validator, bindingContext, propertyMetadata, modelStateKey); - - // // Fall back to default message if HttpBindingBehaviorAttribute required this property or validator - // // (oddly) succeeded. - // if (!addedError) - // { - // bindingContext.ModelState.AddModelError(modelStateKey, - // Error.Format(SRResources.MissingRequiredMember, missingRequiredProperty)); - // } - //} + // Fall back to default message if HttpBindingBehaviorAttribute required this property or validator + // (oddly) succeeded. + if (!addedError) + { + bindingContext.ModelState.AddModelError( + modelStateKey, + Resources.FormatMissingRequiredMember(missingRequiredProperty)); + } + } // for each property that was bound, call the setter, recording exceptions as necessary foreach (var entry in dto.Results) { - ModelMetadata propertyMetadata = entry.Key; - - ComplexModelDtoResult dtoResult = entry.Value; + var propertyMetadata = entry.Key; + var dtoResult = entry.Value; if (dtoResult != null) { SetProperty(bindingContext, propertyMetadata, dtoResult); - // TODO: Validation - // bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode); + bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode); } } } @@ -253,14 +236,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")] protected virtual void SetProperty(ModelBindingContext bindingContext, ModelMetadata propertyMetadata, - ComplexModelDtoResult dtoResult - /*, ModelValidator requiredValidator*/) + ComplexModelDtoResult dtoResult) { - // TODO: This used TypeDescriptor which is no longer available. Lookups performed using System.ComponentModel were - // cached. To maintain parity, we'll need to cache property lookups. - PropertyInfo property = bindingContext.ModelType - .GetRuntimeProperties() - .FirstOrDefault(p => p.Name.Equals(propertyMetadata.PropertyName, StringComparison.OrdinalIgnoreCase)); + var property = bindingContext.ModelType + .GetProperty(propertyMetadata.PropertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); if (property == null || !property.CanWrite) { @@ -268,21 +247,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return; } - object value = dtoResult.Model ?? GetPropertyDefaultValue(property); + var value = dtoResult.Model ?? GetPropertyDefaultValue(property); propertyMetadata.Model = value; - //// 'Required' validators need to run first so that we can provide useful error messages if - //// the property setters throw, e.g. if we're setting entity keys to null. See comments in - //// DefaultModelBinder.SetProperty() for more information. - //if (value == null) - //{ - // string modelStateKey = dtoResult.ValidationNode.ModelStateKey; - // if (bindingContext.ModelState.IsValidField(modelStateKey)) - // { - // RunValidator(requiredValidator, bindingContext, propertyMetadata, modelStateKey); - // } - //} + // 'Required' validators need to run first so that we can provide useful error messages if + // the property setters throw, e.g. if we're setting entity keys to null. + if (value == null) + { + var modelStateKey = dtoResult.ValidationNode.ModelStateKey; + if (bindingContext.ModelState.IsValidField(modelStateKey)) + { + var requiredValidator = bindingContext.GetValidators(propertyMetadata).FirstOrDefault(v => v.IsRequired); + if (requiredValidator != null) + { + var validationContext = bindingContext.CreateValidationContext(propertyMetadata); + foreach (var validationResult in requiredValidator.Validate(validationContext)) + { + bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message); + } + } + } + } if (value != null || property.PropertyType.AllowsNullValue()) { @@ -293,39 +279,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding catch (Exception ex) { // don't display a duplicate error message if a binding error has already occurred for this field - //string modelStateKey = dtoResult.ValidationNode.ModelStateKey; - //if (bindingContext.ModelState.IsValidField(modelStateKey)) - //{ - bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex); - //} + var modelStateKey = dtoResult.ValidationNode.ModelStateKey; + if (bindingContext.ModelState.IsValidField(modelStateKey)) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex); + } } } else { // trying to set a non-nullable value type to null, need to make sure there's a message - //string modelStateKey = dtoResult.ValidationNode.ModelStateKey; - //if (bindingContext.ModelState.IsValidField(modelStateKey)) - //{ - // dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(propertyMetadata, value); - //} + var modelStateKey = dtoResult.ValidationNode.ModelStateKey; + if (bindingContext.ModelState.IsValidField(modelStateKey)) + { + dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(propertyMetadata, value); + } } } - //// Returns true if validator execution adds a model error. - //private static bool RunValidator(ModelValidator validator, ModelBindingContext bindingContext, - // ModelMetadata propertyMetadata, string modelStateKey) - //{ - // bool addedError = false; - // if (validator != null) - // { - // foreach (ModelValidationResult validationResult in validator.Validate(propertyMetadata, bindingContext.Model)) - // { - // bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message); - // addedError = true; - // } - // } + // Returns true if validator execution adds a model error. + private static bool RunValidator(IModelValidator validator, + ModelBindingContext bindingContext, + ModelMetadata propertyMetadata, + string modelStateKey) + { + var validationContext = bindingContext.CreateValidationContext(propertyMetadata); - // return addedError; - //} + var addedError = false; + foreach (var validationResult in validator.Validate(validationContext)) + { + bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message); + addedError = true; + } + return addedError; + } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeMatchModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeMatchModelBinder.cs index 00cdbabcae..23e5d71b1a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeMatchModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeMatchModelBinder.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNet.Mvc.ModelBinding.Internal; +using System.Linq; +using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -17,17 +18,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding object model = valueProviderResult.RawValue; ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model); bindingContext.Model = model; - if (bindingContext.ModelMetadata.IsComplexType) - { - // TODO: Validation - //IBodyModelValidator validator = services.GetBodyModelValidator(); - //ModelMetadataProvider metadataProvider = services.GetModelMetadataProvider(); - //if (validator != null && metadataProvider != null) - //{ - // validator.Validate(model, bindingContext.ModelType, metadataProvider, context, bindingContext.ModelName); - //} - } - + + // TODO: Determine if we need IBodyValidator here. return true; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/DictionaryHelper.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/DictionaryHelper.cs new file mode 100644 index 0000000000..55d1535e0d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/DictionaryHelper.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Internal +{ + public static class DictionaryHelper + { + public static IEnumerable> FindKeysWithPrefix([NotNull] IDictionary dictionary, + [NotNull] string prefix) + { + TValue exactMatchValue; + if (dictionary.TryGetValue(prefix, out exactMatchValue)) + { + yield return new KeyValuePair(prefix, exactMatchValue); + } + + foreach (var entry in dictionary) + { + var key = entry.Key; + + if (key.Length <= prefix.Length) + { + continue; + } + + if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Everything is prefixed by the empty string + if (prefix.Length == 0) + { + yield return entry; + } + else + { + char charAfterPrefix = key[prefix.Length]; + switch (charAfterPrefix) + { + case '[': + case '.': + yield return entry; + break; + } + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/EfficientTypePropertyKey.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/EfficientTypePropertyKey.cs index a320ec27d7..6b4a52c753 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/EfficientTypePropertyKey.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/EfficientTypePropertyKey.cs @@ -1,6 +1,6 @@ using System; -namespace Microsoft.AspNet.Mvc.ModelBinding +namespace Microsoft.AspNet.Mvc.ModelBinding.Internal { internal class EfficientTypePropertyKey : Tuple { diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingContextExtensions.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingContextExtensions.cs new file mode 100644 index 0000000000..b35c51203d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingContextExtensions.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Internal +{ + internal static class ModelBindingContextExtensions + { + public static IEnumerable GetValidators([NotNull] this ModelBindingContext context, + [NotNull] ModelMetadata metadata) + { + return context.ValidatorProviders.SelectMany(vp => vp.GetValidators(metadata)) + .Where(v => v != null); + } + + public static ModelValidationContext CreateValidationContext([NotNull] this ModelBindingContext context, + [NotNull] ModelMetadata metadata) + { + return new ModelValidationContext(metadata, + context.ModelState, + context.MetadataProvider, + context.ValidatorProviders); + } + + public static IEnumerable Validate([NotNull] this ModelBindingContext bindingContext) + { + var validators = GetValidators(bindingContext, bindingContext.ModelMetadata); + var compositeValidator = new CompositeModelValidator(validators); + var modelValidationContext = CreateValidationContext(bindingContext, bindingContext.ModelMetadata); + return compositeValidator.Validate(modelValidationContext); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs index 005fcfe081..6418f63ad9 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs @@ -14,28 +14,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { private readonly ConcurrentDictionary _typeInfoCache = new ConcurrentDictionary(); - public IEnumerable GetMetadataForProperties(object container, Type containerType) + public IEnumerable GetMetadataForProperties(object container, [NotNull] Type containerType) { - if (containerType == null) - { - throw Error.ArgumentNull("containerType"); - } - return GetMetadataForPropertiesCore(container, containerType); } - public ModelMetadata GetMetadataForProperty(Func modelAccessor, Type containerType, string propertyName) + public ModelMetadata GetMetadataForProperty(Func modelAccessor, [NotNull] Type containerType, [NotNull] string propertyName) { - if (containerType == null) - { - throw Error.ArgumentNull("containerType"); - } - if (String.IsNullOrEmpty(propertyName)) + if (string.IsNullOrEmpty(propertyName)) { throw Error.ArgumentNullOrEmpty("propertyName"); } - TypeInformation typeInfo = GetTypeInformation(containerType); + var typeInfo = GetTypeInformation(containerType); PropertyInformation propertyInfo; if (!typeInfo.Properties.TryGetValue(propertyName, out propertyInfo)) { @@ -45,13 +36,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor); } - public ModelMetadata GetMetadataForType(Func modelAccessor, Type modelType) + public ModelMetadata GetMetadataForType(Func modelAccessor, [NotNull] Type modelType) { - if (modelType == null) - { - throw Error.ArgumentNull("modelType"); - } - TModelMetadata prototype = GetTypeInformation(modelType).Prototype; return CreateMetadataFromPrototype(prototype, modelAccessor); } @@ -68,10 +54,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private IEnumerable GetMetadataForPropertiesCore(object container, Type containerType) { - TypeInformation typeInfo = GetTypeInformation(containerType); - foreach (KeyValuePair kvp in typeInfo.Properties) + var typeInfo = GetTypeInformation(containerType); + foreach (var kvp in typeInfo.Properties) { - PropertyInformation propertyInfo = kvp.Value; + var propertyInfo = kvp.Value; Func modelAccessor = null; if (container != null) { @@ -96,24 +82,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private TypeInformation CreateTypeInformation(Type type, IEnumerable associatedAttributes) { - TypeInfo typeInfo = type.GetTypeInfo(); - IEnumerable attributes = typeInfo.GetCustomAttributes(); + var typeInfo = type.GetTypeInfo(); + var attributes = typeInfo.GetCustomAttributes(); if (associatedAttributes != null) { attributes = attributes.Concat(associatedAttributes); } - TypeInformation info = new TypeInformation + var info = new TypeInformation { Prototype = CreateMetadataPrototype(attributes, containerType: null, modelType: type, propertyName: null) }; - // TODO: Determine if we need this. TypeDescriptor does not exist in CoreCLR. - //ICustomTypeDescriptor typeDescriptor = TypeDescriptorHelper.Get(type); - //info.TypeDescriptor = typeDescriptor; - Dictionary properties = new Dictionary(); - - // TODO: Figure out if there's a better way to identify public non-static properties - foreach (PropertyInfo property in type.GetRuntimeProperties().Where(p => p.GetMethod.IsPublic && !p.GetMethod.IsStatic)) + var properties = new Dictionary(); + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { // Avoid re-generating a property descriptor if one has already been generated for the property name if (!properties.ContainsKey(property.Name)) @@ -128,21 +109,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private PropertyInformation CreatePropertyInformation(Type containerType, PropertyInfo property) { - PropertyInformation info = new PropertyInformation(); + var info = new PropertyInformation(); info.ValueAccessor = CreatePropertyValueAccessor(property); - info.Prototype = CreateMetadataPrototype(property.GetCustomAttributes().Cast(), containerType, property.PropertyType, property.Name); + info.Prototype = CreateMetadataPrototype(property.GetCustomAttributes(), + containerType, + property.PropertyType, + property.Name); return info; } private static Func CreatePropertyValueAccessor(PropertyInfo property) { - Type declaringType = property.DeclaringType; - TypeInfo declaringTypeInfo = declaringType.GetTypeInfo(); + var declaringType = property.DeclaringType; + var declaringTypeInfo = declaringType.GetTypeInfo(); if (declaringTypeInfo.IsVisible) { if (property.CanRead) { - MethodInfo getMethodInfo = property.GetMethod; + var getMethodInfo = property.GetMethod; if (getMethodInfo != null) { return CreateDynamicValueAccessor(getMethodInfo, declaringType, property.Name); @@ -161,10 +145,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { Contract.Assert(getMethodInfo != null && getMethodInfo.IsPublic && !getMethodInfo.IsStatic); - TypeInfo declaringTypeInfo = declaringType.GetTypeInfo(); - Type propertyType = getMethodInfo.ReturnType; - DynamicMethod dynamicMethod = new DynamicMethod("Get" + propertyName + "From" + declaringType.Name, typeof(object), new Type[] { typeof(object) }); - ILGenerator ilg = dynamicMethod.GetILGenerator(); + var declaringTypeInfo = declaringType.GetTypeInfo(); + var propertyType = getMethodInfo.ReturnType; + var dynamicMethod = new DynamicMethod("Get" + propertyName + "From" + declaringType.Name, + typeof(object), + new [] { typeof(object) }); + var ilg = dynamicMethod.GetILGenerator(); // Load the container onto the stack, convert from object => declaring type for the property ilg.Emit(OpCodes.Ldarg_0); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs index 8e4387c8fa..9a7fe1ffcd 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs @@ -1,7 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.AspNet.Abstractions; -using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -9,6 +10,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { private string _modelName; private ModelStateDictionary _modelState; + private Dictionary _propertyMetadata; + private ModelValidationNode _validationNode; public ModelBindingContext() { @@ -24,6 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding ValueProvider = bindingContext.ValueProvider; MetadataProvider = bindingContext.MetadataProvider; ModelBinder = bindingContext.ModelBinder; + ValidatorProviders = bindingContext.ValidatorProviders; HttpContext = bindingContext.HttpContext; } } @@ -102,6 +106,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding set; } + public IEnumerable ValidatorProviders + { + get; + set; + } + + public IDictionary PropertyMetadata + { + get + { + if (_propertyMetadata == null) + { + _propertyMetadata = ModelMetadata.Properties.ToDictionary(m => m.PropertyName, StringComparer.OrdinalIgnoreCase); + } + + return _propertyMetadata; + } + } + + public ModelValidationNode ValidationNode + { + get + { + if (_validationNode == null) + { + _validationNode = new ModelValidationNode(ModelMetadata, ModelName); + } + return _validationNode; + } + set { _validationNode = value; } + } + private void EnsureModelMetadata() { if (ModelMetadata == null) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelStateDictionary.cs index 59b46b0287..52b09b705f 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelStateDictionary.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelStateDictionary.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -12,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { } - public ModelStateDictionary([NotNull]ModelStateDictionary dictionary) + public ModelStateDictionary([NotNull] ModelStateDictionary dictionary) { foreach (var entry in dictionary) { @@ -35,7 +36,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetModelStateForKey(key).Errors.Add(errorMessage); } - private ModelState GetModelStateForKey([NotNull]string key) + public bool IsValidField([NotNull] string key) + { + // if the key is not found in the dictionary, we just say that it's valid (since there are no errors) + foreach (var entry in DictionaryHelper.FindKeysWithPrefix(this, key)) + { + if (entry.Value.Errors.Count != 0) + { + return false; + } + } + return true; + } + + private ModelState GetModelStateForKey([NotNull] string key) { ModelState modelState; if (!TryGetValue(key, out modelState)) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs index add3334250..b88036994e 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs @@ -58,6 +58,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return GetString("JQuerySyntaxMissingClosingBracket"); } + /// + /// Property '{0}' on type '{1}' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)]. + /// + internal static string MissingDataMemberIsRequired + { + get { return GetString("MissingDataMemberIsRequired"); } + } + + /// + /// Property '{0}' on type '{1}' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)]. + /// + internal static string FormatMissingDataMemberIsRequired(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MissingDataMemberIsRequired"), p0, p1); + } + + /// + /// The '{0}' property is required. + /// + internal static string MissingRequiredMember + { + get { return GetString("MissingRequiredMember"); } + } + + /// + /// The '{0}' property is required. + /// + internal static string FormatMissingRequiredMember(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MissingRequiredMember"), p0); + } + /// /// The value '{0}' is not valid for {1}. /// @@ -170,6 +202,70 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return GetString("ModelBindingContext_ModelMetadataMustBeSet"); } + /// + /// The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'. + /// + internal static string ValidatableObjectAdapter_IncompatibleType + { + get { return GetString("ValidatableObjectAdapter_IncompatibleType"); } + } + + /// + /// The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'. + /// + internal static string FormatValidatableObjectAdapter_IncompatibleType(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ValidatableObjectAdapter_IncompatibleType"), p0, p1); + } + + /// + /// Field '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on fields are not supported. Consider using a public property for validation instead. + /// + internal static string ValidationAttributeOnField + { + get { return GetString("ValidationAttributeOnField"); } + } + + /// + /// Field '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on fields are not supported. Consider using a public property for validation instead. + /// + internal static string FormatValidationAttributeOnField(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ValidationAttributeOnField"), p0, p1); + } + + /// + /// Non-public property '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on non-public properties are not supported. Consider using a public property for validation instead. + /// + internal static string ValidationAttributeOnNonPublicProperty + { + get { return GetString("ValidationAttributeOnNonPublicProperty"); } + } + + /// + /// Non-public property '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on non-public properties are not supported. Consider using a public property for validation instead. + /// + internal static string FormatValidationAttributeOnNonPublicProperty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ValidationAttributeOnNonPublicProperty"), p0, p1); + } + + /// + /// A value is required but was not present in the request. + /// + internal static string Validation_ValueNotFound + { + get { return GetString("Validation_ValueNotFound"); } + } + + /// + /// A value is required but was not present in the request. + /// + internal static string FormatValidation_ValueNotFound() + { + return GetString("Validation_ValueNotFound"); + } + /// /// The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information. /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx index 235940cfbf..201236556d 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx @@ -126,6 +126,12 @@ The key is invalid JQuery syntax because it is missing a closing bracket. + + Property '{0}' on type '{1}' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)]. + + + The '{0}' property is required. + The value '{0}' is not valid for {1}. @@ -147,6 +153,18 @@ The ModelMetadata property must be set before accessing this property. + + The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'. + + + Field '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on fields are not supported. Consider using a public property for validation instead. + + + Non-public property '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on non-public properties are not supported. Consider using a public property for validation instead. + + + A value is required but was not present in the request. + The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/AssociatedValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/AssociatedValidatorProvider.cs new file mode 100644 index 0000000000..e43f4fef0b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/AssociatedValidatorProvider.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public abstract class AssociatedValidatorProvider : IModelValidatorProvider + { + public IEnumerable GetValidators(ModelMetadata metadata) + { + if (metadata == null) + { + throw new ArgumentNullException("metadata"); + } + + if (metadata.ContainerType != null && !string.IsNullOrEmpty(metadata.PropertyName)) + { + return GetValidatorsForProperty(metadata); + } + + return GetValidatorsForType(metadata); + } + + protected abstract IEnumerable GetValidators(ModelMetadata metadata, + IEnumerable attributes); + + private IEnumerable GetValidatorsForProperty(ModelMetadata metadata) + { + var propertyName = metadata.PropertyName; + var property = metadata.ContainerType + .GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (property == null) + { + throw new ArgumentException( + Resources.FormatCommon_PropertyNotFound( + metadata.ContainerType.FullName, + metadata.PropertyName), + "metadata"); + } + + var attributes = property.GetCustomAttributes(); + return GetValidators(metadata, attributes); + } + + private IEnumerable GetValidatorsForType(ModelMetadata metadata) + { + var attributes = metadata.ModelType + .GetTypeInfo() + .GetCustomAttributes(); + return GetValidators(metadata, attributes); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompositeModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompositeModelValidator.cs new file mode 100644 index 0000000000..33e4311df3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompositeModelValidator.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class CompositeModelValidator : IModelValidator + { + private readonly IEnumerable _validators; + + public CompositeModelValidator(IEnumerable validators) + { + _validators = validators; + } + + public bool IsRequired + { + get { return false; } + } + + public IEnumerable Validate(ModelValidationContext context) + { + var propertiesValid = true; + var metadata = context.ModelMetadata; + + foreach (var propertyMetadata in metadata.Properties) + { + var propertyContext = new ModelValidationContext(context, propertyMetadata); + + foreach (var propertyValidator in _validators) + { + foreach (var validationResult in propertyValidator.Validate(propertyContext)) + { + propertiesValid = false; + yield return CreateSubPropertyResult(propertyMetadata, validationResult); + } + } + } + + if (propertiesValid) + { + foreach (var typeValidator in _validators) + { + foreach (var typeResult in typeValidator.Validate(context)) + { + yield return typeResult; + } + } + } + } + + private static ModelValidationResult CreateSubPropertyResult(ModelMetadata propertyMetadata, ModelValidationResult propertyResult) + { + return new ModelValidationResult(propertyMetadata.PropertyName + '.' + propertyResult.MemberName, + propertyResult.Message); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs new file mode 100644 index 0000000000..3108e49146 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class DataAnnotationsModelValidator : IModelValidator + { + public DataAnnotationsModelValidator(ValidationAttribute attribute) + { + if (attribute == null) + { + throw new ArgumentNullException("attribute"); + } + + Attribute = attribute; + } + + public ValidationAttribute Attribute { get; private set; } + + public bool IsRequired + { + get { return Attribute is RequiredAttribute; } + } + + public IEnumerable Validate(ModelValidationContext validationContext) + { + var metadata = validationContext.ModelMetadata; + var memberName = metadata.PropertyName ?? metadata.ModelType.Name; + var context = new ValidationContext(metadata.Model) + { + DisplayName = metadata.GetDisplayName(), + MemberName = memberName + }; + + var result = Attribute.GetValidationResult(metadata.Model, context); + if (result != ValidationResult.Success) + { + // ModelValidationResult.MemberName is used by invoking validators (such as ModelValidator) to + // construct the ModelKey for ModelStateDictionary. When validating at type level we want to append the + // returned MemberNames if specified (e.g. person.Address.FirstName). For property validation, the + // ModelKey can be constructed using the ModelMetadata and we should ignore MemberName (we don't want + // (person.Name.Name). However the invoking validator does not have a way to distinguish between these two + // cases. Consequently we'll only set MemberName if this validation returns a MemberName that is different + // from the property being validated. + + var errorMemberName = result.MemberNames.FirstOrDefault(); + if (string.Equals(errorMemberName, memberName, StringComparison.Ordinal)) + { + errorMemberName = null; + } + + var validationResult = new ModelValidationResult(errorMemberName, result.ErrorMessage); + return new ModelValidationResult[] { validationResult }; + } + + return Enumerable.Empty(); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs new file mode 100644 index 0000000000..a3ba7a533f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// An implementation of which providers validators + /// for attributes which derive from . It also provides + /// a validator for types which implement . To support + /// client side validation, you can either register adapters through the static methods + /// on this class, or by having your validation attributes implement + /// . The logic to support IClientValidatable + /// is implemented in . + /// + public class DataAnnotationsModelValidatorProvider : AssociatedValidatorProvider + { + // A factory for validators based on ValidationAttribute + private delegate IModelValidator DataAnnotationsModelValidationFactory(ValidationAttribute attribute); + + // A factory for validators based on IValidatableObject + private delegate IModelValidator DataAnnotationsValidatableObjectAdapterFactory(); + + private static bool _addImplicitRequiredAttributeForValueTypes = true; + + // Factories for validation attributes + private static DataAnnotationsModelValidationFactory DefaultAttributeFactory = + (attribute) => new DataAnnotationsModelValidator(attribute); + + private static Dictionary AttributeFactories = + new Dictionary(); + +#if NET45 + // Factories for IValidatableObject models + private static DataAnnotationsValidatableObjectAdapterFactory DefaultValidatableFactory = + () => new ValidatableObjectAdapter(); +#endif + + private static Dictionary ValidatableFactories = + new Dictionary(); + + public static bool AddImplicitRequiredAttributeForValueTypes + { + get { return _addImplicitRequiredAttributeForValueTypes; } + set { _addImplicitRequiredAttributeForValueTypes = value; } + } + + protected override IEnumerable GetValidators(ModelMetadata metadata, IEnumerable attributes) + { + var results = new List(); + + // Produce a validator for each validation attribute we find + foreach (var attribute in attributes.OfType()) + { + DataAnnotationsModelValidationFactory factory; + if (!AttributeFactories.TryGetValue(attribute.GetType(), out factory)) + { + factory = DefaultAttributeFactory; + } + results.Add(factory(attribute)); + } + +#if NET45 + // Produce a validator if the type supports IValidatableObject + if (typeof(IValidatableObject).IsAssignableFrom(metadata.ModelType)) + { + DataAnnotationsValidatableObjectAdapterFactory factory; + if (!ValidatableFactories.TryGetValue(metadata.ModelType, out factory)) + { + factory = DefaultValidatableFactory; + } + results.Add(factory()); + } +#endif + + return results; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs new file mode 100644 index 0000000000..5784518422 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// This provides a required ModelValidator for members marked as [DataMember(IsRequired=true)]. + /// + public class DataMemberModelValidatorProvider : AssociatedValidatorProvider + { + protected override IEnumerable GetValidators(ModelMetadata metadata, + IEnumerable attributes) + { + // Types cannot be required; only properties can + if (metadata.ContainerType == null || string.IsNullOrEmpty(metadata.PropertyName)) + { + return Enumerable.Empty(); + } + + if (IsRequiredDataMember(metadata.ContainerType, attributes)) + { + return new[] { new RequiredMemberModelValidator() }; + } + + return Enumerable.Empty(); + } + + internal static bool IsRequiredDataMember(Type containerType, IEnumerable attributes) + { + var dataMemberAttribute = attributes.OfType() + .FirstOrDefault(); + if (dataMemberAttribute != null) + { + // isDataContract == true iff the container type has at least one DataContractAttribute + bool isDataContract = containerType.GetTypeInfo() + .GetCustomAttributes() + .OfType() + .Any(); + if (isDataContract && dataMemberAttribute.IsRequired) + { + return true; + } + } + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ErrorModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ErrorModelValidator.cs new file mode 100644 index 0000000000..abe165683f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ErrorModelValidator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// A to represent an error. This validator will always throw an exception regardless + /// of the actual model value. + /// This is used to perform meta-validation - that is to verify the validation attributes make sense. + /// + public class ErrorModelValidator : IModelValidator + { + private readonly string _errorMessage; + + public ErrorModelValidator([NotNull] string errorMessage) + { + _errorMessage = errorMessage; + } + + public bool IsRequired { get { return false; } } + + public IEnumerable Validate(ModelValidationContext context) + { + throw new InvalidOperationException(_errorMessage); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IModelValidator.cs new file mode 100644 index 0000000000..ab043873c8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IModelValidator.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public interface IModelValidator + { + bool IsRequired { get; } + + IEnumerable Validate(ModelValidationContext context); + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IModelValidatorProvider.cs new file mode 100644 index 0000000000..02ab6b1cfc --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IModelValidatorProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public interface IModelValidatorProvider + { + IEnumerable GetValidators(ModelMetadata metadata); + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/InvalidModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/InvalidModelValidatorProvider.cs new file mode 100644 index 0000000000..28e0fd88ae --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/InvalidModelValidatorProvider.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class InvalidModelValidatorProvider : AssociatedValidatorProvider + { + protected override IEnumerable GetValidators(ModelMetadata metadata, + IEnumerable attributes) + { + if (metadata.ContainerType == null || String.IsNullOrEmpty(metadata.PropertyName)) + { + // Validate that the type's fields and nonpublic properties don't have any validation attributes on them + // Validation only runs against public properties + var type = metadata.ModelType; + var nonPublicProperties = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var nonPublicProperty in nonPublicProperties) + { + if (nonPublicProperty.GetCustomAttributes(typeof(ValidationAttribute), inherit: true).Any()) + { + yield return new ErrorModelValidator(Resources.FormatValidationAttributeOnNonPublicProperty(nonPublicProperty.Name, type)); + } + } + + var allFields = metadata.ModelType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var field in allFields) + { + if (field.GetCustomAttributes(typeof(ValidationAttribute), inherit: true).Any()) + { + yield return new ErrorModelValidator(Resources.FormatValidationAttributeOnField(field.Name, type)); + } + } + } + else + { + // Validate that value-typed properties marked as [Required] are also marked as [DataMember(IsRequired=true)] + // Certain formatters may not recognize a member as required if it's marked as [Required] but not [DataMember(IsRequired=true)] + // This is not a problem for reference types because [Required] will still cause a model error to be raised after a null value is deserialized + if (metadata.ModelType.GetTypeInfo().IsValueType && attributes.Any(attribute => attribute is RequiredAttribute)) + { + if (!DataMemberModelValidatorProvider.IsRequiredDataMember(metadata.ContainerType, attributes)) + { + yield return new ErrorModelValidator(Resources.FormatMissingDataMemberIsRequired(metadata.PropertyName, metadata.ContainerType)); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidatedEventArgs.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidatedEventArgs.cs new file mode 100644 index 0000000000..22e5881e7d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidatedEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public sealed class ModelValidatedEventArgs : EventArgs + { + public ModelValidatedEventArgs([NotNull] ModelValidationContext validationContext, + [NotNull] ModelValidationNode parentNode) + { + ValidationContext = validationContext; + ParentNode = parentNode; + } + + public ModelValidationContext ValidationContext { get; private set; } + + public ModelValidationNode ParentNode { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidatingEventArgs.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidatingEventArgs.cs new file mode 100644 index 0000000000..78ab557520 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidatingEventArgs.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public sealed class ModelValidatingEventArgs : CancelEventArgs + { + public ModelValidatingEventArgs([NotNull] ModelValidationContext validationContext, + [NotNull] ModelValidationNode parentNode) + { + ValidationContext = validationContext; + ParentNode = parentNode; + } + + public ModelValidationContext ValidationContext { get; private set; } + + public ModelValidationNode ParentNode { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs new file mode 100644 index 0000000000..65d9daa814 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class ModelValidationContext + { + public ModelValidationContext([NotNull] ModelMetadata metadata, + [NotNull] ModelStateDictionary modelState, + [NotNull] IModelMetadataProvider metadataProvider, + [NotNull] IEnumerable validatorProviders) + { + ModelMetadata = metadata; + ModelState = modelState; + MetadataProvider = metadataProvider; + ValidatorProviders = validatorProviders; + } + + public ModelValidationContext([NotNull] ModelValidationContext parentContext, + [NotNull] ModelMetadata metadata) + { + ModelMetadata = metadata; + ContainerMetadata = parentContext.ModelMetadata; + ModelState = parentContext.ModelState; + MetadataProvider = parentContext.MetadataProvider; + ValidatorProviders = parentContext.ValidatorProviders; + } + + public ModelMetadata ModelMetadata { get; private set; } + + public ModelMetadata ContainerMetadata { get; private set; } + + public ModelStateDictionary ModelState { get; private set; } + + public IModelMetadataProvider MetadataProvider { get; private set; } + + public IEnumerable ValidatorProviders { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationNode.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationNode.cs new file mode 100644 index 0000000000..8d3686c438 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationNode.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Mvc.ModelBinding.Internal; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class ModelValidationNode + { + private readonly List _childNodes; + + public ModelValidationNode(ModelMetadata modelMetadata, string modelStateKey) + : this(modelMetadata, modelStateKey, null) + { + } + + public ModelValidationNode([NotNull] ModelMetadata modelMetadata, + [NotNull] string modelStateKey, + IEnumerable childNodes) + { + ModelMetadata = modelMetadata; + ModelStateKey = modelStateKey; + _childNodes = (childNodes != null) ? childNodes.ToList() : new List(); + } + + public event EventHandler Validated; + + public event EventHandler Validating; + + public ICollection ChildNodes + { + get { return _childNodes; } + } + + public ModelMetadata ModelMetadata { get; private set; } + + public string ModelStateKey { get; private set; } + + public bool ValidateAllProperties { get; set; } + + public bool SuppressValidation { get; set; } + + public void CombineWith(ModelValidationNode otherNode) + { + if (otherNode != null && !otherNode.SuppressValidation) + { + Validated += otherNode.Validated; + Validating += otherNode.Validating; + var otherChildNodes = otherNode._childNodes; + for (var i = 0; i < otherChildNodes.Count; i++) + { + var childNode = otherChildNodes[i]; + _childNodes.Add(childNode); + } + } + } + + private void OnValidated(ModelValidatedEventArgs e) + { + if (Validated != null) + { + Validated(this, e); + } + } + + private void OnValidating(ModelValidatingEventArgs e) + { + if (Validating != null) + { + Validating(this, e); + } + } + + private object TryConvertContainerToMetadataType(ModelValidationNode parentNode) + { + if (parentNode != null) + { + var containerInstance = parentNode.ModelMetadata.Model; + if (containerInstance != null) + { + var expectedContainerType = ModelMetadata.ContainerType; + if (expectedContainerType != null) + { + if (expectedContainerType.IsCompatibleWith(containerInstance)) + { + return containerInstance; + } + } + } + } + + return null; + } + + public void Validate(ModelValidationContext validationContext) + { + Validate(validationContext, parentNode: null); + } + + public void Validate(ModelValidationContext validationContext, ModelValidationNode parentNode) + { + if (validationContext == null) + { + throw Error.ArgumentNull("validationContext"); + } + + if (SuppressValidation) + { + // no-op + return; + } + + // pre-validation steps + var validatingEventArgs = new ModelValidatingEventArgs(validationContext, parentNode); + OnValidating(validatingEventArgs); + if (validatingEventArgs.Cancel) + { + return; + } + + ValidateChildren(validationContext); + ValidateThis(validationContext, parentNode); + + // post-validation steps + var validatedEventArgs = new ModelValidatedEventArgs(validationContext, parentNode); + OnValidated(validatedEventArgs); + } + + private void ValidateChildren(ModelValidationContext validationContext) + { + for (var i = 0; i < _childNodes.Count; i++) + { + var child = _childNodes[i]; + var childValidationContext = new ModelValidationContext(validationContext, child.ModelMetadata); + child.Validate(childValidationContext, this); + } + + if (ValidateAllProperties) + { + ValidateProperties(validationContext); + } + } + + private void ValidateProperties(ModelValidationContext validationContext) + { + var modelState = validationContext.ModelState; + + var model = ModelMetadata.Model; + var updatedMetadata = validationContext.MetadataProvider.GetMetadataForType(() => model, ModelMetadata.ModelType); + + foreach (var propertyMetadata in updatedMetadata.Properties) + { + // Only want to add errors to ModelState if something doesn't already exist for the property node, + // else we could end up with duplicate or irrelevant error messages. + var propertyKeyRoot = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, propertyMetadata.PropertyName); + + if (modelState.IsValidField(propertyKeyRoot)) + { + var propertyValidators = GetValidators(validationContext, propertyMetadata); + var propertyValidationContext = new ModelValidationContext(validationContext, propertyMetadata); + foreach (var propertyValidator in propertyValidators) + { + foreach (var propertyResult in propertyValidator.Validate(propertyValidationContext)) + { + var thisErrorKey = ModelBindingHelper.CreatePropertyModelName(propertyKeyRoot, propertyResult.MemberName); + modelState.AddModelError(thisErrorKey, propertyResult.Message); + } + } + } + } + } + + private void ValidateThis(ModelValidationContext validationContext, ModelValidationNode parentNode) + { + var modelState = validationContext.ModelState; + if (!modelState.IsValidField(ModelStateKey)) + { + return; // short-circuit + } + + // If the Model at the current node is null and there is no parent, we cannot validate, and the + // DataAnnotationsModelValidator will throw. So we intercept here to provide a catch-all value-required + // validation error + if (parentNode == null && ModelMetadata.Model == null) + { + var trueModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, ModelMetadata.GetDisplayName()); + modelState.AddModelError(trueModelStateKey, Resources.Validation_ValueNotFound); + return; + } + + var container = TryConvertContainerToMetadataType(parentNode); + var validators = GetValidators(validationContext, ModelMetadata).ToArray(); + for (var i = 0; i < validators.Length; i++) + { + var validator = validators[i]; + foreach (var validationResult in validator.Validate(validationContext)) + { + var trueModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, validationResult.MemberName); + modelState.AddModelError(trueModelStateKey, validationResult.Message); + } + } + } + + private static IEnumerable GetValidators(ModelValidationContext validationContext, ModelMetadata metadata) + { + return validationContext.ValidatorProviders.SelectMany(vp => vp.GetValidators(metadata)); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationResult.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationResult.cs new file mode 100644 index 0000000000..667811ed52 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationResult.cs @@ -0,0 +1,16 @@ + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class ModelValidationResult + { + public ModelValidationResult(string memberName, string message) + { + MemberName = memberName ?? string.Empty; + Message = message ?? string.Empty; + } + + public string MemberName { get; private set; } + + public string Message { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/RequiredMemberModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/RequiredMemberModelValidator.cs new file mode 100644 index 0000000000..e61197004b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/RequiredMemberModelValidator.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class RequiredMemberModelValidator : IModelValidator + { + public bool IsRequired + { + get { return true; } + } + + public IEnumerable Validate(ModelValidationContext context) + { + return Enumerable.Empty(); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ValidatableObjectAdapter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ValidatableObjectAdapter.cs new file mode 100644 index 0000000000..1fbc9a8b15 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ValidatableObjectAdapter.cs @@ -0,0 +1,60 @@ +#if NET45 +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class ValidatableObjectAdapter : IModelValidator + { + public bool IsRequired + { + get { return false; } + } + + public IEnumerable Validate(ModelValidationContext context) + { + var model = context.ModelMetadata.Model; + if (model == null) + { + return Enumerable.Empty(); + } + + var validatable = model as IValidatableObject; + if (validatable == null) + { + var message = Resources.FormatValidatableObjectAdapter_IncompatibleType( + typeof(IValidatableObject).Name, + model.GetType()); + + throw new InvalidOperationException(message); + } + + var validationContext = new ValidationContext(validatable, serviceProvider: null, items: null); + return ConvertResults(validatable.Validate(validationContext)); + } + + private IEnumerable ConvertResults(IEnumerable results) + { + foreach (var result in results) + { + if (result != ValidationResult.Success) + { + if (result.MemberNames == null || !result.MemberNames.Any()) + { + yield return new ModelValidationResult(memberName: null, message: result.ErrorMessage); + } + else + { + foreach (var memberName in result.MemberNames) + { + yield return new ModelValidationResult(memberName, result.ErrorMessage); + } + } + } + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json index 1d6640c009..5dce944a29 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json @@ -2,12 +2,17 @@ "version": "0.1-alpha-*", "dependencies": { "Common": "", - "Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*", + "Microsoft.AspNet.DependencyInjection": "0.1-alpha-*", "Microsoft.AspNet.Abstractions": "0.1-alpha-*", "Newtonsoft.Json": "5.0.8" }, "configurations": { - "net45": {}, + "net45": { + "dependencies": { + "System.ComponentModel.DataAnnotations": "", + "System.Runtime.Serialization": "" + } + }, "k10": { "dependencies": { "System.Collections": "4.0.0.0", @@ -22,12 +27,15 @@ "System.Reflection": "4.0.10.0", "System.Reflection.Emit.ILGeneration": "4.0.0.0", "System.Reflection.Emit.Lightweight": "4.0.0.0", + "System.Reflection.Compatibility": "4.0.0.0", "System.Reflection.Extensions": "4.0.0.0", "System.Reflection.Primitives": "4.0.0.0", "System.Resources.ResourceManager": "4.0.0.0", "System.Runtime": "4.0.20.0", "System.Runtime.Extensions": "4.0.10.0", - "System.Threading.Tasks": "4.0.0.0" + "System.Threading.Tasks": "4.0.0.0", + "System.Runtime.Serialization.Primitives": "4.0.0.0", + "Microsoft.ComponentModel.DataAnnotations": "0.1-alpha-*" } } } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index ef2d423c2a..d7e2cad219 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -61,10 +61,13 @@ namespace Microsoft.AspNet.Mvc yield return describe.Transient(); yield return describe.Transient(); + yield return describe.Transient(); + yield return describe.Transient, NestedProviderManager>(); yield return describe.Transient, DefaultFilterProvider>(); - yield return describe.Transient(); + yield return describe.Singleton(); + yield return describe.Singleton(); } } } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CollectionModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CollectionModelBinderTest.cs index bcaecbf3d6..5beebd1680 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CollectionModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CollectionModelBinderTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Globalization; +using System.Linq; using Moq; using Xunit; @@ -20,12 +21,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var binder = new CollectionModelBinder(); // Act - List boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, new[] { "foo", "bar", "baz" }); + var boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, new[] { "foo", "bar", "baz" }); // Assert Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray()); - // TODO: Validation - // Assert.Equal(new[] { "someName[foo]", "someName[baz]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray()); + Assert.Equal(new[] { "someName[foo]", "someName[baz]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray()); } [Fact] @@ -42,12 +42,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var binder = new CollectionModelBinder(); // Act - List boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null); + var boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null); // Assert Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray()); - // TODO: Validation - // Assert.Equal(new[] { "someName[0]", "someName[1]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray()); + Assert.Equal(new[] { "someName[0]", "someName[1]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray()); } [Fact] @@ -97,7 +96,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var binder = new CollectionModelBinder(); // Act - List boundCollection = binder.BindSimpleCollection(bindingContext: null, rawValue: new object[0], culture: null); + var boundCollection = binder.BindSimpleCollection(bindingContext: null, rawValue: new object[0], culture: null); // Assert Assert.NotNull(boundCollection); @@ -111,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var binder = new CollectionModelBinder(); // Act - List boundCollection = binder.BindSimpleCollection(bindingContext: null, rawValue: null, culture: null); + var boundCollection = binder.BindSimpleCollection(bindingContext: null, rawValue: null, culture: null); // Assert Assert.Null(boundCollection); @@ -124,25 +123,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var culture = CultureInfo.GetCultureInfo("fr-FR"); var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider()); - // TODO: Validation - // ModelValidationNode childValidationNode = null; + ModelValidationNode childValidationNode = null; Mock.Get(bindingContext.ModelBinder) .Setup(o => o.BindModel(It.IsAny())) .Returns((ModelBindingContext mbc) => { Assert.Equal("someName", mbc.ModelName); - // childValidationNode = mbc.ValidationNode; + childValidationNode = mbc.ValidationNode; mbc.Model = 42; return true; }); var modelBinder = new CollectionModelBinder(); // Act - List boundCollection = modelBinder.BindSimpleCollection(bindingContext, new int[1], culture); + var boundCollection = modelBinder.BindSimpleCollection(bindingContext, new int[1], culture); // Assert Assert.Equal(new[] { 42 }, boundCollection.ToArray()); - // Assert.Equal(new[] { childValidationNode }, bindingContext.ValidationNode.ChildNodes.ToArray()); + Assert.Equal(new[] { childValidationNode }, bindingContext.ValidationNode.ChildNodes.ToArray()); } private static ModelBindingContext GetModelBindingContext(IValueProvider valueProvider) diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ComplexModelDtoResultTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ComplexModelDtoResultTest.cs index d8d0cf0e53..e959b64f33 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ComplexModelDtoResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ComplexModelDtoResultTest.cs @@ -4,36 +4,34 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { public class ComplexModelDtoResultTest { - // TODO: Validation - ////[Fact] - ////public void Constructor_ThrowsIfValidationNodeIsNull() - ////{ - //// // Act & assert - //// ExceptionAssert.ThrowsArgumentNull( - //// () => new ComplexModelDtoResult("some string"), - //// "validationNode"); - ////} + [Fact] + public void Constructor_ThrowsIfValidationNodeIsNull() + { + // Act & assert + ExceptionAssert.ThrowsArgumentNull( + () => new ComplexModelDtoResult("some string", validationNode: null), + "validationNode"); + } - // TODO: Validation - //[Fact] - //public void Constructor_SetsProperties() - //{ - // // Arrange - // ModelValidationNode validationNode = GetValidationNode(); + [Fact] + public void Constructor_SetsProperties() + { + // Arrange + var validationNode = GetValidationNode(); - // // Act - // ComplexModelDtoResult result = new ComplexModelDtoResult("some string", validationNode); + // Act + var result = new ComplexModelDtoResult("some string", validationNode); - // // Assert - // Assert.Equal("some string", result.Model); - // Assert.Equal(validationNode, result.ValidationNode); - //} + // Assert + Assert.Equal("some string", result.Model); + Assert.Equal(validationNode, result.ValidationNode); + } - //private static ModelValidationNode GetValidationNode() - //{ - // EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider(); - // ModelMetadata metadata = provider.GetMetadataForType(null, typeof(object)); - // return new ModelValidationNode(metadata, "someKey"); - //} + private static ModelValidationNode GetValidationNode() + { + var provider = new EmptyModelMetadataProvider(); + var metadata = provider.GetMetadataForType(null, typeof(object)); + return new ModelValidationNode(metadata, "someKey"); } } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs index d6a09d58ec..aaa80bb5d5 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Moq; using Xunit; @@ -12,22 +13,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test public void BindModel_SuccessfulBind_RunsValidationAndReturnsModel() { // Arrange - bool validationCalled = false; + var validationCalled = false; - ModelBindingContext bindingContext = new ModelBindingContext + var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)), ModelName = "someName", - //ModelState = executionContext.Controller.ViewData.ModelState, - //PropertyFilter = _ => true, + ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someName", "dummyValue" } - } + }, + ValidatorProviders = Enumerable.Empty() }; - Mock mockIntBinder = new Mock(); + var mockIntBinder = new Mock(); mockIntBinder .Setup(o => o.BindModel(It.IsAny())) .Returns( @@ -38,22 +39,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test Assert.Same(bindingContext.ValueProvider, context.ValueProvider); context.Model = 42; - // TODO: Validation - // mbc.ValidationNode.Validating += delegate { validationCalled = true; }; + bindingContext.ValidationNode.Validating += delegate { validationCalled = true; }; return true; }); - //binderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */); - IModelBinder shimBinder = new CompositeModelBinder(mockIntBinder.Object); + var shimBinder = new CompositeModelBinder(mockIntBinder.Object); // Act - bool isBound = shimBinder.BindModel(bindingContext); + var isBound = shimBinder.BindModel(bindingContext); // Assert Assert.True(isBound); Assert.Equal(42, bindingContext.Model); - // TODO: Validation - // Assert.True(validationCalled); + + Assert.True(validationCalled); Assert.True(bindingContext.ModelState.IsValid); } @@ -61,23 +60,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test public void BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel() { // Arrange - bool validationCalled = false; - List expectedModel = new List { 1, 2, 3, 4, 5 }; + var validationCalled = false; + var expectedModel = new List { 1, 2, 3, 4, 5 }; - ModelBindingContext bindingContext = new ModelBindingContext + var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List)), ModelName = "someName", - //ModelState = executionContext.Controller.ViewData.ModelState, - //PropertyFilter = _ => true, + ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someOtherName", "dummyValue" } - } + }, + ValidatorProviders = Enumerable.Empty() }; - Mock mockIntBinder = new Mock(); + var mockIntBinder = new Mock(); mockIntBinder .Setup(o => o.BindModel(It.IsAny())) .Returns( @@ -93,12 +92,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider); mbc.Model = expectedModel; - // TODO: Validation - // mbc.ValidationNode.Validating += delegate { validationCalled = true; }; + mbc.ValidationNode.Validating += delegate { validationCalled = true; }; return true; }); - //binderProviders.RegisterBinderForType(typeof(List), mockIntBinder.Object, false /* suppressPrefixCheck */); IModelBinder shimBinder = new CompositeModelBinder(mockIntBinder.Object); // Act @@ -107,36 +104,34 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test // Assert Assert.True(isBound); Assert.Equal(expectedModel, bindingContext.Model); - // TODO: Validation - // Assert.True(validationCalled); - // Assert.True(bindingContext.ModelState.IsValid); + Assert.True(validationCalled); + Assert.True(bindingContext.ModelState.IsValid); } [Fact] public void BindModel_UnsuccessfulBind_BinderFails_ReturnsNull() { // Arrange - Mock mockListBinder = new Mock(); + var mockListBinder = new Mock(); mockListBinder.Setup(o => o.BindModel(It.IsAny())) .Returns(false) .Verifiable(); - IModelBinder shimBinder = (IModelBinder)mockListBinder.Object; + var shimBinder = (IModelBinder)mockListBinder.Object; - ModelBindingContext bindingContext = new ModelBindingContext + var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = false, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List)), }; // Act - bool isBound = shimBinder.BindModel(bindingContext); + var isBound = shimBinder.BindModel(bindingContext); // Assert Assert.False(isBound); Assert.Null(bindingContext.Model); - // TODO: Validation - // Assert.True(bindingContext.ModelState.IsValid); + Assert.True(bindingContext.ModelState.IsValid); mockListBinder.Verify(); } @@ -145,17 +140,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { // Arrange var innerBinder = Mock.Of(); - CompositeModelBinder shimBinder = new CompositeModelBinder(innerBinder); + var shimBinder = new CompositeModelBinder(innerBinder); - ModelBindingContext bindingContext = new ModelBindingContext + var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)), - //ModelState = executionContext.Controller.ViewData.ModelState + ModelState = new ModelStateDictionary() }; // Act - bool isBound = shimBinder.BindModel(bindingContext); + var isBound = shimBinder.BindModel(bindingContext); // Assert Assert.False(isBound); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/KeyValuePairModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/KeyValuePairModelBinderTest.cs index 0d3c8c1723..eff5a303cc 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/KeyValuePairModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/KeyValuePairModelBinderTest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Moq; using Xunit; @@ -11,7 +12,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { // Arrange var valueProvider = new SimpleHttpValueProvider(); - ModelBindingContext bindingContext = GetBindingContext(valueProvider, Mock.Of()); + var bindingContext = GetBindingContext(valueProvider, Mock.Of()); var binder = new KeyValuePairModelBinder(); // Act @@ -20,8 +21,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test // Assert Assert.False(retVal); Assert.Null(bindingContext.Model); - // TODO: Validation - // Assert.Empty(bindingContext.ValidationNode.ChildNodes); + Assert.Empty(bindingContext.ValidationNode.ChildNodes); } [Fact] @@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { // Arrange var valueProvider = new SimpleHttpValueProvider(); - ModelBindingContext bindingContext = GetBindingContext(valueProvider); + var bindingContext = GetBindingContext(valueProvider); var binder = new KeyValuePairModelBinder(); // Act @@ -38,28 +38,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test // Assert Assert.True(retVal); Assert.Null(bindingContext.Model); - // TODO: Validation - // Assert.Equal(new[] { "someName.key" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray()); + Assert.Equal(new[] { "someName.key" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray()); } [Fact] public void BindModel_SubBindingSucceeds() { // Arrange - IModelBinder innerBinder = new CompositeModelBinder(CreateStringBinder(), CreateIntBinder()); + var innerBinder = new CompositeModelBinder(CreateStringBinder(), CreateIntBinder()); var valueProvider = new SimpleHttpValueProvider(); - ModelBindingContext bindingContext = GetBindingContext(valueProvider, innerBinder); + var bindingContext = GetBindingContext(valueProvider, innerBinder); var binder = new KeyValuePairModelBinder(); // Act - bool retVal = binder.BindModel(bindingContext); + var retVal = binder.BindModel(bindingContext); // Assert Assert.True(retVal); Assert.Equal(new KeyValuePair(42, "some-value"), bindingContext.Model); - // TODO: Validation - // Assert.Equal(new[] { "someName.key", "someName.value" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray()); + Assert.Equal(new[] { "someName.key", "someName.value" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray()); } [Fact] @@ -71,13 +69,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test // Act int model; - bool retVal = binder.TryBindStrongModel(bindingContext, "key", out model); + var retVal = binder.TryBindStrongModel(bindingContext, "key", out model); // Assert Assert.True(retVal); Assert.Equal(42, model); - // TODO: Validation - // Assert.Single(bindingContext.ValidationNode.ChildNodes); + Assert.Single(bindingContext.ValidationNode.ChildNodes); Assert.Empty(bindingContext.ModelState); } @@ -100,33 +97,33 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test // Act int model; - bool retVal = binder.TryBindStrongModel(bindingContext, "key", out model); + var retVal = binder.TryBindStrongModel(bindingContext, "key", out model); // Assert Assert.True(retVal); Assert.Equal(default(int), model); - // TODO: Validation - // Assert.Single(bindingContext.ValidationNode.ChildNodes); + Assert.Single(bindingContext.ValidationNode.ChildNodes); Assert.Empty(bindingContext.ModelState); } private static ModelBindingContext GetBindingContext(IValueProvider valueProvider, IModelBinder innerBinder = null) { var metataProvider = new EmptyModelMetadataProvider(); - ModelBindingContext bindingContext = new ModelBindingContext + var bindingContext = new ModelBindingContext { ModelMetadata = metataProvider.GetMetadataForType(null, typeof(KeyValuePair)), ModelName = "someName", ValueProvider = valueProvider, ModelBinder = innerBinder ?? CreateIntBinder(), - MetadataProvider = metataProvider + MetadataProvider = metataProvider, + ValidatorProviders = Enumerable.Empty() }; return bindingContext; } private static IModelBinder CreateIntBinder() { - Mock mockIntBinder = new Mock(); + var mockIntBinder = new Mock(); mockIntBinder .Setup(o => o.BindModel(It.IsAny())) .Returns((ModelBindingContext mbc) => @@ -143,8 +140,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test private static IModelBinder CreateStringBinder() { - Mock mockIntBinder = new Mock(); - mockIntBinder + var mockStringBinder = new Mock(); + mockStringBinder .Setup(o => o.BindModel(It.IsAny())) .Returns((ModelBindingContext mbc) => { @@ -155,7 +152,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test } return false; }); - return mockIntBinder.Object; + return mockStringBinder.Object; } } } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ModelBindingContextTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ModelBindingContextTest.cs index b1df4a5b5a..4dc4a3522f 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ModelBindingContextTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ModelBindingContextTest.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test public void CopyConstructor() { // Arrange - ModelBindingContext originalBindingContext = new ModelBindingContext + var originalBindingContext = new ModelBindingContext { ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)), ModelName = "theName", @@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test }; // Act - ModelBindingContext newBindingContext = new ModelBindingContext(originalBindingContext); + var newBindingContext = new ModelBindingContext(originalBindingContext); // Assert Assert.Null(newBindingContext.ModelMetadata); @@ -31,7 +31,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test public void ModelProperty_ThrowsIfModelMetadataDoesNotExist() { // Arrange - ModelBindingContext bindingContext = new ModelBindingContext(); + var bindingContext = new ModelBindingContext(); // Act & assert ExceptionAssert.Throws( @@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test public void ModelAndModelTypeAreFedFromModelMetadata() { // Act - ModelBindingContext bindingContext = new ModelBindingContext + var bindingContext = new ModelBindingContext { ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)) }; @@ -53,38 +53,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test Assert.Equal(typeof(int), bindingContext.ModelType); } - // TODO: Validation - //[Fact] - //public void ValidationNodeProperty() - //{ - // // Act - // ModelBindingContext bindingContext = new ModelBindingContext - // { - // ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)) - // }; + [Fact] + public void ValidationNodeProperty_DefaultValues() + { + // Act + var bindingContext = new ModelBindingContext + { + ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)), + ModelName = "theInt" + }; - // // Act & assert - // MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ValidationNode", new ModelValidationNode(bindingContext.ModelMetadata, "someName")); - //} + // Act + var validationNode = bindingContext.ValidationNode; - // TODO: Validation - //[Fact] - //public void ValidationNodeProperty_DefaultValues() - //{ - // // Act - // ModelBindingContext bindingContext = new ModelBindingContext - // { - // ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)), - // ModelName = "theInt" - // }; - - // // Act - // ModelValidationNode validationNode = bindingContext.ValidationNode; - - // // Assert - // Assert.NotNull(validationNode); - // Assert.Equal(bindingContext.ModelMetadata, validationNode.ModelMetadata); - // Assert.Equal(bindingContext.ModelName, validationNode.ModelStateKey); - //} + // Assert + Assert.NotNull(validationNode); + Assert.Equal(bindingContext.ModelMetadata, validationNode.ModelMetadata); + Assert.Equal(bindingContext.ModelName, validationNode.ModelStateKey); + } } } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/AssociatedMetadataProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/AssociatedMetadataProviderTest.cs index d9c53a7117..17fd5fcd7d 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/AssociatedMetadataProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/AssociatedMetadataProviderTest.cs @@ -10,17 +10,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test.Binders { // GetMetadataForProperties - [Fact] - public void GetMetadataForPropertiesNullContainerTypeThrows() - { - // Arrange - TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); + //[Fact] + //public void GetMetadataForPropertiesNullContainerTypeThrows() + //{ + // // Arrange + // TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => provider.GetMetadataForProperties(new Object(), containerType: null), - "containerType"); - } + // // Act & Assert + // ExceptionAssert.ThrowsArgumentNull( + // () => provider.GetMetadataForProperties(new Object(), containerType: null), + // "containerType"); + //} [Fact] public void GetMetadataForPropertiesCreatesMetadataForAllPropertiesOnModelWithPropertyValues() @@ -72,17 +72,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test.Binders // GetMetadataForProperty - [Fact] - public void GetMetadataForPropertyNullContainerTypeThrows() - { - // Arrange - TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); + //[Fact] + //public void GetMetadataForPropertyNullContainerTypeThrows() + //{ + // // Arrange + // TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => provider.GetMetadataForProperty(modelAccessor: null, containerType: null, propertyName: "propertyName"), - "containerType"); - } + // // Act & Assert + // ExceptionAssert.ThrowsArgumentNull( + // () => provider.GetMetadataForProperty(modelAccessor: null, containerType: null, propertyName: "propertyName"), + // "containerType"); + //} [Fact] public void GetMetadataForPropertyNullOrEmptyPropertyNameThrows() @@ -167,17 +167,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test.Binders // GetMetadataForType - [Fact] - public void GetMetadataForTypeNullModelTypeThrows() - { - // Arrange - TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); + //[Fact] + //public void GetMetadataForTypeNullModelTypeThrows() + //{ + // // Arrange + // TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => provider.GetMetadataForType(() => new Object(), modelType: null), - "modelType"); - } + // // Act & Assert + // ExceptionAssert.ThrowsArgumentNull( + // () => provider.GetMetadataForType(() => new Object(), modelType: null), + // "modelType"); + //} [Fact] public void GetMetadataForTypeIncludesAttributesOnType() diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/AssociatedValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/AssociatedValidatorProviderTest.cs new file mode 100644 index 0000000000..3647e73ed8 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/AssociatedValidatorProviderTest.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class AssociatedValidatorProviderTest + { + private readonly DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); + + [Fact] + public void GetValidatorsThrowsIfMetadataIsNull() + { + // Arrange + var metadata = _metadataProvider.GetMetadataForType(null, typeof(object)); + var provider = new Mock { CallBase = true }; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => provider.Object.GetValidators(metadata: null), + "metadata"); + } + + [Fact] + public void GetValidatorsForPropertyWithLocalAttributes() + { + // Arrange + IEnumerable callbackAttributes = null; + var metadata = _metadataProvider.GetMetadataForProperty(null, typeof(PropertyModel), "LocalAttributes"); + var provider = new Mock { CallBase = true }; + provider.Setup(p => p.AbstractGetValidators(metadata, It.IsAny>())) + .Callback>((m, attributes) => callbackAttributes = attributes) + .Returns(() => null) + .Verifiable(); + + // Act + provider.Object.GetValidators(metadata); + + // Assert + provider.Verify(); + Assert.True(callbackAttributes.Any(a => a is RequiredAttribute)); + } + + [Fact] + public void GetValidatorsForPropertyWithMetadataAttributes() + { + // Arrange + IEnumerable callbackAttributes = null; + var metadata = _metadataProvider.GetMetadataForProperty(null, typeof(PropertyModel), "MetadataAttributes"); + Mock provider = new Mock { CallBase = true }; + provider.Setup(p => p.AbstractGetValidators(metadata, It.IsAny>())) + .Callback>((m, attributes) => callbackAttributes = attributes) + .Returns(() => null) + .Verifiable(); + + // Act + provider.Object.GetValidators(metadata); + + // Assert + provider.Verify(); + Assert.True(callbackAttributes.Any(a => a is RangeAttribute)); + } + + [Fact] + public void GetValidatorsForPropertyWithMixedAttributes() + { + // Arrange + IEnumerable callbackAttributes = null; + var metadata = _metadataProvider.GetMetadataForProperty(null, typeof(PropertyModel), "MixedAttributes"); + Mock provider = new Mock { CallBase = true }; + provider.Setup(p => p.AbstractGetValidators(metadata, It.IsAny>())) + .Callback>((m, attributes) => callbackAttributes = attributes) + .Returns(() => null) + .Verifiable(); + + // Act + provider.Object.GetValidators(metadata); + + // Assert + provider.Verify(); + Assert.True(callbackAttributes.Any(a => a is RangeAttribute)); + Assert.True(callbackAttributes.Any(a => a is RequiredAttribute)); + } + + private class PropertyModel + { + [Required] + public int LocalAttributes { get; set; } + + [Range(10, 100)] + public string MetadataAttributes { get; set; } + + [Required] + [Range(10, 100)] + public double MixedAttributes { get; set; } + } + + public abstract class TestableAssociatedValidatorProvider : AssociatedValidatorProvider + { + protected override IEnumerable GetValidators(ModelMetadata metadata, IEnumerable attributes) + { + return AbstractGetValidators(metadata, attributes); + } + + public abstract IEnumerable AbstractGetValidators(ModelMetadata metadata, IEnumerable attributes); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs new file mode 100644 index 0000000000..22b5398e1b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class DataAnnotationsModelValidatorProviderTest + { + private readonly DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); + + [Fact] + public void UnknownValidationAttributeGetsDefaultAdapter() + { + // Arrange + var provider = new DataAnnotationsModelValidatorProvider(); + var metadata = _metadataProvider.GetMetadataForType(() => null, typeof(DummyClassWithDummyValidationAttribute)); + + // Act + IEnumerable validators = provider.GetValidators(metadata); + + // Assert + var validator = validators.Single(); + Assert.IsType(validator); + } + + private class DummyValidationAttribute : ValidationAttribute + { + } + + [DummyValidation] + private class DummyClassWithDummyValidationAttribute + { + } + + // Default IValidatableObject adapter factory + + [Fact] + public void IValidatableObjectGetsAValidator() + { + // Arrange + var provider = new DataAnnotationsModelValidatorProvider(); + var mockValidatable = new Mock(); + var metadata = _metadataProvider.GetMetadataForType(() => null, mockValidatable.Object.GetType()); + + // Act + var validators = provider.GetValidators(metadata); + + // Assert + Assert.Single(validators); + } + + // Integration with metadata system + + [Fact] + public void DoesNotReadPropertyValue() + { + // Arrange + var provider = new DataAnnotationsModelValidatorProvider(); + var model = new ObservableModel(); + var metadata = _metadataProvider.GetMetadataForProperty(() => model.TheProperty, typeof(ObservableModel), "TheProperty"); + var context = new ModelValidationContext(metadata, null, null, null); + + // Act + var validators = provider.GetValidators(metadata).ToArray(); + var results = validators.SelectMany(o => o.Validate(context)).ToArray(); + + // Assert + Assert.Empty(validators); + Assert.False(model.PropertyWasRead()); + } + + private class ObservableModel + { + private bool _propertyWasRead; + + public string TheProperty + { + get + { + _propertyWasRead = true; + return "Hello"; + } + } + + public bool PropertyWasRead() + { + return _propertyWasRead; + } + } + + private class BaseModel + { + public virtual string MyProperty { get; set; } + } + + private class DerivedModel : BaseModel + { + [StringLength(10)] + public override string MyProperty + { + get { return base.MyProperty; } + set { base.MyProperty = value; } + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs new file mode 100644 index 0000000000..fccbf7b3d7 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs @@ -0,0 +1,231 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Moq; +using Moq.Protected; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class DataAnnotationsModelValidatorTest + { + private static DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); + + [Fact] + public void ConstructorGuards() + { + // Arrange + var metadata = _metadataProvider.GetMetadataForType(null, typeof(object)); + var attribute = new RequiredAttribute(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new DataAnnotationsModelValidator(null), + "attribute"); + } + + [Fact] + public void ValuesSet() + { + // Arrange + var metadata = _metadataProvider.GetMetadataForProperty(() => 15, typeof(string), "Length"); + var attribute = new RequiredAttribute(); + + // Act + var validator = new DataAnnotationsModelValidator(attribute); + + // Assert + Assert.Same(attribute, validator.Attribute); + } + + public static IEnumerable ValidateSetsMemberNamePropertyDataSet + { + get + { + yield return new object[] + { + _metadataProvider.GetMetadataForProperty(() => 15, typeof(string), "Length"), + "Length" + }; + + yield return new object[] + { + _metadataProvider.GetMetadataForType(() => new object(), typeof(SampleModel)), + "SampleModel" + }; + } + } + + [Theory] + [PropertyData("ValidateSetsMemberNamePropertyDataSet")] + public void ValidateSetsMemberNamePropertyOfValidationContextForProperties(ModelMetadata metadata, string expectedMemberName) + { + // Arrange + var attribute = new Mock { CallBase = true }; + attribute.Protected() + .Setup("IsValid", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((object o, ValidationContext context) => + { + Assert.Equal(expectedMemberName, context.MemberName); + }) + .Returns(ValidationResult.Success) + .Verifiable(); + var validator = new DataAnnotationsModelValidator(attribute.Object); + var validationContext = CreateValidationContext(metadata); + + // Act + var results = validator.Validate(validationContext); + + // Assert + Assert.Empty(results); + attribute.VerifyAll(); + } + + [Fact] + public void ValidateWithIsValidTrue() + { + // Arrange + var metadata = _metadataProvider.GetMetadataForProperty(() => 15, typeof(string), "Length"); + var attribute = new Mock { CallBase = true }; + attribute.Setup(a => a.IsValid(metadata.Model)).Returns(true); + var validator = new DataAnnotationsModelValidator(attribute.Object); + var validationContext = CreateValidationContext(metadata); + + // Act + var result = validator.Validate(validationContext); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void ValidateWithIsValidFalse() + { + // Arrange + var metadata = _metadataProvider.GetMetadataForProperty(() => 15, typeof(string), "Length"); + var attribute = new Mock { CallBase = true }; + attribute.Setup(a => a.IsValid(metadata.Model)).Returns(false); + var validator = new DataAnnotationsModelValidator(attribute.Object); + var validationContext = CreateValidationContext(metadata); + + // Act + var result = validator.Validate(validationContext); + + // Assert + var validationResult = result.Single(); + Assert.Equal("", validationResult.MemberName); + Assert.Equal(attribute.Object.FormatErrorMessage("Length"), validationResult.Message); + } + + [Fact] + public void ValidatateWithValidationResultSuccess() + { + // Arrange + var metadata = _metadataProvider.GetMetadataForProperty(() => 15, typeof(string), "Length"); + var attribute = new Mock { CallBase = true }; + attribute.Protected() + .Setup("IsValid", ItExpr.IsAny(), ItExpr.IsAny()) + .Returns(ValidationResult.Success); + DataAnnotationsModelValidator validator = new DataAnnotationsModelValidator(attribute.Object); + var validationContext = CreateValidationContext(metadata); + + // Act + var result = validator.Validate(validationContext); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void ValidateReturnsSingleValidationResultIfMemberNameSequenceIsEmpty() + { + // Arrange + const string errorMessage = "Some error message"; + var metadata = _metadataProvider.GetMetadataForProperty(() => 15, typeof(string), "Length"); + var attribute = new Mock { CallBase = true }; + attribute.Protected() + .Setup("IsValid", ItExpr.IsAny(), ItExpr.IsAny()) + .Returns(new ValidationResult(errorMessage, memberNames: null)); + var validator = new DataAnnotationsModelValidator(attribute.Object); + var validationContext = CreateValidationContext(metadata); + + // Act + var results = validator.Validate(validationContext); + + // Assert + ModelValidationResult validationResult = Assert.Single(results); + Assert.Equal(errorMessage, validationResult.Message); + Assert.Empty(validationResult.MemberName); + } + + [Fact] + public void ValidateReturnsSingleValidationResultIfOneMemberNameIsSpecified() + { + // Arrange + const string errorMessage = "A different error message"; + var metadata = _metadataProvider.GetMetadataForType(() => new object(), typeof(object)); + var attribute = new Mock { CallBase = true }; + attribute.Protected() + .Setup("IsValid", ItExpr.IsAny(), ItExpr.IsAny()) + .Returns(new ValidationResult(errorMessage, new[] { "FirstName" })); + var validator = new DataAnnotationsModelValidator(attribute.Object); + var validationContext = CreateValidationContext(metadata); + + // Act + var results = validator.Validate(validationContext); + + // Assert + ModelValidationResult validationResult = Assert.Single(results); + Assert.Equal(errorMessage, validationResult.Message); + Assert.Equal("FirstName", validationResult.MemberName); + } + + [Fact] + public void ValidateReturnsMemberNameIfItIsDifferentFromDisplayName() + { + // Arrange + var metadata = _metadataProvider.GetMetadataForType(() => new SampleModel(), typeof(SampleModel)); + var attribute = new Mock { CallBase = true }; + attribute.Protected() + .Setup("IsValid", ItExpr.IsAny(), ItExpr.IsAny()) + .Returns(new ValidationResult("Name error", new[] { "Name" })); + var validator = new DataAnnotationsModelValidator(attribute.Object); + var validationContext = CreateValidationContext(metadata); + + // Act + var results = validator.Validate(validationContext); + + // Assert + ModelValidationResult validationResult = Assert.Single(results); + Assert.Equal("Name", validationResult.MemberName); + } + + [Fact] + public void IsRequiredTests() + { + // Arrange + var metadata = _metadataProvider.GetMetadataForProperty(() => 15, typeof(string), "Length"); + + // Act & Assert + Assert.False(new DataAnnotationsModelValidator(new RangeAttribute(10, 20)).IsRequired); + Assert.True(new DataAnnotationsModelValidator(new RequiredAttribute()).IsRequired); + Assert.True(new DataAnnotationsModelValidator(new DerivedRequiredAttribute()).IsRequired); + } + + private static ModelValidationContext CreateValidationContext(ModelMetadata metadata) + { + return new ModelValidationContext(metadata, null, null, null); + } + + class DerivedRequiredAttribute : RequiredAttribute + { + } + + class SampleModel + { + public string Name { get; set; } + } + } +} + diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataMemberModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataMemberModelValidatorProviderTest.cs new file mode 100644 index 0000000000..784689c84d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataMemberModelValidatorProviderTest.cs @@ -0,0 +1,92 @@ +using System.Runtime.Serialization; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class DataMemberModelValidatorProviderTest + { + private readonly DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); + + [Fact] + public void ClassWithoutAttributes_NoValidator() + { + // Arrange + var provider = new DataMemberModelValidatorProvider(); + var metadata = _metadataProvider.GetMetadataForProperty(() => null, typeof(ClassWithoutAttributes), "TheProperty"); + + // Act + var validators = provider.GetValidators(metadata); + + // Assert + Assert.Empty(validators); + } + + class ClassWithoutAttributes + { + public int TheProperty { get; set; } + } + + [Fact] + public void ClassWithDataMemberIsRequiredTrue_Validator() + { + // Arrange + var provider = new DataMemberModelValidatorProvider(); + var metadata = _metadataProvider.GetMetadataForProperty(() => null, typeof(ClassWithDataMemberIsRequiredTrue), "TheProperty"); + + // Act + var validators = provider.GetValidators(metadata); + + // Assert + var validator = Assert.Single(validators); + Assert.True(validator.IsRequired); + } + + [DataContract] + class ClassWithDataMemberIsRequiredTrue + { + [DataMember(IsRequired = true)] + public int TheProperty { get; set; } + } + + [Fact] + public void ClassWithDataMemberIsRequiredFalse_NoValidator() + { + // Arrange + var provider = new DataMemberModelValidatorProvider(); + var metadata = _metadataProvider.GetMetadataForProperty(() => null, typeof(ClassWithDataMemberIsRequiredFalse), "TheProperty"); + + // Act + var validators = provider.GetValidators(metadata); + + // Assert + Assert.Empty(validators); + } + + [DataContract] + class ClassWithDataMemberIsRequiredFalse + { + [DataMember(IsRequired = false)] + public int TheProperty { get; set; } + } + + [Fact] + public void ClassWithDataMemberIsRequiredTrueWithoutDataContract_NoValidator() + { + // Arrange + var provider = new DataMemberModelValidatorProvider(); + var metadata = _metadataProvider.GetMetadataForProperty(() => null, typeof(ClassWithDataMemberIsRequiredTrueWithoutDataContract), "TheProperty"); + + // Act + var validators = provider.GetValidators(metadata); + + // Assert + Assert.Empty(validators); + } + + class ClassWithDataMemberIsRequiredTrueWithoutDataContract + { + [DataMember(IsRequired = true)] + public int TheProperty { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ErrorModelValidatorTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ErrorModelValidatorTest.cs new file mode 100644 index 0000000000..edd57283dd --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ErrorModelValidatorTest.cs @@ -0,0 +1,29 @@ +using System; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class ErrorModelValidatorTest + { + private readonly DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); + + [Fact] + public void ConstructorGuards() + { + // Act and Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ErrorModelValidator(errorMessage: null), + "errorMessage"); + } + + [Fact] + public void ValidateThrowsException() + { + // Arrange + var validator = new ErrorModelValidator("error"); + + // Act and Assert + ExceptionAssert.Throws(() => validator.Validate(null), "error"); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/InvalidModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/InvalidModelValidatorProviderTest.cs new file mode 100644 index 0000000000..c46c8710ee --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/InvalidModelValidatorProviderTest.cs @@ -0,0 +1,94 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class InvalidModelValidatorProviderTest + { + private static DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); + + [Fact] + public void GetValidatorsReturnsNothingForValidModel() + { + // Arrange + var validatorProvider = new InvalidModelValidatorProvider(); + + // Act + var validators = validatorProvider.GetValidators(_metadataProvider.GetMetadataForType(null, typeof(ValidModel))); + + // Assert + Assert.Empty(validators); + } + + [Fact] + public void GetValidatorsReturnsInvalidModelValidatorsForInvalidModelType() + { + // Arrange + var name = typeof(InvalidModel).FullName; + var validatorProvider = new InvalidModelValidatorProvider(); + + // Act + var validators = validatorProvider.GetValidators(_metadataProvider.GetMetadataForType(null, typeof(InvalidModel))); + + // Assert + Assert.Equal(2, validators.Count()); + ExceptionAssert.Throws(() => validators.ElementAt(0).Validate(null), + "Non-public property 'Internal' on type '" + name + "' is attributed with one or more validation attributes. Validation attributes on non-public properties are not supported. Consider using a public property for validation instead."); + ExceptionAssert.Throws(() => validators.ElementAt(1).Validate(null), + "Field 'Field' on type '" + name + "' is attributed with one or more validation attributes. Validation attributes on fields are not supported. Consider using a public property for validation instead."); + } + + [Fact] + public void GetValidatorsReturnsInvalidModelValidatorsForInvalidModelProperty() + { + // Arrange + var name = typeof(InvalidModel).FullName; + var validatorProvider = new InvalidModelValidatorProvider(); + + // Act + var validators = validatorProvider.GetValidators(_metadataProvider.GetMetadataForProperty(null, typeof(InvalidModel), "Value")); + + // Assert + Assert.Equal(1, validators.Count()); + ExceptionAssert.Throws(() => validators.First().Validate(null), + "Property 'Value' on type '" + name + "' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)]."); + } + + [DataContract] + public class ValidModel + { + [Required] + [DataMember] + [StringLength(10)] + public string Ref { get; set; } + + [DataMember] + internal string Internal { get; set; } + + [Required] + [DataMember(IsRequired = true)] + public int Value { get; set; } + + public string Field; + } + + public class InvalidModel + { + [Required] + public string Ref { get; set; } + + [StringLength(10)] + [RegularExpression("pattern")] + internal string Internal { get; set; } + + [Required] + public int Value { get; set; } + + [StringLength(10)] + public string Field; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelValidationNodeTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelValidationNodeTest.cs new file mode 100644 index 0000000000..382edda2f6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelValidationNodeTest.cs @@ -0,0 +1,326 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNet.Mvc.ModelBinding.Test; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class ModelValidationNodeTest + { + [Fact] + public void ConstructorSetsCollectionInstance() + { + // Arrange + var metadata = GetModelMetadata(); + var modelStateKey = "someKey"; + var childNodes = new[] + { + new ModelValidationNode(metadata, "someKey0"), + new ModelValidationNode(metadata, "someKey1") + }; + + // Act + var node = new ModelValidationNode(metadata, modelStateKey, childNodes); + + // Assert + Assert.Equal(childNodes, node.ChildNodes.ToArray()); + } + + [Fact] + public void PropertiesAreSet() + { + // Arrange + var metadata = GetModelMetadata(); + var modelStateKey = "someKey"; + + // Act + var node = new ModelValidationNode(metadata, modelStateKey); + + // Assert + Assert.Equal(metadata, node.ModelMetadata); + Assert.Equal(modelStateKey, node.ModelStateKey); + Assert.NotNull(node.ChildNodes); + Assert.Empty(node.ChildNodes); + } + + [Fact] + public void CombineWith() + { + // Arrange + var log = new List(); + + var allChildNodes = new[] + { + new ModelValidationNode(GetModelMetadata(), "key1"), + new ModelValidationNode(GetModelMetadata(), "key2"), + new ModelValidationNode(GetModelMetadata(), "key3"), + }; + + var parentNode1 = new ModelValidationNode(GetModelMetadata(), "parent1"); + parentNode1.ChildNodes.Add(allChildNodes[0]); + parentNode1.Validating += (sender, e) => log.Add("Validating parent1."); + parentNode1.Validated += (sender, e) => log.Add("Validated parent1."); + + var parentNode2 = new ModelValidationNode(GetModelMetadata(), "parent2"); + parentNode2.ChildNodes.Add(allChildNodes[1]); + parentNode2.ChildNodes.Add(allChildNodes[2]); + parentNode2.Validating += (sender, e) => log.Add("Validating parent2."); + parentNode2.Validated += (sender, e) => log.Add("Validated parent2."); + var context = CreateContext(); + + // Act + parentNode1.CombineWith(parentNode2); + parentNode1.Validate(context); + + // Assert + Assert.Equal(new[] { "Validating parent1.", "Validating parent2.", "Validated parent1.", "Validated parent2." }, log.ToArray()); + Assert.Equal(allChildNodes, parentNode1.ChildNodes.ToArray()); + } + + [Fact] + public void CombineWith_OtherNodeIsSuppressed_DoesNothing() + { + // Arrange + var log = new List(); + + var allChildNodes = new[] + { + new ModelValidationNode(GetModelMetadata(), "key1"), + new ModelValidationNode(GetModelMetadata(), "key2"), + new ModelValidationNode(GetModelMetadata(), "key3"), + }; + + var expectedChildNodes = new[] + { + allChildNodes[0] + }; + + var parentNode1 = new ModelValidationNode(GetModelMetadata(), "parent1"); + parentNode1.ChildNodes.Add(allChildNodes[0]); + parentNode1.Validating += (sender, e) => log.Add("Validating parent1."); + parentNode1.Validated += (sender, e) => log.Add("Validated parent1."); + + var parentNode2 = new ModelValidationNode(GetModelMetadata(), "parent2"); + parentNode2.ChildNodes.Add(allChildNodes[1]); + parentNode2.ChildNodes.Add(allChildNodes[2]); + parentNode2.Validating += (sender, e) => log.Add("Validating parent2."); + parentNode2.Validated += (sender, e) => log.Add("Validated parent2."); + parentNode2.SuppressValidation = true; + var context = CreateContext(); + + // Act + parentNode1.CombineWith(parentNode2); + parentNode1.Validate(context); + + // Assert + Assert.Equal(new[] { "Validating parent1.", "Validated parent1." }, log.ToArray()); + Assert.Equal(expectedChildNodes, parentNode1.ChildNodes.ToArray()); + } + + [Fact] + public void Validate_Ordering() + { + // Proper order of invocation: + // 1. OnValidating() + // 2. Child validators + // 3. This validator + // 4. OnValidated() + + // Arrange + var log = new List(); + var model = new LoggingValidatableObject(log); + var modelMetadata = GetModelMetadata(model); + var childMetadata = new EmptyModelMetadataProvider().GetMetadataForProperty(() => model, model.GetType(), "ValidStringProperty"); + var node = new ModelValidationNode(modelMetadata, "theKey"); + node.Validating += (sender, e) => log.Add("In OnValidating()"); + node.Validated += (sender, e) => log.Add("In OnValidated()"); + node.ChildNodes.Add(new ModelValidationNode(childMetadata, "theKey.ValidStringProperty")); + var context = CreateContext(modelMetadata); + + // Act + node.Validate(context); + + // Assert + Assert.Equal(new[] { "In OnValidating()", "In LoggingValidatonAttribute.IsValid()", "In IValidatableObject.Validate()", "In OnValidated()" }, log.ToArray()); + } + + [Fact] + public void Validate_SkipsRemainingValidationIfModelStateIsInvalid() + { + // Because a property validator fails, the model validator shouldn't run + + // Arrange + var log = new List(); + var model = new LoggingValidatableObject(log); + var modelMetadata = GetModelMetadata(model); + var childMetadata = new EmptyModelMetadataProvider().GetMetadataForProperty(() => model, model.GetType(), "InvalidStringProperty"); + var node = new ModelValidationNode(modelMetadata, "theKey"); + node.ChildNodes.Add(new ModelValidationNode(childMetadata, "theKey.InvalidStringProperty")); + node.Validating += (sender, e) => log.Add("In OnValidating()"); + node.Validated += (sender, e) => log.Add("In OnValidated()"); + var context = CreateContext(modelMetadata); + + // Act + node.Validate(context); + + // Assert + Assert.Equal(new[] { "In OnValidating()", "In IValidatableObject.Validate()", "In OnValidated()" }, log.ToArray()); + Assert.Equal("Sample error message", context.ModelState["theKey.InvalidStringProperty"].Errors[0].ErrorMessage); + } + + [Fact] + public void Validate_SkipsValidationIfHandlerCancels() + { + // Arrange + var log = new List(); + var model = new LoggingValidatableObject(log); + var modelMetadata = GetModelMetadata(model); + var node = new ModelValidationNode(modelMetadata, "theKey"); + node.Validating += (sender, e) => + { + log.Add("In OnValidating()"); + e.Cancel = true; + }; + node.Validated += (sender, e) => log.Add("In OnValidated()"); + var context = CreateContext(modelMetadata); + + // Act + node.Validate(context); + + // Assert + Assert.Equal(new[] { "In OnValidating()" }, log.ToArray()); + } + + [Fact] + public void Validate_SkipsValidationIfSuppressed() + { + // Arrange + var log = new List(); + var model = new LoggingValidatableObject(log); + var modelMetadata = GetModelMetadata(model); + var node = new ModelValidationNode(modelMetadata, "theKey") + { + SuppressValidation = true + }; + + node.Validating += (sender, e) => log.Add("In OnValidating()"); + node.Validated += (sender, e) => log.Add("In OnValidated()"); + var context = CreateContext(); + + // Act + node.Validate(context); + + // Assert + Assert.Empty(log); + } + + [Fact] + public void Validate_ThrowsIfControllerContextIsNull() + { + // Arrange + var node = new ModelValidationNode(GetModelMetadata(), "someKey"); + + // Act & assert + ExceptionAssert.ThrowsArgumentNull( + () => node.Validate(null), + "validationContext"); + } + + [Fact] + [ReplaceCulture] + public void Validate_ValidateAllProperties_AddsValidationErrors() + { + // Arrange + var model = new ValidateAllPropertiesModel + { + RequiredString = null /* error */, + RangedInt = 0 /* error */, + ValidString = "dog" + }; + + var modelMetadata = GetModelMetadata(model); + var node = new ModelValidationNode(modelMetadata, "theKey") + { + ValidateAllProperties = true + }; + var context = CreateContext(modelMetadata); + context.ModelState.AddModelError("theKey.RequiredString.Dummy", "existing Error Text"); + + // Act + node.Validate(context); + + // Assert + Assert.False(context.ModelState.ContainsKey("theKey.RequiredString")); + Assert.Equal("existing Error Text", context.ModelState["theKey.RequiredString.Dummy"].Errors[0].ErrorMessage); + Assert.Equal("The field RangedInt must be between 10 and 30.", context.ModelState["theKey.RangedInt"].Errors[0].ErrorMessage); + Assert.False(context.ModelState.ContainsKey("theKey.ValidString")); + Assert.False(context.ModelState.ContainsKey("theKey")); + } + + private static ModelMetadata GetModelMetadata() + { + return new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)); + } + + private static ModelMetadata GetModelMetadata(object o) + { + return new DataAnnotationsModelMetadataProvider().GetMetadataForType(() => o, o.GetType()); + } + + private static ModelValidationContext CreateContext(ModelMetadata metadata = null) + { + var providers = new IModelValidatorProvider[] { + new DataAnnotationsModelValidatorProvider(), + new DataMemberModelValidatorProvider() + }; + + return new ModelValidationContext(metadata, + new ModelStateDictionary(), + new EmptyModelMetadataProvider(), + providers); + } + + private sealed class LoggingValidatableObject : IValidatableObject + { + private readonly IList _log; + + public LoggingValidatableObject(IList log) + { + _log = log; + } + + [LoggingValidation] + public string ValidStringProperty { get; set; } + public string InvalidStringProperty { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + _log.Add("In IValidatableObject.Validate()"); + yield return new ValidationResult("Sample error message", new[] { "InvalidStringProperty" }); + } + + private sealed class LoggingValidationAttribute : ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + LoggingValidatableObject lvo = (LoggingValidatableObject)value; + lvo._log.Add("In LoggingValidatonAttribute.IsValid()"); + return ValidationResult.Success; + } + } + } + + private class ValidateAllPropertiesModel + { + [Required] + public string RequiredString { get; set; } + + [Range(10, 30)] + public int RangedInt { get; set; } + + [RegularExpression("dog")] + public string ValidString { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json index 832b5fcddf..1a7e77f038 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json @@ -10,6 +10,16 @@ "Xunit.extensions": "1.9.1" }, "configurations": { - "net45": { } + "net45": { + dependencies: { + "System.ComponentModel.DataAnnotations": "", + "System.Runtime.Serialization": "" + } + }, + "k10" : { + dependencies: { + "Microsoft.ComponentModel.DataAnnotations" : "0.1-alpha-*" + } + } } } \ No newline at end of file diff --git a/test/TestCommon/CultureReplacer.cs b/test/TestCommon/CultureReplacer.cs index efb05c5cf6..72c6248e88 100644 --- a/test/TestCommon/CultureReplacer.cs +++ b/test/TestCommon/CultureReplacer.cs @@ -3,9 +3,9 @@ using System.Globalization; using System.Threading; using Xunit; -namespace Microsoft.AspNet.Mvc.ModelBinding.Test +namespace Microsoft.AspNet.Mvc { - public class CultureReplacer : IDisposable + internal class CultureReplacer : IDisposable { private const string _defaultCultureName = "en-GB"; private const string _defaultUICultureName = "en-US"; diff --git a/test/TestCommon/ExceptionAssert.cs b/test/TestCommon/ExceptionAssert.cs index 634944dad4..afc5b65fd8 100644 --- a/test/TestCommon/ExceptionAssert.cs +++ b/test/TestCommon/ExceptionAssert.cs @@ -2,9 +2,9 @@ using System.Reflection; using Xunit; -namespace Microsoft.AspNet.Mvc.ModelBinding.Test +namespace Microsoft.AspNet.Mvc { - public static class ExceptionAssert + internal static class ExceptionAssert { /// /// Verifies that an exception of the given type (or optionally a derived type) is thrown. diff --git a/test/TestCommon/ReplaceCulture.cs b/test/TestCommon/ReplaceCulture.cs new file mode 100644 index 0000000000..940d7d3a00 --- /dev/null +++ b/test/TestCommon/ReplaceCulture.cs @@ -0,0 +1,56 @@ +using System; +using System.Globalization; +using System.Reflection; +using System.Threading; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Replaces the current culture and UI culture for the test. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ReplaceCultureAttribute : Xunit.BeforeAfterTestAttribute + { + private const string _defaultCultureName = "en-GB"; + private const string _defaultUICultureName = "en-US"; + private static readonly CultureInfo _defaultCulture = CultureInfo.GetCultureInfo(_defaultCultureName); + private CultureInfo _originalCulture; + private CultureInfo _originalUICulture; + + public ReplaceCultureAttribute() + { + Culture = _defaultCulture; + UICulture = _defaultCulture; + } + + /// + /// Sets for the test. Defaults to en-GB. + /// + /// + /// en-GB is used here as the default because en-US is equivalent to the InvariantCulture. We + /// want to be able to find bugs where we're accidentally relying on the Invariant instead of the + /// user's culture. + /// + public CultureInfo Culture { get; set; } + + /// + /// Sets for the test. Defaults to en-US. + /// + public CultureInfo UICulture { get; set; } + + public override void Before(MethodInfo methodUnderTest) + { + _originalCulture = Thread.CurrentThread.CurrentCulture; + _originalUICulture = Thread.CurrentThread.CurrentUICulture; + + Thread.CurrentThread.CurrentCulture = Culture; + Thread.CurrentThread.CurrentUICulture = UICulture; + } + + public override void After(MethodInfo methodUnderTest) + { + Thread.CurrentThread.CurrentCulture = _originalCulture; + Thread.CurrentThread.CurrentUICulture = _originalUICulture; + } + } +}