diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs index 3d68fd45bb..f78815a7d1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs @@ -38,12 +38,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } /// - /// Gets the container type of this metadata if it represents a property, otherwise null. + /// Gets the type containing the property if this metadata is for a property; otherwise. /// public Type ContainerType => Identity.ContainerType; /// - /// Gets the metadata of the container type that the current instance is part of. + /// Gets the metadata for if this metadata is for a property; + /// otherwise. /// public virtual ModelMetadata ContainerMetadata { @@ -64,9 +65,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public Type ModelType => Identity.ModelType; /// - /// Gets the property name represented by the current instance. + /// Gets the name of the parameter or property if this metadata is for a parameter or property; + /// otherwise i.e. if this is the metadata for a type. /// - public string PropertyName => Identity.Name; + public string Name => Identity.Name; + + /// + /// Gets the name of the parameter if this metadata is for a parameter; otherwise. + /// + public string ParameterName => MetadataKind == ModelMetadataKind.Parameter ? Identity.Name : null; + + /// + /// Gets the name of the property if this metadata is for a property; otherwise. + /// + public string PropertyName => MetadataKind == ModelMetadataKind.Property ? Identity.Name : null; /// /// Gets the key for the current instance. @@ -384,12 +396,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// /// will return the first of the following expressions which has a - /// non-null value: DisplayName, PropertyName, ModelType.Name. + /// non- value: , , or ModelType.Name. /// /// The display name. public string GetDisplayName() { - return DisplayName ?? PropertyName ?? ModelType.Name; + return DisplayName ?? Name ?? ModelType.Name; } /// @@ -470,13 +482,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding private string DebuggerToString() { - if (Identity.MetadataKind == ModelMetadataKind.Type) + switch (MetadataKind) { - return $"ModelMetadata (Type: '{ModelType.Name}')"; - } - else - { - return $"ModelMetadata (Property: '{ContainerType.Name}.{PropertyName}' Type: '{ModelType.Name}')"; + case ModelMetadataKind.Parameter: + return $"ModelMetadata (Parameter: '{ParameterName}' Type: '{ModelType.Name}')"; + case ModelMetadataKind.Property: + return $"ModelMetadata (Property: '{ContainerType.Name}.{PropertyName}' Type: '{ModelType.Name}')"; + case ModelMetadataKind.Type: + return $"ModelMetadata (Type: '{ModelType.Name}')"; + default: + return $"Unsupported MetadataKind '{MetadataKind}'."; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs index 647a76da5c..970e5b6975 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs @@ -180,7 +180,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// /// This method allows adding the to the current - /// when is not available or the exact + /// when is not available or the exact /// must be maintained for later use (even if it is for example a ). /// Where is available, use instead. /// diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs index 4dda71ab01..50734ccded 100644 --- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs @@ -620,7 +620,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer ModelMetadata = metadata, BinderModelName = bindingInfo?.BinderModelName ?? metadata.BinderModelName, BindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource, - PropertyName = propertyName ?? metadata.PropertyName + PropertyName = propertyName ?? metadata.Name, }; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs index 8fe2fe37b5..1dd72c189f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -104,7 +105,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal private static readonly Action _unableToInferParameterSources; private static readonly Action _registeredModelBinderProviders; private static readonly Action _foundNoValueForPropertyInRequest; - private static readonly Action _foundNoValueInRequest; + private static readonly Action _foundNoValueForParameterInRequest; + private static readonly Action _foundNoValueInRequest; private static readonly Action _noPublicSettableProperties; private static readonly Action _cannotBindToComplexType; private static readonly Action _cannotBindToFilesCollectionDueToUnsupportedContentType; @@ -115,6 +117,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal private static readonly Action _attemptingToBindCollectionUsingIndices; private static readonly Action _attemptingToBindCollectionOfKeyValuePair; private static readonly Action _noKeyValueFormatForDictionaryModelBinder; + private static readonly Action _attemptingToBindParameterModel; + private static readonly Action _doneAttemptingToBindParameterModel; private static readonly Action _attemptingToBindPropertyModel; private static readonly Action _doneAttemptingToBindPropertyModel; private static readonly Action _attemptingToBindModel; @@ -475,7 +479,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal 15, "Could not find a value in the request with name '{ModelName}' for binding property '{PropertyContainerType}.{ModelFieldName}' of type '{ModelType}'."); - _foundNoValueInRequest = LoggerMessage.Define( + _foundNoValueForParameterInRequest = LoggerMessage.Define( LogLevel.Debug, 16, "Could not find a value in the request with name '{ModelName}' for binding parameter '{ModelFieldName}' of type '{ModelType}'."); @@ -610,6 +614,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal LogLevel.Debug, 43, "Could not create a binder for type '{ModelType}' as this binder only supports 'System.String' type or a collection of 'System.String'."); + + _attemptingToBindParameterModel = LoggerMessage.Define( + LogLevel.Debug, + 44, + "Attempting to bind parameter '{ParameterName}' of type '{ModelType}' using the name '{ModelName}' in request data ..."); + + _doneAttemptingToBindParameterModel = LoggerMessage.Define( + LogLevel.Debug, + 45, + "Done attempting to bind parameter '{ParameterName}' of type '{ModelType}'."); + + _foundNoValueInRequest = LoggerMessage.Define( + LogLevel.Debug, + 46, + "Could not find a value in the request with name '{ModelName}' of type '{ModelType}'."); } public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable outputFormatters) @@ -1157,26 +1176,32 @@ namespace Microsoft.AspNetCore.Mvc.Internal } var modelMetadata = bindingContext.ModelMetadata; - var isProperty = modelMetadata.ContainerType != null; - - if (isProperty) + switch (modelMetadata.MetadataKind) { - _foundNoValueForPropertyInRequest( - logger, - bindingContext.ModelName, - modelMetadata.ContainerType, - modelMetadata.PropertyName, - bindingContext.ModelType, - null); - } - else - { - _foundNoValueInRequest( - logger, - bindingContext.ModelName, - modelMetadata.PropertyName, - bindingContext.ModelType, - null); + case ModelMetadataKind.Parameter: + _foundNoValueForParameterInRequest( + logger, + bindingContext.ModelName, + modelMetadata.ParameterName, + bindingContext.ModelType, + null); + break; + case ModelMetadataKind.Property: + _foundNoValueForPropertyInRequest( + logger, + bindingContext.ModelName, + modelMetadata.ContainerType, + modelMetadata.PropertyName, + bindingContext.ModelType, + null); + break; + case ModelMetadataKind.Type: + _foundNoValueInRequest( + logger, + bindingContext.ModelName, + bindingContext.ModelType, + null); + break; } } @@ -1218,89 +1243,237 @@ namespace Microsoft.AspNetCore.Mvc.Internal } var modelMetadata = bindingContext.ModelMetadata; - var isProperty = modelMetadata.ContainerType != null; - - if (isProperty) + switch (modelMetadata.MetadataKind) { - _attemptingToBindPropertyModel( - logger, - modelMetadata.ContainerType, - modelMetadata.PropertyName, - modelMetadata.ModelType, - bindingContext.ModelName, - null); - } - else - { - _attemptingToBindModel(logger, bindingContext.ModelType, bindingContext.ModelName, null); + case ModelMetadataKind.Parameter: + _attemptingToBindParameterModel( + logger, + modelMetadata.ParameterName, + modelMetadata.ModelType, + bindingContext.ModelName, + null); + break; + case ModelMetadataKind.Property: + _attemptingToBindPropertyModel( + logger, + modelMetadata.ContainerType, + modelMetadata.PropertyName, + modelMetadata.ModelType, + bindingContext.ModelName, + null); + break; + case ModelMetadataKind.Type: + _attemptingToBindModel(logger, bindingContext.ModelType, bindingContext.ModelName, null); + break; } } public static void DoneAttemptingToBindModel(this ILogger logger, ModelBindingContext bindingContext) { + if (!logger.IsEnabled(LogLevel.Debug)) + { + return; + } + var modelMetadata = bindingContext.ModelMetadata; - var isProperty = modelMetadata.ContainerType != null; - - if (isProperty) + switch (modelMetadata.MetadataKind) { - _doneAttemptingToBindPropertyModel( - logger, - modelMetadata.ContainerType, - modelMetadata.PropertyName, - modelMetadata.ModelType, - null); - } - else - { - _doneAttemptingToBindModel(logger, bindingContext.ModelType, bindingContext.ModelName, null); + case ModelMetadataKind.Parameter: + _doneAttemptingToBindParameterModel( + logger, + modelMetadata.ParameterName, + modelMetadata.ModelType, + null); + break; + case ModelMetadataKind.Property: + _doneAttemptingToBindPropertyModel( + logger, + modelMetadata.ContainerType, + modelMetadata.PropertyName, + modelMetadata.ModelType, + null); + break; + case ModelMetadataKind.Type: + _doneAttemptingToBindModel(logger, bindingContext.ModelType, bindingContext.ModelName, null); + break; } } - public static void AttemptingToBindParameterOrProperty(this ILogger logger, ParameterDescriptor parameter, ModelBindingContext bindingContext) + public static void AttemptingToBindParameterOrProperty( + this ILogger logger, + ParameterDescriptor parameter, + ModelBindingContext bindingContext) { - if (parameter is ControllerBoundPropertyDescriptor propertyDescriptor) + if (!logger.IsEnabled(LogLevel.Debug)) { - _attemptingToBindProperty(logger, propertyDescriptor.PropertyInfo.DeclaringType, parameter.Name, bindingContext.ModelType, null); + return; } - else + + var modelMetadata = bindingContext.ModelMetadata; + switch (modelMetadata.MetadataKind) { - _attemptingToBindParameter(logger, parameter.Name, bindingContext.ModelType, null); + case ModelMetadataKind.Parameter: + _attemptingToBindParameter(logger, modelMetadata.ParameterName, modelMetadata.ModelType, null); + break; + case ModelMetadataKind.Property: + _attemptingToBindProperty( + logger, + modelMetadata.ContainerType, + modelMetadata.PropertyName, + modelMetadata.ModelType, + null); + break; + case ModelMetadataKind.Type: + if (parameter is ControllerParameterDescriptor parameterDescriptor) + { + _attemptingToBindParameter( + logger, + parameterDescriptor.ParameterInfo.Name, + modelMetadata.ModelType, + null); + } + else + { + // Likely binding a page handler parameter. Due to various special cases, parameter.Name may + // be empty. No way to determine actual name. + _attemptingToBindParameter(logger, parameter.Name, modelMetadata.ModelType, null); + } + break; } } - public static void DoneAttemptingToBindParameterOrProperty(this ILogger logger, ParameterDescriptor parameter, ModelBindingContext bindingContext) + public static void DoneAttemptingToBindParameterOrProperty( + this ILogger logger, + ParameterDescriptor parameter, + ModelBindingContext bindingContext) { - if (parameter is ControllerBoundPropertyDescriptor propertyDescriptor) + if (!logger.IsEnabled(LogLevel.Debug)) { - _doneAttemptingToBindProperty(logger, propertyDescriptor.PropertyInfo.DeclaringType, parameter.Name, bindingContext.ModelType, null); + return; } - else + + var modelMetadata = bindingContext.ModelMetadata; + switch (modelMetadata.MetadataKind) { - _doneAttemptingToBindParameter(logger, parameter.Name, bindingContext.ModelType, null); + case ModelMetadataKind.Parameter: + _doneAttemptingToBindParameter(logger, modelMetadata.ParameterName, modelMetadata.ModelType, null); + break; + case ModelMetadataKind.Property: + _doneAttemptingToBindProperty( + logger, + modelMetadata.ContainerType, + modelMetadata.PropertyName, + modelMetadata.ModelType, + null); + break; + case ModelMetadataKind.Type: + if (parameter is ControllerParameterDescriptor parameterDescriptor) + { + _doneAttemptingToBindParameter( + logger, + parameterDescriptor.ParameterInfo.Name, + modelMetadata.ModelType, + null); + } + else + { + // Likely binding a page handler parameter. Due to various special cases, parameter.Name may + // be empty. No way to determine actual name. + _doneAttemptingToBindParameter(logger, parameter.Name, modelMetadata.ModelType, null); + } + break; } } - public static void AttemptingToValidateParameterOrProperty(this ILogger logger, ParameterDescriptor parameter, ModelBindingContext bindingContext) + public static void AttemptingToValidateParameterOrProperty( + this ILogger logger, + ParameterDescriptor parameter, + ModelBindingContext bindingContext) { - if (parameter is ControllerBoundPropertyDescriptor propertyDescriptor) + if (!logger.IsEnabled(LogLevel.Debug)) { - _attemptingToValidateProperty(logger, propertyDescriptor.PropertyInfo.DeclaringType, parameter.Name, bindingContext.ModelType, null); + return; } - else + + var modelMetadata = bindingContext.ModelMetadata; + switch (modelMetadata.MetadataKind) { - _attemptingToValidateParameter(logger, parameter.Name, bindingContext.ModelType, null); + case ModelMetadataKind.Parameter: + _attemptingToValidateParameter(logger, modelMetadata.ParameterName, modelMetadata.ModelType, null); + break; + case ModelMetadataKind.Property: + _attemptingToValidateProperty( + logger, + modelMetadata.ContainerType, + modelMetadata.PropertyName, + modelMetadata.ModelType, + null); + break; + case ModelMetadataKind.Type: + if (parameter is ControllerParameterDescriptor parameterDescriptor) + { + _attemptingToValidateParameter( + logger, + parameterDescriptor.ParameterInfo.Name, + modelMetadata.ModelType, + null); + } + else + { + // Likely binding a page handler parameter. Due to various special cases, parameter.Name may + // be empty. No way to determine actual name. This case is less likely than for binding logging + // (above). Should occur only with a legacy IModelMetadataProvider implementation. + _attemptingToValidateParameter(logger, parameter.Name, modelMetadata.ModelType, null); + } + break; } } - public static void DoneAttemptingToValidateParameterOrProperty(this ILogger logger, ParameterDescriptor parameter, ModelBindingContext bindingContext) + public static void DoneAttemptingToValidateParameterOrProperty( + this ILogger logger, + ParameterDescriptor parameter, + ModelBindingContext bindingContext) { - if (parameter is ControllerBoundPropertyDescriptor propertyDescriptor) + if (!logger.IsEnabled(LogLevel.Debug)) { - _doneAttemptingToValidateProperty(logger, propertyDescriptor.PropertyInfo.DeclaringType, parameter.Name, bindingContext.ModelType, null); + return; } - else + + var modelMetadata = bindingContext.ModelMetadata; + switch (modelMetadata.MetadataKind) { - _doneAttemptingToValidateParameter(logger, parameter.Name, bindingContext.ModelType, null); + case ModelMetadataKind.Parameter: + _doneAttemptingToValidateParameter( + logger, + modelMetadata.ParameterName, + modelMetadata.ModelType, + null); + break; + case ModelMetadataKind.Property: + _doneAttemptingToValidateProperty( + logger, + modelMetadata.ContainerType, + modelMetadata.PropertyName, + modelMetadata.ModelType, + null); + break; + case ModelMetadataKind.Type: + if (parameter is ControllerParameterDescriptor parameterDescriptor) + { + _doneAttemptingToValidateParameter( + logger, + parameterDescriptor.ParameterInfo.Name, + modelMetadata.ModelType, + null); + } + else + { + // Likely binding a page handler parameter. Due to various special cases, parameter.Name may + // be empty. No way to determine actual name. This case is less likely than for binding logging + // (above). Should occur only with a legacy IModelMetadataProvider implementation. + _doneAttemptingToValidateParameter(logger, parameter.Name, modelMetadata.ModelType, null); + } + break; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinder.cs index 55a5e41055..dd02365c96 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinder.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -357,24 +358,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders // application developer should know that this was an invalid type to try to bind to. if (_modelCreator == null) { - // The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as + // The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as // reflection does not provide information about the implicit parameterless constructor for a struct. // This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression // compile fails to construct it. var modelTypeInfo = bindingContext.ModelType.GetTypeInfo(); if (modelTypeInfo.IsAbstract || modelTypeInfo.GetConstructor(Type.EmptyTypes) == null) { - if (bindingContext.IsTopLevelObject) + var metadata = bindingContext.ModelMetadata; + switch (metadata.MetadataKind) { - throw new InvalidOperationException( - Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject(modelTypeInfo.FullName)); + case ModelMetadataKind.Parameter: + throw new InvalidOperationException( + Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForParameter( + modelTypeInfo.FullName, + metadata.ParameterName)); + case ModelMetadataKind.Property: + throw new InvalidOperationException( + Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForProperty( + modelTypeInfo.FullName, + metadata.PropertyName, + bindingContext.ModelMetadata.ContainerType.FullName)); + case ModelMetadataKind.Type: + throw new InvalidOperationException( + Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForType( + modelTypeInfo.FullName)); } - - throw new InvalidOperationException( - Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForProperty( - modelTypeInfo.FullName, - bindingContext.ModelName, - bindingContext.ModelMetadata.ContainerType.FullName)); } _modelCreator = Expression diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs index a6735bdaa4..82f74e6e58 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs @@ -79,8 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding typeof(IModelBinderProvider).FullName)); } - IModelBinder binder; - if (TryGetCachedBinder(context.Metadata, context.CacheToken, out binder)) + if (TryGetCachedBinder(context.Metadata, context.CacheToken, out var binder)) { return binder; } @@ -106,8 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding // so that all intermediate results can be cached. private IModelBinder CreateBinderCoreCached(DefaultModelBinderProviderContext providerContext, object token) { - IModelBinder binder; - if (TryGetCachedBinder(providerContext.Metadata, token, out binder)) + if (TryGetCachedBinder(providerContext.Metadata, token, out var binder)) { return binder; } @@ -145,8 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding // PlaceholderBinder because that would result in lots of unnecessary indirection and allocations. var visited = providerContext.Visited; - IModelBinder binder; - if (visited.TryGetValue(key, out binder)) + if (visited.TryGetValue(key, out var binder)) { if (binder != null) { @@ -178,8 +175,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } // If the PlaceholderBinder was created, then it means we recursed. Hook it up to the 'real' binder. - var placeholderBinder = visited[key] as PlaceholderBinder; - if (placeholderBinder != null) + if (visited[key] is PlaceholderBinder placeholderBinder) { // It's also possible that user code called into `CreateBinder` but then returned null, we don't // want to create something that will null-ref later so just hook this up to the no-op binder. @@ -347,13 +343,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public override string ToString() { - if (_metadata.MetadataKind == ModelMetadataKind.Type) + switch (_metadata.MetadataKind) { - return $"{_token} (Type: '{_metadata.ModelType.Name}')"; - } - else - { - return $"{_token} (Property: '{_metadata.ContainerType.Name}.{_metadata.PropertyName}' Type: '{_metadata.ModelType.Name}')"; + case ModelMetadataKind.Parameter: + return $"{_token} (Parameter: '{_metadata.ParameterName}' Type: '{_metadata.ModelType.Name}')"; + case ModelMetadataKind.Property: + return $"{_token} (Property: '{_metadata.ContainerType.Name}.{_metadata.PropertyName}' " + + $"Type: '{_metadata.ModelType.Name}')"; + case ModelMetadataKind.Type: + return $"{_token} (Type: '{_metadata.ModelType.Name}')"; + default: + return $"Unsupported MetadataKind '{_metadata.MetadataKind}'."; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs index d128b04dd6..9f621f3b9e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation ModelState = actionContext.ModelState; CurrentPath = new ValidationStack(); } - + protected IModelValidatorProvider ValidatorProvider { get; } protected IModelMetadataProvider MetadataProvider { get; } protected ValidatorCache Cache { get; } @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation protected IValidationStrategy Strategy { get; set; } /// - /// Indicates whether validation of a complex type should be performed if validation fails for any of its children. The default behavior is false. + /// Indicates whether validation of a complex type should be performed if validation fails for any of its children. The default behavior is false. /// public bool ValidateComplexTypesIfChildValidationFails { get; set; } /// @@ -146,11 +146,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation var result = results[i]; var key = ModelNames.CreatePropertyModelName(Key, result.MemberName); - // If this is a top-level parameter/property, the key would be empty, - // so use the name of the top-level property - if (string.IsNullOrEmpty(key) && Metadata.PropertyName != null) + // If this is a top-level parameter/property, the key would be empty. + // So, use the name of the top-level property/property. + if (string.IsNullOrEmpty(key) && Metadata.Name != null) { - key = Metadata.PropertyName; + key = Metadata.Name; } ModelState.TryAddModelError(key, result.Message); @@ -306,8 +306,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation return null; } - ValidationStateEntry entry; - ValidationState.TryGetValue(model, out entry); + ValidationState.TryGetValue(model, out var entry); return entry; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index 8e42894851..ec92b11640 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -1273,16 +1273,16 @@ namespace Microsoft.AspNetCore.Mvc.Core /// /// Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. /// - internal static string ComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject + internal static string ComplexTypeModelBinder_NoParameterlessConstructor_ForType { - get => GetString("ComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject"); + get => GetString("ComplexTypeModelBinder_NoParameterlessConstructor_ForType"); } /// /// Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. /// - internal static string FormatComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("ComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject"), p0); + internal static string FormatComplexTypeModelBinder_NoParameterlessConstructor_ForType(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ComplexTypeModelBinder_NoParameterlessConstructor_ForType"), p0); /// /// Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, set the '{1}' property to a non-null value in the '{2}' constructor. @@ -1438,6 +1438,20 @@ namespace Microsoft.AspNetCore.Mvc.Core internal static string FormatApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("ApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional"), p0, p1); + /// + /// Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, give the '{1}' parameter a non-null default value. + /// + internal static string ComplexTypeModelBinder_NoParameterlessConstructor_ForParameter + { + get => GetString("ComplexTypeModelBinder_NoParameterlessConstructor_ForParameter"); + } + + /// + /// Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, give the '{1}' parameter a non-null default value. + /// + internal static string FormatComplexTypeModelBinder_NoParameterlessConstructor_ForParameter(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("ComplexTypeModelBinder_NoParameterlessConstructor_ForParameter"), p0, p1); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx index e4326aba1d..671190495e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx @@ -400,7 +400,7 @@ '{0}' and '{1}' are out of bounds for the string. '{0}' and '{1}' are the parameters which combine to be out of bounds. - + Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. @@ -436,4 +436,7 @@ Assembly '{0}' declared as a related assembly by assembly '{1}' cannot define additional related assemblies. + + Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, give the '{1}' parameter a non-null default value. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsModelValidator.cs index 790356eba3..563fa078a7 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsModelValidator.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsModelValidator.cs @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal } var metadata = validationContext.ModelMetadata; - var memberName = metadata.PropertyName; + var memberName = metadata.Name; var container = validationContext.Container; var context = new ValidationContext( diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs index d10c310e28..edc0286909 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs @@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal } var messageProvider = modelMetadata.ModelBindingMessageProvider; - var name = modelMetadata.DisplayName ?? modelMetadata.PropertyName; + var name = modelMetadata.DisplayName ?? modelMetadata.Name; if (name == null) { return messageProvider.NonPropertyValueMustBeANumberAccessor(); diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs index dc0a594120..55a9ca23a1 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs @@ -19,8 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal return Enumerable.Empty(); } - var validatable = model as IValidatableObject; - if (validatable == null) + if (!(model is IValidatableObject validatable)) { var message = Resources.FormatValidatableObjectAdapter_IncompatibleType( typeof(IValidatableObject).Name, @@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal items: null) { DisplayName = context.ModelMetadata.GetDisplayName(), - MemberName = context.ModelMetadata.PropertyName, + MemberName = context.ModelMetadata.Name, }; return ConvertResults(validatable.Validate(validationContext)); @@ -66,4 +65,4 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs index 7be03ba94d..b637350e3f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Reflection; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Xunit; @@ -24,10 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [InlineData(typeof(int))] public void IsComplexType_ReturnsFalseForSimpleTypes(Type type) { - // Arrange - var provider = new EmptyModelMetadataProvider(); - - // Act + // Arrange & Act var modelMetadata = new TestModelMetadata(type); // Assert @@ -41,10 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [InlineData(typeof(Nullable))] public void IsComplexType_ReturnsTrueForComplexTypes(Type type) { - // Arrange - var provider = new EmptyModelMetadataProvider(); - - // Act + // Arrange & Act var modelMetadata = new TestModelMetadata(type); // Assert @@ -106,10 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [InlineData(typeof(JustEnumerable))] public void IsCollectionType_ReturnsFalseForNonCollectionTypes(Type type) { - // Arrange - var provider = new EmptyModelMetadataProvider(); - - // Act + // Arrange & Act var modelMetadata = new TestModelMetadata(type); // Assert @@ -120,10 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [MemberData(nameof(CollectionAndEnumerableData))] public void IsCollectionType_ReturnsTrueForCollectionTypes(Type type) { - // Arrange - var provider = new EmptyModelMetadataProvider(); - - // Act + // Arrange & Act var modelMetadata = new TestModelMetadata(type); // Assert @@ -134,10 +123,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [MemberData(nameof(NonCollectionNonEnumerableData))] public void IsEnumerableType_ReturnsFalseForNonEnumerableTypes(Type type) { - // Arrange - var provider = new EmptyModelMetadataProvider(); - - // Act + // Arrange & Act var modelMetadata = new TestModelMetadata(type); // Assert @@ -151,10 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [InlineData(typeof(JustEnumerable))] public void IsEnumerableType_ReturnsTrueForEnumerableTypes(Type type) { - // Arrange - var provider = new EmptyModelMetadataProvider(); - - // Act + // Arrange & Act var modelMetadata = new TestModelMetadata(type); // Assert @@ -173,10 +156,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [InlineData(typeof(Nullable), true)] public void IsNullableValueType_ReturnsExpectedValue(Type modelType, bool expected) { - // Arrange + // Arrange & Act var modelMetadata = new TestModelMetadata(modelType); - // Act & Assert + // Assert Assert.Equal(expected, modelMetadata.IsNullableValueType); } @@ -192,10 +175,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [InlineData(typeof(Nullable), true)] public void IsReferenceOrNullableType_ReturnsExpectedValue(Type modelType, bool expected) { - // Arrange + // Arrange & Act var modelMetadata = new TestModelMetadata(modelType); - // Act & Assert + // Assert Assert.Equal(expected, modelMetadata.IsReferenceOrNullableType); } @@ -211,13 +194,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [InlineData(typeof(Nullable), typeof(IsComplexTypeModel))] public void UnderlyingOrModelType_ReturnsExpectedValue(Type modelType, Type expected) { - // Arrange + // Arrange & Act var modelMetadata = new TestModelMetadata(modelType); - // Act & Assert + // Assert Assert.Equal(expected, modelMetadata.UnderlyingOrModelType); } + // ElementType + [Theory] [InlineData(typeof(object))] [InlineData(typeof(int))] @@ -257,13 +242,86 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Equal(expected, elementType); } + // ContainerType + + [Fact] + public void ContainerType_IsNull_ForType() + { + // Arrange & Act + var metadata = new TestModelMetadata(typeof(int)); + + // Assert + Assert.Null(metadata.ContainerType); + } + + [Fact] + public void ContainerType_IsNull_ForParameter() + { + // Arrange & Act + var method = typeof(CollectionImplementation).GetMethod(nameof(CollectionImplementation.Add)); + var parameter = method.GetParameters()[0]; // Add(string item) + var metadata = new TestModelMetadata(parameter); + + // Assert + Assert.Null(metadata.ContainerType); + } + + [Fact] + public void ContainerType_ReturnExpectedMetadata_ForProperty() + { + // Arrange & Act + var metadata = new TestModelMetadata(typeof(int), nameof(string.Length), typeof(string)); + + // Assert + Assert.Equal(typeof(string), metadata.ContainerType); + } + + // Name / ParameterName / PropertyName + + [Fact] + public void Names_ReturnExpectedMetadata_ForType() + { + // Arrange & Act + var metadata = new TestModelMetadata(typeof(int)); + + // Assert + Assert.Null(metadata.Name); + Assert.Null(metadata.ParameterName); + Assert.Null(metadata.PropertyName); + } + + [Fact] + public void Names_ReturnExpectedMetadata_ForParameter() + { + // Arrange & Act + var method = typeof(CollectionImplementation).GetMethod(nameof(CollectionImplementation.Add)); + var parameter = method.GetParameters()[0]; // Add(string item) + var metadata = new TestModelMetadata(parameter); + + // Assert + Assert.Equal("item", metadata.Name); + Assert.Equal("item", metadata.ParameterName); + Assert.Null(metadata.PropertyName); + } + + [Fact] + public void Names_ReturnExpectedMetadata_ForProperty() + { + // Arrange & Act + var metadata = new TestModelMetadata(typeof(int), nameof(string.Length), typeof(string)); + + // Assert + Assert.Equal(nameof(string.Length), metadata.Name); + Assert.Null(metadata.ParameterName); + Assert.Equal(nameof(string.Length), metadata.PropertyName); + } + // GetDisplayName() [Fact] public void GetDisplayName_ReturnsDisplayName_IfSet() { // Arrange - var provider = new EmptyModelMetadataProvider(); var metadata = new TestModelMetadata(typeof(int), "Length", typeof(string)); metadata.SetDisplayName("displayName"); @@ -274,11 +332,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Equal("displayName", result); } + [Fact] + public void GetDisplayName_ReturnsParameterName_WhenSetAndDisplayNameIsNull() + { + // Arrange + var method = typeof(CollectionImplementation).GetMethod(nameof(CollectionImplementation.Add)); + var parameter = method.GetParameters()[0]; // Add(string item) + var metadata = new TestModelMetadata(parameter); + + // Act + var result = metadata.GetDisplayName(); + + // Assert + Assert.Equal("item", result); + } + [Fact] public void GetDisplayName_ReturnsPropertyName_WhenSetAndDisplayNameIsNull() { // Arrange - var provider = new EmptyModelMetadataProvider(); var metadata = new TestModelMetadata(typeof(int), "Length", typeof(string)); // Act @@ -292,7 +364,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void GetDisplayName_ReturnsTypeName_WhenPropertyNameAndDisplayNameAreNull() { // Arrange - var provider = new EmptyModelMetadataProvider(); var metadata = new TestModelMetadata(typeof(string)); // Act @@ -302,6 +373,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Equal("String", result); } + // Virtual methods and properties that throw NotImplementedException in the abstract class. + [Fact] public void GetContainerMetadata_ThrowsNotImplementedException_ByDefault() { @@ -341,6 +414,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { } + public TestModelMetadata(ParameterInfo parameter) + : base(ModelMetadataIdentity.ForParameter(parameter)) + { + } + public TestModelMetadata(Type modelType, string propertyName, Type containerType) : base(ModelMetadataIdentity.ForProperty(modelType, propertyName, containerType)) { diff --git a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs index 7fb7de7464..75a01945d2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs @@ -971,7 +971,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } [Fact] - public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet_WithNonProperty() + public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet_WithParameter() { // Arrange var expected = "Hmm, the supplied value is not valid."; @@ -981,7 +981,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); var optionsAccessor = new OptionsAccessor(); optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor( - () => $"Hmm, the supplied value is not valid."); + () => "Hmm, the supplied value is not valid."); + + var method = typeof(string).GetMethod(nameof(string.Copy)); + var parameter = method.GetParameters()[0]; // Copy(string str) + var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor); + var metadata = provider.GetMetadataForParameter(parameter); + + // 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_AddsCustomErrorMessage_WhenModelStateNotSet_WithType() + { + // 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)); @@ -1058,7 +1086,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } [Fact] - public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet_WithNonProperty() + public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet_WithParameter() + { + // 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 method = typeof(string).GetMethod(nameof(string.Copy)); + var parameter = method.GetParameters()[0]; // Copy(string str) + var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor); + var metadata = provider.GetMetadataForParameter(parameter); + + // 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_AddsCustomErrorMessage_WhenModelStateSet_WithType() { // Arrange var expected = "Hmm, the value 'some value' is not valid."; @@ -1477,4 +1534,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public override string Message { get; } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs index cc9caed07e..c76eb27c0a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Reflection; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -371,6 +370,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders exception.Message); } + [Fact] + public void CreateModel_ForClassWithNoParameterlessConstructor_AsElement_ThrowsException() + { + // Arrange + var expectedMessage = "Could not create an instance of type " + + $"'{typeof(ClassWithNoParameterlessConstructor)}'. Model bound complex types must not be abstract " + + "or value types and must have a parameterless constructor."; + var metadata = GetMetadataForType(typeof(ClassWithNoParameterlessConstructor)); + var bindingContext = new DefaultModelBindingContext + { + ModelMetadata = metadata, + }; + var binder = CreateBinder(metadata); + + // Act & Assert + var exception = Assert.Throws(() => binder.CreateModelPublic(bindingContext)); + Assert.Equal(expectedMessage, exception.Message); + } + [Fact] public void CreateModel_ForStructModelType_AsProperty_ThrowsException() { @@ -1096,6 +1114,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders public double Y { get; } } + private class ClassWithNoParameterlessConstructor + { + public ClassWithNoParameterlessConstructor(string name) + { + Name = name; + } + + public string Name { get; set; } + } + private class BindingOptionalProperty { [BindingBehavior(BindingBehavior.Optional)] 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 4831151d13..dc4dbc9aa3 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders @@ -157,12 +158,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Assert.Equal(message, error.ErrorMessage); } - [Fact] - public async Task BindModel_EmptyValueProviderResult_ReturnsFailed() + public static TheoryData IntegerModelMetadataDataSet + { + get + { + var metadataProvider = new EmptyModelMetadataProvider(); + var method = typeof(MetadataClass).GetMethod(nameof(MetadataClass.IsLovely)); + var parameter = method.GetParameters()[0]; // IsLovely(int parameter) + + return new TheoryData + { + metadataProvider.GetMetadataForParameter(parameter), + metadataProvider.GetMetadataForProperty(typeof(MetadataClass), nameof(MetadataClass.Property)), + metadataProvider.GetMetadataForType(typeof(int)), + }; + } + } + + [Theory] + [MemberData(nameof(IntegerModelMetadataDataSet))] + public async Task BindModel_EmptyValueProviderResult_ReturnsFailedAndLogsSuccessfully(ModelMetadata metadata) { // Arrange var bindingContext = GetBindingContext(typeof(int)); - var binder = new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance); + bindingContext.ModelMetadata = metadata; + + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var binder = new SimpleTypeModelBinder(typeof(int), loggerFactory); // Act await binder.BindModelAsync(bindingContext); @@ -170,6 +193,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders // Assert Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result); Assert.Empty(bindingContext.ModelState); + Assert.Equal(2, sink.Writes.Count); } [Theory] @@ -236,17 +260,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Assert.True(bindingContext.ModelState.ContainsKey("theModelName")); } - [Fact] - public async Task BindModel_ValidValueProviderResult_ReturnsModel() + [Theory] + [MemberData(nameof(IntegerModelMetadataDataSet))] + public async Task BindModel_ValidValueProviderResult_ReturnsModelAndLogsSuccessfully(ModelMetadata metadata) { // Arrange var bindingContext = GetBindingContext(typeof(int)); + bindingContext.ModelMetadata = metadata; bindingContext.ValueProvider = new SimpleValueProvider { { "theModelName", "42" } }; - var binder = new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance); + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var binder = new SimpleTypeModelBinder(typeof(int), loggerFactory); // Act await binder.BindModelAsync(bindingContext); @@ -255,6 +283,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Assert.True(bindingContext.Result.IsModelSet); Assert.Equal(42, bindingContext.Result.Model); Assert.True(bindingContext.ModelState.ContainsKey("theModelName")); + Assert.Equal(2, sink.Writes.Count); } public static TheoryData BiggerNumericTypes @@ -488,5 +517,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Value2 = 2, MaxValue = int.MaxValue } + + private class MetadataClass + { + public int Property { get; set; } + + public bool IsLovely(int parameter) + { + return true; + } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs index 4838cf3f62..461a3c8bca 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs @@ -2,11 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.Internal; @@ -15,6 +17,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -309,6 +312,107 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Empty(actionContext.ModelState); } + public static TheoryData EnforcesTopLevelRequiredDataSet + { + get + { + var attribute = new RequiredAttribute(); + var bindingInfo = new BindingInfo + { + BinderModelName = string.Empty, + }; + var parameterDescriptor = new ParameterDescriptor + { + Name = string.Empty, + BindingInfo = bindingInfo, + ParameterType = typeof(Person), + }; + + var method = typeof(Person).GetMethod(nameof(Person.Equals), new[] { typeof(Person) }); + var parameter = method.GetParameters()[0]; // Equals(Person other) + var controllerParameterDescriptor = new ControllerParameterDescriptor + { + Name = string.Empty, + BindingInfo = bindingInfo, + ParameterInfo = parameter, + ParameterType = typeof(Person), + }; + + var provider1 = new TestModelMetadataProvider(); + provider1 + .ForParameter(parameter) + .ValidationDetails(d => + { + d.IsRequired = true; + d.ValidatorMetadata.Add(attribute); + }); + provider1 + .ForProperty(typeof(Family), nameof(Family.Mom)) + .ValidationDetails(d => + { + d.IsRequired = true; + d.ValidatorMetadata.Add(attribute); + }); + + var provider2 = new TestModelMetadataProvider(); + provider2 + .ForType(typeof(Person)) + .ValidationDetails(d => + { + d.IsRequired = true; + d.ValidatorMetadata.Add(attribute); + }); + + return new TheoryData + { + { attribute, parameterDescriptor, provider1.GetMetadataForParameter(parameter) }, + { attribute, parameterDescriptor, provider1.GetMetadataForProperty(typeof(Family), nameof(Family.Mom)) }, + { attribute, parameterDescriptor, provider2.GetMetadataForType(typeof(Person)) }, + { attribute, controllerParameterDescriptor, provider2.GetMetadataForType(typeof(Person)) }, + }; + } + } + + [Theory] + [MemberData(nameof(EnforcesTopLevelRequiredDataSet))] + public async Task BindModelAsync_EnforcesTopLevelRequiredAndLogsSuccessfully_WithEmptyPrefix( + RequiredAttribute attribute, + ParameterDescriptor parameterDescriptor, + ModelMetadata metadata) + { + // Arrange + var expectedKey = metadata.Name ?? string.Empty; + var expectedFieldName = metadata.Name ?? nameof(Person); + + var actionContext = GetControllerContext(); + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute, + stringLocalizer: null); + + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var parameterBinder = CreateParameterBinder(metadata, validator, loggerFactory: loggerFactory); + var modelBindingResult = ModelBindingResult.Success(null); + + // Act + var result = await parameterBinder.BindModelAsync( + actionContext, + CreateMockModelBinder(modelBindingResult), + CreateMockValueProvider(), + parameterDescriptor, + metadata, + "ignoredvalue"); + + // Assert + Assert.False(actionContext.ModelState.IsValid); + var modelState = Assert.Single(actionContext.ModelState); + Assert.Equal(expectedKey, modelState.Key); + var error = Assert.Single(modelState.Value.Errors); + Assert.Equal(attribute.FormatErrorMessage(expectedFieldName), error.ErrorMessage); + Assert.Equal(4, sink.Writes.Count); + } + [Fact] public async Task BindModelAsync_EnforcesTopLevelDataAnnotationsAttribute() { @@ -426,7 +530,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding private static ParameterBinder CreateParameterBinder( ModelMetadata modelMetadata, IModelValidator validator = null, - IOptions optionsAccessor = null) + IOptions optionsAccessor = null, + ILoggerFactory loggerFactory = null) { var mockModelMetadataProvider = new Mock(MockBehavior.Strict); mockModelMetadataProvider @@ -442,7 +547,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding mockModelMetadataProvider.Object, new[] { GetModelValidatorProvider(validator) }), optionsAccessor, - NullLoggerFactory.Instance); + loggerFactory ?? NullLoggerFactory.Instance); } private static IModelValidatorProvider GetModelValidatorProvider(IModelValidator validator = null) @@ -527,6 +632,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } } + private class Family + { + public Person Dad { get; set; } + + public Person Mom { get; set; } + + public IList Kids { get; } = new List(); + } + public abstract class FakeModelMetadata : ModelMetadata { public FakeModelMetadata() diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorTest.cs index 8930283bce..17669fd0d4 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorTest.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -9,7 +8,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Internal; using Microsoft.Extensions.Localization; using Moq; using Xunit; @@ -18,7 +16,8 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { public class DataAnnotationsModelValidatorTest { - private static IModelMetadataProvider _metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + private static readonly ModelMetadataProvider _metadataProvider + = TestModelMetadataProvider.CreateDefaultProvider(); [Fact] public void Constructor_SetsAttribute() @@ -36,11 +35,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal Assert.Same(attribute, validator.Attribute); } - public static TheoryData Validate_SetsMemberName_AsExpectedData + public static TheoryData Validate_SetsMemberName_AsExpectedData { get { var array = new[] { new SampleModel { Name = "one" }, new SampleModel { Name = "two" } }; + var method = typeof(ModelValidationResultComparer).GetMethod( + nameof(ModelValidationResultComparer.GetHashCode), + new[] { typeof(ModelValidationResult) }); + var parameter = method.GetParameters()[0]; // GetHashCode(ModelValidationResult obj) // metadata, container, model, expected MemberName return new TheoryData @@ -52,7 +55,21 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal nameof(string.Length) }, { - // Validating a top-level model + // Validating a top-level property. + _metadataProvider.GetMetadataForProperty(typeof(SampleModel), nameof(SampleModel.Name)), + null, + "Fred", + nameof(SampleModel.Name) + }, + { + // Validating a parameter. + _metadataProvider.GetMetadataForParameter(parameter), + null, + new ModelValidationResult(memberName: string.Empty, message: string.Empty), + "obj" + }, + { + // Validating a top-level parameter as if using old-fashioned metadata provider. _metadataProvider.GetMetadataForType(typeof(SampleModel)), null, 15, @@ -542,39 +559,5 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { void DoSomething(); } - - private class ModelValidationResultComparer : IEqualityComparer - { - public static readonly ModelValidationResultComparer Instance = new ModelValidationResultComparer(); - - private ModelValidationResultComparer() - { - } - - public bool Equals(ModelValidationResult x, ModelValidationResult y) - { - if (x == null || y == null) - { - return x == null && y == null; - } - - return string.Equals(x.MemberName, y.MemberName, StringComparison.Ordinal) && - string.Equals(x.Message, y.Message, StringComparison.Ordinal); - } - - public int GetHashCode(ModelValidationResult obj) - { - if (obj == null) - { - throw new ArgumentNullException(nameof(obj)); - } - - var hashCodeCombiner = HashCodeCombiner.Start(); - hashCodeCombiner.Add(obj.MemberName, StringComparer.Ordinal); - hashCodeCombiner.Add(obj.Message, StringComparer.Ordinal); - - return hashCodeCombiner.CombinedHash; - } - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelValidationResultComparer.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelValidationResultComparer.cs new file mode 100644 index 0000000000..289f30ec35 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelValidationResultComparer.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal +{ + public class ModelValidationResultComparer : IEqualityComparer + { + public static readonly ModelValidationResultComparer Instance = new ModelValidationResultComparer(); + + private ModelValidationResultComparer() + { + } + + public bool Equals(ModelValidationResult x, ModelValidationResult y) + { + if (x == null || y == null) + { + return x == null && y == null; + } + + return string.Equals(x.MemberName, y.MemberName, StringComparison.Ordinal) && + string.Equals(x.Message, y.Message, StringComparison.Ordinal); + } + + public int GetHashCode(ModelValidationResult obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(obj.MemberName, StringComparer.Ordinal); + hashCodeCombiner.Add(obj.Message, StringComparer.Ordinal); + + return hashCodeCombiner.CombinedHash; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs index 1921085893..71e0b6adfb 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs @@ -70,7 +70,39 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal } [Fact] - public void AddValidation_CorrectValidationTypeAndOverriddenErrorMessage_WithNonProperty() + public void AddValidation_CorrectValidationTypeAndOverriddenErrorMessage_WithParameter() + { + // Arrange + var expectedMessage = "Error message about 'number' from override."; + + var method = typeof(TypeWithNumericProperty).GetMethod(nameof(TypeWithNumericProperty.IsLovely)); + var parameter = method.GetParameters()[0]; // IsLovely(double number) + var provider = new TestModelMetadataProvider(); + provider + .ForParameter(parameter) + .BindingDetails(d => + { + d.ModelBindingMessageProvider.SetValueMustBeANumberAccessor( + name => $"Error message about '{ name }' from override."); + }); + var metadata = provider.GetMetadataForParameter(parameter); + + 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] + public void AddValidation_CorrectValidationTypeAndOverriddenErrorMessage_WithType() { // Arrange var expectedMessage = "Error message from override."; @@ -125,6 +157,11 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { [Display(Name = "DisplayId")] public float Id { get; set; } + + public bool IsLovely(double number) + { + return true; + } } } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidatableObjectAdapterTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidatableObjectAdapterTest.cs new file mode 100644 index 0000000000..56a127ea25 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidatableObjectAdapterTest.cs @@ -0,0 +1,221 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal +{ + public class ValidatableObjectAdapterTest + { + private static readonly ModelMetadataProvider _metadataProvider + = TestModelMetadataProvider.CreateDefaultProvider(); + + // Inspired by DataAnnotationsModelValidatorTest.Validate_SetsMemberName_AsExpectedData but using a type that + // implements IValidatableObject. Values are metadata, expected DisplayName, and expected MemberName. + public static TheoryData Validate_PassesExpectedNamesData + { + get + { + var method = typeof(SampleModel).GetMethod(nameof(SampleModel.IsLovely)); + var parameter = method.GetParameters()[0]; // IsLovely(SampleModel other) + return new TheoryData + { + { + // Validating a property. + _metadataProvider.GetMetadataForProperty( + typeof(SampleModelContainer), + nameof(SampleModelContainer.SampleModel)), + nameof(SampleModelContainer.SampleModel), + nameof(SampleModelContainer.SampleModel) + }, + { + // Validating a property with [Display(Name = "...")]. + _metadataProvider.GetMetadataForProperty( + typeof(SampleModelContainer), + nameof(SampleModelContainer.SampleModelWithDisplay)), + "sample model", + nameof(SampleModelContainer.SampleModelWithDisplay) + }, + { + // Validating a parameter. + _metadataProvider.GetMetadataForParameter(parameter), + "other", + "other" + }, + { + // Validating a top-level parameter when using old-fashioned metadata provider. + // Or, validating an element of a collection. + _metadataProvider.GetMetadataForType(typeof(SampleModel)), + nameof(SampleModel), + null + }, + }; + } + } + + public static TheoryData Validate_ReturnsExpectedResultsData + { + get + { + return new TheoryData + { + { + new[] { new ValidationResult("Error message") }, + new[] { new ModelValidationResult(memberName: null, message: "Error message") } + }, + { + new[] { new ValidationResult("Error message", new[] { nameof(SampleModel.FirstName) }) }, + new[] { new ModelValidationResult(nameof(SampleModel.FirstName), "Error message") } + }, + { + new[] + { + new ValidationResult("Error message1"), + new ValidationResult("Error message2", new[] { nameof(SampleModel.FirstName) }), + new ValidationResult("Error message3", new[] { nameof(SampleModel.LastName) }), + new ValidationResult("Error message4", new[] { nameof(SampleModel) }), + }, + new[] + { + new ModelValidationResult(memberName: null, message: "Error message1"), + new ModelValidationResult(nameof(SampleModel.FirstName), "Error message2"), + new ModelValidationResult(nameof(SampleModel.LastName), "Error message3"), + // No special case for ValidationContext.MemberName==ValidationResult.MemberName + new ModelValidationResult(nameof(SampleModel), "Error message4"), + } + }, + { + new[] + { + new ValidationResult("Error message1", new[] + { + nameof(SampleModel.FirstName), + nameof(SampleModel.LastName), + }), + new ValidationResult("Error message2"), + }, + new[] + { + new ModelValidationResult(nameof(SampleModel.FirstName), "Error message1"), + new ModelValidationResult(nameof(SampleModel.LastName), "Error message1"), + new ModelValidationResult(memberName: null, message: "Error message2"), + } + }, + }; + } + } + + + [Theory] + [MemberData(nameof(Validate_PassesExpectedNamesData))] + public void Validate_PassesExpectedNames( + ModelMetadata metadata, + string expectedDisplayName, + string expectedMemberName) + { + // Arrange + var adapter = new ValidatableObjectAdapter(); + var model = new SampleModel(); + var validationContext = new ModelValidationContext( + new ActionContext(), + metadata, + _metadataProvider, + container: new SampleModelContainer(), + model: model); + + // Act + var results = adapter.Validate(validationContext); + + // Assert + Assert.NotNull(results); + Assert.Empty(results); + + Assert.Equal(expectedDisplayName, model.DisplayName); + Assert.Equal(expectedMemberName, model.MemberName); + Assert.Equal(model, model.ObjectInstance); + } + + [Theory] + [MemberData(nameof(Validate_ReturnsExpectedResultsData))] + public void Validate_ReturnsExpectedResults( + ValidationResult[] innerResults, + ModelValidationResult[] expectedResults) + { + // Arrange + var adapter = new ValidatableObjectAdapter(); + var model = new SampleModel(); + foreach (var result in innerResults) + { + model.ValidationResults.Add(result); + } + + var metadata = _metadataProvider.GetMetadataForProperty( + typeof(SampleModelContainer), + nameof(SampleModelContainer.SampleModel)); + var validationContext = new ModelValidationContext( + new ActionContext(), + metadata, + _metadataProvider, + container: null, + model: model); + + // Act + var results = adapter.Validate(validationContext); + + // Assert + Assert.NotNull(results); + Assert.Equal(expectedResults, results, ModelValidationResultComparer.Instance); + } + + private class SampleModel : IValidatableObject + { + // "Real" properties. + + public string FirstName { get; set; } + + public string LastName { get; set; } + + // ValidationContext members passed to Validate(...) + + public string DisplayName { get; private set; } + + public string MemberName { get; private set; } + + public object ObjectInstance { get; private set; } + + // What Validate(...) should return. + + public IList ValidationResults { get; } = new List(); + + // Test method. + + public bool IsLovely(SampleModel other) + { + return true; + } + + // IValidatableObject for realz. + + public IEnumerable Validate(ValidationContext validationContext) + { + DisplayName = validationContext.DisplayName; + MemberName = validationContext.MemberName; + ObjectInstance = validationContext.ObjectInstance; + + return ValidationResults; + } + } + + private class SampleModelContainer + { + [Display(Name = "sample model")] + public SampleModel SampleModelWithDisplay { get; set; } + + public SampleModel SampleModel { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs index fd743e60e4..62952ab4c6 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs @@ -112,6 +112,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding return builder; } + public IMetadataBuilder ForParameter(ParameterInfo parameter) + { + var key = ModelMetadataIdentity.ForParameter(parameter); + var builder = new MetadataBuilder(key); + _detailsProvider.Builders.Add(builder); + + return builder; + } + public IMetadataBuilder ForProperty(string propertyName) { return ForProperty(typeof(TContainer), propertyName); @@ -223,4 +232,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } } } -} \ No newline at end of file +}