diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs index 1594497fce..4bcd69a49d 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs @@ -15,6 +15,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Display = attributes.OfType().FirstOrDefault(); DisplayFormat = attributes.OfType().FirstOrDefault(); Editable = attributes.OfType().FirstOrDefault(); + Required = attributes.OfType().FirstOrDefault(); } public DisplayAttribute Display { get; protected set; } @@ -22,5 +23,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public DisplayFormatAttribute DisplayFormat { get; protected set; } public EditableAttribute Editable { get; protected set; } + + public RequiredAttribute Required { get; protected set; } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs index 31857cdd33..43d547c1a8 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs @@ -58,6 +58,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return base.ComputeIsReadOnly(); } + protected override bool ComputeIsRequired() + { + return (PrototypeCache.Required != null) || base.ComputeIsRequired(); + } + public override string GetDisplayName() { // DisplayAttribute doesn't require you to set a name, so this could be null. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs index e389885acd..49ade31a91 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs @@ -20,12 +20,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private string _description; private bool _isReadOnly; private bool _isComplexType; + private bool _isRequired; private bool _convertEmptyStringToNullComputed; private bool _nullDisplayTextComputed; private bool _descriptionComputed; private bool _isReadOnlyComputed; private bool _isComplexTypeComputed; + private bool _isRequiredComputed; // Constructor for creating real instances of the metadata class based on a prototype protected CachedModelMetadata(CachedModelMetadata prototype, Func modelAccessor) @@ -125,6 +127,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + public sealed override bool IsRequired + { + get + { + if (!_isRequiredComputed) + { + _isRequired = ComputeIsRequired(); + _isRequiredComputed = true; + } + return _isRequired; + } + set + { + _isRequired = value; + _isRequiredComputed = true; + } + } + public sealed override bool IsComplexType { get @@ -160,6 +180,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return base.IsReadOnly; } + protected virtual bool ComputeIsRequired() + { + return base.IsRequired; + } + protected virtual bool ComputeIsComplexType() { return base.IsComplexType; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs index 65a407d70e..ef288c6a43 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs @@ -38,6 +38,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding _modelAccessor = modelAccessor; _modelType = modelType; _propertyName = propertyName; + IsRequired = !modelType.AllowsNullValue(); } public Type ContainerType @@ -71,6 +72,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public virtual bool IsReadOnly { get; set; } + public virtual bool IsRequired { get; set; } + public virtual int Order { get { return _order; } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs index 2785130bad..dcdf510b11 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs @@ -92,6 +92,49 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.False(new ModelMetadata(provider.Object, null, null, typeof(int), null).IsNullableValueType); } + // IsRequired + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(IDisposable))] + [InlineData(typeof(Nullable))] + public void IsRequired_ReturnsFalse_ForNullableTypes(Type modelType) + { + // Arrange + var provider = new Mock(); + var metadata = new ModelMetadata(provider.Object, + containerType: null, + modelAccessor: null, + modelType: modelType, + propertyName: null); + + // Act + var isRequired = metadata.IsRequired; + + // Assert + Assert.False(isRequired); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(DayOfWeek))] + public void IsRequired_ReturnsTrue_ForNonNullableTypes(Type modelType) + { + // Arrange + var provider = new Mock(); + var metadata = new ModelMetadata(provider.Object, + containerType: null, + modelAccessor: null, + modelType: modelType, + propertyName: null); + + // Act + var isRequired = metadata.IsRequired; + + // Assert + Assert.True(isRequired); + } + // Properties [Fact] diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs index 2151cf12ab..887a375b13 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs @@ -16,6 +16,41 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { private readonly DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); + [Fact] + public void GetValidators_ReturnsValidatorForIValidatableObject() + { + // Arrange + var provider = new DataAnnotationsModelValidatorProvider(); + var mockValidatable = Mock.Of(); + var metadata = _metadataProvider.GetMetadataForType(() => null, mockValidatable.GetType()); + + // Act + var validators = provider.GetValidators(metadata); + + // Assert + var validator = Assert.Single(validators); + Assert.IsType(validator); + + } + + [Fact] + public void GetValidators_DoesNotAddRequiredAttribute_ForNonNullableValueTypes_IfAttributeIsSpecifiedExplicitly() + { + // Arrange + var provider = new DataAnnotationsModelValidatorProvider(); + var metadata = _metadataProvider.GetMetadataForProperty(() => null, + typeof(DummyRequiredAttributeHelperClass), + "WithAttribute"); + + // Act + var validators = provider.GetValidators(metadata); + + // Assert + var validator = Assert.Single(validators); + var adapter = Assert.IsType(validator); + Assert.Equal("Custom Required Message", adapter.Attribute.ErrorMessage); + } + public static IEnumerable DataAnnotationAdapters { get @@ -198,5 +233,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding set { base.MyProperty = value; } } } + + private class DummyRequiredAttributeHelperClass + { + [Required(ErrorMessage = "Custom Required Message")] + public int WithAttribute { get; set; } + + public int WithoutAttribute { get; set; } + } } }