Restore `ModelMetadata.PropertyName != null` behaviour

- #7413 part 2 of 2
- add `ModelMetadata.Name` and `ParameterName`
  - use `Name` instead of `PropertyName` in most cases
- update `ModelMetadata.ContainerType` and other property use
  - choose using `MetadataKind` almost everywhere; support all possibilties
    - usually parameter metadata was possible but not handled
    - worst case was one or two potential NREs, especially `ContainerType.*` dereferences
  - improve `MvcCoreLoggerExtensions` metadata handling
    - add three new debug messages, one for type metadata and two for parameter metadata
- update `ModelMetadata.ContainerMetadata`, `ContainerType` and `PropertyName` doc comments
- no changes needed in Microsoft.AspNetCore.Mvc.ViewFeatures because parameters aren't viewed

nits:
- add missing `TestModelMetadataProvider.ForParameter(...)` method
- remove unused `EmptyModelMetadataProvider` instances in `ModelMetadataTest`
- refactor `ModelValidationResultComparer` out of DataAnnotationsModelValidatorTest`
- take VS suggestions, mostly related to variable inlining and object initializers
This commit is contained in:
Doug Bunting 2018-03-20 15:37:12 -07:00
parent f8e315d03d
commit fc3a815e57
No known key found for this signature in database
GPG Key ID: 888B4EB7822B32E9
22 changed files with 1034 additions and 212 deletions

View File

@ -38,12 +38,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
/// <summary>
/// Gets the container type of this metadata if it represents a property, otherwise <c>null</c>.
/// Gets the type containing the property if this metadata is for a property; <see langword="null"/> otherwise.
/// </summary>
public Type ContainerType => Identity.ContainerType;
/// <summary>
/// Gets the metadata of the container type that the current instance is part of.
/// Gets the metadata for <see cref="ContainerType"/> if this metadata is for a property;
/// <see langword="null"/> otherwise.
/// </summary>
public virtual ModelMetadata ContainerMetadata
{
@ -64,9 +65,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public Type ModelType => Identity.ModelType;
/// <summary>
/// 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;
/// <see langword="null"/> otherwise i.e. if this is the metadata for a type.
/// </summary>
public string PropertyName => Identity.Name;
public string Name => Identity.Name;
/// <summary>
/// Gets the name of the parameter if this metadata is for a parameter; <see langword="null"/> otherwise.
/// </summary>
public string ParameterName => MetadataKind == ModelMetadataKind.Parameter ? Identity.Name : null;
/// <summary>
/// Gets the name of the property if this metadata is for a property; <see langword="null"/> otherwise.
/// </summary>
public string PropertyName => MetadataKind == ModelMetadataKind.Property ? Identity.Name : null;
/// <summary>
/// Gets the key for the current instance.
@ -384,12 +396,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
/// <remarks>
/// <see cref="GetDisplayName()"/> will return the first of the following expressions which has a
/// non-<c>null</c> value: <c>DisplayName</c>, <c>PropertyName</c>, <c>ModelType.Name</c>.
/// non-<see langword="null"/> value: <see cref="DisplayName"/>, <see cref="Name"/>, or <c>ModelType.Name</c>.
/// </remarks>
/// <returns>The display name.</returns>
public string GetDisplayName()
{
return DisplayName ?? PropertyName ?? ModelType.Name;
return DisplayName ?? Name ?? ModelType.Name;
}
/// <inheritdoc />
@ -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}'.";
}
}

View File

@ -180,7 +180,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
/// <remarks>
/// This method allows adding the <paramref name="exception"/> to the current <see cref="ModelStateDictionary"/>
/// when <see cref="ModelMetadata"/> is not available or the exact <paramref name="exception"/>
/// when <see cref="ModelMetadata"/> is not available or the exact <paramref name="exception"/>
/// must be maintained for later use (even if it is for example a <see cref="FormatException"/>).
/// Where <see cref="ModelMetadata"/> is available, use <see cref="AddModelError(string, Exception, ModelMetadata)"/> instead.
/// </remarks>

View File

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

View File

@ -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<ILogger, MethodInfo, Exception> _unableToInferParameterSources;
private static readonly Action<ILogger, IModelBinderProvider[], Exception> _registeredModelBinderProviders;
private static readonly Action<ILogger, string, Type, string, Type, Exception> _foundNoValueForPropertyInRequest;
private static readonly Action<ILogger, string, string, Type, Exception> _foundNoValueInRequest;
private static readonly Action<ILogger, string, string, Type, Exception> _foundNoValueForParameterInRequest;
private static readonly Action<ILogger, string, Type, Exception> _foundNoValueInRequest;
private static readonly Action<ILogger, string, Type, Exception> _noPublicSettableProperties;
private static readonly Action<ILogger, Type, Exception> _cannotBindToComplexType;
private static readonly Action<ILogger, string, Type, Exception> _cannotBindToFilesCollectionDueToUnsupportedContentType;
@ -115,6 +117,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static readonly Action<ILogger, string, string, string, string, string, string, Exception> _attemptingToBindCollectionUsingIndices;
private static readonly Action<ILogger, string, string, string, string, string, string, Exception> _attemptingToBindCollectionOfKeyValuePair;
private static readonly Action<ILogger, string, string, string, Exception> _noKeyValueFormatForDictionaryModelBinder;
private static readonly Action<ILogger, string, Type, string, Exception> _attemptingToBindParameterModel;
private static readonly Action<ILogger, string, Type, Exception> _doneAttemptingToBindParameterModel;
private static readonly Action<ILogger, Type, string, Type, string, Exception> _attemptingToBindPropertyModel;
private static readonly Action<ILogger, Type, string, Type, Exception> _doneAttemptingToBindPropertyModel;
private static readonly Action<ILogger, Type, string, Exception> _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<string, string, Type>(
_foundNoValueForParameterInRequest = LoggerMessage.Define<string, string, Type>(
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<string, Type, string>(
LogLevel.Debug,
44,
"Attempting to bind parameter '{ParameterName}' of type '{ModelType}' using the name '{ModelName}' in request data ...");
_doneAttemptingToBindParameterModel = LoggerMessage.Define<string, Type>(
LogLevel.Debug,
45,
"Done attempting to bind parameter '{ParameterName}' of type '{ModelType}'.");
_foundNoValueInRequest = LoggerMessage.Define<string, Type>(
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<IOutputFormatter> 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;
}
}

View File

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

View File

@ -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}'.";
}
}
}

View File

@ -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; }
/// <summary>
/// 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.
/// </summary>
public bool ValidateComplexTypesIfChildValidationFails { get; set; }
/// <summary>
@ -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;
}

View File

@ -1273,16 +1273,16 @@ namespace Microsoft.AspNetCore.Mvc.Core
/// <summary>
/// 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.
/// </summary>
internal static string ComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject
internal static string ComplexTypeModelBinder_NoParameterlessConstructor_ForType
{
get => GetString("ComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject");
get => GetString("ComplexTypeModelBinder_NoParameterlessConstructor_ForType");
}
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>
/// 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);
/// <summary>
/// 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.
/// </summary>
internal static string ComplexTypeModelBinder_NoParameterlessConstructor_ForParameter
{
get => GetString("ComplexTypeModelBinder_NoParameterlessConstructor_ForParameter");
}
/// <summary>
/// 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.
/// </summary>
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);

View File

@ -400,7 +400,7 @@
<value>'{0}' and '{1}' are out of bounds for the string.</value>
<comment>'{0}' and '{1}' are the parameters which combine to be out of bounds.</comment>
</data>
<data name="ComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject" xml:space="preserve">
<data name="ComplexTypeModelBinder_NoParameterlessConstructor_ForType" xml:space="preserve">
<value>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.</value>
</data>
<data name="ComplexTypeModelBinder_NoParameterlessConstructor_ForProperty" xml:space="preserve">
@ -436,4 +436,7 @@
<data name="ApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional" xml:space="preserve">
<value>Assembly '{0}' declared as a related assembly by assembly '{1}' cannot define additional related assemblies.</value>
</data>
<data name="ComplexTypeModelBinder_NoParameterlessConstructor_ForParameter" xml:space="preserve">
<value>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.</value>
</data>
</root>

View File

@ -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(

View File

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

View File

@ -19,8 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
return Enumerable.Empty<ModelValidationResult>();
}
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
}
}
}
}
}

View File

@ -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<IsComplexTypeModel>))]
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<IsComplexTypeModel>), 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<IsComplexTypeModel>), 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<IsComplexTypeModel>), 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))
{

View File

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

View File

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

View File

@ -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<ModelMetadata> 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<ModelMetadata>
{
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<Type> 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;
}
}
}
}

View File

@ -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<RequiredAttribute, ParameterDescriptor, ModelMetadata> 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<RequiredAttribute, ParameterDescriptor, ModelMetadata>
{
{ 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<MvcOptions> optionsAccessor = null)
IOptions<MvcOptions> optionsAccessor = null,
ILoggerFactory loggerFactory = null)
{
var mockModelMetadataProvider = new Mock<IModelMetadataProvider>(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<Person> Kids { get; } = new List<Person>();
}
public abstract class FakeModelMetadata : ModelMetadata
{
public FakeModelMetadata()

View File

@ -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<ModelMetadata, object, object, string> 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<ModelMetadata, object, object, string>
@ -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<ModelValidationResult>
{
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;
}
}
}
}

View File

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

View File

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

View File

@ -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<ModelMetadata, string, string> Validate_PassesExpectedNamesData
{
get
{
var method = typeof(SampleModel).GetMethod(nameof(SampleModel.IsLovely));
var parameter = method.GetParameters()[0]; // IsLovely(SampleModel other)
return new TheoryData<ModelMetadata, string, string>
{
{
// 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<ValidationResult[], ModelValidationResult[]> Validate_ReturnsExpectedResultsData
{
get
{
return new TheoryData<ValidationResult[], ModelValidationResult[]>
{
{
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<ValidationResult> ValidationResults { get; } = new List<ValidationResult>();
// Test method.
public bool IsLovely(SampleModel other)
{
return true;
}
// IValidatableObject for realz.
public IEnumerable<ValidationResult> 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; }
}
}
}

View File

@ -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<TContainer>(string propertyName)
{
return ForProperty(typeof(TContainer), propertyName);
@ -223,4 +232,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
}
}
}
}