diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelBindingMessageProvider.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelBindingMessageProvider.cs index 23918bd649..5ab37cfd8b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelBindingMessageProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelBindingMessageProvider.cs @@ -40,18 +40,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata /// /// Error message the model binding system adds when is of type - /// or and value is known. + /// or , value is known, and error is associated + /// with a property. /// /// Default is "The value '{0}' is not valid for {1}.". public virtual Func AttemptedValueIsInvalidAccessor { get; } /// /// Error message the model binding system adds when is of type - /// or and value is unknown. + /// or , value is known, and error is associated + /// with a collection element or action parameter. + /// + /// Default is "The value '{0}' is not valid.". + public virtual Func NonPropertyAttemptedValueIsInvalidAccessor { get; } + + /// + /// Error message the model binding system adds when is of type + /// or , value is unknown, and error is associated + /// with a property. /// /// Default is "The supplied value is invalid for {0}.". public virtual Func UnknownValueIsInvalidAccessor { get; } + /// + /// Error message the model binding system adds when is of type + /// or , value is unknown, and error is associated + /// with a collection element or action parameter. + /// + /// Default is "The supplied value is invalid.". + public virtual Func NonPropertyUnknownValueIsInvalidAccessor { get; } + /// /// Fallback error message HTML and tag helpers display when a property is invalid but the /// s have null s. @@ -61,9 +79,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata /// /// Error message HTML and tag helpers add for client-side validation of numeric formats. Visible in the - /// browser if the field for a float property (for example) does not have a correctly-formatted value. + /// browser if the field for a float (for example) property does not have a correctly-formatted value. /// /// Default is "The field {0} must be a number.". public virtual Func ValueMustBeANumberAccessor { get; } + + /// + /// Error message HTML and tag helpers add for client-side validation of numeric formats. Visible in the + /// browser if the field for a float (for example) collection element or action parameter does not have a + /// correctly-formatted value. + /// + /// Default is "The field must be a number.". + public virtual Func NonPropertyValueMustBeANumberAccessor { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs index b56b8d5cdc..13af65ec27 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs @@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new NotImplementedException(); } } - + /// /// Gets a value indicating the kind of metadata element represented by the current instance. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs index 9f7f6cce98..27a89bcb03 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs @@ -163,8 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - ModelStateEntry entry; - TryGetValue(key, out entry); + TryGetValue(key, out var entry); return entry; } } @@ -237,20 +236,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding if (exception is FormatException || exception is OverflowException) { // Convert FormatExceptions and OverflowExceptions to Invalid value messages. - ModelStateEntry entry; - TryGetValue(key, out entry); + TryGetValue(key, out var entry); - var name = metadata.GetDisplayName(); + // Not using metadata.GetDisplayName() or a single resource to avoid strange messages like + // "The value '' is not valid." (when no value was provided, not even an empty string) and + // "The supplied value is invalid for Int32." (when error is for an element or parameter). + var messageProvider = metadata.ModelBindingMessageProvider; + var name = metadata.DisplayName ?? metadata.PropertyName; string errorMessage; - if (entry == null) + if (entry == null && name == null) { - errorMessage = metadata.ModelBindingMessageProvider.UnknownValueIsInvalidAccessor(name); + errorMessage = messageProvider.NonPropertyUnknownValueIsInvalidAccessor(); + } + else if (entry == null) + { + errorMessage = messageProvider.UnknownValueIsInvalidAccessor(name); + } + else if (name == null) + { + errorMessage = messageProvider.NonPropertyAttemptedValueIsInvalidAccessor(entry.AttemptedValue); } else { - errorMessage = metadata.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor( - entry.AttemptedValue, - name); + errorMessage = messageProvider.AttemptedValueIsInvalidAccessor(entry.AttemptedValue, name); } return TryAddModelError(key, errorMessage); @@ -354,8 +362,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - ModelStateEntry validationState; - if (TryGetValue(key, out validationState)) + if (TryGetValue(key, out var validationState)) { return validationState.ValidationState; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelBindingMessageProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelBindingMessageProvider.cs index 938d4f854e..30cffb2147 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelBindingMessageProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelBindingMessageProvider.cs @@ -16,9 +16,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata private Func _missingRequestBodyRequiredValueAccessor; private Func _valueMustNotBeNullAccessor; private Func _attemptedValueIsInvalidAccessor; + private Func _nonPropertyAttemptedValueIsInvalidAccessor; private Func _unknownValueIsInvalidAccessor; + private Func _nonPropertyUnknownValueIsInvalidAccessor; private Func _valueIsInvalidAccessor; private Func _valueMustBeANumberAccessor; + private Func _nonPropertyValueMustBeANumberAccessor; /// /// Initializes a new instance of the class. @@ -30,9 +33,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata SetMissingRequestBodyRequiredValueAccessor(Resources.FormatModelBinding_MissingRequestBodyRequiredMember); SetValueMustNotBeNullAccessor(Resources.FormatModelBinding_NullValueNotValid); SetAttemptedValueIsInvalidAccessor(Resources.FormatModelState_AttemptedValueIsInvalid); + SetNonPropertyAttemptedValueIsInvalidAccessor(Resources.FormatModelState_NonPropertyAttemptedValueIsInvalid); SetUnknownValueIsInvalidAccessor(Resources.FormatModelState_UnknownValueIsInvalid); + SetNonPropertyUnknownValueIsInvalidAccessor(Resources.FormatModelState_NonPropertyUnknownValueIsInvalid); SetValueIsInvalidAccessor(Resources.FormatHtmlGeneration_ValueIsInvalid); SetValueMustBeANumberAccessor(Resources.FormatHtmlGeneration_ValueMustBeNumber); + SetNonPropertyValueMustBeANumberAccessor(Resources.FormatHtmlGeneration_NonPropertyValueMustBeNumber); } /// @@ -52,9 +58,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata SetMissingRequestBodyRequiredValueAccessor(originalProvider.MissingRequestBodyRequiredValueAccessor); SetValueMustNotBeNullAccessor(originalProvider.ValueMustNotBeNullAccessor); SetAttemptedValueIsInvalidAccessor(originalProvider.AttemptedValueIsInvalidAccessor); + SetNonPropertyAttemptedValueIsInvalidAccessor(originalProvider.NonPropertyAttemptedValueIsInvalidAccessor); SetUnknownValueIsInvalidAccessor(originalProvider.UnknownValueIsInvalidAccessor); + SetNonPropertyUnknownValueIsInvalidAccessor(originalProvider.NonPropertyUnknownValueIsInvalidAccessor); SetValueIsInvalidAccessor(originalProvider.ValueIsInvalidAccessor); SetValueMustBeANumberAccessor(originalProvider.ValueMustBeANumberAccessor); + SetNonPropertyValueMustBeANumberAccessor(originalProvider.NonPropertyValueMustBeANumberAccessor); } /// @@ -142,6 +151,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata _attemptedValueIsInvalidAccessor = attemptedValueIsInvalidAccessor; } + /// + public override Func NonPropertyAttemptedValueIsInvalidAccessor => _nonPropertyAttemptedValueIsInvalidAccessor; + + /// + /// Sets the property. + /// + /// The value to set. + public void SetNonPropertyAttemptedValueIsInvalidAccessor( + Func nonPropertyAttemptedValueIsInvalidAccessor) + { + if (nonPropertyAttemptedValueIsInvalidAccessor == null) + { + throw new ArgumentNullException(nameof(nonPropertyAttemptedValueIsInvalidAccessor)); + } + + _nonPropertyAttemptedValueIsInvalidAccessor = nonPropertyAttemptedValueIsInvalidAccessor; + } + /// public override Func UnknownValueIsInvalidAccessor => _unknownValueIsInvalidAccessor; @@ -159,6 +186,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata _unknownValueIsInvalidAccessor = unknownValueIsInvalidAccessor; } + /// + public override Func NonPropertyUnknownValueIsInvalidAccessor => _nonPropertyUnknownValueIsInvalidAccessor; + + /// + /// Sets the property. + /// + /// The value to set. + public void SetNonPropertyUnknownValueIsInvalidAccessor(Func nonPropertyUnknownValueIsInvalidAccessor) + { + if (nonPropertyUnknownValueIsInvalidAccessor == null) + { + throw new ArgumentNullException(nameof(nonPropertyUnknownValueIsInvalidAccessor)); + } + + _nonPropertyUnknownValueIsInvalidAccessor = nonPropertyUnknownValueIsInvalidAccessor; + } + /// public override Func ValueIsInvalidAccessor => _valueIsInvalidAccessor; @@ -192,5 +236,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata _valueMustBeANumberAccessor = valueMustBeANumberAccessor; } + + /// + public override Func NonPropertyValueMustBeANumberAccessor => _nonPropertyValueMustBeANumberAccessor; + + /// + /// Sets the property. + /// + /// The value to set. + public void SetNonPropertyValueMustBeANumberAccessor(Func nonPropertyValueMustBeANumberAccessor) + { + if (nonPropertyValueMustBeANumberAccessor == null) + { + throw new ArgumentNullException(nameof(nonPropertyValueMustBeANumberAccessor)); + } + + _nonPropertyValueMustBeANumberAccessor = nonPropertyValueMustBeANumberAccessor; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index 720dbacd87..887aa1d73d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -906,6 +906,20 @@ namespace Microsoft.AspNetCore.Mvc.Core internal static string FormatModelState_AttemptedValueIsInvalid(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("ModelState_AttemptedValueIsInvalid"), p0, p1); + /// + /// The value '{0}' is not valid. + /// + internal static string ModelState_NonPropertyAttemptedValueIsInvalid + { + get => GetString("ModelState_NonPropertyAttemptedValueIsInvalid"); + } + + /// + /// The value '{0}' is not valid. + /// + internal static string FormatModelState_NonPropertyAttemptedValueIsInvalid(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ModelState_NonPropertyAttemptedValueIsInvalid"), p0); + /// /// The supplied value is invalid for {0}. /// @@ -920,6 +934,20 @@ namespace Microsoft.AspNetCore.Mvc.Core internal static string FormatModelState_UnknownValueIsInvalid(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("ModelState_UnknownValueIsInvalid"), p0); + /// + /// The supplied value is invalid. + /// + internal static string ModelState_NonPropertyUnknownValueIsInvalid + { + get => GetString("ModelState_NonPropertyUnknownValueIsInvalid"); + } + + /// + /// The supplied value is invalid. + /// + internal static string FormatModelState_NonPropertyUnknownValueIsInvalid() + => GetString("ModelState_NonPropertyUnknownValueIsInvalid"); + /// /// The value '{0}' is invalid. /// @@ -948,6 +976,20 @@ namespace Microsoft.AspNetCore.Mvc.Core internal static string FormatHtmlGeneration_ValueMustBeNumber(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0); + /// + /// The field must be a number. + /// + internal static string HtmlGeneration_NonPropertyValueMustBeNumber + { + get => GetString("HtmlGeneration_NonPropertyValueMustBeNumber"); + } + + /// + /// The field must be a number. + /// + internal static string FormatHtmlGeneration_NonPropertyValueMustBeNumber() + => GetString("HtmlGeneration_NonPropertyValueMustBeNumber"); + /// /// The list of '{0}' must not be empty. Add at least one supported encoding. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx index 82da0b9f83..c251ced57a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx @@ -1,17 +1,17 @@  - @@ -319,15 +319,24 @@ The value '{0}' is not valid for {1}. + + The value '{0}' is not valid. + The supplied value is invalid for {0}. + + The supplied value is invalid. + The value '{0}' is invalid. The field {0} must be a number. + + The field must be a number. + The list of '{0}' must not be empty. Add at least one supported encoding. diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs index a743844827..cefc789cb4 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs @@ -176,8 +176,8 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal // EnumDisplayNamesAndValues and EnumNamesAndValues // - // Order EnumDisplayNamesAndValues by DisplayAttribute.Order, then by the order of Enum.GetNames(). - // That method orders by absolute value, then its behavior is undefined (but hopefully stable). + // Order EnumDisplayNamesAndValues by DisplayAttribute.Order, then by the order of Enum.GetNames(). + // That method orders by absolute value, then its behavior is undefined (but hopefully stable). // Add to EnumNamesAndValues in same order but Dictionary does not guarantee order will be preserved. var groupedDisplayNamesAndValues = new List>(); diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs index ceaea0b05c..d10c310e28 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs @@ -41,8 +41,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal throw new ArgumentNullException(nameof(modelMetadata)); } - return modelMetadata.ModelBindingMessageProvider.ValueMustBeANumberAccessor( - modelMetadata.GetDisplayName()); + var messageProvider = modelMetadata.ModelBindingMessageProvider; + var name = modelMetadata.DisplayName ?? modelMetadata.PropertyName; + if (name == null) + { + return messageProvider.NonPropertyValueMustBeANumberAccessor(); + } + + return messageProvider.ValueMustBeANumberAccessor(name); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs index c11a873294..dc0a594120 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs @@ -23,16 +23,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal if (validatable == null) { var message = Resources.FormatValidatableObjectAdapter_IncompatibleType( - typeof(IValidatableObject).Name, - model.GetType()); + typeof(IValidatableObject).Name, + model.GetType()); throw new InvalidOperationException(message); } + // The constructed ValidationContext is intentionally slightly different from what + // DataAnnotationsModelValidator creates. The instance parameter would be context.Container + // (if non-null) in that class. But, DataAnnotationsModelValidator _also_ passes context.Model + // separately to any ValidationAttribute. var validationContext = new ValidationContext( - instance: validatable, - serviceProvider: context.ActionContext?.HttpContext?.RequestServices, - items: null); + instance: validatable, + serviceProvider: context.ActionContext?.HttpContext?.RequestServices, + items: null) + { + DisplayName = context.ModelMetadata.GetDisplayName(), + MemberName = context.ModelMetadata.PropertyName, + }; return ConvertResults(validatable.Validate(validationContext)); } diff --git a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs index d9f891ccb2..dd69d69f1c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs @@ -906,6 +906,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Equal(expected, error.ErrorMessage); } + [Fact] + public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet_WithNonProperty() + { + // Arrange + var expected = "Hmm, the supplied value is not valid."; + var dictionary = new ModelStateDictionary(); + + var bindingMetadataProvider = new DefaultBindingMetadataProvider(); + var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); + var optionsAccessor = new OptionsAccessor(); + optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor( + () => $"Hmm, the supplied value is not valid."); + + var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor); + var metadata = provider.GetMetadataForType(typeof(int)); + + // Act + dictionary.TryAddModelError("key", new FormatException(), metadata); + + // Assert + var entry = Assert.Single(dictionary); + Assert.Equal("key", entry.Key); + var error = Assert.Single(entry.Value.Errors); + Assert.Equal(expected, error.ErrorMessage); + } + [Fact] public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet() { @@ -951,6 +977,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Equal(expected, error.ErrorMessage); } + [Fact] + public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet_WithNonProperty() + { + // Arrange + var expected = "Hmm, the value 'some value' is not valid."; + var dictionary = new ModelStateDictionary(); + dictionary.SetModelValue("key", new string[] { "some value" }, "some value"); + + var bindingMetadataProvider = new DefaultBindingMetadataProvider(); + var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); + var optionsAccessor = new OptionsAccessor(); + optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor( + (value) => $"Hmm, the value '{ value }' is not valid."); + + var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor); + var metadata = provider.GetMetadataForType(typeof(int)); + + // Act + dictionary.TryAddModelError("key", new FormatException(), metadata); + + // Assert + var entry = Assert.Single(dictionary); + Assert.Equal("key", entry.Key); + var error = Assert.Single(entry.Value.Errors); + Assert.Equal(expected, error.ErrorMessage); + } + [Fact] public void ModelStateDictionary_NoErrorMessage_ForNonFormatException() { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs index 6f6e31a295..d1aae93d21 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs @@ -473,7 +473,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var entry = modelState["parameter"]; Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); var error = Assert.Single(entry.Errors); - Assert.Equal("Error1", error.ErrorMessage); + Assert.Equal("Error1 about '' (display: 'ValidatableModel').", error.ErrorMessage); entry = modelState["parameter.Property1"]; Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); @@ -491,6 +491,70 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.Equal("Error3", error.ErrorMessage); } + [Fact] + [ReplaceCulture] + public void Validate_NestedComplexType_IValidatableObject_Invalid() + { + // Arrange + var actionContext = new ActionContext(); + var modelState = actionContext.ModelState; + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)new ValidatableModelContainer + { + ValidatableModelProperty = new ValidatableModel(), + }; + + modelState.SetModelValue("parameter", "model", "model"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(actionContext, validationState, "parameter", model); + + // Assert + Assert.False(modelState.IsValid); + Assert.Collection( + modelState, + entry => + { + Assert.Equal("parameter", entry.Key); + Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState); + Assert.Empty(entry.Value.Errors); + }, + entry => + { + Assert.Equal("parameter.ValidatableModelProperty", entry.Key); + Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState); + var error = Assert.Single(entry.Value.Errors); + Assert.Equal( + "Error1 about 'ValidatableModelProperty' (display: 'Never valid').", + error.ErrorMessage); + }, + entry => + { + Assert.Equal("parameter.ValidatableModelProperty.Property1", entry.Key); + Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState); + var error = Assert.Single(entry.Value.Errors); + Assert.Equal("Error2", error.ErrorMessage); + }, + entry => + { + Assert.Equal("parameter.ValidatableModelProperty.Property2", entry.Key); + Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState); + var error = Assert.Single(entry.Value.Errors); + Assert.Equal("Error3", error.ErrorMessage); + }, + entry => + { + Assert.Equal("parameter.ValidatableModelProperty.Property3", entry.Key); + Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState); + var error = Assert.Single(entry.Value.Errors); + Assert.Equal("Error3", error.ErrorMessage); + }); + } + [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono)] public void Validate_ComplexType_IValidatableObject_CanUseRequestServices() @@ -1116,8 +1180,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionContext = new ActionContext(); var modelState = actionContext.ModelState; modelState.SetModelValue("parameter", rawValue: null, attemptedValue: null); - var validationState = new ValidationStateDictionary(); - validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + var validationState = new ValidationStateDictionary + { + { model, new ValidationStateEntry() { Key = "parameter" } } + }; // Act validator.Validate(actionContext, validationState, "parameter", model); @@ -1212,12 +1278,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal { public IEnumerable Validate(ValidationContext validationContext) { - yield return new ValidationResult("Error1", new string[] { }); + yield return new ValidationResult( + $"Error1 about '{validationContext.MemberName}' (display: '{validationContext.DisplayName}').", + new string[] { }); yield return new ValidationResult("Error2", new[] { "Property1" }); yield return new ValidationResult("Error3", new[] { "Property2", "Property3" }); } } + private class ValidatableModelContainer + { + [Display(Name = "Never valid")] + public ValidatableModel ValidatableModelProperty { get; set; } + } + private class TypeThatOverridesEquals { [StringLength(2)] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs index 050d1564d6..fa43421765 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders public async Task BindModelAddsModelErrorsOnInvalidCharacters() { // Arrange - var expected = "The value '\"Fys1\"' is not valid for Byte[]."; + var expected = "The value '\"Fys1\"' is not valid."; var valueProvider = new SimpleValueProvider() { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs index 7ab5c007ef..231c907750 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs @@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState() { // Arrange - var message = "The value 'not an integer' is not valid for Int32."; + var message = "The value 'not an integer' is not valid."; var bindingContext = GetBindingContext(typeof(int)); bindingContext.ValueProvider = new SimpleValueProvider { @@ -303,7 +303,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Assert.Null(bindingContext.Result.Model); var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors); - Assert.Equal("The value '12,5' is not valid for Decimal.", error.ErrorMessage, StringComparer.Ordinal); + Assert.Equal("The value '12,5' is not valid.", error.ErrorMessage, StringComparer.Ordinal); Assert.Null(error.Exception); } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs index e1f0f28264..1921085893 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs @@ -69,6 +69,32 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal kvp => { Assert.Equal("data-val-number", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }); } + [Fact] + public void AddValidation_CorrectValidationTypeAndOverriddenErrorMessage_WithNonProperty() + { + // Arrange + var expectedMessage = "Error message from override."; + var provider = new TestModelMetadataProvider(); + provider + .ForType(typeof(int)) + .BindingDetails(d => d.ModelBindingMessageProvider.SetNonPropertyValueMustBeANumberAccessor( + () => $"Error message from override.")); + var metadata = provider.GetMetadataForType(typeof(int)); + + var adapter = new NumericClientModelValidator(); + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-number", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }); + } + [Fact] [ReplaceCulture] public void AddValidation_DoesNotTrounceExistingAttributes() diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs index 59ab7b9b9d..09e5be855a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs @@ -360,7 +360,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Assert Assert.True(result.HasError); - Assert.Equal("The supplied value is invalid for Byte.", modelState["[2]"].Errors[0].ErrorMessage); + Assert.Equal("The supplied value is invalid.", modelState["[2]"].Errors[0].ErrorMessage); Assert.Null(modelState["[2]"].Errors[0].Exception); } diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs index f8efb8d476..db81815e0c 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs @@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests var error = Assert.Single(entry.Errors); Assert.Null(error.Exception); - Assert.Equal("The value 'abcd' is not valid for Int32.", error.ErrorMessage); + Assert.Equal("The value 'abcd' is not valid.", error.ErrorMessage); } [Fact] @@ -256,8 +256,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests .BindingDetails(binding => { // A real details provider could customize message based on BindingMetadataProviderContext. - binding.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor( - (value, name) => $"Hmm, '{ value }' is not a valid value for '{ name }'."); + binding.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor( + (value) => $"Hmm, '{ value }' is not a valid value."); }); var parameterBinder = ModelBindingTestHelper.GetParameterBinder(metadataProvider); var parameter = new ParameterDescriptor() @@ -300,7 +300,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests var error = Assert.Single(entry.Errors); Assert.Null(error.Exception); - Assert.Equal($"Hmm, 'abcd' is not a valid value for 'Int32'.", error.ErrorMessage); + Assert.Equal($"Hmm, 'abcd' is not a valid value.", error.ErrorMessage); } [Theory] diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ValidationIntegrationTests.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ValidationIntegrationTests.cs index 497ec4b21b..a0cc60067d 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ValidationIntegrationTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ValidationIntegrationTests.cs @@ -1225,7 +1225,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests public IEnumerable Validate(ValidationContext validationContext) { - return new[] { new ValidationResult("This is not valid.") }; + var result = new ValidationResult( + $"'{validationContext.MemberName}' (display: '{validationContext.DisplayName}') is not valid due " + + $"to its {nameof(NeverValid)} type."); + return new[] { result }; } } @@ -1240,15 +1243,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests return ValidationResult.Success; } - return new ValidationResult("Properties with this are not valid."); + return new ValidationResult( + $"'{validationContext.MemberName}' (display: '{validationContext.DisplayName}') is not valid due " + + $"to its associated {nameof(NeverValidAttribute)}."); } } private class ValidateSomeProperties { - public NeverValid NeverValid { get; set; } + [Display(Name = "Not ever valid")] + public NeverValid NeverValidBecauseType { get; set; } [NeverValid] + [Display(Name = "Never valid")] public string NeverValidBecauseAttribute { get; set; } [ValidateNever] @@ -1276,7 +1283,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests var testContext = ModelBindingTestHelper.GetTestContext( request => request.QueryString - = new QueryString($"?{nameof(ValidateSomeProperties.NeverValid)}.{nameof(NeverValid.NeverValidProperty)}=1")); + = new QueryString($"?{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}=1")); var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var modelState = testContext.ModelState; @@ -1287,7 +1294,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // Assert Assert.True(result.IsModelSet); var model = Assert.IsType(result.Model); - Assert.Equal("1", model.NeverValid.NeverValidProperty); + Assert.Equal("1", model.NeverValidBecauseType.NeverValidProperty); Assert.False(modelState.IsValid); Assert.Equal(1, modelState.ErrorCount); @@ -1295,17 +1302,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests modelState, state => { - Assert.Equal(nameof(ValidateSomeProperties.NeverValid), state.Key); + Assert.Equal(nameof(ValidateSomeProperties.NeverValidBecauseType), state.Key); Assert.Equal(ModelValidationState.Invalid, state.Value.ValidationState); var error = Assert.Single(state.Value.Errors); - Assert.Equal("This is not valid.", error.ErrorMessage); + Assert.Equal( + "'NeverValidBecauseType' (display: 'Not ever valid') is not valid due to its NeverValid type.", + error.ErrorMessage); Assert.Null(error.Exception); }, state => { Assert.Equal( - $"{nameof(ValidateSomeProperties.NeverValid)}.{nameof(NeverValid.NeverValidProperty)}", + $"{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}", state.Key); Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState); }); @@ -1344,7 +1353,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.NotNull(state); Assert.Equal(ModelValidationState.Invalid, state.ValidationState); var error = Assert.Single(state.Errors); - Assert.Equal("Properties with this are not valid.", error.ErrorMessage); + Assert.Equal( + "'NeverValidBecauseAttribute' (display: 'Never valid') is not valid due to its associated NeverValidAttribute.", + error.ErrorMessage); Assert.Null(error.Exception); } @@ -1410,7 +1421,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } [Theory] - [InlineData(nameof(ValidateSomeProperties.NeverValid) + "." + nameof(NeverValid.NeverValidProperty))] + [InlineData(nameof(ValidateSomeProperties.NeverValidBecauseType) + "." + nameof(NeverValid.NeverValidProperty))] [InlineData(nameof(ValidateSomeProperties.NeverValidBecauseAttribute))] [InlineData(nameof(ValidateSomeProperties.ValidateNever))] public async Task PropertyWithinValidateNeverType_IsSkipped(string propertyName)