Reintroduce model validation

This commit is contained in:
Pranav K 2014-02-27 23:20:28 -08:00
parent cca3f92c5f
commit e434918337
56 changed files with 2498 additions and 457 deletions

View File

@ -1,7 +1,11 @@
namespace MvcSample.Models using System.ComponentModel.DataAnnotations;
namespace MvcSample.Models
{ {
public class User public class User
{ {
[Required]
[MinLength(4)]
public string Name { get; set; } public string Name { get; set; }
public string Address { get; set; } public string Address { get; set; }
public int Age { get; set; } public int Age { get; set; }

View File

@ -1,40 +1,42 @@
{ {
"version" : "0.1-alpha-*", "version": "0.1-alpha-*",
"dependencies": { "dependencies": {
"Microsoft.AspNet.Abstractions": "0.1-alpha-*", "Microsoft.AspNet.Abstractions": "0.1-alpha-*",
"Microsoft.AspNet.ConfigurationModel": "0.1-alpha-*", "Microsoft.AspNet.ConfigurationModel": "0.1-alpha-*",
"Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*", "Microsoft.AspNet.DependencyInjection": "0.1-alpha-*",
"Microsoft.AspNet.Routing" : "0.1-alpha-*", "Microsoft.AspNet.Routing": "0.1-alpha-*",
"Microsoft.AspNet.Mvc.ModelBinding" : "", "Microsoft.AspNet.Mvc.ModelBinding": "",
"Microsoft.AspNet.Mvc.Core" : "", "Microsoft.AspNet.Mvc.Core": "",
"Microsoft.AspNet.Mvc" : "", "Microsoft.AspNet.Mvc": "",
"Microsoft.AspNet.Mvc.Razor": "", "Microsoft.AspNet.Mvc.Razor": "",
"Microsoft.AspNet.Mvc.Rendering" : "" "Microsoft.AspNet.Mvc.Rendering": ""
}, },
"configurations": { "configurations": {
"net45": { "net45": {
"dependencies": { "dependencies": {
"Autofac": "3.3.0", "Autofac": "3.3.0",
"Owin": "1.0", "Owin": "1.0",
"Microsoft.AspNet.DependencyInjection.Autofac": "0.1-alpha-*", "Microsoft.AspNet.DependencyInjection.Autofac": "0.1-alpha-*",
"Microsoft.Owin": "2.1.0", "Microsoft.Owin": "2.1.0",
"Microsoft.Owin.Diagnostics": "2.1.0", "Microsoft.Owin.Diagnostics": "2.1.0",
"Microsoft.Owin.Hosting": "2.1.0", "Microsoft.Owin.Hosting": "2.1.0",
"Microsoft.Owin.Host.HttpListener": "2.1.0", "Microsoft.Owin.Host.HttpListener": "2.1.0",
"Microsoft.AspNet.AppBuilderSupport": "0.1-alpha-*" "Microsoft.AspNet.AppBuilderSupport": "0.1-alpha-*",
} "System.ComponentModel.DataAnnotations": ""
}
}, },
"k10" : { "k10": {
"dependencies": { "dependencies": {
"System.ComponentModel": "4.0.0.0", "System.ComponentModel": "4.0.0.0",
"System.Console": "4.0.0.0", "System.Console": "4.0.0.0",
"System.Diagnostics.Debug": "4.0.10.0", "System.Diagnostics.Debug": "4.0.10.0",
"System.Diagnostics.Tools": "4.0.0.0", "System.Diagnostics.Tools": "4.0.0.0",
"System.Dynamic.Runtime": "4.0.0.0", "System.Dynamic.Runtime": "4.0.0.0",
"System.Runtime": "4.0.20.0", "System.Runtime": "4.0.20.0",
"System.Runtime.InteropServices": "4.0.10.0", "System.Runtime.InteropServices": "4.0.10.0",
"System.Threading.Tasks": "4.0.0.0" "System.Threading.Tasks": "4.0.0.0",
} "Microsoft.ComponentModel.DataAnnotations": "0.1-alpha-*"
}
} }
} }
} }

View File

@ -29,6 +29,7 @@ namespace Microsoft.AspNet.Mvc.Internal
ModelMetadata = modelMetadata, ModelMetadata = modelMetadata,
ModelBinder = actionBindingContext.ModelBinder, ModelBinder = actionBindingContext.ModelBinder,
ValueProvider = actionBindingContext.ValueProvider, ValueProvider = actionBindingContext.ValueProvider,
ValidatorProviders = actionBindingContext.ValidatorProviders,
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
HttpContext = actionBindingContext.ActionContext.HttpContext, HttpContext = actionBindingContext.ActionContext.HttpContext,
FallbackToEmptyPrefix = true FallbackToEmptyPrefix = true

View File

@ -1,4 +1,5 @@
using Microsoft.AspNet.Mvc.ModelBinding; using System.Collections.Generic;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
@ -8,13 +9,15 @@ namespace Microsoft.AspNet.Mvc
IModelMetadataProvider metadataProvider, IModelMetadataProvider metadataProvider,
IModelBinder modelBinder, IModelBinder modelBinder,
IValueProvider valueProvider, IValueProvider valueProvider,
IInputFormatter inputFormatter) IInputFormatter inputFormatter,
IEnumerable<IModelValidatorProvider> validatorProviders)
{ {
ActionContext = context; ActionContext = context;
MetadataProvider = metadataProvider; MetadataProvider = metadataProvider;
ModelBinder = modelBinder; ModelBinder = modelBinder;
ValueProvider = valueProvider; ValueProvider = valueProvider;
InputFormatter = inputFormatter; InputFormatter = inputFormatter;
ValidatorProviders = validatorProviders;
} }
public ActionContext ActionContext { get; private set; } public ActionContext ActionContext { get; private set; }
@ -26,5 +29,7 @@ namespace Microsoft.AspNet.Mvc
public IValueProvider ValueProvider { get; private set; } public IValueProvider ValueProvider { get; private set; }
public IInputFormatter InputFormatter { get; private set; } public IInputFormatter InputFormatter { get; private set; }
public IEnumerable<IModelValidatorProvider> ValidatorProviders { get; private set; }
} }
} }

View File

@ -10,17 +10,20 @@ namespace Microsoft.AspNet.Mvc
private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IEnumerable<IModelBinder> _modelBinders; private readonly IEnumerable<IModelBinder> _modelBinders;
private readonly IEnumerable<IValueProviderFactory> _valueProviderFactories; private readonly IEnumerable<IValueProviderFactory> _valueProviderFactories;
private readonly IEnumerable<IInputFormatter> _bodyReaders; private readonly IEnumerable<IInputFormatter> _inputFormatters;
private readonly IEnumerable<IModelValidatorProvider> _validatorProviders;
public DefaultActionBindingContextProvider(IModelMetadataProvider modelMetadataProvider, public DefaultActionBindingContextProvider(IModelMetadataProvider modelMetadataProvider,
IEnumerable<IModelBinder> modelBinders, IEnumerable<IModelBinder> modelBinders,
IEnumerable<IValueProviderFactory> valueProviderFactories, IEnumerable<IValueProviderFactory> valueProviderFactories,
IEnumerable<IInputFormatter> bodyReaders) IEnumerable<IInputFormatter> inputFormatters,
IEnumerable<IModelValidatorProvider> validatorProviders)
{ {
_modelMetadataProvider = modelMetadataProvider; _modelMetadataProvider = modelMetadataProvider;
_modelBinders = modelBinders.OrderBy(binder => binder.GetType() == typeof(ComplexModelDtoModelBinder) ? 1 : 0); _modelBinders = modelBinders.OrderBy(binder => binder.GetType() == typeof(ComplexModelDtoModelBinder) ? 1 : 0);
_valueProviderFactories = valueProviderFactories; _valueProviderFactories = valueProviderFactories;
_bodyReaders = bodyReaders; _inputFormatters = inputFormatters;
_validatorProviders = validatorProviders;
} }
public async Task<ActionBindingContext> GetActionBindingContextAsync(ActionContext actionContext) public async Task<ActionBindingContext> GetActionBindingContextAsync(ActionContext actionContext)
@ -35,7 +38,8 @@ namespace Microsoft.AspNet.Mvc
_modelMetadataProvider, _modelMetadataProvider,
new CompositeModelBinder(_modelBinders), new CompositeModelBinder(_modelBinders),
new CompositeValueProvider(valueProviders), new CompositeValueProvider(valueProviders),
new CompositeInputFormatter(_bodyReaders) new CompositeInputFormatter(_inputFormatters),
_validatorProviders
); );
} }
} }

View File

@ -148,7 +148,6 @@ namespace Microsoft.AspNet.Mvc
actionBindingContext.ModelBinder.BindModel(modelBindingContext); actionBindingContext.ModelBinder.BindModel(modelBindingContext);
parameterValues[parameter.Name] = modelBindingContext.Model; parameterValues[parameter.Name] = modelBindingContext.Model;
} }
} }
return parameterValues; return parameterValues;

View File

@ -60,8 +60,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (bindingContext.ModelBinder.BindModel(innerBindingContext)) if (bindingContext.ModelBinder.BindModel(innerBindingContext))
{ {
boundValue = innerBindingContext.Model; boundValue = innerBindingContext.Model;
// TODO: validation bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode);
// bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode);
} }
boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue)); boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
} }
@ -114,9 +113,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
didBind = true; didBind = true;
boundValue = childBindingContext.Model; boundValue = childBindingContext.Model;
// TODO: Validation
// merge validation up // merge validation up
// bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode); bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode);
} }
// infinite size collection stops on first bind failure // infinite size collection stops on first bind failure

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (bindingContext.ModelBinder.BindModel(propertyBindingContext)) if (bindingContext.ModelBinder.BindModel(propertyBindingContext))
{ {
dto.Results[propertyMetadata] = new ComplexModelDtoResult(propertyBindingContext.Model/*, propertyBindingContext.ValidationNode*/); dto.Results[propertyMetadata] = new ComplexModelDtoResult(propertyBindingContext.Model, propertyBindingContext.ValidationNode);
} }
} }

View File

@ -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 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; Model = model;
//ValidationNode = validationNode; ValidationNode = validationNode;
} }
public object Model { get; private set; } public object Model { get; private set; }
//public ModelValidationNode ValidationNode { get; private set; } public ModelValidationNode ValidationNode { get; private set; }
} }
} }

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
@ -29,14 +28,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public virtual bool BindModel(ModelBindingContext bindingContext) public virtual bool BindModel(ModelBindingContext bindingContext)
{ {
ModelBindingContext newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName); var newBindingContext = CreateNewBindingContext(bindingContext,
bindingContext.ModelName,
reuseValidationNode: true);
bool boundSuccessfully = TryBind(newBindingContext); bool boundSuccessfully = TryBind(newBindingContext);
if (!boundSuccessfully && !String.IsNullOrEmpty(bindingContext.ModelName) if (!boundSuccessfully && !string.IsNullOrEmpty(bindingContext.ModelName)
&& bindingContext.FallbackToEmptyPrefix) && bindingContext.FallbackToEmptyPrefix)
{ {
// fallback to empty prefix? // fallback to empty prefix?
newBindingContext = CreateNewBindingContext(bindingContext, modelName: String.Empty); newBindingContext = CreateNewBindingContext(bindingContext,
modelName: string.Empty,
reuseValidationNode: false);
boundSuccessfully = TryBind(newBindingContext); 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, // 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. // 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. // 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))
//if (!newBindingContext.ModelMetadata.IsComplexType && String.IsNullOrEmpty(newBindingContext.ModelName)) {
//{ newBindingContext.ValidationNode = new ModelValidationNode(newBindingContext.ModelMetadata, bindingContext.ModelName);
// newBindingContext.ValidationNode = new Validation.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; bindingContext.Model = newBindingContext.Model;
return true; 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. // TODO: RuntimeHelpers.EnsureSufficientExecutionStack does not exist in the CoreCLR.
// Protects against stack overflow for deeply nested model binding // Protects against stack overflow for deeply nested model binding
// RuntimeHelpers.EnsureSufficientExecutionStack(); // RuntimeHelpers.EnsureSufficientExecutionStack();
foreach (IModelBinder binder in Binders) foreach (var binder in Binders)
{ {
if (binder.BindModel(bindingContext)) if (binder.BindModel(bindingContext))
{ {
@ -85,7 +85,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return false; return false;
} }
private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext, string modelName) private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext,
string modelName,
bool reuseValidationNode)
{ {
var newBindingContext = new ModelBindingContext var newBindingContext = new ModelBindingContext
{ {
@ -93,17 +95,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelName = modelName, ModelName = modelName,
ModelState = oldBindingContext.ModelState, ModelState = oldBindingContext.ModelState,
ValueProvider = oldBindingContext.ValueProvider, ValueProvider = oldBindingContext.ValueProvider,
ValidatorProviders = oldBindingContext.ValidatorProviders,
MetadataProvider = oldBindingContext.MetadataProvider, MetadataProvider = oldBindingContext.MetadataProvider,
ModelBinder = oldBindingContext.ModelBinder, ModelBinder = oldBindingContext.ModelBinder,
HttpContext = oldBindingContext.HttpContext HttpContext = oldBindingContext.HttpContext
}; };
// TODO: Validation // validation is expensive to create, so copy it over if we can
//// validation is expensive to create, so copy it over if we can if (reuseValidationNode)
//if (Object.ReferenceEquals(modelName, oldBindingContext.ModelName)) {
//{ newBindingContext.ValidationNode = oldBindingContext.ValidationNode;
// newBindingContext.ValidationNode = oldBindingContext.ValidationNode; }
//}
return newBindingContext; return newBindingContext;
} }

View File

@ -37,8 +37,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
object untypedModel = propertyBindingContext.Model; object untypedModel = propertyBindingContext.Model;
model = ModelBindingHelper.CastOrDefault<TModel>(untypedModel); model = ModelBindingHelper.CastOrDefault<TModel>(untypedModel);
// TODO: Revive once we get validation parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode);
// parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode);
return true; return true;
} }

View File

@ -21,13 +21,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
EnsureModel(bindingContext); EnsureModel(bindingContext);
IEnumerable<ModelMetadata> propertyMetadatas = GetMetadataForProperties(bindingContext); var propertyMetadatas = GetMetadataForProperties(bindingContext);
ComplexModelDto dto = CreateAndPopulateDto(bindingContext, propertyMetadatas); var dto = CreateAndPopulateDto(bindingContext, propertyMetadatas);
// post-processing, e.g. property setters and hooking up validation // post-processing, e.g. property setters and hooking up validation
ProcessDto(bindingContext, dto); ProcessDto(bindingContext, dto);
// TODO: Validation // complex models require full validation
// bindingContext.ValidationNode.ValidateAllProperties = true; // complex models require full validation bindingContext.ValidationNode.ValidateAllProperties = true;
return true; return true;
} }
@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private static bool CanBindType(Type modelType) private static bool CanBindType(Type modelType)
{ {
// Simple types cannot use this binder // Simple types cannot use this binder
bool isComplexType = !modelType.HasStringConverter(); var isComplexType = !modelType.HasStringConverter();
if (!isComplexType) if (!isComplexType)
{ {
return false; return false;
@ -87,8 +87,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private ComplexModelDto CreateAndPopulateDto(ModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas) private ComplexModelDto CreateAndPopulateDto(ModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas)
{ {
// create a DTO and call into the DTO binder // create a DTO and call into the DTO binder
ComplexModelDto originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas); var originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas);
ModelBindingContext dtoBindingContext = new ModelBindingContext(bindingContext) var dtoBindingContext = new ModelBindingContext(bindingContext)
{ {
ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(() => originalDto, typeof(ComplexModelDto)), ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(() => originalDto, typeof(ComplexModelDto)),
ModelName = bindingContext.ModelName ModelName = bindingContext.ModelName
@ -105,24 +105,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return Activator.CreateInstance(bindingContext.ModelType); return Activator.CreateInstance(bindingContext.ModelType);
} }
//// Called when the property setter null check failed, allows us to add our own error message to ModelState. // Called when the property setter null check failed, allows us to add our own error message to ModelState.
//internal static EventHandler<ModelValidatedEventArgs> CreateNullCheckFailedHandler(ModelMetadata modelMetadata, object incomingValue) internal static EventHandler<ModelValidatedEventArgs> CreateNullCheckFailedHandler(ModelMetadata modelMetadata, object incomingValue)
//{ {
// return (sender, e) => return (sender, e) =>
// { {
// ModelValidationNode validationNode = (ModelValidationNode)sender; var validationNode = (ModelValidationNode)sender;
// ModelStateDictionary modelState = e.ActionContext.ModelState; var modelState = e.ValidationContext.ModelState;
// if (modelState.IsValidField(validationNode.ModelStateKey)) if (modelState.IsValidField(validationNode.ModelStateKey))
// { {
// string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ActionContext, modelMetadata, incomingValue); // TODO: Revive ModelBinderConfig
// if (errorMessage != null) // string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ValidationContext, modelMetadata, incomingValue);
// { var errorMessage = e.ValidationContext.ModelMetadata.PropertyName + " is required";
// modelState.AddModelError(validationNode.ModelStateKey, errorMessage); if (errorMessage != null)
// } {
// } modelState.AddModelError(validationNode.ModelStateKey, errorMessage);
// }; }
//} }
};
}
protected virtual void EnsureModel(ModelBindingContext bindingContext) protected virtual void EnsureModel(ModelBindingContext bindingContext)
{ {
@ -134,12 +136,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext) protected virtual IEnumerable<ModelMetadata> 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 // keep a set of the required properties so that we can cross-reference bound properties later
HashSet<string> requiredProperties = new HashSet<string>(); HashSet<string> requiredProperties;
// Dictionary<string, ModelValidator> requiredValidators; Dictionary<string, IModelValidator> requiredValidators;
HashSet<string> skipProperties = new HashSet<string>(); HashSet<string> skipProperties;
// GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out skipProperties); GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out requiredValidators, out skipProperties);
return from propertyMetadata in bindingContext.ModelMetadata.Properties return from propertyMetadata in bindingContext.ModelMetadata.Properties
let propertyName = propertyMetadata.PropertyName let propertyName = propertyMetadata.PropertyName
@ -150,102 +151,84 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private static object GetPropertyDefaultValue(PropertyInfo propertyInfo) private static object GetPropertyDefaultValue(PropertyInfo propertyInfo)
{ {
DefaultValueAttribute attr = propertyInfo.GetCustomAttribute<DefaultValueAttribute>(); var attr = propertyInfo.GetCustomAttribute<DefaultValueAttribute>();
return (attr != null) ? attr.Value : null; return (attr != null) ? attr.Value : null;
} }
//internal static void GetRequiredPropertiesCollection(ModelBindingContext bindingContext, out HashSet<string> requiredProperties, out HashSet<string> skipProperties) internal static void GetRequiredPropertiesCollection(ModelBindingContext bindingContext,
//{ out HashSet<string> requiredProperties,
// requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase); out Dictionary<string, IModelValidator> requiredValidators,
// // requiredValidators = new Dictionary<string, ModelValidator>(StringComparer.OrdinalIgnoreCase); out HashSet<string> skipProperties)
// skipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase); {
requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
requiredValidators = new Dictionary<string, IModelValidator>(StringComparer.OrdinalIgnoreCase);
skipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// // Use attributes on the property before attributes on the type. // TODO: HttpBindingBehaviorAttribute
// Type modelType = bindingContext.ModelType; var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
// ICustomTypeDescriptor modelDescriptor = new AssociatedMetadataTypeTypeDescriptionProvider(modelType) foreach (var propertyMetadata in bindingContext.ModelMetadata.Properties)
// .GetTypeDescriptor(modelType); {
var propertyName = propertyMetadata.PropertyName;
var requiredValidator = bindingContext.GetValidators(propertyMetadata)
.FirstOrDefault(v => v.IsRequired);
// TODO: Revive HttpBindingBehaviorAttribute
// PropertyDescriptorCollection propertyDescriptors = modelDescriptor.GetProperties(); if (requiredValidator != null)
{
// // TODO: Revive HttpBindingBehavior requiredValidators[propertyName] = requiredValidator;
// // HttpBindingBehaviorAttribute typeAttr = modelDescriptor.GetAttributes().OfType<HttpBindingBehaviorAttribute>().SingleOrDefault(); requiredProperties.Add(propertyName);
}
// 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<HttpBindingBehaviorAttribute>().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);
// }
// }
//}
internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto) internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto)
{ {
// TODO: Uncomment this once we revive validation HashSet<string> requiredProperties;
Dictionary<string, IModelValidator> requiredValidators;
HashSet<string> skipProperties;
GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out requiredValidators, out skipProperties);
//HashSet<string> requiredProperties; // Eliminate provided properties from requiredProperties; leaving just *missing* required properties.
//// Dictionary<string, ModelValidator> requiredValidators; requiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName));
//HashSet<string> skipProperties;
//GetRequiredPropertiesCollection(context, bindingContext, out requiredProperties, out requiredValidators, out skipProperties);
//// Eliminate provided properties from requiredProperties; leaving just *missing* required properties. foreach (var missingRequiredProperty in requiredProperties)
//requiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName)); {
var addedError = false;
var modelStateKey = ModelBindingHelper.CreatePropertyModelName(
bindingContext.ValidationNode.ModelStateKey, missingRequiredProperty);
//foreach (string missingRequiredProperty in requiredProperties) // 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.
// string modelStateKey = ModelBindingHelper.CreatePropertyModelName( // (Otherwise, propertyMetadata.Model is likely already null.)
// bindingContext.ValidationNode.ModelStateKey, missingRequiredProperty); var propertyMetadata = bindingContext.PropertyMetadata[missingRequiredProperty];
propertyMetadata.Model = null;
// // Update Model as SetProperty() would: Place null value where validator will check for non-null. This // Execute validator (if any) to get custom error message.
// // ensures a failure result from a required validator (if any) even for a non-nullable property. IModelValidator validator;
// // (Otherwise, propertyMetadata.Model is likely already null.) if (requiredValidators.TryGetValue(missingRequiredProperty, out validator))
// ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[missingRequiredProperty]; {
// propertyMetadata.Model = null; addedError = RunValidator(validator, bindingContext, propertyMetadata, modelStateKey);
}
// // Execute validator (if any) to get custom error message. // Fall back to default message if HttpBindingBehaviorAttribute required this property or validator
// ModelValidator validator = requiredValidators[missingRequiredProperty]; // (oddly) succeeded.
// bool addedError = RunValidator(validator, bindingContext, propertyMetadata, modelStateKey); if (!addedError)
{
// // Fall back to default message if HttpBindingBehaviorAttribute required this property or validator bindingContext.ModelState.AddModelError(
// // (oddly) succeeded. modelStateKey,
// if (!addedError) Resources.FormatMissingRequiredMember(missingRequiredProperty));
// { }
// bindingContext.ModelState.AddModelError(modelStateKey, }
// Error.Format(SRResources.MissingRequiredMember, missingRequiredProperty));
// }
//}
// for each property that was bound, call the setter, recording exceptions as necessary // for each property that was bound, call the setter, recording exceptions as necessary
foreach (var entry in dto.Results) foreach (var entry in dto.Results)
{ {
ModelMetadata propertyMetadata = entry.Key; var propertyMetadata = entry.Key;
var dtoResult = entry.Value;
ComplexModelDtoResult dtoResult = entry.Value;
if (dtoResult != null) if (dtoResult != null)
{ {
SetProperty(bindingContext, propertyMetadata, dtoResult); 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.")] [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, protected virtual void SetProperty(ModelBindingContext bindingContext,
ModelMetadata propertyMetadata, ModelMetadata propertyMetadata,
ComplexModelDtoResult dtoResult ComplexModelDtoResult dtoResult)
/*, ModelValidator requiredValidator*/)
{ {
// TODO: This used TypeDescriptor which is no longer available. Lookups performed using System.ComponentModel were var property = bindingContext.ModelType
// cached. To maintain parity, we'll need to cache property lookups. .GetProperty(propertyMetadata.PropertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
PropertyInfo property = bindingContext.ModelType
.GetRuntimeProperties()
.FirstOrDefault(p => p.Name.Equals(propertyMetadata.PropertyName, StringComparison.OrdinalIgnoreCase));
if (property == null || !property.CanWrite) if (property == null || !property.CanWrite)
{ {
@ -268,21 +247,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return; return;
} }
object value = dtoResult.Model ?? GetPropertyDefaultValue(property); var value = dtoResult.Model ?? GetPropertyDefaultValue(property);
propertyMetadata.Model = value; propertyMetadata.Model = value;
//// 'Required' validators need to run first so that we can provide useful error messages if // '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 // the property setters throw, e.g. if we're setting entity keys to null.
//// DefaultModelBinder.SetProperty() for more information. if (value == null)
//if (value == null) {
//{ var modelStateKey = dtoResult.ValidationNode.ModelStateKey;
// string modelStateKey = dtoResult.ValidationNode.ModelStateKey; if (bindingContext.ModelState.IsValidField(modelStateKey))
// if (bindingContext.ModelState.IsValidField(modelStateKey)) {
// { var requiredValidator = bindingContext.GetValidators(propertyMetadata).FirstOrDefault(v => v.IsRequired);
// RunValidator(requiredValidator, bindingContext, propertyMetadata, modelStateKey); 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()) if (value != null || property.PropertyType.AllowsNullValue())
{ {
@ -293,39 +279,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
catch (Exception ex) catch (Exception ex)
{ {
// don't display a duplicate error message if a binding error has already occurred for this field // don't display a duplicate error message if a binding error has already occurred for this field
//string modelStateKey = dtoResult.ValidationNode.ModelStateKey; var modelStateKey = dtoResult.ValidationNode.ModelStateKey;
//if (bindingContext.ModelState.IsValidField(modelStateKey)) if (bindingContext.ModelState.IsValidField(modelStateKey))
//{ {
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex); bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
//} }
} }
} }
else else
{ {
// trying to set a non-nullable value type to null, need to make sure there's a message // trying to set a non-nullable value type to null, need to make sure there's a message
//string modelStateKey = dtoResult.ValidationNode.ModelStateKey; var modelStateKey = dtoResult.ValidationNode.ModelStateKey;
//if (bindingContext.ModelState.IsValidField(modelStateKey)) if (bindingContext.ModelState.IsValidField(modelStateKey))
//{ {
// dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(propertyMetadata, value); dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(propertyMetadata, value);
//} }
} }
} }
//// Returns true if validator execution adds a model error. // Returns true if validator execution adds a model error.
//private static bool RunValidator(ModelValidator validator, ModelBindingContext bindingContext, private static bool RunValidator(IModelValidator validator,
// ModelMetadata propertyMetadata, string modelStateKey) ModelBindingContext bindingContext,
//{ ModelMetadata propertyMetadata,
// bool addedError = false; string modelStateKey)
// if (validator != null) {
// { var validationContext = bindingContext.CreateValidationContext(propertyMetadata);
// foreach (ModelValidationResult validationResult in validator.Validate(propertyMetadata, bindingContext.Model))
// {
// bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
// addedError = true;
// }
// }
// return addedError; var addedError = false;
//} foreach (var validationResult in validator.Validate(validationContext))
{
bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
addedError = true;
}
return addedError;
}
} }
} }

View File

@ -1,4 +1,5 @@
using Microsoft.AspNet.Mvc.ModelBinding.Internal; using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
@ -17,17 +18,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
object model = valueProviderResult.RawValue; object model = valueProviderResult.RawValue;
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model); ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model);
bindingContext.Model = model; bindingContext.Model = model;
if (bindingContext.ModelMetadata.IsComplexType)
{ // TODO: Determine if we need IBodyValidator here.
// TODO: Validation
//IBodyModelValidator validator = services.GetBodyModelValidator();
//ModelMetadataProvider metadataProvider = services.GetModelMetadataProvider();
//if (validator != null && metadataProvider != null)
//{
// validator.Validate(model, bindingContext.ModelType, metadataProvider, context, bindingContext.ModelName);
//}
}
return true; return true;
} }

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
{
public static class DictionaryHelper
{
public static IEnumerable<KeyValuePair<string, TValue>> FindKeysWithPrefix<TValue>([NotNull] IDictionary<string, TValue> dictionary,
[NotNull] string prefix)
{
TValue exactMatchValue;
if (dictionary.TryGetValue(prefix, out exactMatchValue))
{
yield return new KeyValuePair<string, TValue>(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;
}
}
}
}
}
}

View File

@ -1,6 +1,6 @@
using System; using System;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
{ {
internal class EfficientTypePropertyKey<T1, T2> : Tuple<T1, T2> internal class EfficientTypePropertyKey<T1, T2> : Tuple<T1, T2>
{ {

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
{
internal static class ModelBindingContextExtensions
{
public static IEnumerable<IModelValidator> 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<ModelValidationResult> 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);
}
}
}

View File

@ -14,28 +14,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
private readonly ConcurrentDictionary<Type, TypeInformation> _typeInfoCache = new ConcurrentDictionary<Type, TypeInformation>(); private readonly ConcurrentDictionary<Type, TypeInformation> _typeInfoCache = new ConcurrentDictionary<Type, TypeInformation>();
public IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType) public IEnumerable<ModelMetadata> GetMetadataForProperties(object container, [NotNull] Type containerType)
{ {
if (containerType == null)
{
throw Error.ArgumentNull("containerType");
}
return GetMetadataForPropertiesCore(container, containerType); return GetMetadataForPropertiesCore(container, containerType);
} }
public ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName) public ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, [NotNull] Type containerType, [NotNull] string propertyName)
{ {
if (containerType == null) if (string.IsNullOrEmpty(propertyName))
{
throw Error.ArgumentNull("containerType");
}
if (String.IsNullOrEmpty(propertyName))
{ {
throw Error.ArgumentNullOrEmpty("propertyName"); throw Error.ArgumentNullOrEmpty("propertyName");
} }
TypeInformation typeInfo = GetTypeInformation(containerType); var typeInfo = GetTypeInformation(containerType);
PropertyInformation propertyInfo; PropertyInformation propertyInfo;
if (!typeInfo.Properties.TryGetValue(propertyName, out propertyInfo)) if (!typeInfo.Properties.TryGetValue(propertyName, out propertyInfo))
{ {
@ -45,13 +36,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor); return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor);
} }
public ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType) public ModelMetadata GetMetadataForType(Func<object> modelAccessor, [NotNull] Type modelType)
{ {
if (modelType == null)
{
throw Error.ArgumentNull("modelType");
}
TModelMetadata prototype = GetTypeInformation(modelType).Prototype; TModelMetadata prototype = GetTypeInformation(modelType).Prototype;
return CreateMetadataFromPrototype(prototype, modelAccessor); return CreateMetadataFromPrototype(prototype, modelAccessor);
} }
@ -68,10 +54,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private IEnumerable<ModelMetadata> GetMetadataForPropertiesCore(object container, Type containerType) private IEnumerable<ModelMetadata> GetMetadataForPropertiesCore(object container, Type containerType)
{ {
TypeInformation typeInfo = GetTypeInformation(containerType); var typeInfo = GetTypeInformation(containerType);
foreach (KeyValuePair<string, PropertyInformation> kvp in typeInfo.Properties) foreach (var kvp in typeInfo.Properties)
{ {
PropertyInformation propertyInfo = kvp.Value; var propertyInfo = kvp.Value;
Func<object> modelAccessor = null; Func<object> modelAccessor = null;
if (container != null) if (container != null)
{ {
@ -96,24 +82,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private TypeInformation CreateTypeInformation(Type type, IEnumerable<Attribute> associatedAttributes) private TypeInformation CreateTypeInformation(Type type, IEnumerable<Attribute> associatedAttributes)
{ {
TypeInfo typeInfo = type.GetTypeInfo(); var typeInfo = type.GetTypeInfo();
IEnumerable<Attribute> attributes = typeInfo.GetCustomAttributes(); var attributes = typeInfo.GetCustomAttributes();
if (associatedAttributes != null) if (associatedAttributes != null)
{ {
attributes = attributes.Concat(associatedAttributes); attributes = attributes.Concat(associatedAttributes);
} }
TypeInformation info = new TypeInformation var info = new TypeInformation
{ {
Prototype = CreateMetadataPrototype(attributes, containerType: null, modelType: type, propertyName: null) 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<string, PropertyInformation> properties = new Dictionary<string, PropertyInformation>(); var properties = new Dictionary<string, PropertyInformation>();
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
// 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))
{ {
// Avoid re-generating a property descriptor if one has already been generated for the property name // Avoid re-generating a property descriptor if one has already been generated for the property name
if (!properties.ContainsKey(property.Name)) if (!properties.ContainsKey(property.Name))
@ -128,21 +109,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private PropertyInformation CreatePropertyInformation(Type containerType, PropertyInfo property) private PropertyInformation CreatePropertyInformation(Type containerType, PropertyInfo property)
{ {
PropertyInformation info = new PropertyInformation(); var info = new PropertyInformation();
info.ValueAccessor = CreatePropertyValueAccessor(property); info.ValueAccessor = CreatePropertyValueAccessor(property);
info.Prototype = CreateMetadataPrototype(property.GetCustomAttributes().Cast<Attribute>(), containerType, property.PropertyType, property.Name); info.Prototype = CreateMetadataPrototype(property.GetCustomAttributes(),
containerType,
property.PropertyType,
property.Name);
return info; return info;
} }
private static Func<object, object> CreatePropertyValueAccessor(PropertyInfo property) private static Func<object, object> CreatePropertyValueAccessor(PropertyInfo property)
{ {
Type declaringType = property.DeclaringType; var declaringType = property.DeclaringType;
TypeInfo declaringTypeInfo = declaringType.GetTypeInfo(); var declaringTypeInfo = declaringType.GetTypeInfo();
if (declaringTypeInfo.IsVisible) if (declaringTypeInfo.IsVisible)
{ {
if (property.CanRead) if (property.CanRead)
{ {
MethodInfo getMethodInfo = property.GetMethod; var getMethodInfo = property.GetMethod;
if (getMethodInfo != null) if (getMethodInfo != null)
{ {
return CreateDynamicValueAccessor(getMethodInfo, declaringType, property.Name); return CreateDynamicValueAccessor(getMethodInfo, declaringType, property.Name);
@ -161,10 +145,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
Contract.Assert(getMethodInfo != null && getMethodInfo.IsPublic && !getMethodInfo.IsStatic); Contract.Assert(getMethodInfo != null && getMethodInfo.IsPublic && !getMethodInfo.IsStatic);
TypeInfo declaringTypeInfo = declaringType.GetTypeInfo(); var declaringTypeInfo = declaringType.GetTypeInfo();
Type propertyType = getMethodInfo.ReturnType; var propertyType = getMethodInfo.ReturnType;
DynamicMethod dynamicMethod = new DynamicMethod("Get" + propertyName + "From" + declaringType.Name, typeof(object), new Type[] { typeof(object) }); var dynamicMethod = new DynamicMethod("Get" + propertyName + "From" + declaringType.Name,
ILGenerator ilg = dynamicMethod.GetILGenerator(); typeof(object),
new [] { typeof(object) });
var ilg = dynamicMethod.GetILGenerator();
// Load the container onto the stack, convert from object => declaring type for the property // Load the container onto the stack, convert from object => declaring type for the property
ilg.Emit(OpCodes.Ldarg_0); ilg.Emit(OpCodes.Ldarg_0);

View File

@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
@ -9,6 +10,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
private string _modelName; private string _modelName;
private ModelStateDictionary _modelState; private ModelStateDictionary _modelState;
private Dictionary<string, ModelMetadata> _propertyMetadata;
private ModelValidationNode _validationNode;
public ModelBindingContext() public ModelBindingContext()
{ {
@ -24,6 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ValueProvider = bindingContext.ValueProvider; ValueProvider = bindingContext.ValueProvider;
MetadataProvider = bindingContext.MetadataProvider; MetadataProvider = bindingContext.MetadataProvider;
ModelBinder = bindingContext.ModelBinder; ModelBinder = bindingContext.ModelBinder;
ValidatorProviders = bindingContext.ValidatorProviders;
HttpContext = bindingContext.HttpContext; HttpContext = bindingContext.HttpContext;
} }
} }
@ -102,6 +106,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
set; set;
} }
public IEnumerable<IModelValidatorProvider> ValidatorProviders
{
get;
set;
}
public IDictionary<string, ModelMetadata> 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() private void EnsureModelMetadata()
{ {
if (ModelMetadata == null) if (ModelMetadata == null)

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding 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) foreach (var entry in dictionary)
{ {
@ -35,7 +36,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
GetModelStateForKey(key).Errors.Add(errorMessage); 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; ModelState modelState;
if (!TryGetValue(key, out modelState)) if (!TryGetValue(key, out modelState))

View File

@ -58,6 +58,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("JQuerySyntaxMissingClosingBracket"); return GetString("JQuerySyntaxMissingClosingBracket");
} }
/// <summary>
/// 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)].
/// </summary>
internal static string MissingDataMemberIsRequired
{
get { return GetString("MissingDataMemberIsRequired"); }
}
/// <summary>
/// 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)].
/// </summary>
internal static string FormatMissingDataMemberIsRequired(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MissingDataMemberIsRequired"), p0, p1);
}
/// <summary>
/// The '{0}' property is required.
/// </summary>
internal static string MissingRequiredMember
{
get { return GetString("MissingRequiredMember"); }
}
/// <summary>
/// The '{0}' property is required.
/// </summary>
internal static string FormatMissingRequiredMember(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MissingRequiredMember"), p0);
}
/// <summary> /// <summary>
/// The value '{0}' is not valid for {1}. /// The value '{0}' is not valid for {1}.
/// </summary> /// </summary>
@ -170,6 +202,70 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("ModelBindingContext_ModelMetadataMustBeSet"); return GetString("ModelBindingContext_ModelMetadataMustBeSet");
} }
/// <summary>
/// The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'.
/// </summary>
internal static string ValidatableObjectAdapter_IncompatibleType
{
get { return GetString("ValidatableObjectAdapter_IncompatibleType"); }
}
/// <summary>
/// The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'.
/// </summary>
internal static string FormatValidatableObjectAdapter_IncompatibleType(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ValidatableObjectAdapter_IncompatibleType"), p0, p1);
}
/// <summary>
/// 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.
/// </summary>
internal static string ValidationAttributeOnField
{
get { return GetString("ValidationAttributeOnField"); }
}
/// <summary>
/// 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.
/// </summary>
internal static string FormatValidationAttributeOnField(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ValidationAttributeOnField"), p0, p1);
}
/// <summary>
/// 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.
/// </summary>
internal static string ValidationAttributeOnNonPublicProperty
{
get { return GetString("ValidationAttributeOnNonPublicProperty"); }
}
/// <summary>
/// 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.
/// </summary>
internal static string FormatValidationAttributeOnNonPublicProperty(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ValidationAttributeOnNonPublicProperty"), p0, p1);
}
/// <summary>
/// A value is required but was not present in the request.
/// </summary>
internal static string Validation_ValueNotFound
{
get { return GetString("Validation_ValueNotFound"); }
}
/// <summary>
/// A value is required but was not present in the request.
/// </summary>
internal static string FormatValidation_ValueNotFound()
{
return GetString("Validation_ValueNotFound");
}
/// <summary> /// <summary>
/// The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information. /// The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.
/// </summary> /// </summary>

View File

@ -126,6 +126,12 @@
<data name="JQuerySyntaxMissingClosingBracket" xml:space="preserve"> <data name="JQuerySyntaxMissingClosingBracket" xml:space="preserve">
<value>The key is invalid JQuery syntax because it is missing a closing bracket.</value> <value>The key is invalid JQuery syntax because it is missing a closing bracket.</value>
</data> </data>
<data name="MissingDataMemberIsRequired" xml:space="preserve">
<value>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)].</value>
</data>
<data name="MissingRequiredMember" xml:space="preserve">
<value>The '{0}' property is required.</value>
</data>
<data name="ModelBinderConfig_ValueInvalid" xml:space="preserve"> <data name="ModelBinderConfig_ValueInvalid" xml:space="preserve">
<value>The value '{0}' is not valid for {1}.</value> <value>The value '{0}' is not valid for {1}.</value>
</data> </data>
@ -147,6 +153,18 @@
<data name="ModelBindingContext_ModelMetadataMustBeSet" xml:space="preserve"> <data name="ModelBindingContext_ModelMetadataMustBeSet" xml:space="preserve">
<value>The ModelMetadata property must be set before accessing this property.</value> <value>The ModelMetadata property must be set before accessing this property.</value>
</data> </data>
<data name="ValidatableObjectAdapter_IncompatibleType" xml:space="preserve">
<value>The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'.</value>
</data>
<data name="ValidationAttributeOnField" xml:space="preserve">
<value>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.</value>
</data>
<data name="ValidationAttributeOnNonPublicProperty" xml:space="preserve">
<value>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.</value>
</data>
<data name="Validation_ValueNotFound" xml:space="preserve">
<value>A value is required but was not present in the request.</value>
</data>
<data name="ValueProviderResult_ConversionThrew" xml:space="preserve"> <data name="ValueProviderResult_ConversionThrew" xml:space="preserve">
<value>The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.</value> <value>The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.</value>
</data> </data>

View File

@ -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<IModelValidator> 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<IModelValidator> GetValidators(ModelMetadata metadata,
IEnumerable<Attribute> attributes);
private IEnumerable<IModelValidator> 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<IModelValidator> GetValidatorsForType(ModelMetadata metadata)
{
var attributes = metadata.ModelType
.GetTypeInfo()
.GetCustomAttributes();
return GetValidators(metadata, attributes);
}
}
}

View File

@ -0,0 +1,56 @@
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class CompositeModelValidator : IModelValidator
{
private readonly IEnumerable<IModelValidator> _validators;
public CompositeModelValidator(IEnumerable<IModelValidator> validators)
{
_validators = validators;
}
public bool IsRequired
{
get { return false; }
}
public IEnumerable<ModelValidationResult> 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);
}
}
}

View File

@ -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<ModelValidationResult> 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<ModelValidationResult>();
}
}
}

View File

@ -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
{
/// <summary>
/// An implementation of <see cref="ModelValidatorProvider"/> which providers validators
/// for attributes which derive from <see cref="ValidationAttribute"/>. It also provides
/// a validator for types which implement <see cref="IValidatableObject"/>. To support
/// client side validation, you can either register adapters through the static methods
/// on this class, or by having your validation attributes implement
/// <see cref="IClientValidatable"/>. The logic to support IClientValidatable
/// is implemented in <see cref="DataAnnotationsModelValidator"/>.
/// </summary>
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<Type, DataAnnotationsModelValidationFactory> AttributeFactories =
new Dictionary<Type, DataAnnotationsModelValidationFactory>();
#if NET45
// Factories for IValidatableObject models
private static DataAnnotationsValidatableObjectAdapterFactory DefaultValidatableFactory =
() => new ValidatableObjectAdapter();
#endif
private static Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory> ValidatableFactories =
new Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory>();
public static bool AddImplicitRequiredAttributeForValueTypes
{
get { return _addImplicitRequiredAttributeForValueTypes; }
set { _addImplicitRequiredAttributeForValueTypes = value; }
}
protected override IEnumerable<IModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<Attribute> attributes)
{
var results = new List<IModelValidator>();
// Produce a validator for each validation attribute we find
foreach (var attribute in attributes.OfType<ValidationAttribute>())
{
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;
}
}
}

View File

@ -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
{
/// <summary>
/// This <see cref="ModelValidatorProvider"/> provides a required ModelValidator for members marked as [DataMember(IsRequired=true)].
/// </summary>
public class DataMemberModelValidatorProvider : AssociatedValidatorProvider
{
protected override IEnumerable<IModelValidator> GetValidators(ModelMetadata metadata,
IEnumerable<Attribute> attributes)
{
// Types cannot be required; only properties can
if (metadata.ContainerType == null || string.IsNullOrEmpty(metadata.PropertyName))
{
return Enumerable.Empty<IModelValidator>();
}
if (IsRequiredDataMember(metadata.ContainerType, attributes))
{
return new[] { new RequiredMemberModelValidator() };
}
return Enumerable.Empty<IModelValidator>();
}
internal static bool IsRequiredDataMember(Type containerType, IEnumerable<Attribute> attributes)
{
var dataMemberAttribute = attributes.OfType<DataMemberAttribute>()
.FirstOrDefault();
if (dataMemberAttribute != null)
{
// isDataContract == true iff the container type has at least one DataContractAttribute
bool isDataContract = containerType.GetTypeInfo()
.GetCustomAttributes()
.OfType<DataContractAttribute>()
.Any();
if (isDataContract && dataMemberAttribute.IsRequired)
{
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A <see cref="IModelValidator"/> 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.
/// </summary>
public class ErrorModelValidator : IModelValidator
{
private readonly string _errorMessage;
public ErrorModelValidator([NotNull] string errorMessage)
{
_errorMessage = errorMessage;
}
public bool IsRequired { get { return false; } }
public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
{
throw new InvalidOperationException(_errorMessage);
}
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IModelValidator
{
bool IsRequired { get; }
IEnumerable<ModelValidationResult> Validate(ModelValidationContext context);
}
}

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IModelValidatorProvider
{
IEnumerable<IModelValidator> GetValidators(ModelMetadata metadata);
}
}

View File

@ -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<IModelValidator> GetValidators(ModelMetadata metadata,
IEnumerable<Attribute> 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));
}
}
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<IModelValidatorProvider> 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<IModelValidatorProvider> ValidatorProviders { get; private set; }
}
}

View File

@ -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<ModelValidationNode> _childNodes;
public ModelValidationNode(ModelMetadata modelMetadata, string modelStateKey)
: this(modelMetadata, modelStateKey, null)
{
}
public ModelValidationNode([NotNull] ModelMetadata modelMetadata,
[NotNull] string modelStateKey,
IEnumerable<ModelValidationNode> childNodes)
{
ModelMetadata = modelMetadata;
ModelStateKey = modelStateKey;
_childNodes = (childNodes != null) ? childNodes.ToList() : new List<ModelValidationNode>();
}
public event EventHandler<ModelValidatedEventArgs> Validated;
public event EventHandler<ModelValidatingEventArgs> Validating;
public ICollection<ModelValidationNode> 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<IModelValidator> GetValidators(ModelValidationContext validationContext, ModelMetadata metadata)
{
return validationContext.ValidatorProviders.SelectMany(vp => vp.GetValidators(metadata));
}
}
}

View File

@ -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; }
}
}

View File

@ -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<ModelValidationResult> Validate(ModelValidationContext context)
{
return Enumerable.Empty<ModelValidationResult>();
}
}
}

View File

@ -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<ModelValidationResult> Validate(ModelValidationContext context)
{
var model = context.ModelMetadata.Model;
if (model == null)
{
return Enumerable.Empty<ModelValidationResult>();
}
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<ModelValidationResult> ConvertResults(IEnumerable<ValidationResult> 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

View File

@ -2,12 +2,17 @@
"version": "0.1-alpha-*", "version": "0.1-alpha-*",
"dependencies": { "dependencies": {
"Common": "", "Common": "",
"Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*", "Microsoft.AspNet.DependencyInjection": "0.1-alpha-*",
"Microsoft.AspNet.Abstractions": "0.1-alpha-*", "Microsoft.AspNet.Abstractions": "0.1-alpha-*",
"Newtonsoft.Json": "5.0.8" "Newtonsoft.Json": "5.0.8"
}, },
"configurations": { "configurations": {
"net45": {}, "net45": {
"dependencies": {
"System.ComponentModel.DataAnnotations": "",
"System.Runtime.Serialization": ""
}
},
"k10": { "k10": {
"dependencies": { "dependencies": {
"System.Collections": "4.0.0.0", "System.Collections": "4.0.0.0",
@ -22,12 +27,15 @@
"System.Reflection": "4.0.10.0", "System.Reflection": "4.0.10.0",
"System.Reflection.Emit.ILGeneration": "4.0.0.0", "System.Reflection.Emit.ILGeneration": "4.0.0.0",
"System.Reflection.Emit.Lightweight": "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.Extensions": "4.0.0.0",
"System.Reflection.Primitives": "4.0.0.0", "System.Reflection.Primitives": "4.0.0.0",
"System.Resources.ResourceManager": "4.0.0.0", "System.Resources.ResourceManager": "4.0.0.0",
"System.Runtime": "4.0.20.0", "System.Runtime": "4.0.20.0",
"System.Runtime.Extensions": "4.0.10.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-*"
} }
} }
} }

View File

@ -61,10 +61,13 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IModelBinder, MutableObjectModelBinder>(); yield return describe.Transient<IModelBinder, MutableObjectModelBinder>();
yield return describe.Transient<IModelBinder, ComplexModelDtoModelBinder>(); yield return describe.Transient<IModelBinder, ComplexModelDtoModelBinder>();
yield return describe.Transient<IInputFormatter, JsonInputFormatter>();
yield return describe.Transient<INestedProviderManager<FilterProviderContext>, NestedProviderManager<FilterProviderContext>>(); yield return describe.Transient<INestedProviderManager<FilterProviderContext>, NestedProviderManager<FilterProviderContext>>();
yield return describe.Transient<INestedProvider<FilterProviderContext>, DefaultFilterProvider>(); yield return describe.Transient<INestedProvider<FilterProviderContext>, DefaultFilterProvider>();
yield return describe.Transient<IInputFormatter, JsonInputFormatter>(); yield return describe.Singleton<IModelValidatorProvider, DataAnnotationsModelValidatorProvider>();
yield return describe.Singleton<IModelValidatorProvider, DataMemberModelValidatorProvider>();
} }
} }
} }

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using Moq; using Moq;
using Xunit; using Xunit;
@ -20,12 +21,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>();
// Act // Act
List<int> boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, new[] { "foo", "bar", "baz" }); var boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, new[] { "foo", "bar", "baz" });
// Assert // Assert
Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray()); 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] [Fact]
@ -42,12 +42,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>();
// Act // Act
List<int> boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null); var boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null);
// Assert // Assert
Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray()); 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] [Fact]
@ -97,7 +96,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>();
// Act // Act
List<int> 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
Assert.NotNull(boundCollection); Assert.NotNull(boundCollection);
@ -111,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>();
// Act // Act
List<int> boundCollection = binder.BindSimpleCollection(bindingContext: null, rawValue: null, culture: null); var boundCollection = binder.BindSimpleCollection(bindingContext: null, rawValue: null, culture: null);
// Assert // Assert
Assert.Null(boundCollection); Assert.Null(boundCollection);
@ -124,25 +123,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var culture = CultureInfo.GetCultureInfo("fr-FR"); var culture = CultureInfo.GetCultureInfo("fr-FR");
var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider()); var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider());
// TODO: Validation ModelValidationNode childValidationNode = null;
// ModelValidationNode childValidationNode = null;
Mock.Get<IModelBinder>(bindingContext.ModelBinder) Mock.Get<IModelBinder>(bindingContext.ModelBinder)
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>())) .Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) => .Returns((ModelBindingContext mbc) =>
{ {
Assert.Equal("someName", mbc.ModelName); Assert.Equal("someName", mbc.ModelName);
// childValidationNode = mbc.ValidationNode; childValidationNode = mbc.ValidationNode;
mbc.Model = 42; mbc.Model = 42;
return true; return true;
}); });
var modelBinder = new CollectionModelBinder<int>(); var modelBinder = new CollectionModelBinder<int>();
// Act // Act
List<int> boundCollection = modelBinder.BindSimpleCollection(bindingContext, new int[1], culture); var boundCollection = modelBinder.BindSimpleCollection(bindingContext, new int[1], culture);
// Assert // Assert
Assert.Equal(new[] { 42 }, boundCollection.ToArray()); 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) private static ModelBindingContext GetModelBindingContext(IValueProvider valueProvider)

View File

@ -4,36 +4,34 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ {
public class ComplexModelDtoResultTest public class ComplexModelDtoResultTest
{ {
// TODO: Validation [Fact]
////[Fact] public void Constructor_ThrowsIfValidationNodeIsNull()
////public void Constructor_ThrowsIfValidationNodeIsNull() {
////{ // Act & assert
//// // Act & assert ExceptionAssert.ThrowsArgumentNull(
//// ExceptionAssert.ThrowsArgumentNull( () => new ComplexModelDtoResult("some string", validationNode: null),
//// () => new ComplexModelDtoResult("some string"), "validationNode");
//// "validationNode"); }
////}
// TODO: Validation [Fact]
//[Fact] public void Constructor_SetsProperties()
//public void Constructor_SetsProperties() {
//{ // Arrange
// // Arrange var validationNode = GetValidationNode();
// ModelValidationNode validationNode = GetValidationNode();
// // Act // Act
// ComplexModelDtoResult result = new ComplexModelDtoResult("some string", validationNode); var result = new ComplexModelDtoResult("some string", validationNode);
// // Assert // Assert
// Assert.Equal("some string", result.Model); Assert.Equal("some string", result.Model);
// Assert.Equal(validationNode, result.ValidationNode); Assert.Equal(validationNode, result.ValidationNode);
//} }
//private static ModelValidationNode GetValidationNode() private static ModelValidationNode GetValidationNode()
//{ {
// EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider(); var provider = new EmptyModelMetadataProvider();
// ModelMetadata metadata = provider.GetMetadataForType(null, typeof(object)); var metadata = provider.GetMetadataForType(null, typeof(object));
// return new ModelValidationNode(metadata, "someKey"); return new ModelValidationNode(metadata, "someKey");
//}
} }
} }
}

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using Moq; using Moq;
using Xunit; using Xunit;
@ -12,22 +13,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public void BindModel_SuccessfulBind_RunsValidationAndReturnsModel() public void BindModel_SuccessfulBind_RunsValidationAndReturnsModel()
{ {
// Arrange // Arrange
bool validationCalled = false; var validationCalled = false;
ModelBindingContext bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)), ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
ModelName = "someName", ModelName = "someName",
//ModelState = executionContext.Controller.ViewData.ModelState, ModelState = new ModelStateDictionary(),
//PropertyFilter = _ => true,
ValueProvider = new SimpleValueProvider ValueProvider = new SimpleValueProvider
{ {
{ "someName", "dummyValue" } { "someName", "dummyValue" }
} },
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
}; };
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>(); var mockIntBinder = new Mock<IModelBinder>();
mockIntBinder mockIntBinder
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>())) .Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
.Returns( .Returns(
@ -38,22 +39,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Same(bindingContext.ValueProvider, context.ValueProvider); Assert.Same(bindingContext.ValueProvider, context.ValueProvider);
context.Model = 42; context.Model = 42;
// TODO: Validation bindingContext.ValidationNode.Validating += delegate { validationCalled = true; };
// mbc.ValidationNode.Validating += delegate { validationCalled = true; };
return true; return true;
}); });
//binderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */); var shimBinder = new CompositeModelBinder(mockIntBinder.Object);
IModelBinder shimBinder = new CompositeModelBinder(mockIntBinder.Object);
// Act // Act
bool isBound = shimBinder.BindModel(bindingContext); var isBound = shimBinder.BindModel(bindingContext);
// Assert // Assert
Assert.True(isBound); Assert.True(isBound);
Assert.Equal(42, bindingContext.Model); Assert.Equal(42, bindingContext.Model);
// TODO: Validation
// Assert.True(validationCalled); Assert.True(validationCalled);
Assert.True(bindingContext.ModelState.IsValid); Assert.True(bindingContext.ModelState.IsValid);
} }
@ -61,23 +60,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public void BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel() public void BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel()
{ {
// Arrange // Arrange
bool validationCalled = false; var validationCalled = false;
List<int> expectedModel = new List<int> { 1, 2, 3, 4, 5 }; var expectedModel = new List<int> { 1, 2, 3, 4, 5 };
ModelBindingContext bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)), ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
ModelName = "someName", ModelName = "someName",
//ModelState = executionContext.Controller.ViewData.ModelState, ModelState = new ModelStateDictionary(),
//PropertyFilter = _ => true,
ValueProvider = new SimpleValueProvider ValueProvider = new SimpleValueProvider
{ {
{ "someOtherName", "dummyValue" } { "someOtherName", "dummyValue" }
} },
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
}; };
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>(); var mockIntBinder = new Mock<IModelBinder>();
mockIntBinder mockIntBinder
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>())) .Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
.Returns( .Returns(
@ -93,12 +92,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider); Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
mbc.Model = expectedModel; mbc.Model = expectedModel;
// TODO: Validation mbc.ValidationNode.Validating += delegate { validationCalled = true; };
// mbc.ValidationNode.Validating += delegate { validationCalled = true; };
return true; return true;
}); });
//binderProviders.RegisterBinderForType(typeof(List<int>), mockIntBinder.Object, false /* suppressPrefixCheck */);
IModelBinder shimBinder = new CompositeModelBinder(mockIntBinder.Object); IModelBinder shimBinder = new CompositeModelBinder(mockIntBinder.Object);
// Act // Act
@ -107,36 +104,34 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.True(isBound); Assert.True(isBound);
Assert.Equal(expectedModel, bindingContext.Model); Assert.Equal(expectedModel, bindingContext.Model);
// TODO: Validation Assert.True(validationCalled);
// Assert.True(validationCalled); Assert.True(bindingContext.ModelState.IsValid);
// Assert.True(bindingContext.ModelState.IsValid);
} }
[Fact] [Fact]
public void BindModel_UnsuccessfulBind_BinderFails_ReturnsNull() public void BindModel_UnsuccessfulBind_BinderFails_ReturnsNull()
{ {
// Arrange // Arrange
Mock<IModelBinder> mockListBinder = new Mock<IModelBinder>(); var mockListBinder = new Mock<IModelBinder>();
mockListBinder.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>())) mockListBinder.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
.Returns(false) .Returns(false)
.Verifiable(); .Verifiable();
IModelBinder shimBinder = (IModelBinder)mockListBinder.Object; var shimBinder = (IModelBinder)mockListBinder.Object;
ModelBindingContext bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
FallbackToEmptyPrefix = false, FallbackToEmptyPrefix = false,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)), ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
}; };
// Act // Act
bool isBound = shimBinder.BindModel(bindingContext); var isBound = shimBinder.BindModel(bindingContext);
// Assert // Assert
Assert.False(isBound); Assert.False(isBound);
Assert.Null(bindingContext.Model); Assert.Null(bindingContext.Model);
// TODO: Validation Assert.True(bindingContext.ModelState.IsValid);
// Assert.True(bindingContext.ModelState.IsValid);
mockListBinder.Verify(); mockListBinder.Verify();
} }
@ -145,17 +140,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ {
// Arrange // Arrange
var innerBinder = Mock.Of<IModelBinder>(); var innerBinder = Mock.Of<IModelBinder>();
CompositeModelBinder shimBinder = new CompositeModelBinder(innerBinder); var shimBinder = new CompositeModelBinder(innerBinder);
ModelBindingContext bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)), ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
//ModelState = executionContext.Controller.ViewData.ModelState ModelState = new ModelStateDictionary()
}; };
// Act // Act
bool isBound = shimBinder.BindModel(bindingContext); var isBound = shimBinder.BindModel(bindingContext);
// Assert // Assert
Assert.False(isBound); Assert.False(isBound);

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Moq; using Moq;
using Xunit; using Xunit;
@ -11,7 +12,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ {
// Arrange // Arrange
var valueProvider = new SimpleHttpValueProvider(); var valueProvider = new SimpleHttpValueProvider();
ModelBindingContext bindingContext = GetBindingContext(valueProvider, Mock.Of<IModelBinder>()); var bindingContext = GetBindingContext(valueProvider, Mock.Of<IModelBinder>());
var binder = new KeyValuePairModelBinder<int, string>(); var binder = new KeyValuePairModelBinder<int, string>();
// Act // Act
@ -20,8 +21,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.False(retVal); Assert.False(retVal);
Assert.Null(bindingContext.Model); Assert.Null(bindingContext.Model);
// TODO: Validation Assert.Empty(bindingContext.ValidationNode.ChildNodes);
// Assert.Empty(bindingContext.ValidationNode.ChildNodes);
} }
[Fact] [Fact]
@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ {
// Arrange // Arrange
var valueProvider = new SimpleHttpValueProvider(); var valueProvider = new SimpleHttpValueProvider();
ModelBindingContext bindingContext = GetBindingContext(valueProvider); var bindingContext = GetBindingContext(valueProvider);
var binder = new KeyValuePairModelBinder<int, string>(); var binder = new KeyValuePairModelBinder<int, string>();
// Act // Act
@ -38,28 +38,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.True(retVal); Assert.True(retVal);
Assert.Null(bindingContext.Model); 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] [Fact]
public void BindModel_SubBindingSucceeds() public void BindModel_SubBindingSucceeds()
{ {
// Arrange // Arrange
IModelBinder innerBinder = new CompositeModelBinder(CreateStringBinder(), CreateIntBinder()); var innerBinder = new CompositeModelBinder(CreateStringBinder(), CreateIntBinder());
var valueProvider = new SimpleHttpValueProvider(); var valueProvider = new SimpleHttpValueProvider();
ModelBindingContext bindingContext = GetBindingContext(valueProvider, innerBinder); var bindingContext = GetBindingContext(valueProvider, innerBinder);
var binder = new KeyValuePairModelBinder<int, string>(); var binder = new KeyValuePairModelBinder<int, string>();
// Act // Act
bool retVal = binder.BindModel(bindingContext); var retVal = binder.BindModel(bindingContext);
// Assert // Assert
Assert.True(retVal); Assert.True(retVal);
Assert.Equal(new KeyValuePair<int, string>(42, "some-value"), bindingContext.Model); Assert.Equal(new KeyValuePair<int, string>(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] [Fact]
@ -71,13 +69,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Act // Act
int model; int model;
bool retVal = binder.TryBindStrongModel(bindingContext, "key", out model); var retVal = binder.TryBindStrongModel(bindingContext, "key", out model);
// Assert // Assert
Assert.True(retVal); Assert.True(retVal);
Assert.Equal(42, model); Assert.Equal(42, model);
// TODO: Validation Assert.Single(bindingContext.ValidationNode.ChildNodes);
// Assert.Single(bindingContext.ValidationNode.ChildNodes);
Assert.Empty(bindingContext.ModelState); Assert.Empty(bindingContext.ModelState);
} }
@ -100,33 +97,33 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Act // Act
int model; int model;
bool retVal = binder.TryBindStrongModel(bindingContext, "key", out model); var retVal = binder.TryBindStrongModel(bindingContext, "key", out model);
// Assert // Assert
Assert.True(retVal); Assert.True(retVal);
Assert.Equal(default(int), model); Assert.Equal(default(int), model);
// TODO: Validation Assert.Single(bindingContext.ValidationNode.ChildNodes);
// Assert.Single(bindingContext.ValidationNode.ChildNodes);
Assert.Empty(bindingContext.ModelState); Assert.Empty(bindingContext.ModelState);
} }
private static ModelBindingContext GetBindingContext(IValueProvider valueProvider, IModelBinder innerBinder = null) private static ModelBindingContext GetBindingContext(IValueProvider valueProvider, IModelBinder innerBinder = null)
{ {
var metataProvider = new EmptyModelMetadataProvider(); var metataProvider = new EmptyModelMetadataProvider();
ModelBindingContext bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
ModelMetadata = metataProvider.GetMetadataForType(null, typeof(KeyValuePair<int, string>)), ModelMetadata = metataProvider.GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
ModelName = "someName", ModelName = "someName",
ValueProvider = valueProvider, ValueProvider = valueProvider,
ModelBinder = innerBinder ?? CreateIntBinder(), ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider MetadataProvider = metataProvider,
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
}; };
return bindingContext; return bindingContext;
} }
private static IModelBinder CreateIntBinder() private static IModelBinder CreateIntBinder()
{ {
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>(); var mockIntBinder = new Mock<IModelBinder>();
mockIntBinder mockIntBinder
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>())) .Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) => .Returns((ModelBindingContext mbc) =>
@ -143,8 +140,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
private static IModelBinder CreateStringBinder() private static IModelBinder CreateStringBinder()
{ {
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>(); var mockStringBinder = new Mock<IModelBinder>();
mockIntBinder mockStringBinder
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>())) .Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) => .Returns((ModelBindingContext mbc) =>
{ {
@ -155,7 +152,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
return false; return false;
}); });
return mockIntBinder.Object; return mockStringBinder.Object;
} }
} }
} }

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public void CopyConstructor() public void CopyConstructor()
{ {
// Arrange // Arrange
ModelBindingContext originalBindingContext = new ModelBindingContext var originalBindingContext = new ModelBindingContext
{ {
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)), ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)),
ModelName = "theName", ModelName = "theName",
@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}; };
// Act // Act
ModelBindingContext newBindingContext = new ModelBindingContext(originalBindingContext); var newBindingContext = new ModelBindingContext(originalBindingContext);
// Assert // Assert
Assert.Null(newBindingContext.ModelMetadata); Assert.Null(newBindingContext.ModelMetadata);
@ -31,7 +31,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public void ModelProperty_ThrowsIfModelMetadataDoesNotExist() public void ModelProperty_ThrowsIfModelMetadataDoesNotExist()
{ {
// Arrange // Arrange
ModelBindingContext bindingContext = new ModelBindingContext(); var bindingContext = new ModelBindingContext();
// Act & assert // Act & assert
ExceptionAssert.Throws<InvalidOperationException>( ExceptionAssert.Throws<InvalidOperationException>(
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public void ModelAndModelTypeAreFedFromModelMetadata() public void ModelAndModelTypeAreFedFromModelMetadata()
{ {
// Act // Act
ModelBindingContext bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)) ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int))
}; };
@ -53,38 +53,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal(typeof(int), bindingContext.ModelType); Assert.Equal(typeof(int), bindingContext.ModelType);
} }
// TODO: Validation [Fact]
//[Fact] public void ValidationNodeProperty_DefaultValues()
//public void ValidationNodeProperty() {
//{ // Act
// // Act var bindingContext = new ModelBindingContext
// ModelBindingContext bindingContext = new ModelBindingContext {
// { ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)) ModelName = "theInt"
// }; };
// // Act & assert // Act
// MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ValidationNode", new ModelValidationNode(bindingContext.ModelMetadata, "someName")); var validationNode = bindingContext.ValidationNode;
//}
// TODO: Validation // Assert
//[Fact] Assert.NotNull(validationNode);
//public void ValidationNodeProperty_DefaultValues() Assert.Equal(bindingContext.ModelMetadata, validationNode.ModelMetadata);
//{ Assert.Equal(bindingContext.ModelName, validationNode.ModelStateKey);
// // 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);
//}
} }
} }

View File

@ -10,17 +10,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test.Binders
{ {
// GetMetadataForProperties // GetMetadataForProperties
[Fact] //[Fact]
public void GetMetadataForPropertiesNullContainerTypeThrows() //public void GetMetadataForPropertiesNullContainerTypeThrows()
{ //{
// Arrange // // Arrange
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); // TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
// Act & Assert // // Act & Assert
ExceptionAssert.ThrowsArgumentNull( // ExceptionAssert.ThrowsArgumentNull(
() => provider.GetMetadataForProperties(new Object(), containerType: null), // () => provider.GetMetadataForProperties(new Object(), containerType: null),
"containerType"); // "containerType");
} //}
[Fact] [Fact]
public void GetMetadataForPropertiesCreatesMetadataForAllPropertiesOnModelWithPropertyValues() public void GetMetadataForPropertiesCreatesMetadataForAllPropertiesOnModelWithPropertyValues()
@ -72,17 +72,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test.Binders
// GetMetadataForProperty // GetMetadataForProperty
[Fact] //[Fact]
public void GetMetadataForPropertyNullContainerTypeThrows() //public void GetMetadataForPropertyNullContainerTypeThrows()
{ //{
// Arrange // // Arrange
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); // TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
// Act & Assert // // Act & Assert
ExceptionAssert.ThrowsArgumentNull( // ExceptionAssert.ThrowsArgumentNull(
() => provider.GetMetadataForProperty(modelAccessor: null, containerType: null, propertyName: "propertyName"), // () => provider.GetMetadataForProperty(modelAccessor: null, containerType: null, propertyName: "propertyName"),
"containerType"); // "containerType");
} //}
[Fact] [Fact]
public void GetMetadataForPropertyNullOrEmptyPropertyNameThrows() public void GetMetadataForPropertyNullOrEmptyPropertyNameThrows()
@ -167,17 +167,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test.Binders
// GetMetadataForType // GetMetadataForType
[Fact] //[Fact]
public void GetMetadataForTypeNullModelTypeThrows() //public void GetMetadataForTypeNullModelTypeThrows()
{ //{
// Arrange // // Arrange
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider(); // TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
// Act & Assert // // Act & Assert
ExceptionAssert.ThrowsArgumentNull( // ExceptionAssert.ThrowsArgumentNull(
() => provider.GetMetadataForType(() => new Object(), modelType: null), // () => provider.GetMetadataForType(() => new Object(), modelType: null),
"modelType"); // "modelType");
} //}
[Fact] [Fact]
public void GetMetadataForTypeIncludesAttributesOnType() public void GetMetadataForTypeIncludesAttributesOnType()

View File

@ -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<AssociatedValidatorProvider> { CallBase = true };
// Act & Assert
ExceptionAssert.ThrowsArgumentNull(
() => provider.Object.GetValidators(metadata: null),
"metadata");
}
[Fact]
public void GetValidatorsForPropertyWithLocalAttributes()
{
// Arrange
IEnumerable<Attribute> callbackAttributes = null;
var metadata = _metadataProvider.GetMetadataForProperty(null, typeof(PropertyModel), "LocalAttributes");
var provider = new Mock<TestableAssociatedValidatorProvider> { CallBase = true };
provider.Setup(p => p.AbstractGetValidators(metadata, It.IsAny<IEnumerable<Attribute>>()))
.Callback<ModelMetadata, IEnumerable<Attribute>>((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<Attribute> callbackAttributes = null;
var metadata = _metadataProvider.GetMetadataForProperty(null, typeof(PropertyModel), "MetadataAttributes");
Mock<TestableAssociatedValidatorProvider> provider = new Mock<TestableAssociatedValidatorProvider> { CallBase = true };
provider.Setup(p => p.AbstractGetValidators(metadata, It.IsAny<IEnumerable<Attribute>>()))
.Callback<ModelMetadata, IEnumerable<Attribute>>((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<Attribute> callbackAttributes = null;
var metadata = _metadataProvider.GetMetadataForProperty(null, typeof(PropertyModel), "MixedAttributes");
Mock<TestableAssociatedValidatorProvider> provider = new Mock<TestableAssociatedValidatorProvider> { CallBase = true };
provider.Setup(p => p.AbstractGetValidators(metadata, It.IsAny<IEnumerable<Attribute>>()))
.Callback<ModelMetadata, IEnumerable<Attribute>>((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<IModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<Attribute> attributes)
{
return AbstractGetValidators(metadata, attributes);
}
public abstract IEnumerable<IModelValidator> AbstractGetValidators(ModelMetadata metadata, IEnumerable<Attribute> attributes);
}
}
}

View File

@ -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<IModelValidator> validators = provider.GetValidators(metadata);
// Assert
var validator = validators.Single();
Assert.IsType<DataAnnotationsModelValidator>(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<IValidatableObject>();
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; }
}
}
}
}

View File

@ -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<object[]> 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<ValidationAttribute> { CallBase = true };
attribute.Protected()
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.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<ValidationAttribute> { 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<ValidationAttribute> { 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<ValidationAttribute> { CallBase = true };
attribute.Protected()
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.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<ValidationAttribute> { CallBase = true };
attribute.Protected()
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.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<ValidationAttribute> { CallBase = true };
attribute.Protected()
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.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<ValidationAttribute> { CallBase = true };
attribute.Protected()
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.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; }
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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<InvalidOperationException>(() => validator.Validate(null), "error");
}
}
}

View File

@ -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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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;
}
}
}

View File

@ -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<string>();
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<string>();
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<string>();
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<string>();
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<string>();
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<string>();
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<string> _log;
public LoggingValidatableObject(IList<string> log)
{
_log = log;
}
[LoggingValidation]
public string ValidStringProperty { get; set; }
public string InvalidStringProperty { get; set; }
public IEnumerable<ValidationResult> 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; }
}
}
}

View File

@ -10,6 +10,16 @@
"Xunit.extensions": "1.9.1" "Xunit.extensions": "1.9.1"
}, },
"configurations": { "configurations": {
"net45": { } "net45": {
dependencies: {
"System.ComponentModel.DataAnnotations": "",
"System.Runtime.Serialization": ""
}
},
"k10" : {
dependencies: {
"Microsoft.ComponentModel.DataAnnotations" : "0.1-alpha-*"
}
}
} }
} }

View File

@ -3,9 +3,9 @@ using System.Globalization;
using System.Threading; using System.Threading;
using Xunit; 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 _defaultCultureName = "en-GB";
private const string _defaultUICultureName = "en-US"; private const string _defaultUICultureName = "en-US";

View File

@ -2,9 +2,9 @@
using System.Reflection; using System.Reflection;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Test namespace Microsoft.AspNet.Mvc
{ {
public static class ExceptionAssert internal static class ExceptionAssert
{ {
/// <summary> /// <summary>
/// Verifies that an exception of the given type (or optionally a derived type) is thrown. /// Verifies that an exception of the given type (or optionally a derived type) is thrown.

View File

@ -0,0 +1,56 @@
using System;
using System.Globalization;
using System.Reflection;
using System.Threading;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Replaces the current culture and UI culture for the test.
/// </summary>
[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;
}
/// <summary>
/// Sets <see cref="Thread.CurrentCulture"/> for the test. Defaults to en-GB.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public CultureInfo Culture { get; set; }
/// <summary>
/// Sets <see cref="Thread.CurrentUICulture"/> for the test. Defaults to en-US.
/// </summary>
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;
}
}
}