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:
parent
f8e315d03d
commit
fc3a815e57
|
|
@ -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}'.";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}'.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue