Reintroducing BindingBehaviorAttribute attribute
* Porting MutableObjectModelBinder tests * Fix issues in MutableObjectModelBinder introduced by changing from TypeDescriptors to reflection
This commit is contained in:
parent
9cd99a42a7
commit
f8dd52dfe3
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class BindNeverAttribute : BindingBehaviorAttribute
|
||||
{
|
||||
public BindNeverAttribute()
|
||||
: base(BindingBehavior.Never)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class BindRequiredAttribute : BindingBehaviorAttribute
|
||||
{
|
||||
public BindRequiredAttribute()
|
||||
: base(BindingBehavior.Required)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
public enum BindingBehavior
|
||||
{
|
||||
Optional = 0,
|
||||
Never,
|
||||
Required
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public class BindingBehaviorAttribute : Attribute
|
||||
{
|
||||
public BindingBehaviorAttribute(BindingBehavior behavior)
|
||||
{
|
||||
Behavior = behavior;
|
||||
}
|
||||
|
||||
public BindingBehavior Behavior { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -57,10 +57,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
newBindingContext.ValidationNode = new ModelValidationNode(newBindingContext.ModelMetadata, bindingContext.ModelName);
|
||||
}
|
||||
|
||||
var validationContext = new ModelValidationContext(bindingContext.ModelMetadata,
|
||||
bindingContext.ModelState,
|
||||
bindingContext.MetadataProvider,
|
||||
bindingContext.ValidatorProviders);
|
||||
var validationContext = new ModelValidationContext(bindingContext.MetadataProvider,
|
||||
bindingContext.ValidatorProviders,
|
||||
bindingContext.ModelState,
|
||||
bindingContext.ModelMetadata,
|
||||
containerMetadata: null);
|
||||
|
||||
newBindingContext.ValidationNode.Validate(validationContext, parentNode: null);
|
||||
bindingContext.Model = newBindingContext.Model;
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
{
|
||||
// TODO: Revive ModelBinderConfig
|
||||
// string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ValidationContext, modelMetadata, incomingValue);
|
||||
var errorMessage = e.ValidationContext.ModelMetadata.PropertyName + " is required";
|
||||
var errorMessage = "A value is required.";
|
||||
if (errorMessage != null)
|
||||
{
|
||||
modelState.AddModelError(validationNode.ModelStateKey, errorMessage);
|
||||
|
|
@ -136,17 +136,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext)
|
||||
{
|
||||
// keep a set of the required properties so that we can cross-reference bound properties later
|
||||
HashSet<string> requiredProperties;
|
||||
Dictionary<string, IModelValidator> requiredValidators;
|
||||
HashSet<string> skipProperties;
|
||||
GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out requiredValidators, out skipProperties);
|
||||
|
||||
return from propertyMetadata in bindingContext.ModelMetadata.Properties
|
||||
let propertyName = propertyMetadata.PropertyName
|
||||
let shouldUpdateProperty = requiredProperties.Contains(propertyName) || !skipProperties.Contains(propertyName)
|
||||
where shouldUpdateProperty && CanUpdateProperty(propertyMetadata)
|
||||
select propertyMetadata;
|
||||
var validationInfo = GetPropertyValidationInfo(bindingContext);
|
||||
return bindingContext.ModelMetadata.Properties
|
||||
.Where(propertyMetadata =>
|
||||
(validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) ||
|
||||
!validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) &&
|
||||
CanUpdateProperty(propertyMetadata));
|
||||
}
|
||||
|
||||
private static object GetPropertyDefaultValue(PropertyInfo propertyInfo)
|
||||
|
|
@ -155,43 +150,55 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
return (attr != null) ? attr.Value : null;
|
||||
}
|
||||
|
||||
internal static void GetRequiredPropertiesCollection(ModelBindingContext bindingContext,
|
||||
out HashSet<string> requiredProperties,
|
||||
out Dictionary<string, IModelValidator> requiredValidators,
|
||||
out HashSet<string> skipProperties)
|
||||
internal static PropertyValidationInfo GetPropertyValidationInfo(ModelBindingContext bindingContext)
|
||||
{
|
||||
requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
requiredValidators = new Dictionary<string, IModelValidator>(StringComparer.OrdinalIgnoreCase);
|
||||
skipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// TODO: HttpBindingBehaviorAttribute
|
||||
var validationInfo = new PropertyValidationInfo();
|
||||
var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
|
||||
foreach (var propertyMetadata in bindingContext.ModelMetadata.Properties)
|
||||
var typeAttribute = modelTypeInfo.GetCustomAttribute<BindingBehaviorAttribute>();
|
||||
var properties = bindingContext.ModelType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propertyName = propertyMetadata.PropertyName;
|
||||
var propertyName = property.Name;
|
||||
var propertyMetadata = bindingContext.PropertyMetadata[propertyName];
|
||||
var requiredValidator = bindingContext.GetValidators(propertyMetadata)
|
||||
.FirstOrDefault(v => v.IsRequired);
|
||||
// TODO: Revive HttpBindingBehaviorAttribute
|
||||
|
||||
if (requiredValidator != null)
|
||||
{
|
||||
requiredValidators[propertyName] = requiredValidator;
|
||||
requiredProperties.Add(propertyName);
|
||||
validationInfo.RequiredValidators[propertyName] = requiredValidator;
|
||||
}
|
||||
|
||||
var propertyAttribute = property.GetCustomAttribute<BindingBehaviorAttribute>();
|
||||
var bindingBehaviorAttribute = propertyAttribute ?? typeAttribute;
|
||||
if (bindingBehaviorAttribute != null)
|
||||
{
|
||||
switch (bindingBehaviorAttribute.Behavior)
|
||||
{
|
||||
case BindingBehavior.Required:
|
||||
validationInfo.RequiredProperties.Add(propertyName);
|
||||
break;
|
||||
|
||||
case BindingBehavior.Never:
|
||||
validationInfo.SkipProperties.Add(propertyName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (requiredValidator != null)
|
||||
{
|
||||
validationInfo.RequiredProperties.Add(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
return validationInfo;
|
||||
}
|
||||
|
||||
internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto)
|
||||
{
|
||||
HashSet<string> requiredProperties;
|
||||
Dictionary<string, IModelValidator> requiredValidators;
|
||||
HashSet<string> skipProperties;
|
||||
GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out requiredValidators, out skipProperties);
|
||||
|
||||
var validationInfo = GetPropertyValidationInfo(bindingContext);
|
||||
|
||||
// Eliminate provided properties from requiredProperties; leaving just *missing* required properties.
|
||||
requiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName));
|
||||
validationInfo.RequiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName));
|
||||
|
||||
foreach (var missingRequiredProperty in requiredProperties)
|
||||
foreach (var missingRequiredProperty in validationInfo.RequiredProperties)
|
||||
{
|
||||
var addedError = false;
|
||||
var modelStateKey = ModelBindingHelper.CreatePropertyModelName(
|
||||
|
|
@ -205,12 +212,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
// Execute validator (if any) to get custom error message.
|
||||
IModelValidator validator;
|
||||
if (requiredValidators.TryGetValue(missingRequiredProperty, out validator))
|
||||
if (validationInfo.RequiredValidators.TryGetValue(missingRequiredProperty, out validator))
|
||||
{
|
||||
addedError = RunValidator(validator, bindingContext, propertyMetadata, modelStateKey);
|
||||
}
|
||||
|
||||
// Fall back to default message if HttpBindingBehaviorAttribute required this property or validator
|
||||
// Fall back to default message if BindingBehaviorAttribute required this property or validator
|
||||
// (oddly) succeeded.
|
||||
if (!addedError)
|
||||
{
|
||||
|
|
@ -227,7 +234,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
var dtoResult = entry.Value;
|
||||
if (dtoResult != null)
|
||||
{
|
||||
SetProperty(bindingContext, propertyMetadata, dtoResult);
|
||||
IModelValidator requiredValidator;
|
||||
validationInfo.RequiredValidators.TryGetValue(propertyMetadata.PropertyName, out requiredValidator);
|
||||
SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode);
|
||||
}
|
||||
}
|
||||
|
|
@ -236,7 +245,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
|
||||
protected virtual void SetProperty(ModelBindingContext bindingContext,
|
||||
ModelMetadata propertyMetadata,
|
||||
ComplexModelDtoResult dtoResult)
|
||||
ComplexModelDtoResult dtoResult,
|
||||
IModelValidator requiredValidator)
|
||||
{
|
||||
var property = bindingContext.ModelType
|
||||
.GetProperty(propertyMetadata.PropertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
|
||||
|
|
@ -258,10 +268,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
var modelStateKey = dtoResult.ValidationNode.ModelStateKey;
|
||||
if (bindingContext.ModelState.IsValidField(modelStateKey))
|
||||
{
|
||||
var requiredValidator = bindingContext.GetValidators(propertyMetadata).FirstOrDefault(v => v.IsRequired);
|
||||
if (requiredValidator != null)
|
||||
{
|
||||
var validationContext = bindingContext.CreateValidationContext(propertyMetadata);
|
||||
var validationContext = new ModelValidationContext(bindingContext, propertyMetadata);
|
||||
foreach (var validationResult in requiredValidator.Validate(validationContext))
|
||||
{
|
||||
bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
|
||||
|
|
@ -279,10 +288,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
catch (Exception ex)
|
||||
{
|
||||
// don't display a duplicate error message if a binding error has already occurred for this field
|
||||
var targetInvocationException = ex as TargetInvocationException;
|
||||
if (targetInvocationException != null &&
|
||||
targetInvocationException.InnerException != null)
|
||||
{
|
||||
ex = targetInvocationException.InnerException;
|
||||
}
|
||||
var modelStateKey = dtoResult.ValidationNode.ModelStateKey;
|
||||
if (bindingContext.ModelState.IsValidField(modelStateKey))
|
||||
{
|
||||
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
|
||||
bindingContext.ModelState.AddModelError(modelStateKey, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -303,7 +318,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
ModelMetadata propertyMetadata,
|
||||
string modelStateKey)
|
||||
{
|
||||
var validationContext = bindingContext.CreateValidationContext(propertyMetadata);
|
||||
var validationContext = new ModelValidationContext(bindingContext, propertyMetadata);
|
||||
|
||||
var addedError = false;
|
||||
foreach (var validationResult in validator.Validate(validationContext))
|
||||
|
|
@ -313,5 +328,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
}
|
||||
return addedError;
|
||||
}
|
||||
|
||||
internal sealed class PropertyValidationInfo
|
||||
{
|
||||
public PropertyValidationInfo()
|
||||
{
|
||||
RequiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
RequiredValidators = new Dictionary<string, IModelValidator>(StringComparer.OrdinalIgnoreCase);
|
||||
SkipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public HashSet<string> RequiredProperties { get; private set; }
|
||||
|
||||
public Dictionary<string, IModelValidator> RequiredValidators { get; private set; }
|
||||
|
||||
public HashSet<string> SkipProperties { get; private set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
|||
.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);
|
||||
var modelValidationContext = new ModelValidationContext(bindingContext, bindingContext.ModelMetadata);
|
||||
return compositeValidator.Validate(modelValidationContext);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
throw Error.Argument("propertyName", Resources.FormatCommon_PropertyNotFound(containerType, propertyName));
|
||||
}
|
||||
|
||||
return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor);
|
||||
return CreatePropertyMetadata(modelAccessor, propertyInfo);
|
||||
}
|
||||
|
||||
public ModelMetadata GetMetadataForType(Func<object> modelAccessor, [NotNull] Type modelType)
|
||||
|
|
@ -64,10 +64,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
Func<object, object> propertyGetter = propertyInfo.ValueAccessor;
|
||||
modelAccessor = () => propertyGetter(container);
|
||||
}
|
||||
yield return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor);
|
||||
yield return CreatePropertyMetadata(modelAccessor, propertyInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private TModelMetadata CreatePropertyMetadata(Func<object> modelAccessor, PropertyInformation propertyInfo)
|
||||
{
|
||||
var metadata = CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor);
|
||||
if (propertyInfo.IsReadOnly)
|
||||
{
|
||||
metadata.IsReadOnly = true;
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private TypeInformation GetTypeInformation(Type type, IEnumerable<Attribute> associatedAttributes = null)
|
||||
{
|
||||
// This retrieval is implemented as a TryGetValue/TryAdd instead of a GetOrAdd to avoid the performance cost of creating instance delegates
|
||||
|
|
@ -109,13 +119,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
private PropertyInformation CreatePropertyInformation(Type containerType, PropertyInfo property)
|
||||
{
|
||||
var info = new PropertyInformation();
|
||||
info.ValueAccessor = CreatePropertyValueAccessor(property);
|
||||
info.Prototype = CreateMetadataPrototype(property.GetCustomAttributes(),
|
||||
containerType,
|
||||
property.PropertyType,
|
||||
property.Name);
|
||||
return info;
|
||||
return new PropertyInformation
|
||||
{
|
||||
ValueAccessor = CreatePropertyValueAccessor(property),
|
||||
Prototype = CreateMetadataPrototype(property.GetCustomAttributes(),
|
||||
containerType,
|
||||
property.PropertyType,
|
||||
property.Name),
|
||||
IsReadOnly = !property.CanWrite || property.SetMethod.IsPrivate
|
||||
};
|
||||
}
|
||||
|
||||
private static Func<object, object> CreatePropertyValueAccessor(PropertyInfo property)
|
||||
|
|
@ -196,6 +208,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
{
|
||||
public Func<object, object> ValueAccessor { get; set; }
|
||||
public TModelMetadata Prototype { get; set; }
|
||||
public bool IsReadOnly { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
{
|
||||
var metadata = validationContext.ModelMetadata;
|
||||
var memberName = metadata.PropertyName ?? metadata.ModelType.Name;
|
||||
var context = new ValidationContext(metadata.Model)
|
||||
var instance = metadata.Model ?? validationContext.ContainerMetadata.Model;
|
||||
var context = new ValidationContext(instance)
|
||||
{
|
||||
DisplayName = metadata.GetDisplayName(),
|
||||
MemberName = memberName
|
||||
|
|
|
|||
|
|
@ -4,18 +4,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
{
|
||||
public class ModelValidationContext
|
||||
{
|
||||
public ModelValidationContext([NotNull] ModelMetadata metadata,
|
||||
[NotNull] ModelStateDictionary modelState,
|
||||
[NotNull] IModelMetadataProvider metadataProvider,
|
||||
[NotNull] IEnumerable<IModelValidatorProvider> validatorProviders)
|
||||
public ModelValidationContext([NotNull] ModelBindingContext bindingContext,
|
||||
[NotNull] ModelMetadata metadata)
|
||||
: this(bindingContext.MetadataProvider,
|
||||
bindingContext.ValidatorProviders,
|
||||
bindingContext.ModelState,
|
||||
metadata,
|
||||
bindingContext.ModelMetadata)
|
||||
{
|
||||
}
|
||||
|
||||
public ModelValidationContext([NotNull] IModelMetadataProvider metadataProvider,
|
||||
[NotNull] IEnumerable<IModelValidatorProvider> validatorProviders,
|
||||
[NotNull] ModelStateDictionary modelState,
|
||||
[NotNull] ModelMetadata metadata,
|
||||
ModelMetadata containerMetadata)
|
||||
{
|
||||
ModelMetadata = metadata;
|
||||
ModelState = modelState;
|
||||
MetadataProvider = metadataProvider;
|
||||
ValidatorProviders = validatorProviders;
|
||||
ContainerMetadata = containerMetadata;
|
||||
}
|
||||
|
||||
public ModelValidationContext([NotNull] ModelValidationContext parentContext,
|
||||
public ModelValidationContext([NotNull] ModelValidationContext parentContext,
|
||||
[NotNull] ModelMetadata metadata)
|
||||
{
|
||||
ModelMetadata = metadata;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,953 @@
|
|||
#if NET45
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
public class MutableObjectModelBinderTest
|
||||
{
|
||||
[Fact]
|
||||
public void BindModel_InitsInstance()
|
||||
{
|
||||
// Arrange
|
||||
var mockValueProvider = new Mock<IValueProvider>();
|
||||
mockValueProvider.Setup(o => o.ContainsPrefix(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
var mockDtoBinder = new Mock<IModelBinder>();
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = GetMetadataForObject(new Person()),
|
||||
ModelName = "someName",
|
||||
ValueProvider = mockValueProvider.Object,
|
||||
ModelBinder = mockDtoBinder.Object,
|
||||
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
|
||||
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
|
||||
};
|
||||
|
||||
mockDtoBinder
|
||||
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||
.Returns((ModelBindingContext mbc) =>
|
||||
{
|
||||
// just return the DTO unchanged
|
||||
return true;
|
||||
});
|
||||
|
||||
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
|
||||
testableBinder.Setup(o => o.EnsureModelPublic(bindingContext)).Verifiable();
|
||||
testableBinder.Setup(o => o.GetMetadataForPropertiesPublic(bindingContext))
|
||||
.Returns(new ModelMetadata[0]).Verifiable();
|
||||
|
||||
// Act
|
||||
var retValue = testableBinder.Object.BindModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(retValue);
|
||||
Assert.IsType<Person>(bindingContext.Model);
|
||||
Assert.True(bindingContext.ValidationNode.ValidateAllProperties);
|
||||
testableBinder.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_HasPublicSetter_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadWriteString");
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.True(canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_ReadOnlyArray_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyArray");
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.False(canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_ReadOnlyReferenceTypeNotBlacklisted_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyObject");
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.True(canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_ReadOnlyString_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyString");
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.False(canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_ReadOnlyValueType_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyInt");
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.False(canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateModel_InstantiatesInstanceOfMetadataType()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = GetMetadataForType(typeof(Person))
|
||||
};
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
var retModel = testableBinder.CreateModelPublic(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<Person>(retModel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureModel_ModelIsNotNull_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = GetMetadataForObject(new Person())
|
||||
};
|
||||
|
||||
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
|
||||
|
||||
// Act
|
||||
var originalModel = bindingContext.Model;
|
||||
testableBinder.Object.EnsureModelPublic(bindingContext);
|
||||
var newModel = bindingContext.Model;
|
||||
|
||||
// Assert
|
||||
Assert.Same(originalModel, newModel);
|
||||
testableBinder.Verify(o => o.CreateModelPublic(bindingContext), Times.Never());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureModel_ModelIsNull_CallsCreateModel()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = GetMetadataForType(typeof(Person))
|
||||
};
|
||||
|
||||
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
|
||||
testableBinder.Setup(o => o.CreateModelPublic(bindingContext))
|
||||
.Returns(new Person()).Verifiable();
|
||||
|
||||
// Act
|
||||
object originalModel = bindingContext.Model;
|
||||
testableBinder.Object.EnsureModelPublic(bindingContext);
|
||||
object newModel = bindingContext.Model;
|
||||
|
||||
// Assert
|
||||
Assert.Null(originalModel);
|
||||
Assert.IsType<Person>(newModel);
|
||||
testableBinder.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMetadataForProperties_WithBindAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var expectedPropertyNames = new[] { "FirstName", "LastName" };
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
|
||||
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
|
||||
};
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext);
|
||||
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedPropertyNames, returnedPropertyNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMetadataForProperties_WithoutBindAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var expectedPropertyNames = new[] { "DateOfBirth", "DateOfDeath", "ValueTypeRequired", "FirstName", "LastName", "PropertyWithDefaultValue" };
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = GetMetadataForType(typeof(Person)),
|
||||
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
|
||||
};
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext);
|
||||
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedPropertyNames, returnedPropertyNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRequiredPropertiesCollection_MixedAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = GetMetadataForObject(new ModelWithMixedBindingBehaviors()),
|
||||
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var validationInfo = MutableObjectModelBinder.GetPropertyValidationInfo(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { "Required" }, validationInfo.RequiredProperties);
|
||||
Assert.Equal(new[] { "Never" }, validationInfo.SkipProperties);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullCheckFailedHandler_ModelStateAlreadyInvalid_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var modelState = new ModelStateDictionary();
|
||||
modelState.AddModelError("foo.bar", "Some existing error.");
|
||||
|
||||
var modelMetadata = GetMetadataForType(typeof(Person));
|
||||
var validationNode = new ModelValidationNode(modelMetadata, "foo");
|
||||
var validationContext = new ModelValidationContext(new DataAnnotationsModelMetadataProvider(),
|
||||
Enumerable.Empty<IModelValidatorProvider>(),
|
||||
modelState,
|
||||
modelMetadata,
|
||||
null);
|
||||
var e = new ModelValidatedEventArgs(validationContext, parentNode: null);
|
||||
|
||||
// Act
|
||||
var handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, incomingValue: null);
|
||||
handler(validationNode, e);
|
||||
|
||||
// Assert
|
||||
Assert.False(modelState.ContainsKey("foo"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullCheckFailedHandler_ModelStateValid_AddsErrorString()
|
||||
{
|
||||
// Arrange
|
||||
var modelState = new ModelStateDictionary();
|
||||
var modelMetadata = GetMetadataForType(typeof(Person));
|
||||
var validationNode = new ModelValidationNode(modelMetadata, "foo");
|
||||
var validationContext = new ModelValidationContext(new DataAnnotationsModelMetadataProvider(),
|
||||
Enumerable.Empty<IModelValidatorProvider>(),
|
||||
modelState,
|
||||
modelMetadata,
|
||||
null);
|
||||
var e = new ModelValidatedEventArgs(validationContext, parentNode: null);
|
||||
|
||||
// Act
|
||||
var handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, incomingValue: null);
|
||||
handler(validationNode, e);
|
||||
|
||||
// Assert
|
||||
Assert.True(modelState.ContainsKey("foo"));
|
||||
Assert.Equal("A value is required.", modelState["foo"].Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void ProcessDto_BindRequiredFieldMissing_RaisesModelError()
|
||||
{
|
||||
// Arrange
|
||||
var model = new ModelWithBindRequired
|
||||
{
|
||||
Name = "original value",
|
||||
Age = -20
|
||||
};
|
||||
|
||||
var containerMetadata = GetMetadataForObject(model);
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = containerMetadata,
|
||||
ModelName = "theModel",
|
||||
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
|
||||
};
|
||||
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||
|
||||
var nameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "Name");
|
||||
dto.Results[nameProperty] = new ComplexModelDtoResult("John Doe", new ModelValidationNode(nameProperty, ""));
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.ProcessDto(bindingContext, dto);
|
||||
|
||||
// Assert
|
||||
var modelStateDictionary = bindingContext.ModelState;
|
||||
Assert.False(modelStateDictionary.IsValid);
|
||||
Assert.Equal(1, modelStateDictionary.Count);
|
||||
|
||||
// Check Age error.
|
||||
ModelState modelState;
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState));
|
||||
Assert.Equal(1, modelState.Errors.Count);
|
||||
|
||||
var modelError = modelState.Errors[0];
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(modelError.ErrorMessage);
|
||||
Assert.Equal("The 'Age' property is required.", modelError.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void ProcessDto_BindRequiredFieldNull_RaisesModelError()
|
||||
{
|
||||
// Arrange
|
||||
var model = new ModelWithBindRequired
|
||||
{
|
||||
Name = "original value",
|
||||
Age = -20
|
||||
};
|
||||
|
||||
var containerMetadata = GetMetadataForObject(model);
|
||||
var bindingContext = new ModelBindingContext()
|
||||
{
|
||||
ModelMetadata = containerMetadata,
|
||||
ModelName = "theModel",
|
||||
ModelState = new ModelStateDictionary(),
|
||||
ValidatorProviders = Enumerable.Empty<IModelValidatorProvider>()
|
||||
};
|
||||
var validationContext = new ModelValidationContext(new EmptyModelMetadataProvider(),
|
||||
bindingContext.ValidatorProviders,
|
||||
bindingContext.ModelState,
|
||||
containerMetadata,
|
||||
null);
|
||||
|
||||
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
var propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Name");
|
||||
dto.Results[propertyMetadata] =
|
||||
new ComplexModelDtoResult("John Doe", new ModelValidationNode(propertyMetadata, "theModel.Name"));
|
||||
|
||||
// Attempt to set non-Nullable property to null. BindRequiredAttribute should not be relevant in this
|
||||
// case because the binding exists.
|
||||
propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Age");
|
||||
dto.Results[propertyMetadata] =
|
||||
new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.Age"));
|
||||
|
||||
// Act; must also Validate because null-check error handler is late-bound
|
||||
testableBinder.ProcessDto(bindingContext, dto);
|
||||
bindingContext.ValidationNode.Validate(validationContext);
|
||||
|
||||
// Assert
|
||||
var modelStateDictionary = bindingContext.ModelState;
|
||||
Assert.False(modelStateDictionary.IsValid);
|
||||
Assert.Equal(1, modelStateDictionary.Count);
|
||||
|
||||
// Check Age error.
|
||||
ModelState modelState;
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState));
|
||||
Assert.Equal(1, modelState.Errors.Count);
|
||||
|
||||
var modelError = modelState.Errors[0];
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(modelError.ErrorMessage);
|
||||
Assert.Equal("A value is required.", modelError.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void ProcessDto_RequiredFieldMissing_RaisesModelError()
|
||||
{
|
||||
// Arrange
|
||||
var model = new ModelWithRequired();
|
||||
var containerMetadata = GetMetadataForObject(model);
|
||||
var bindingContext = CreateContext(containerMetadata);
|
||||
|
||||
// Set no properties though Age (a non-Nullable struct) and City (a class) properties are required.
|
||||
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.ProcessDto(bindingContext, dto);
|
||||
|
||||
// Assert
|
||||
var modelStateDictionary = bindingContext.ModelState;
|
||||
Assert.False(modelStateDictionary.IsValid);
|
||||
Assert.Equal(2, modelStateDictionary.Count);
|
||||
|
||||
// Check Age error.
|
||||
ModelState modelState;
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState));
|
||||
Assert.Equal(1, modelState.Errors.Count);
|
||||
|
||||
var modelError = modelState.Errors[0];
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(modelError.ErrorMessage);
|
||||
Assert.Equal("The Age field is required.", modelError.ErrorMessage);
|
||||
|
||||
// Check City error.
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.City", out modelState));
|
||||
Assert.Equal(1, modelState.Errors.Count);
|
||||
|
||||
modelError = modelState.Errors[0];
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(modelError.ErrorMessage);
|
||||
Assert.Equal("The City field is required.", modelError.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void ProcessDto_RequiredFieldNull_RaisesModelError()
|
||||
{
|
||||
// Arrange
|
||||
var model = new ModelWithRequired();
|
||||
var containerMetadata = GetMetadataForObject(model);
|
||||
var bindingContext = CreateContext(containerMetadata);
|
||||
|
||||
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Make Age valid and City invalid.
|
||||
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "Age");
|
||||
dto.Results[propertyMetadata] =
|
||||
new ComplexModelDtoResult(23, new ModelValidationNode(propertyMetadata, "theModel.Age"));
|
||||
propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "City");
|
||||
dto.Results[propertyMetadata] =
|
||||
new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.City"));
|
||||
|
||||
// Act
|
||||
testableBinder.ProcessDto(bindingContext, dto);
|
||||
|
||||
// Assert
|
||||
var modelStateDictionary = bindingContext.ModelState;
|
||||
Assert.False(modelStateDictionary.IsValid);
|
||||
Assert.Equal(1, modelStateDictionary.Count);
|
||||
|
||||
// Check City error.
|
||||
ModelState modelState;
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.City", out modelState));
|
||||
Assert.Equal(1, modelState.Errors.Count);
|
||||
|
||||
var modelError = modelState.Errors[0];
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(modelError.ErrorMessage);
|
||||
Assert.Equal("The City field is required.", modelError.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessDto_RequiredFieldMissing_RaisesModelErrorWithMessage()
|
||||
{
|
||||
// Arrange
|
||||
var model = new Person();
|
||||
var containerMetadata = GetMetadataForObject(model);
|
||||
var bindingContext = CreateContext(containerMetadata);
|
||||
|
||||
// Set no properties though ValueTypeRequired (a non-Nullable struct) property is required.
|
||||
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.ProcessDto(bindingContext, dto);
|
||||
|
||||
// Assert
|
||||
var modelStateDictionary = bindingContext.ModelState;
|
||||
Assert.False(modelStateDictionary.IsValid);
|
||||
Assert.Equal(1, modelStateDictionary.Count);
|
||||
|
||||
// Check ValueTypeRequired error.
|
||||
ModelState modelState;
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState));
|
||||
Assert.Equal(1, modelState.Errors.Count);
|
||||
|
||||
var modelError = modelState.Errors[0];
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(modelError.ErrorMessage);
|
||||
Assert.Equal("Sample message", modelError.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage()
|
||||
{
|
||||
// Arrange
|
||||
var model = new Person();
|
||||
var containerMetadata = GetMetadataForObject(model);
|
||||
|
||||
var bindingContext = CreateContext(containerMetadata);
|
||||
|
||||
ComplexModelDto dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||
TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Make ValueTypeRequired invalid.
|
||||
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "ValueTypeRequired");
|
||||
dto.Results[propertyMetadata] =
|
||||
new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.ValueTypeRequired"));
|
||||
|
||||
// Act
|
||||
testableBinder.ProcessDto(bindingContext, dto);
|
||||
|
||||
// Assert
|
||||
ModelStateDictionary modelStateDictionary = bindingContext.ModelState;
|
||||
Assert.False(modelStateDictionary.IsValid);
|
||||
Assert.Equal(1, modelStateDictionary.Count);
|
||||
|
||||
// Check ValueTypeRequired error.
|
||||
ModelState modelState;
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState));
|
||||
Assert.Equal(1, modelState.Errors.Count);
|
||||
|
||||
ModelError modelError = modelState.Errors[0];
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(modelError.ErrorMessage);
|
||||
Assert.Equal("Sample message", modelError.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessDto_Success()
|
||||
{
|
||||
// Arrange
|
||||
var dob = new DateTime(2001, 1, 1);
|
||||
var model = new PersonWithBindExclusion
|
||||
{
|
||||
DateOfBirth = dob
|
||||
};
|
||||
var containerMetadata = GetMetadataForObject(model);
|
||||
|
||||
var bindingContext = CreateContext(containerMetadata);
|
||||
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||
|
||||
var firstNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "FirstName");
|
||||
dto.Results[firstNameProperty] = new ComplexModelDtoResult("John", new ModelValidationNode(firstNameProperty, ""));
|
||||
var lastNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "LastName");
|
||||
dto.Results[lastNameProperty] = new ComplexModelDtoResult("Doe", new ModelValidationNode(lastNameProperty, ""));
|
||||
var dobProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "DateOfBirth");
|
||||
dto.Results[dobProperty] = null;
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.ProcessDto(bindingContext, dto);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("John", model.FirstName);
|
||||
Assert.Equal("Doe", model.LastName);
|
||||
Assert.Equal(dob, model.DateOfBirth);
|
||||
Assert.True(bindingContext.ModelState.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProperty_PropertyHasDefaultValue_SetsDefaultValue()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateContext(GetMetadataForObject(new Person()));
|
||||
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties.First(o => o.PropertyName == "PropertyWithDefaultValue");
|
||||
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
|
||||
var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode);
|
||||
var requiredValidator = bindingContext.ValidatorProviders
|
||||
.SelectMany(v => v.GetValidators(propertyMetadata))
|
||||
.Where(v => v.IsRequired)
|
||||
.FirstOrDefault();
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
|
||||
// Assert
|
||||
var person = Assert.IsType<Person>(bindingContext.Model);
|
||||
Assert.Equal(123.456m, person.PropertyWithDefaultValue);
|
||||
Assert.True(bindingContext.ModelState.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProperty_PropertyIsReadOnly_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateContext(GetMetadataForObject(new Person()));
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NonUpdateableProperty");
|
||||
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
|
||||
var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator: null);
|
||||
|
||||
// Assert
|
||||
// If didn't throw, success!
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProperty_PropertyIsSettable_CallsSetter()
|
||||
{
|
||||
// Arrange
|
||||
var model = new Person();
|
||||
var bindingContext = CreateContext(GetMetadataForObject(model));
|
||||
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
|
||||
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
|
||||
var dtoResult = new ComplexModelDtoResult(new DateTime(2001, 1, 1), validationNode);
|
||||
var requiredValidator = bindingContext.ValidatorProviders
|
||||
.SelectMany(v => v.GetValidators(propertyMetadata))
|
||||
.Where(v => v.IsRequired)
|
||||
.FirstOrDefault();
|
||||
var validationContext = new ModelValidationContext(bindingContext, propertyMetadata);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
|
||||
// Assert
|
||||
validationNode.Validate(validationContext);
|
||||
Assert.True(bindingContext.ModelState.IsValid);
|
||||
Assert.Equal(new DateTime(2001, 1, 1), model.DateOfBirth);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void SetProperty_PropertyIsSettable_SetterThrows_RecordsError()
|
||||
{
|
||||
// Arrange
|
||||
var model = new Person
|
||||
{
|
||||
DateOfBirth = new DateTime(1900, 1, 1)
|
||||
};
|
||||
var bindingContext = CreateContext(GetMetadataForObject(model));
|
||||
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfDeath");
|
||||
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
|
||||
var dtoResult = new ComplexModelDtoResult(new DateTime(1800, 1, 1), validationNode);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Date of death can't be before date of birth." + Environment.NewLine
|
||||
+ "Parameter name: value",
|
||||
bindingContext.ModelState["foo"].Errors[0].Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorNotPresent_RegistersValidationCallback()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateContext(GetMetadataForObject(new Person()));
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
|
||||
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
|
||||
var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode);
|
||||
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
|
||||
var validationContext = new ModelValidationContext(bindingContext, propertyMetadata);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.ModelState.IsValid);
|
||||
validationNode.Validate(validationContext, bindingContext.ValidationNode);
|
||||
Assert.False(bindingContext.ModelState.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorPresent_AddsModelError()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateContext(GetMetadataForObject(new Person()));
|
||||
bindingContext.ModelName = " foo";
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "ValueTypeRequired");
|
||||
var validationNode = new ModelValidationNode(propertyMetadata, "foo.ValueTypeRequired");
|
||||
var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode);
|
||||
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
|
||||
// Assert
|
||||
Assert.False(bindingContext.ModelState.IsValid);
|
||||
Assert.Equal("Sample message", bindingContext.ModelState["foo.ValueTypeRequired"].Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void SetProperty_SettingNullableTypeToNull_RequiredValidatorNotPresent_PropertySetterThrows_AddsRequiredMessageString()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateContext(GetMetadataForObject(new ModelWhosePropertySetterThrows()));
|
||||
bindingContext.ModelName = "foo";
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NameNoAttribute");
|
||||
var validationNode = new ModelValidationNode(propertyMetadata, "foo.NameNoAttribute");
|
||||
var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode);
|
||||
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
|
||||
// Assert
|
||||
Assert.False(bindingContext.ModelState.IsValid);
|
||||
Assert.Equal(1, bindingContext.ModelState["foo.NameNoAttribute"].Errors.Count);
|
||||
Assert.Equal("This is a different exception." + Environment.NewLine
|
||||
+ "Parameter name: value",
|
||||
bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProperty_SettingNullableTypeToNull_RequiredValidatorPresent_PropertySetterThrows_AddsRequiredMessageString()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateContext(GetMetadataForObject(new ModelWhosePropertySetterThrows()));
|
||||
bindingContext.ModelName = "foo";
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "Name");
|
||||
var validationNode = new ModelValidationNode(propertyMetadata, "foo.Name");
|
||||
var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode);
|
||||
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
|
||||
// Assert
|
||||
Assert.False(bindingContext.ModelState.IsValid);
|
||||
Assert.Equal(1, bindingContext.ModelState["foo.Name"].Errors.Count);
|
||||
Assert.Equal("This message comes from the [Required] attribute.", bindingContext.ModelState["foo.Name"].Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
private static ModelBindingContext CreateContext(ModelMetadata metadata)
|
||||
{
|
||||
return new ModelBindingContext
|
||||
{
|
||||
ModelState = new ModelStateDictionary(),
|
||||
ModelMetadata = metadata,
|
||||
ModelName = "theModel",
|
||||
ValidatorProviders = new IModelValidatorProvider[]
|
||||
{
|
||||
new DataAnnotationsModelValidatorProvider(),
|
||||
new DataMemberModelValidatorProvider()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static IModelValidator GetRequiredValidator(ModelBindingContext bindingContext, ModelMetadata propertyMetadata)
|
||||
{
|
||||
return bindingContext.ValidatorProviders
|
||||
.SelectMany(v => v.GetValidators(propertyMetadata))
|
||||
.Where(v => v.IsRequired)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static ModelMetadata GetMetadataForCanUpdateProperty(string propertyName)
|
||||
{
|
||||
DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider();
|
||||
return metadataProvider.GetMetadataForProperty(null, typeof(MyModelTestingCanUpdateProperty), propertyName);
|
||||
}
|
||||
|
||||
private static ModelMetadata GetMetadataForObject(object o)
|
||||
{
|
||||
DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider();
|
||||
return metadataProvider.GetMetadataForType(() => o, o.GetType());
|
||||
}
|
||||
|
||||
private static ModelMetadata GetMetadataForType(Type t)
|
||||
{
|
||||
DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider();
|
||||
return metadataProvider.GetMetadataForType(null, t);
|
||||
}
|
||||
|
||||
private class Person
|
||||
{
|
||||
private DateTime? _dateOfDeath;
|
||||
|
||||
public DateTime DateOfBirth { get; set; }
|
||||
|
||||
public DateTime? DateOfDeath
|
||||
{
|
||||
get { return _dateOfDeath; }
|
||||
set
|
||||
{
|
||||
if (value < DateOfBirth)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("value", "Date of death can't be before date of birth.");
|
||||
}
|
||||
_dateOfDeath = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Sample message")]
|
||||
public int ValueTypeRequired { get; set; }
|
||||
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string NonUpdateableProperty { get; private set; }
|
||||
|
||||
[DefaultValue(typeof(decimal), "123.456")]
|
||||
public decimal PropertyWithDefaultValue { get; set; }
|
||||
}
|
||||
|
||||
private class PersonWithBindExclusion
|
||||
{
|
||||
[BindNever]
|
||||
public DateTime DateOfBirth { get; set; }
|
||||
|
||||
[BindNever]
|
||||
public DateTime? DateOfDeath { get; set; }
|
||||
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string NonUpdateableProperty { get; private set; }
|
||||
}
|
||||
|
||||
private class ModelWithRequired
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
public int Age { get; set; }
|
||||
|
||||
[Required]
|
||||
public string City { get; set; }
|
||||
}
|
||||
|
||||
private class ModelWithBindRequired
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
[BindRequired]
|
||||
public int Age { get; set; }
|
||||
}
|
||||
|
||||
[BindRequired]
|
||||
private class ModelWithMixedBindingBehaviors
|
||||
{
|
||||
public string Required { get; set; }
|
||||
|
||||
[BindNever]
|
||||
public string Never { get; set; }
|
||||
|
||||
[BindingBehavior(BindingBehavior.Optional)]
|
||||
public string Optional { get; set; }
|
||||
}
|
||||
|
||||
private sealed class MyModelTestingCanUpdateProperty
|
||||
{
|
||||
public int ReadOnlyInt { get; private set; }
|
||||
public string ReadOnlyString { get; private set; }
|
||||
public string[] ReadOnlyArray { get; private set; }
|
||||
public object ReadOnlyObject { get; private set; }
|
||||
public string ReadWriteString { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ModelWhosePropertySetterThrows
|
||||
{
|
||||
[Required(ErrorMessage = "This message comes from the [Required] attribute.")]
|
||||
public string Name
|
||||
{
|
||||
get { return null; }
|
||||
set { throw new ArgumentException("This is an exception.", "value"); }
|
||||
}
|
||||
|
||||
public string NameNoAttribute
|
||||
{
|
||||
get { return null; }
|
||||
set { throw new ArgumentException("This is a different exception.", "value"); }
|
||||
}
|
||||
}
|
||||
|
||||
public class TestableMutableObjectModelBinder : MutableObjectModelBinder
|
||||
{
|
||||
public virtual bool CanUpdatePropertyPublic(ModelMetadata propertyMetadata)
|
||||
{
|
||||
return base.CanUpdateProperty(propertyMetadata);
|
||||
}
|
||||
|
||||
protected override bool CanUpdateProperty(ModelMetadata propertyMetadata)
|
||||
{
|
||||
return CanUpdatePropertyPublic(propertyMetadata);
|
||||
}
|
||||
|
||||
public virtual object CreateModelPublic(ModelBindingContext bindingContext)
|
||||
{
|
||||
return base.CreateModel(bindingContext);
|
||||
}
|
||||
|
||||
protected override object CreateModel(ModelBindingContext bindingContext)
|
||||
{
|
||||
return CreateModelPublic(bindingContext);
|
||||
}
|
||||
|
||||
public virtual void EnsureModelPublic(ModelBindingContext bindingContext)
|
||||
{
|
||||
base.EnsureModel(bindingContext);
|
||||
}
|
||||
|
||||
protected override void EnsureModel(ModelBindingContext bindingContext)
|
||||
{
|
||||
EnsureModelPublic(bindingContext);
|
||||
}
|
||||
|
||||
public virtual IEnumerable<ModelMetadata> GetMetadataForPropertiesPublic(ModelBindingContext bindingContext)
|
||||
{
|
||||
return base.GetMetadataForProperties(bindingContext);
|
||||
}
|
||||
|
||||
protected override IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext)
|
||||
{
|
||||
return GetMetadataForPropertiesPublic(bindingContext);
|
||||
}
|
||||
|
||||
public virtual void SetPropertyPublic(ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult, IModelValidator requiredValidator)
|
||||
{
|
||||
base.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
}
|
||||
|
||||
protected override void SetProperty(ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult, IModelValidator requiredValidator)
|
||||
{
|
||||
SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -64,7 +64,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
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);
|
||||
var context = new ModelValidationContext(null, null, null, metadata, null);
|
||||
|
||||
// Act
|
||||
var validators = provider.GetValidators(metadata).ToArray();
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
private static ModelValidationContext CreateValidationContext(ModelMetadata metadata)
|
||||
{
|
||||
return new ModelValidationContext(metadata, null, null, null);
|
||||
return new ModelValidationContext(null, null, null, metadata, null);
|
||||
}
|
||||
|
||||
class DerivedRequiredAttribute : RequiredAttribute
|
||||
|
|
|
|||
|
|
@ -275,10 +275,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
new DataMemberModelValidatorProvider()
|
||||
};
|
||||
|
||||
return new ModelValidationContext(metadata,
|
||||
return new ModelValidationContext(new EmptyModelMetadataProvider(),
|
||||
providers,
|
||||
new ModelStateDictionary(),
|
||||
new EmptyModelMetadataProvider(),
|
||||
providers);
|
||||
metadata,
|
||||
null);
|
||||
}
|
||||
|
||||
private sealed class LoggingValidatableObject : IValidatableObject
|
||||
|
|
|
|||
Loading…
Reference in New Issue