[Fixes #5859] Consider allowing binding header values to types other than string and string collections
This commit is contained in:
parent
19c89db48b
commit
0215740183
|
|
@ -17,6 +17,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
/// <returns>An <see cref="IModelBinder"/>.</returns>
|
/// <returns>An <see cref="IModelBinder"/>.</returns>
|
||||||
public abstract IModelBinder CreateBinder(ModelMetadata metadata);
|
public abstract IModelBinder CreateBinder(ModelMetadata metadata);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an <see cref="IModelBinder"/> for the given <paramref name="metadata"/>
|
||||||
|
/// and <paramref name="bindingInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata">The <see cref="ModelMetadata"/> for the model.</param>
|
||||||
|
/// <param name="bindingInfo">The <see cref="BindingInfo"/> that should be used
|
||||||
|
/// for creating the binder.</param>
|
||||||
|
/// <returns>An <see cref="IModelBinder"/>.</returns>
|
||||||
|
public virtual IModelBinder CreateBinder(ModelMetadata metadata, BindingInfo bindingInfo)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="BindingInfo"/>.
|
/// Gets the <see cref="BindingInfo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
if (Version >= CompatibilityVersion.Version_2_1)
|
if (Version >= CompatibilityVersion.Version_2_1)
|
||||||
{
|
{
|
||||||
values[nameof(MvcOptions.AllowCombiningAuthorizeFilters)] = true;
|
values[nameof(MvcOptions.AllowCombiningAuthorizeFilters)] = true;
|
||||||
|
values[nameof(MvcOptions.AllowBindingHeaderValuesToNonStringModelTypes)] = true;
|
||||||
values[nameof(MvcOptions.InputFormatterExceptionPolicy)] = InputFormatterExceptionPolicy.MalformedInputExceptions;
|
values[nameof(MvcOptions.InputFormatterExceptionPolicy)] = InputFormatterExceptionPolicy.MalformedInputExceptions;
|
||||||
values[nameof(MvcOptions.SuppressBindingUndefinedValueToEnumType)] = true;
|
values[nameof(MvcOptions.SuppressBindingUndefinedValueToEnumType)] = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
private static readonly Action<ILogger, Type, Exception> _cannotBindToComplexType;
|
private static readonly Action<ILogger, Type, Exception> _cannotBindToComplexType;
|
||||||
private static readonly Action<ILogger, string, Type, Exception> _cannotBindToFilesCollectionDueToUnsupportedContentType;
|
private static readonly Action<ILogger, string, Type, Exception> _cannotBindToFilesCollectionDueToUnsupportedContentType;
|
||||||
private static readonly Action<ILogger, Type, Exception> _cannotCreateHeaderModelBinder;
|
private static readonly Action<ILogger, Type, Exception> _cannotCreateHeaderModelBinder;
|
||||||
|
private static readonly Action<ILogger, Type, Exception> _cannotCreateHeaderModelBinderCompatVersion_2_0;
|
||||||
private static readonly Action<ILogger, Exception> _noFilesFoundInRequest;
|
private static readonly Action<ILogger, Exception> _noFilesFoundInRequest;
|
||||||
private static readonly Action<ILogger, string, string, Exception> _noNonIndexBasedFormatFoundForCollection;
|
private static readonly Action<ILogger, string, string, Exception> _noNonIndexBasedFormatFoundForCollection;
|
||||||
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> _attemptingToBindCollectionUsingIndices;
|
||||||
|
|
@ -490,7 +491,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
_cannotCreateHeaderModelBinder = LoggerMessage.Define<Type>(
|
_cannotCreateHeaderModelBinder = LoggerMessage.Define<Type>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
20,
|
20,
|
||||||
"Could not create a binder for type '{ModelType}' as this binder only supports 'System.String' type or a collection of 'System.String'.");
|
"Could not create a binder for type '{ModelType}' as this binder only supports simple types (like string, int, bool, enum) or a collection of simple types.");
|
||||||
|
|
||||||
_noFilesFoundInRequest = LoggerMessage.Define(
|
_noFilesFoundInRequest = LoggerMessage.Define(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
|
|
@ -597,6 +598,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
42,
|
42,
|
||||||
"Done attempting to validate the bound property '{PropertyContainerType}.{PropertyName}' of type '{ModelType}'.");
|
"Done attempting to validate the bound property '{PropertyContainerType}.{PropertyName}' of type '{ModelType}'.");
|
||||||
|
|
||||||
|
_cannotCreateHeaderModelBinderCompatVersion_2_0 = LoggerMessage.Define<Type>(
|
||||||
|
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'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable<IOutputFormatter> outputFormatters)
|
public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable<IOutputFormatter> outputFormatters)
|
||||||
|
|
@ -1169,6 +1175,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
_cannotBindToFilesCollectionDueToUnsupportedContentType(logger, bindingContext.ModelName, bindingContext.ModelType, null);
|
_cannotBindToFilesCollectionDueToUnsupportedContentType(logger, bindingContext.ModelName, bindingContext.ModelType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void CannotCreateHeaderModelBinderCompatVersion_2_0(this ILogger logger, Type modelType)
|
||||||
|
{
|
||||||
|
_cannotCreateHeaderModelBinderCompatVersion_2_0(logger, modelType, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static void CannotCreateHeaderModelBinder(this ILogger logger, Type modelType)
|
public static void CannotCreateHeaderModelBinder(this ILogger logger, Type modelType)
|
||||||
{
|
{
|
||||||
_cannotCreateHeaderModelBinder(logger, modelType, null);
|
_cannotCreateHeaderModelBinder(logger, modelType, null);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
|
@ -21,11 +23,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <para>This constructor is obsolete and will be removed in a future version. The recommended alternative
|
/// <para>This constructor is obsolete and will be removed in a future version. The recommended alternative
|
||||||
/// is the overload that takes an <see cref="ILoggerFactory"/>.</para>
|
/// is the overload that takes an <see cref="ILoggerFactory"/> and an <see cref="IModelBinder"/>.</para>
|
||||||
/// <para>Initializes a new instance of <see cref="HeaderModelBinder"/>.</para>
|
/// <para>Initializes a new instance of <see cref="HeaderModelBinder"/>.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("This constructor is obsolete and will be removed in a future version. The recommended alternative"
|
[Obsolete("This constructor is obsolete and will be removed in a future version. The recommended alternative"
|
||||||
+ " is the overload that takes an " + nameof(ILoggerFactory) + ".")]
|
+ " is the overload that takes an " + nameof(ILoggerFactory) + " and an " + nameof(IModelBinder) + ".")]
|
||||||
public HeaderModelBinder()
|
public HeaderModelBinder()
|
||||||
: this(NullLoggerFactory.Instance)
|
: this(NullLoggerFactory.Instance)
|
||||||
{
|
{
|
||||||
|
|
@ -36,35 +38,117 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||||
public HeaderModelBinder(ILoggerFactory loggerFactory)
|
public HeaderModelBinder(ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory.CreateLogger<HeaderModelBinder>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="HeaderModelBinder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||||
|
/// <param name="innerModelBinder">The <see cref="IModelBinder"/> which does the actual
|
||||||
|
/// binding of values.</param>
|
||||||
|
public HeaderModelBinder(ILoggerFactory loggerFactory, IModelBinder innerModelBinder)
|
||||||
{
|
{
|
||||||
if (loggerFactory == null)
|
if (loggerFactory == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(loggerFactory));
|
throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (innerModelBinder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(innerModelBinder));
|
||||||
|
}
|
||||||
|
|
||||||
_logger = loggerFactory.CreateLogger<HeaderModelBinder>();
|
_logger = loggerFactory.CreateLogger<HeaderModelBinder>();
|
||||||
|
InnerModelBinder = innerModelBinder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// to enable unit testing
|
||||||
|
internal IModelBinder InnerModelBinder { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
{
|
{
|
||||||
if (bindingContext == null)
|
if (bindingContext == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(bindingContext));
|
throw new ArgumentNullException(nameof(bindingContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = bindingContext.HttpContext.Request;
|
_logger.AttemptingToBindModel(bindingContext);
|
||||||
|
|
||||||
// Property name can be null if the model metadata represents a type (rather than a property or parameter).
|
// Property name can be null if the model metadata represents a type (rather than a property or parameter).
|
||||||
var headerName = bindingContext.FieldName;
|
var headerName = bindingContext.FieldName;
|
||||||
|
|
||||||
_logger.AttemptingToBindModel(bindingContext);
|
// Do not set ModelBindingResult to Failed on not finding the value in the header as we want the inner
|
||||||
|
// modelbinder to do that. This would give a chance to the inner binder to add more useful information.
|
||||||
|
// For example, SimpleTypeModelBinder adds a model error when binding to let's say an integer and the
|
||||||
|
// model is null.
|
||||||
|
var request = bindingContext.HttpContext.Request;
|
||||||
if (!request.Headers.ContainsKey(headerName))
|
if (!request.Headers.ContainsKey(headerName))
|
||||||
{
|
{
|
||||||
_logger.FoundNoValueInRequest(bindingContext);
|
_logger.FoundNoValueInRequest(bindingContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (InnerModelBinder == null)
|
||||||
|
{
|
||||||
|
BindWithoutInnerBinder(bindingContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerValueProvider = GetHeaderValueProvider(headerName, bindingContext);
|
||||||
|
|
||||||
|
// Capture the top level object here as entering nested scope would make it 'false'.
|
||||||
|
var isTopLevelObject = bindingContext.IsTopLevelObject;
|
||||||
|
|
||||||
|
// Create a new binding scope in order to supply the HeaderValueProvider so that the binders like
|
||||||
|
// SimpleTypeModelBinder can find values from header.
|
||||||
|
ModelBindingResult result;
|
||||||
|
using (bindingContext.EnterNestedScope(
|
||||||
|
bindingContext.ModelMetadata,
|
||||||
|
fieldName: bindingContext.FieldName,
|
||||||
|
modelName: bindingContext.ModelName,
|
||||||
|
model: bindingContext.Model))
|
||||||
|
{
|
||||||
|
bindingContext.IsTopLevelObject = isTopLevelObject;
|
||||||
|
bindingContext.ValueProvider = headerValueProvider;
|
||||||
|
|
||||||
|
await InnerModelBinder.BindModelAsync(bindingContext);
|
||||||
|
result = bindingContext.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindingContext.Result = result;
|
||||||
|
|
||||||
|
_logger.DoneAttemptingToBindModel(bindingContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HeaderValueProvider GetHeaderValueProvider(string headerName, ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
var request = bindingContext.HttpContext.Request;
|
||||||
|
|
||||||
|
// Prevent breaking existing users in scenarios where they are binding to a 'string' property
|
||||||
|
// and expect the whole comma separated string, if any, as a single string and not as a string array.
|
||||||
|
var values = Array.Empty<string>();
|
||||||
|
if (request.Headers.ContainsKey(headerName))
|
||||||
|
{
|
||||||
|
if (bindingContext.ModelMetadata.IsEnumerableType)
|
||||||
|
{
|
||||||
|
values = request.Headers.GetCommaSeparatedValues(headerName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
values = new[] { (string)request.Headers[headerName] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HeaderValueProvider(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindWithoutInnerBinder(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
var headerName = bindingContext.FieldName;
|
||||||
|
var request = bindingContext.HttpContext.Request;
|
||||||
|
|
||||||
object model;
|
object model;
|
||||||
if (bindingContext.ModelType == typeof(string))
|
if (bindingContext.ModelType == typeof(string))
|
||||||
{
|
{
|
||||||
|
|
@ -101,7 +185,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.DoneAttemptingToBindModel(bindingContext);
|
_logger.DoneAttemptingToBindModel(bindingContext);
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object GetCompatibleCollection(ModelBindingContext bindingContext, string[] values)
|
private static object GetCompatibleCollection(ModelBindingContext bindingContext, string[] values)
|
||||||
|
|
@ -126,5 +209,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class HeaderValueProvider : IValueProvider
|
||||||
|
{
|
||||||
|
private readonly string[] _values;
|
||||||
|
|
||||||
|
public HeaderValueProvider(string[] values)
|
||||||
|
{
|
||||||
|
Debug.Assert(values != null);
|
||||||
|
|
||||||
|
_values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsPrefix(string prefix)
|
||||||
|
{
|
||||||
|
return _values.Length != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueProviderResult GetValue(string key)
|
||||||
|
{
|
||||||
|
if (_values.Length == 0)
|
||||||
|
{
|
||||||
|
return ValueProviderResult.None;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new ValueProviderResult(_values, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
{
|
{
|
||||||
|
|
@ -21,26 +22,65 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.BindingInfo.BindingSource != null &&
|
var bindingInfo = context.BindingInfo;
|
||||||
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
|
if (bindingInfo.BindingSource == null ||
|
||||||
|
!bindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
|
||||||
{
|
{
|
||||||
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
|
return null;
|
||||||
var logger = loggerFactory.CreateLogger<HeaderModelBinderProvider>();
|
}
|
||||||
|
|
||||||
// We only support strings and collections of strings. Some cases can fail
|
var modelMetadata = context.Metadata;
|
||||||
// at runtime due to collections we can't modify.
|
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
|
||||||
if (context.Metadata.ModelType == typeof(string) ||
|
var logger = loggerFactory.CreateLogger<HeaderModelBinderProvider>();
|
||||||
context.Metadata.ElementType == typeof(string))
|
|
||||||
|
var options = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
|
||||||
|
if (!options.AllowBindingHeaderValuesToNonStringModelTypes)
|
||||||
|
{
|
||||||
|
if (modelMetadata.ModelType == typeof(string) ||
|
||||||
|
modelMetadata.ElementType == typeof(string))
|
||||||
{
|
{
|
||||||
return new HeaderModelBinder(loggerFactory);
|
return new HeaderModelBinder(loggerFactory);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.CannotCreateHeaderModelBinder(context.Metadata.ModelType);
|
logger.CannotCreateHeaderModelBinderCompatVersion_2_0(modelMetadata.ModelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
|
if (!IsSimpleType(modelMetadata))
|
||||||
|
{
|
||||||
|
logger.CannotCreateHeaderModelBinder(modelMetadata.ModelType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we are delegating the binding of the current model type to other binders, modify the
|
||||||
|
// binding source of the current model type to a non-FromHeader binding source in order to avoid an
|
||||||
|
// infinite recursion into this binder provider.
|
||||||
|
var nestedBindingInfo = new BindingInfo(bindingInfo)
|
||||||
|
{
|
||||||
|
BindingSource = BindingSource.ModelBinding
|
||||||
|
};
|
||||||
|
|
||||||
|
var innerModelBinder = context.CreateBinder(
|
||||||
|
modelMetadata.GetMetadataForType(modelMetadata.ModelType),
|
||||||
|
nestedBindingInfo);
|
||||||
|
|
||||||
|
if (innerModelBinder == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HeaderModelBinder(loggerFactory, innerModelBinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support binding only to simple types or collection of simple types.
|
||||||
|
private bool IsSimpleType(ModelMetadata modelMetadata)
|
||||||
|
{
|
||||||
|
var metadata = modelMetadata.ElementMetadata ?? modelMetadata;
|
||||||
|
return !metadata.IsComplexType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,21 +252,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
|
|
||||||
private DefaultModelBinderProviderContext(
|
private DefaultModelBinderProviderContext(
|
||||||
DefaultModelBinderProviderContext parent,
|
DefaultModelBinderProviderContext parent,
|
||||||
ModelMetadata metadata)
|
ModelMetadata metadata,
|
||||||
|
BindingInfo bindingInfo)
|
||||||
{
|
{
|
||||||
Metadata = metadata;
|
Metadata = metadata;
|
||||||
|
|
||||||
_factory = parent._factory;
|
_factory = parent._factory;
|
||||||
MetadataProvider = parent.MetadataProvider;
|
MetadataProvider = parent.MetadataProvider;
|
||||||
Visited = parent.Visited;
|
Visited = parent.Visited;
|
||||||
|
BindingInfo = bindingInfo;
|
||||||
BindingInfo = new BindingInfo()
|
|
||||||
{
|
|
||||||
BinderModelName = metadata.BinderModelName,
|
|
||||||
BinderType = metadata.BinderType,
|
|
||||||
BindingSource = metadata.BindingSource,
|
|
||||||
PropertyFilterProvider = metadata.PropertyFilterProvider,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override BindingInfo BindingInfo { get; }
|
public override BindingInfo BindingInfo { get; }
|
||||||
|
|
@ -280,18 +274,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
public override IServiceProvider Services => _factory._serviceProvider;
|
public override IServiceProvider Services => _factory._serviceProvider;
|
||||||
|
|
||||||
public override IModelBinder CreateBinder(ModelMetadata metadata)
|
public override IModelBinder CreateBinder(ModelMetadata metadata)
|
||||||
|
{
|
||||||
|
return CreateBinder(
|
||||||
|
metadata,
|
||||||
|
new BindingInfo()
|
||||||
|
{
|
||||||
|
BinderModelName = metadata.BinderModelName,
|
||||||
|
BinderType = metadata.BinderType,
|
||||||
|
BindingSource = metadata.BindingSource,
|
||||||
|
PropertyFilterProvider = metadata.PropertyFilterProvider,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IModelBinder CreateBinder(ModelMetadata metadata, BindingInfo bindingInfo)
|
||||||
{
|
{
|
||||||
if (metadata == null)
|
if (metadata == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(metadata));
|
throw new ArgumentNullException(nameof(metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bindingInfo == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(bindingInfo));
|
||||||
|
}
|
||||||
|
|
||||||
// For non-root nodes we use the ModelMetadata as the cache token. This ensures that all non-root
|
// For non-root nodes we use the ModelMetadata as the cache token. This ensures that all non-root
|
||||||
// nodes with the same metadata will have the same binder. This is OK because for an non-root
|
// nodes with the same metadata will have the same binder. This is OK because for an non-root
|
||||||
// node there's no opportunity to customize binding info like there is for a parameter.
|
// node there's no opportunity to customize binding info like there is for a parameter.
|
||||||
var token = metadata;
|
var token = metadata;
|
||||||
|
|
||||||
var nestedContext = new DefaultModelBinderProviderContext(this, metadata);
|
var nestedContext = new DefaultModelBinderProviderContext(this, metadata, bindingInfo);
|
||||||
return _factory.CreateBinderCoreCached(nestedContext, token);
|
return _factory.CreateBinderCoreCached(nestedContext, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors;
|
private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors;
|
||||||
|
|
||||||
// See CompatibilitySwitch.cs for guide on how to implement these.
|
// See CompatibilitySwitch.cs for guide on how to implement these.
|
||||||
|
private readonly CompatibilitySwitch<bool> _allowBindingHeaderValuesToNonStringModelTypes;
|
||||||
private readonly CompatibilitySwitch<bool> _allowCombiningAuthorizeFilters;
|
private readonly CompatibilitySwitch<bool> _allowCombiningAuthorizeFilters;
|
||||||
private readonly CompatibilitySwitch<InputFormatterExceptionPolicy> _inputFormatterExceptionPolicy;
|
private readonly CompatibilitySwitch<InputFormatterExceptionPolicy> _inputFormatterExceptionPolicy;
|
||||||
private readonly CompatibilitySwitch<bool> _suppressBindingUndefinedValueToEnumType;
|
private readonly CompatibilitySwitch<bool> _suppressBindingUndefinedValueToEnumType;
|
||||||
|
|
@ -47,12 +49,14 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
ValueProviderFactories = new List<IValueProviderFactory>();
|
ValueProviderFactories = new List<IValueProviderFactory>();
|
||||||
|
|
||||||
_allowCombiningAuthorizeFilters = new CompatibilitySwitch<bool>(nameof(AllowCombiningAuthorizeFilters));
|
_allowCombiningAuthorizeFilters = new CompatibilitySwitch<bool>(nameof(AllowCombiningAuthorizeFilters));
|
||||||
|
_allowBindingHeaderValuesToNonStringModelTypes = new CompatibilitySwitch<bool>(nameof(AllowBindingHeaderValuesToNonStringModelTypes));
|
||||||
_inputFormatterExceptionPolicy = new CompatibilitySwitch<InputFormatterExceptionPolicy>(nameof(InputFormatterExceptionPolicy), InputFormatterExceptionPolicy.AllExceptions);
|
_inputFormatterExceptionPolicy = new CompatibilitySwitch<InputFormatterExceptionPolicy>(nameof(InputFormatterExceptionPolicy), InputFormatterExceptionPolicy.AllExceptions);
|
||||||
_suppressBindingUndefinedValueToEnumType = new CompatibilitySwitch<bool>(nameof(SuppressBindingUndefinedValueToEnumType));
|
_suppressBindingUndefinedValueToEnumType = new CompatibilitySwitch<bool>(nameof(SuppressBindingUndefinedValueToEnumType));
|
||||||
|
|
||||||
_switches = new ICompatibilitySwitch[]
|
_switches = new ICompatibilitySwitch[]
|
||||||
{
|
{
|
||||||
_allowCombiningAuthorizeFilters,
|
_allowCombiningAuthorizeFilters,
|
||||||
|
_allowBindingHeaderValuesToNonStringModelTypes,
|
||||||
_inputFormatterExceptionPolicy,
|
_inputFormatterExceptionPolicy,
|
||||||
_suppressBindingUndefinedValueToEnumType,
|
_suppressBindingUndefinedValueToEnumType,
|
||||||
};
|
};
|
||||||
|
|
@ -108,6 +112,38 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
set => _allowCombiningAuthorizeFilters.Value = value;
|
set => _allowCombiningAuthorizeFilters.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that determines if <see cref="HeaderModelBinder"/> should bind to types other than
|
||||||
|
/// <see cref="String"/> or a collection of <see cref="String"/>. If set to <c>true</c>,
|
||||||
|
/// <see cref="HeaderModelBinder"/> would bind to simple types (like <see cref="String"/>, <see cref="Int32"/>,
|
||||||
|
/// <see cref="Enum"/>, <see cref="Boolean"/> etc.) or a collection of simple types. The default value of the
|
||||||
|
/// property is <c>false</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This property is associated with a compatibility switch and can provide a different behavior depending on
|
||||||
|
/// the configured compatibility version for the application. See <see cref="CompatibilityVersion"/> for
|
||||||
|
/// guidance and examples of setting the application's compatibility version.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Configuring the desired value of the compatibility switch by calling this property's setter will take precedence
|
||||||
|
/// over the value implied by the application's <see cref="CompatibilityVersion"/>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_0"/> then
|
||||||
|
/// this setting will have the value <c>false</c> unless explicitly configured.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_1"/> or
|
||||||
|
/// higher then this setting will have the value <c>true</c> unless explicitly configured.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public bool AllowBindingHeaderValuesToNonStringModelTypes
|
||||||
|
{
|
||||||
|
get => _allowBindingHeaderValuesToNonStringModelTypes.Value;
|
||||||
|
set => _allowBindingHeaderValuesToNonStringModelTypes.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a Dictionary of CacheProfile Names, <see cref="CacheProfile"/> which are pre-defined settings for
|
/// Gets a Dictionary of CacheProfile Names, <see cref="CacheProfile"/> which are pre-defined settings for
|
||||||
/// response caching.
|
/// response caching.
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,65 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
{
|
{
|
||||||
public class HeaderModelBinderProviderTest
|
public class HeaderModelBinderProviderTest
|
||||||
{
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(string))]
|
||||||
|
[InlineData(typeof(string[]))]
|
||||||
|
[InlineData(typeof(List<string>))]
|
||||||
|
public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForStringTypes_And_CompatVersion_2_0(
|
||||||
|
Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var testBinder = Mock.Of<IModelBinder>();
|
||||||
|
var context = GetTestModelBinderProviderContext(
|
||||||
|
modelType,
|
||||||
|
allowBindingHeaderValuesToNonStringModelTypes: false);
|
||||||
|
context.BindingInfo.BindingSource = BindingSource.Header;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<HeaderModelBinder>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(int))]
|
||||||
|
[InlineData(typeof(int?))]
|
||||||
|
[InlineData(typeof(IEnumerable<int>))]
|
||||||
|
[InlineData(typeof(double))]
|
||||||
|
[InlineData(typeof(double?))]
|
||||||
|
[InlineData(typeof(IEnumerable<double>))]
|
||||||
|
[InlineData(typeof(CarEnumType))]
|
||||||
|
[InlineData(typeof(CarEnumType?))]
|
||||||
|
[InlineData(typeof(IEnumerable<CarEnumType>))]
|
||||||
|
public void Create_WhenBindingSourceIsFromHeader_ReturnsNull_ForNonStringTypes_And_CompatVersion_2_0(
|
||||||
|
Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var context = GetTestModelBinderProviderContext(
|
||||||
|
modelType,
|
||||||
|
allowBindingHeaderValuesToNonStringModelTypes: false);
|
||||||
|
context.BindingInfo.BindingSource = BindingSource.Header;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
public static TheoryData<BindingSource> NonHeaderBindingSources
|
public static TheoryData<BindingSource> NonHeaderBindingSources
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -29,8 +82,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var provider = new HeaderModelBinderProvider();
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var testBinder = Mock.Of<IModelBinder>();
|
||||||
var context = new TestModelBinderProviderContext(typeof(string));
|
var context = GetTestModelBinderProviderContext(typeof(string));
|
||||||
|
context.OnCreatingBinder(modelMetadata => testBinder);
|
||||||
context.BindingInfo.BindingSource = source;
|
context.BindingInfo.BindingSource = source;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -40,52 +94,91 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var provider = new HeaderModelBinderProvider();
|
|
||||||
|
|
||||||
var context = new TestModelBinderProviderContext(typeof(string));
|
|
||||||
context.BindingInfo.BindingSource = BindingSource.Header;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = provider.GetBinder(context);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.IsType<HeaderModelBinder>(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(string))]
|
[InlineData(typeof(string))]
|
||||||
[InlineData(typeof(IEnumerable<string>))]
|
[InlineData(typeof(bool))]
|
||||||
[InlineData(typeof(string[]))]
|
[InlineData(typeof(int))]
|
||||||
[InlineData(typeof(Collection<string>))]
|
[InlineData(typeof(DateTime))]
|
||||||
public void Create_WhenModelTypeIsSupportedType_ReturnsBinder(Type modelType)
|
[InlineData(typeof(double))]
|
||||||
|
[InlineData(typeof(CarEnumType))]
|
||||||
|
public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForSimpleTypes(Type modelType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var provider = new HeaderModelBinderProvider();
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var testBinder = Mock.Of<IModelBinder>();
|
||||||
var context = new TestModelBinderProviderContext(modelType);
|
var context = GetTestModelBinderProviderContext(modelType);
|
||||||
|
context.OnCreatingBinder(modelMetadata => testBinder);
|
||||||
context.BindingInfo.BindingSource = BindingSource.Header;
|
context.BindingInfo.BindingSource = BindingSource.Header;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = provider.GetBinder(context);
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsType<HeaderModelBinder>(result);
|
var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
|
||||||
|
Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(Dictionary<int, string>))]
|
[InlineData(typeof(bool?))]
|
||||||
[InlineData(typeof(Collection<int>))]
|
[InlineData(typeof(int?))]
|
||||||
[InlineData(typeof(Person))]
|
[InlineData(typeof(DateTime?))]
|
||||||
public void Create_WhenModelTypeIsUnsupportedType_ReturnsNull(Type modelType)
|
[InlineData(typeof(double?))]
|
||||||
|
[InlineData(typeof(CarEnumType?))]
|
||||||
|
public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForNullableSimpleTypes(Type modelType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var provider = new HeaderModelBinderProvider();
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var testBinder = Mock.Of<IModelBinder>();
|
||||||
|
var context = GetTestModelBinderProviderContext(modelType);
|
||||||
|
context.OnCreatingBinder(modelMetadata => testBinder);
|
||||||
|
context.BindingInfo.BindingSource = BindingSource.Header;
|
||||||
|
|
||||||
var context = new TestModelBinderProviderContext(modelType);
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
|
||||||
|
Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(string[]))]
|
||||||
|
[InlineData(typeof(IEnumerable<bool>))]
|
||||||
|
[InlineData(typeof(List<byte>))]
|
||||||
|
[InlineData(typeof(Collection<short>))]
|
||||||
|
[InlineData(typeof(float[]))]
|
||||||
|
[InlineData(typeof(IEnumerable<decimal>))]
|
||||||
|
[InlineData(typeof(List<double>))]
|
||||||
|
[InlineData(typeof(ICollection<CarEnumType>))]
|
||||||
|
public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForCollectionOfSimpleTypes(Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var testBinder = Mock.Of<IModelBinder>();
|
||||||
|
var context = GetTestModelBinderProviderContext(modelType);
|
||||||
|
context.OnCreatingBinder(modelMetadata => testBinder);
|
||||||
|
context.BindingInfo.BindingSource = BindingSource.Header;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
|
||||||
|
Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(CustomerStruct))]
|
||||||
|
[InlineData(typeof(IEnumerable<CustomerStruct>))]
|
||||||
|
[InlineData(typeof(Person))]
|
||||||
|
[InlineData(typeof(IEnumerable<Person>))]
|
||||||
|
public void Create_WhenBindingSourceIsFromHeader_ReturnsNull_ForNonSimpleModelType(Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var testBinder = Mock.Of<IModelBinder>();
|
||||||
|
var context = GetTestModelBinderProviderContext(modelType);
|
||||||
|
context.OnCreatingBinder(modelMetadata => testBinder);
|
||||||
context.BindingInfo.BindingSource = BindingSource.Header;
|
context.BindingInfo.BindingSource = BindingSource.Header;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -95,11 +188,89 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(ProductWithTypeConverter))]
|
||||||
|
[InlineData(typeof(IEnumerable<ProductWithTypeConverter>))]
|
||||||
|
[InlineData(typeof(CustomerStructWithTypeConverter))]
|
||||||
|
[InlineData(typeof(IEnumerable<CustomerStructWithTypeConverter>))]
|
||||||
|
public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForNonSimpleModelType_HavingTypeConverter(
|
||||||
|
Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var testBinder = Mock.Of<IModelBinder>();
|
||||||
|
var context = GetTestModelBinderProviderContext(modelType);
|
||||||
|
context.OnCreatingBinder(modelMetadata => testBinder);
|
||||||
|
context.BindingInfo.BindingSource = BindingSource.Header;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
|
||||||
|
Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_WhenBindingSourceIsFromHeader_NoInnerBinderAvailable_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new HeaderModelBinderProvider();
|
||||||
|
var context = GetTestModelBinderProviderContext(typeof(string));
|
||||||
|
context.OnCreatingBinder(modelMetadata => null);
|
||||||
|
context.BindingInfo.BindingSource = BindingSource.Header;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestModelBinderProviderContext GetTestModelBinderProviderContext(
|
||||||
|
Type modelType,
|
||||||
|
bool allowBindingHeaderValuesToNonStringModelTypes = true)
|
||||||
|
{
|
||||||
|
var context = new TestModelBinderProviderContext(modelType);
|
||||||
|
var options = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
|
||||||
|
options.AllowBindingHeaderValuesToNonStringModelTypes = allowBindingHeaderValuesToNonStringModelTypes;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CarEnumType
|
||||||
|
{
|
||||||
|
Sedan,
|
||||||
|
Coupe
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CustomerStruct
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[TypeConverter(typeof(CanConvertFromStringConverter))]
|
||||||
|
private struct CustomerStructWithTypeConverter
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
private class Person
|
private class Person
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public int Age { get; set; }
|
[TypeConverter(typeof(CanConvertFromStringConverter))]
|
||||||
|
private class ProductWithTypeConverter
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CanConvertFromStringConverter : TypeConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
|
{
|
||||||
|
return sourceType == typeof(string);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,44 +3,46 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
{
|
{
|
||||||
public class HeaderModelBinderTests
|
public class HeaderModelBinderTests
|
||||||
{
|
{
|
||||||
[Theory]
|
public static TheoryData<HeaderModelBinder> HeaderModelBinderWithoutInnerBinderData
|
||||||
[InlineData(typeof(string))]
|
|
||||||
[InlineData(typeof(string[]))]
|
|
||||||
[InlineData(typeof(object))]
|
|
||||||
[InlineData(typeof(int))]
|
|
||||||
[InlineData(typeof(int[]))]
|
|
||||||
[InlineData(typeof(BindingSource))]
|
|
||||||
public async Task BindModelAsync_ReturnsNonEmptyResult_ForAllTypes_WithHeaderBindingSource(Type type)
|
|
||||||
{
|
{
|
||||||
// Arrange
|
get
|
||||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
{
|
||||||
var bindingContext = GetBindingContext(type);
|
var data = new TheoryData<HeaderModelBinder>();
|
||||||
|
#pragma warning disable CS0618
|
||||||
// Act
|
data.Add(new HeaderModelBinder());
|
||||||
await binder.BindModelAsync(bindingContext);
|
#pragma warning restore CS0618
|
||||||
|
data.Add(new HeaderModelBinder(NullLoggerFactory.Instance));
|
||||||
// Assert
|
return data;
|
||||||
Assert.False(bindingContext.Result.IsModelSet);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task HeaderBinder_BindsHeaders_ToStringCollection()
|
[MemberData(nameof(HeaderModelBinderWithoutInnerBinderData))]
|
||||||
|
public async Task HeaderBinder_BindsHeaders_ToStringCollection_WithoutInnerModelBinder(
|
||||||
|
HeaderModelBinder binder)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var type = typeof(string[]);
|
var type = typeof(string[]);
|
||||||
var header = "Accept";
|
var header = "Accept";
|
||||||
var headerValue = "application/json,text/json";
|
var headerValue = "application/json,text/json";
|
||||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
|
||||||
var bindingContext = GetBindingContext(type);
|
var bindingContext = CreateContext(type);
|
||||||
|
|
||||||
bindingContext.FieldName = header;
|
bindingContext.FieldName = header;
|
||||||
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
||||||
|
|
@ -53,15 +55,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.Equal(headerValue.Split(','), bindingContext.Result.Model);
|
Assert.Equal(headerValue.Split(','), bindingContext.Result.Model);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task HeaderBinder_BindsHeaders_ToStringType()
|
[MemberData(nameof(HeaderModelBinderWithoutInnerBinderData))]
|
||||||
|
public async Task HeaderBinder_BindsHeaders_ToStringType_WithoutInnerModelBinder(HeaderModelBinder binder)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var type = typeof(string);
|
var type = typeof(string);
|
||||||
var header = "User-Agent";
|
var header = "User-Agent";
|
||||||
var headerValue = "UnitTest";
|
var headerValue = "UnitTest";
|
||||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
var bindingContext = CreateContext(type);
|
||||||
var bindingContext = GetBindingContext(type);
|
|
||||||
|
|
||||||
bindingContext.FieldName = header;
|
bindingContext.FieldName = header;
|
||||||
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
||||||
|
|
@ -81,13 +83,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
[InlineData(typeof(List<string>))]
|
[InlineData(typeof(List<string>))]
|
||||||
[InlineData(typeof(LinkedList<string>))]
|
[InlineData(typeof(LinkedList<string>))]
|
||||||
[InlineData(typeof(StringList))]
|
[InlineData(typeof(StringList))]
|
||||||
public async Task HeaderBinder_BindsHeaders_ForCollectionsItCanCreate(Type destinationType)
|
public async Task HeaderBinder_BindsHeaders_ForCollectionsItCanCreate_WithoutInnerModelBinder(
|
||||||
|
Type destinationType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var header = "Accept";
|
var header = "Accept";
|
||||||
var headerValue = "application/json,text/json";
|
var headerValue = "application/json,text/json";
|
||||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
#pragma warning disable CS0618
|
||||||
var bindingContext = GetBindingContext(destinationType);
|
var binder = new HeaderModelBinder();
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
var bindingContext = CreateContext(destinationType);
|
||||||
|
|
||||||
bindingContext.FieldName = header;
|
bindingContext.FieldName = header;
|
||||||
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
||||||
|
|
@ -101,17 +106,146 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.Equal(headerValue.Split(','), bindingContext.Result.Model as IEnumerable<string>);
|
Assert.Equal(headerValue.Split(','), bindingContext.Result.Model as IEnumerable<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HeaderBinder_BindsHeaders_ToStringCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(string[]);
|
||||||
|
var headerValue = "application/json,text/json";
|
||||||
|
var bindingContext = CreateContext(type);
|
||||||
|
var binder = CreateBinder(bindingContext);
|
||||||
|
bindingContext.HttpContext.Request.Headers.Add("Header", new[] { headerValue });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal(headerValue.Split(','), bindingContext.Result.Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<string, Type, object> BinderHeaderToSimpleTypesData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var guid = new Guid("3916A5B1-5FE4-4E09-9812-5CDC127FA5B1");
|
||||||
|
|
||||||
|
return new TheoryData<string, Type, object>()
|
||||||
|
{
|
||||||
|
{ "10", typeof(int), 10 },
|
||||||
|
{ "10.50", typeof(double), 10.50 },
|
||||||
|
{ "10.50", typeof(IEnumerable<double>), new List<double>() { 10.50 } },
|
||||||
|
{ "Sedan", typeof(CarType), CarType.Sedan },
|
||||||
|
{ "", typeof(CarType?), null },
|
||||||
|
{ "", typeof(string[]), Array.Empty<string>() },
|
||||||
|
{ null, typeof(string[]), Array.Empty<string>() },
|
||||||
|
{ "", typeof(IEnumerable<string>), new List<string>() },
|
||||||
|
{ null, typeof(IEnumerable<string>), new List<string>() },
|
||||||
|
{ guid.ToString(), typeof(Guid), guid },
|
||||||
|
{ "foo", typeof(string), "foo" },
|
||||||
|
{ "foo, bar", typeof(string), "foo, bar" },
|
||||||
|
{ "foo, bar", typeof(string[]), new[]{ "foo", "bar" } },
|
||||||
|
{ "foo, \"bar\"", typeof(string[]), new[]{ "foo", "bar" } },
|
||||||
|
{ "\"foo,bar\"", typeof(string[]), new[]{ "foo,bar" } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(BinderHeaderToSimpleTypesData))]
|
||||||
|
public async Task HeaderBinder_BindsHeaders_ToSimpleTypes(
|
||||||
|
string headerValue,
|
||||||
|
Type modelType,
|
||||||
|
object expectedModel)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = CreateContext(modelType);
|
||||||
|
var binder = CreateBinder(bindingContext);
|
||||||
|
|
||||||
|
if (headerValue != null)
|
||||||
|
{
|
||||||
|
bindingContext.HttpContext.Request.Headers.Add("Header", headerValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal(expectedModel, bindingContext.Result.Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(CarType?), null)]
|
||||||
|
[InlineData(typeof(int?), null)]
|
||||||
|
public async Task HeaderBinder_DoesNotSetModel_ForHeaderNotPresentOnRequest(
|
||||||
|
Type modelType,
|
||||||
|
object expectedModel)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = CreateContext(modelType);
|
||||||
|
var binder = CreateBinder(bindingContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(string[]), null)]
|
||||||
|
[InlineData(typeof(IEnumerable<string>), null)]
|
||||||
|
public async Task HeaderBinder_DoesNotCreateEmptyCollection_ForNonTopLevelObjects(
|
||||||
|
Type modelType,
|
||||||
|
object expectedModel)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = CreateContext(modelType);
|
||||||
|
bindingContext.IsTopLevelObject = false;
|
||||||
|
var binder = CreateBinder(bindingContext);
|
||||||
|
// No header on the request that the header value provider is looking for
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(IEnumerable<string>))]
|
||||||
|
[InlineData(typeof(ICollection<string>))]
|
||||||
|
[InlineData(typeof(IList<string>))]
|
||||||
|
[InlineData(typeof(List<string>))]
|
||||||
|
[InlineData(typeof(LinkedList<string>))]
|
||||||
|
[InlineData(typeof(StringList))]
|
||||||
|
public async Task HeaderBinder_BindsHeaders_ForCollectionsItCanCreate(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var headerValue = "application/json,text/json";
|
||||||
|
var bindingContext = CreateContext(destinationType);
|
||||||
|
var binder = CreateBinder(bindingContext);
|
||||||
|
bindingContext.HttpContext.Request.Headers.Add("Header", new[] { headerValue });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.IsAssignableFrom(destinationType, bindingContext.Result.Model);
|
||||||
|
Assert.Equal(headerValue.Split(','), bindingContext.Result.Model as IEnumerable<string>);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HeaderBinder_ReturnsResult_ForReadOnlyDestination()
|
public async Task HeaderBinder_ReturnsResult_ForReadOnlyDestination()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var header = "Accept";
|
var bindingContext = CreateContext(GetMetadataForReadOnlyArray());
|
||||||
var headerValue = "application/json,text/json";
|
var binder = CreateBinder(bindingContext);
|
||||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
bindingContext.HttpContext.Request.Headers.Add("Header", "application/json,text/json");
|
||||||
var bindingContext = GetBindingContextForReadOnlyArray();
|
|
||||||
|
|
||||||
bindingContext.FieldName = header;
|
|
||||||
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await binder.BindModelAsync(bindingContext);
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
@ -122,66 +256,186 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HeaderBinder_ReturnsFailedResult_ForCollectionsItCannotCreate()
|
public async Task HeaderBinder_ResetsTheBindingScope_GivingOriginalValueProvider()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var header = "Accept";
|
var expectedValueProvider = Mock.Of<IValueProvider>();
|
||||||
var headerValue = "application/json,text/json";
|
var bindingContext = CreateContext(GetMetadataForType(typeof(string)), expectedValueProvider);
|
||||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
var binder = CreateBinder(bindingContext);
|
||||||
var bindingContext = GetBindingContext(typeof(ISet<string>));
|
bindingContext.HttpContext.Request.Headers.Add("Header", "application/json,text/json");
|
||||||
|
|
||||||
bindingContext.FieldName = header;
|
|
||||||
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await binder.BindModelAsync(bindingContext);
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(bindingContext.Result.IsModelSet);
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
Assert.Null(bindingContext.Result.Model);
|
Assert.Equal("application/json,text/json", bindingContext.Result.Model);
|
||||||
|
Assert.Same(expectedValueProvider, bindingContext.ValueProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DefaultModelBindingContext GetBindingContext(Type modelType)
|
[Fact]
|
||||||
|
public async Task HeaderBinder_UsesValues_OnlyFromHeaderValueProvider()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testValueProvider = new Mock<IValueProvider>();
|
||||||
|
testValueProvider
|
||||||
|
.Setup(vp => vp.ContainsPrefix(It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
testValueProvider
|
||||||
|
.Setup(vp => vp.GetValue(It.IsAny<string>()))
|
||||||
|
.Returns(new ValueProviderResult(new StringValues("foo,bar")));
|
||||||
|
var bindingContext = CreateContext(GetMetadataForType(typeof(string)), testValueProvider.Object);
|
||||||
|
var binder = CreateBinder(bindingContext);
|
||||||
|
bindingContext.HttpContext.Request.Headers.Add("Header", "application/json,text/json");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal("application/json,text/json", bindingContext.Result.Model);
|
||||||
|
Assert.Same(testValueProvider.Object, bindingContext.ValueProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(int), "not-an-integer")]
|
||||||
|
[InlineData(typeof(double), "not-an-double")]
|
||||||
|
[InlineData(typeof(CarType?), "boo")]
|
||||||
|
public async Task HeaderBinder_BindModelAsync_AddsErrorToModelState_OnInvalidInput(
|
||||||
|
Type modelType,
|
||||||
|
string headerValue)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = CreateContext(modelType);
|
||||||
|
var binder = CreateBinder(bindingContext);
|
||||||
|
bindingContext.HttpContext.Request.Headers.Add("Header", headerValue);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var entry = bindingContext.ModelState["someprefix.Header"];
|
||||||
|
Assert.NotNull(entry);
|
||||||
|
var error = Assert.Single(entry.Errors);
|
||||||
|
Assert.Equal($"The value '{headerValue}' is not valid.", error.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(int[]), "a, b")]
|
||||||
|
[InlineData(typeof(IEnumerable<double>), "a, b")]
|
||||||
|
[InlineData(typeof(ICollection<CarType>), "a, b")]
|
||||||
|
public async Task HeaderBinder_BindModelAsync_AddsErrorToModelState_OnInvalid_CollectionInput(
|
||||||
|
Type modelType,
|
||||||
|
string headerValue)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var headerValues = headerValue.Split(',').Select(s => s.Trim()).ToArray();
|
||||||
|
var bindingContext = CreateContext(modelType);
|
||||||
|
var binder = CreateBinder(bindingContext);
|
||||||
|
bindingContext.HttpContext.Request.Headers.Add("Header", headerValue);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var entry = bindingContext.ModelState["someprefix.Header"];
|
||||||
|
Assert.NotNull(entry);
|
||||||
|
Assert.Equal(2, entry.Errors.Count);
|
||||||
|
Assert.Equal($"The value '{headerValues[0]}' is not valid.", entry.Errors[0].ErrorMessage);
|
||||||
|
Assert.Equal($"The value '{headerValues[1]}' is not valid.", entry.Errors[1].ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultModelBindingContext CreateContext(
|
||||||
|
Type modelType,
|
||||||
|
bool allowBindingHeaderValuesToNonStringModelTypes = true)
|
||||||
|
{
|
||||||
|
return CreateContext(
|
||||||
|
metadata: GetMetadataForType(modelType),
|
||||||
|
valueProvider: null,
|
||||||
|
allowBindingHeaderValuesToNonStringModelTypes: allowBindingHeaderValuesToNonStringModelTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultModelBindingContext CreateContext(
|
||||||
|
ModelMetadata metadata,
|
||||||
|
IValueProvider valueProvider = null,
|
||||||
|
bool allowBindingHeaderValuesToNonStringModelTypes = true)
|
||||||
|
{
|
||||||
|
if (valueProvider == null)
|
||||||
|
{
|
||||||
|
valueProvider = Mock.Of<IValueProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new MvcOptions()
|
||||||
|
{
|
||||||
|
AllowBindingHeaderValuesToNonStringModelTypes = allowBindingHeaderValuesToNonStringModelTypes
|
||||||
|
};
|
||||||
|
var setup = new MvcCoreMvcOptionsSetup(new TestHttpRequestStreamReaderFactory());
|
||||||
|
setup.Configure(options);
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||||
|
services.AddSingleton(Options.Create(options));
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var headerName = "Header";
|
||||||
|
|
||||||
|
return new DefaultModelBindingContext()
|
||||||
|
{
|
||||||
|
IsTopLevelObject = true,
|
||||||
|
ModelMetadata = metadata,
|
||||||
|
BinderModelName = metadata.BinderModelName,
|
||||||
|
BindingSource = metadata.BindingSource,
|
||||||
|
|
||||||
|
// HeaderModelBinder must always use the field name when getting the values from header value provider
|
||||||
|
// but add keys into ModelState using the ModelName. This is for back compat reasons.
|
||||||
|
ModelName = $"somePrefix.{headerName}",
|
||||||
|
FieldName = headerName,
|
||||||
|
|
||||||
|
ValueProvider = valueProvider,
|
||||||
|
ModelState = new ModelStateDictionary(),
|
||||||
|
ActionContext = new ActionContext()
|
||||||
|
{
|
||||||
|
HttpContext = new DefaultHttpContext()
|
||||||
|
{
|
||||||
|
RequestServices = serviceProvider
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IModelBinder CreateBinder(DefaultModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
var factory = TestModelBinderFactory.Create(bindingContext.HttpContext.RequestServices);
|
||||||
|
var metadata = bindingContext.ModelMetadata;
|
||||||
|
return factory.CreateBinder(new ModelBinderFactoryContext()
|
||||||
|
{
|
||||||
|
Metadata = metadata,
|
||||||
|
BindingInfo = new BindingInfo()
|
||||||
|
{
|
||||||
|
BinderModelName = metadata.BinderModelName,
|
||||||
|
BinderType = metadata.BinderType,
|
||||||
|
BindingSource = metadata.BindingSource,
|
||||||
|
PropertyFilterProvider = metadata.PropertyFilterProvider,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelMetadata GetMetadataForType(Type modelType)
|
||||||
{
|
{
|
||||||
var metadataProvider = new TestModelMetadataProvider();
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
metadataProvider.ForType(modelType).BindingDetails(d => d.BindingSource = BindingSource.Header);
|
metadataProvider.ForType(modelType).BindingDetails(d => d.BindingSource = BindingSource.Header);
|
||||||
var modelMetadata = metadataProvider.GetMetadataForType(modelType);
|
return metadataProvider.GetMetadataForType(modelType);
|
||||||
|
|
||||||
return GetBindingContext(metadataProvider, modelMetadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DefaultModelBindingContext GetBindingContextForReadOnlyArray()
|
private static ModelMetadata GetMetadataForReadOnlyArray()
|
||||||
{
|
{
|
||||||
var metadataProvider = new TestModelMetadataProvider();
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
metadataProvider
|
metadataProvider
|
||||||
.ForProperty<ModelWithReadOnlyArray>(nameof(ModelWithReadOnlyArray.ArrayProperty))
|
.ForProperty<ModelWithReadOnlyArray>(nameof(ModelWithReadOnlyArray.ArrayProperty))
|
||||||
.BindingDetails(bd => bd.BindingSource = BindingSource.Header);
|
.BindingDetails(bd => bd.BindingSource = BindingSource.Header);
|
||||||
var modelMetadata = metadataProvider.GetMetadataForProperty(
|
return metadataProvider.GetMetadataForProperty(
|
||||||
typeof(ModelWithReadOnlyArray),
|
typeof(ModelWithReadOnlyArray),
|
||||||
nameof(ModelWithReadOnlyArray.ArrayProperty));
|
nameof(ModelWithReadOnlyArray.ArrayProperty));
|
||||||
|
|
||||||
return GetBindingContext(metadataProvider, modelMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DefaultModelBindingContext GetBindingContext(
|
|
||||||
IModelMetadataProvider metadataProvider,
|
|
||||||
ModelMetadata modelMetadata)
|
|
||||||
{
|
|
||||||
var bindingContext = new DefaultModelBindingContext
|
|
||||||
{
|
|
||||||
ActionContext = new ActionContext()
|
|
||||||
{
|
|
||||||
HttpContext = new DefaultHttpContext(),
|
|
||||||
},
|
|
||||||
ModelMetadata = modelMetadata,
|
|
||||||
ModelName = "modelName",
|
|
||||||
FieldName = "modelName",
|
|
||||||
ModelState = new ModelStateDictionary(),
|
|
||||||
BinderModelName = modelMetadata.BinderModelName,
|
|
||||||
BindingSource = modelMetadata.BindingSource,
|
|
||||||
};
|
|
||||||
|
|
||||||
return bindingContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ModelWithReadOnlyArray
|
private class ModelWithReadOnlyArray
|
||||||
|
|
@ -192,5 +446,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
private class StringList : List<string>
|
private class StringList : List<string>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum CarType
|
||||||
|
{
|
||||||
|
Sedan,
|
||||||
|
Coupe
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,11 +6,14 @@ using System.Collections.Generic;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
{
|
{
|
||||||
public class TestModelBinderProviderContext : ModelBinderProviderContext
|
public class TestModelBinderProviderContext : ModelBinderProviderContext
|
||||||
{
|
{
|
||||||
|
private BindingInfo _bindingInfo;
|
||||||
|
|
||||||
// Has to be internal because TestModelMetadataProvider is 'shared' code.
|
// Has to be internal because TestModelMetadataProvider is 'shared' code.
|
||||||
internal static readonly TestModelMetadataProvider CachedMetadataProvider = new TestModelMetadataProvider();
|
internal static readonly TestModelMetadataProvider CachedMetadataProvider = new TestModelMetadataProvider();
|
||||||
|
|
||||||
|
|
@ -18,10 +21,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
new List<Func<ModelMetadata, IModelBinder>>();
|
new List<Func<ModelMetadata, IModelBinder>>();
|
||||||
|
|
||||||
public TestModelBinderProviderContext(Type modelType)
|
public TestModelBinderProviderContext(Type modelType)
|
||||||
|
: this(modelType, bindingInfo: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestModelBinderProviderContext(Type modelType, BindingInfo bindingInfo)
|
||||||
{
|
{
|
||||||
Metadata = CachedMetadataProvider.GetMetadataForType(modelType);
|
Metadata = CachedMetadataProvider.GetMetadataForType(modelType);
|
||||||
MetadataProvider = CachedMetadataProvider;
|
MetadataProvider = CachedMetadataProvider;
|
||||||
BindingInfo = new BindingInfo()
|
_bindingInfo = bindingInfo ?? new BindingInfo
|
||||||
{
|
{
|
||||||
BinderModelName = Metadata.BinderModelName,
|
BinderModelName = Metadata.BinderModelName,
|
||||||
BinderType = Metadata.BinderType,
|
BinderType = Metadata.BinderType,
|
||||||
|
|
@ -31,22 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
Services = GetServices();
|
Services = GetServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestModelBinderProviderContext(ModelMetadata metadata, BindingInfo bindingInfo)
|
public override BindingInfo BindingInfo => _bindingInfo;
|
||||||
{
|
|
||||||
Metadata = metadata;
|
|
||||||
BindingInfo = bindingInfo ?? new BindingInfo
|
|
||||||
{
|
|
||||||
BinderModelName = metadata.BinderModelName,
|
|
||||||
BinderType = metadata.BinderType,
|
|
||||||
BindingSource = metadata.BindingSource,
|
|
||||||
PropertyFilterProvider = metadata.PropertyFilterProvider,
|
|
||||||
};
|
|
||||||
|
|
||||||
MetadataProvider = CachedMetadataProvider;
|
|
||||||
Services = GetServices();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override BindingInfo BindingInfo { get; }
|
|
||||||
|
|
||||||
public override ModelMetadata Metadata { get; }
|
public override ModelMetadata Metadata { get; }
|
||||||
|
|
||||||
|
|
@ -68,6 +61,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override IModelBinder CreateBinder(ModelMetadata metadata, BindingInfo bindingInfo)
|
||||||
|
{
|
||||||
|
_bindingInfo = bindingInfo;
|
||||||
|
return this.CreateBinder(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
public void OnCreatingBinder(Func<ModelMetadata, IModelBinder> binderCreator)
|
public void OnCreatingBinder(Func<ModelMetadata, IModelBinder> binderCreator)
|
||||||
{
|
{
|
||||||
_binderCreators.Add(binderCreator);
|
_binderCreators.Add(binderCreator);
|
||||||
|
|
@ -82,6 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||||
|
services.AddSingleton(Options.Create(new MvcOptions()));
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -31,7 +33,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
public async Task BindPropertyFromHeader_NoData_UsesFullPathAsKeyForModelStateErrors()
|
public async Task BindPropertyFromHeader_NoData_UsesFullPathAsKeyForModelStateErrors()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
|
||||||
var parameter = new ParameterDescriptor()
|
var parameter = new ParameterDescriptor()
|
||||||
{
|
{
|
||||||
Name = "Parameter1",
|
Name = "Parameter1",
|
||||||
|
|
@ -43,7 +44,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
};
|
};
|
||||||
|
|
||||||
// Do not add any headers.
|
// Do not add any headers.
|
||||||
var testContext = ModelBindingTestHelper.GetTestContext();
|
var testContext = GetModelBindingTestContext();
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||||
var modelState = testContext.ModelState;
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -70,7 +72,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
public async Task BindPropertyFromHeader_WithPrefix_GetsBound()
|
public async Task BindPropertyFromHeader_WithPrefix_GetsBound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
|
||||||
var parameter = new ParameterDescriptor()
|
var parameter = new ParameterDescriptor()
|
||||||
{
|
{
|
||||||
Name = "Parameter1",
|
Name = "Parameter1",
|
||||||
|
|
@ -81,8 +82,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
ParameterType = typeof(Person)
|
ParameterType = typeof(Person)
|
||||||
};
|
};
|
||||||
|
|
||||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
var testContext = GetModelBindingTestContext(
|
||||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||||
var modelState = testContext.ModelState;
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -106,7 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Empty(entry.Value.Errors);
|
Assert.Empty(entry.Value.Errors);
|
||||||
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
|
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
|
||||||
Assert.Equal("someValue", entry.Value.AttemptedValue);
|
Assert.Equal("someValue", entry.Value.AttemptedValue);
|
||||||
Assert.Equal(new string[] { "someValue" }, entry.Value.RawValue);
|
Assert.Equal("someValue", entry.Value.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The scenario is interesting as we to bind the top level model we fallback to empty prefix,
|
// The scenario is interesting as we to bind the top level model we fallback to empty prefix,
|
||||||
|
|
@ -115,7 +117,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
public async Task BindPropertyFromHeader_WithData_WithEmptyPrefix_GetsBound()
|
public async Task BindPropertyFromHeader_WithData_WithEmptyPrefix_GetsBound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
|
||||||
var parameter = new ParameterDescriptor()
|
var parameter = new ParameterDescriptor()
|
||||||
{
|
{
|
||||||
Name = "Parameter1",
|
Name = "Parameter1",
|
||||||
|
|
@ -123,8 +124,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
ParameterType = typeof(Person)
|
ParameterType = typeof(Person)
|
||||||
};
|
};
|
||||||
|
|
||||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
var testContext = GetModelBindingTestContext(
|
||||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||||
var modelState = testContext.ModelState;
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -148,7 +150,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Empty(entry.Value.Errors);
|
Assert.Empty(entry.Value.Errors);
|
||||||
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
|
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
|
||||||
Assert.Equal("someValue", entry.Value.AttemptedValue);
|
Assert.Equal("someValue", entry.Value.AttemptedValue);
|
||||||
Assert.Equal(new string[] { "someValue" }, entry.Value.RawValue);
|
Assert.Equal("someValue", entry.Value.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ListContainer1
|
private class ListContainer1
|
||||||
|
|
@ -161,7 +163,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
public async Task BindCollectionPropertyFromHeader_WithData_IsBound()
|
public async Task BindCollectionPropertyFromHeader_WithData_IsBound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
|
||||||
var parameter = new ParameterDescriptor
|
var parameter = new ParameterDescriptor
|
||||||
{
|
{
|
||||||
Name = "Parameter1",
|
Name = "Parameter1",
|
||||||
|
|
@ -169,8 +170,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
ParameterType = typeof(ListContainer1),
|
ParameterType = typeof(ListContainer1),
|
||||||
};
|
};
|
||||||
|
|
||||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
var testContext = GetModelBindingTestContext(
|
||||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||||
var modelState = testContext.ModelState;
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -195,7 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Empty(modelStateEntry.Errors);
|
Assert.Empty(modelStateEntry.Errors);
|
||||||
Assert.Equal(ModelValidationState.Valid, modelStateEntry.ValidationState);
|
Assert.Equal(ModelValidationState.Valid, modelStateEntry.ValidationState);
|
||||||
Assert.Equal("someValue", modelStateEntry.AttemptedValue);
|
Assert.Equal("someValue", modelStateEntry.AttemptedValue);
|
||||||
Assert.Equal(new[] { "someValue" }, modelStateEntry.RawValue);
|
Assert.Equal("someValue", modelStateEntry.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ListContainer2
|
private class ListContainer2
|
||||||
|
|
@ -208,7 +210,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
public async Task BindReadOnlyCollectionPropertyFromHeader_WithData_IsBound()
|
public async Task BindReadOnlyCollectionPropertyFromHeader_WithData_IsBound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
|
||||||
var parameter = new ParameterDescriptor
|
var parameter = new ParameterDescriptor
|
||||||
{
|
{
|
||||||
Name = "Parameter1",
|
Name = "Parameter1",
|
||||||
|
|
@ -216,8 +217,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
ParameterType = typeof(ListContainer2),
|
ParameterType = typeof(ListContainer2),
|
||||||
};
|
};
|
||||||
|
|
||||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
var testContext = GetModelBindingTestContext(
|
||||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||||
var modelState = testContext.ModelState;
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -242,7 +244,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Empty(modelStateEntry.Errors);
|
Assert.Empty(modelStateEntry.Errors);
|
||||||
Assert.Equal(ModelValidationState.Valid, modelStateEntry.ValidationState);
|
Assert.Equal(ModelValidationState.Valid, modelStateEntry.ValidationState);
|
||||||
Assert.Equal("someValue", modelStateEntry.AttemptedValue);
|
Assert.Equal("someValue", modelStateEntry.AttemptedValue);
|
||||||
Assert.Equal(new[] { "someValue" }, modelStateEntry.RawValue);
|
Assert.Equal("someValue", modelStateEntry.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -251,20 +253,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
public async Task BindParameterFromHeader_WithData_WithPrefix_ModelGetsBound(Type modelType, string value)
|
public async Task BindParameterFromHeader_WithData_WithPrefix_ModelGetsBound(Type modelType, string value)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
object expectedValue;
|
string expectedAttemptedValue;
|
||||||
object expectedRawValue;
|
object expectedRawValue;
|
||||||
if (modelType == typeof(string))
|
if (modelType == typeof(string))
|
||||||
{
|
{
|
||||||
expectedValue = value;
|
expectedAttemptedValue = value;
|
||||||
expectedRawValue = new string[] { value };
|
expectedRawValue = value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
expectedValue = value.Split(',').Select(v => v.Trim()).ToArray();
|
expectedAttemptedValue = value.Replace(" ", "");
|
||||||
expectedRawValue = expectedValue;
|
expectedRawValue = value.Split(',').Select(v => v.Trim()).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
|
||||||
var parameter = new ParameterDescriptor
|
var parameter = new ParameterDescriptor
|
||||||
{
|
{
|
||||||
Name = "Parameter1",
|
Name = "Parameter1",
|
||||||
|
|
@ -276,8 +277,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
ParameterType = modelType
|
ParameterType = modelType
|
||||||
};
|
};
|
||||||
|
|
||||||
Action<HttpRequest> action = r => r.Headers.Add("CustomParameter", new[] { value });
|
Action<HttpRequest> action = r => r.Headers.Add("CustomParameter", new[] { expectedAttemptedValue });
|
||||||
var testContext = ModelBindingTestHelper.GetTestContext(action);
|
var testContext = GetModelBindingTestContext(action);
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||||
|
|
||||||
// Do not add any headers.
|
// Do not add any headers.
|
||||||
var httpContext = testContext.HttpContext;
|
var httpContext = testContext.HttpContext;
|
||||||
|
|
@ -301,8 +303,221 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Equal("CustomParameter", entry.Key);
|
Assert.Equal("CustomParameter", entry.Key);
|
||||||
Assert.Empty(entry.Value.Errors);
|
Assert.Empty(entry.Value.Errors);
|
||||||
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
|
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
|
||||||
Assert.Equal(value, entry.Value.AttemptedValue);
|
Assert.Equal(expectedAttemptedValue, entry.Value.AttemptedValue);
|
||||||
Assert.Equal(expectedRawValue, entry.Value.RawValue);
|
Assert.Equal(expectedRawValue, entry.Value.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BindPropertyFromHeader_WithPrefix_GetsBound_ForSimpleTypes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = "Parameter1",
|
||||||
|
BindingInfo = new BindingInfo()
|
||||||
|
{
|
||||||
|
BinderModelName = "prefix",
|
||||||
|
},
|
||||||
|
ParameterType = typeof(Product)
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = GetModelBindingTestContext(
|
||||||
|
request =>
|
||||||
|
{
|
||||||
|
request.Headers.Add("NoCommaString", "someValue");
|
||||||
|
request.Headers.Add("OneCommaSeparatedString", "one, two, three");
|
||||||
|
request.Headers.Add("IntProperty", "10");
|
||||||
|
request.Headers.Add("NullableIntProperty", "300");
|
||||||
|
request.Headers.Add("ArrayOfString", "first, second");
|
||||||
|
request.Headers.Add("EnumerableOfDouble", "10.51, 45.44");
|
||||||
|
request.Headers.Add("ListOfEnum", "Sedan, Coupe");
|
||||||
|
request.Headers.Add("ListOfOrderWithTypeConverter", "10");
|
||||||
|
});
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
// ModelBindingResult
|
||||||
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
|
// Model
|
||||||
|
var product = Assert.IsType<Product>(modelBindingResult.Model);
|
||||||
|
Assert.NotNull(product);
|
||||||
|
Assert.NotNull(product.Manufacturer);
|
||||||
|
Assert.Equal("someValue", product.Manufacturer.NoCommaString);
|
||||||
|
Assert.Equal("one, two, three", product.Manufacturer.OneCommaSeparatedStringProperty);
|
||||||
|
Assert.Equal(10, product.Manufacturer.IntProperty);
|
||||||
|
Assert.Equal(300, product.Manufacturer.NullableIntProperty);
|
||||||
|
Assert.Null(product.Manufacturer.NullableLongProperty);
|
||||||
|
Assert.Equal(new[] { "first", "second" }, product.Manufacturer.ArrayOfString);
|
||||||
|
Assert.Equal(new double[] { 10.51, 45.44 }, product.Manufacturer.EnumerableOfDoubleProperty);
|
||||||
|
Assert.Equal(new CarType[] { CarType.Sedan, CarType.Coupe }, product.Manufacturer.ListOfEnum);
|
||||||
|
var orderWithTypeConverter = Assert.Single(product.Manufacturer.ListOfOrderWithTypeConverterProperty);
|
||||||
|
Assert.Equal(10, orderWithTypeConverter.Id);
|
||||||
|
|
||||||
|
// ModelState
|
||||||
|
Assert.True(modelState.IsValid);
|
||||||
|
Assert.Collection(
|
||||||
|
modelState.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("prefix.Manufacturer.ArrayOfString", kvp.Key);
|
||||||
|
var entry = kvp.Value;
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Equal("first,second", entry.AttemptedValue);
|
||||||
|
Assert.Equal(new[] { "first", "second" }, entry.RawValue);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("prefix.Manufacturer.EnumerableOfDouble", kvp.Key);
|
||||||
|
var entry = kvp.Value;
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Equal("10.51,45.44", entry.AttemptedValue);
|
||||||
|
Assert.Equal(new[] { "10.51", "45.44" }, entry.RawValue);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("prefix.Manufacturer.IntProperty", kvp.Key);
|
||||||
|
var entry = kvp.Value;
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Equal("10", entry.AttemptedValue);
|
||||||
|
Assert.Equal("10", entry.RawValue);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("prefix.Manufacturer.ListOfEnum", kvp.Key);
|
||||||
|
var entry = kvp.Value;
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Equal("Sedan,Coupe", entry.AttemptedValue);
|
||||||
|
Assert.Equal(new[] { "Sedan", "Coupe" }, entry.RawValue);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("prefix.Manufacturer.ListOfOrderWithTypeConverter", kvp.Key);
|
||||||
|
var entry = kvp.Value;
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Equal("10", entry.AttemptedValue);
|
||||||
|
Assert.Equal("10", entry.RawValue);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("prefix.Manufacturer.NoCommaString", kvp.Key);
|
||||||
|
var entry = kvp.Value;
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Equal("someValue", entry.AttemptedValue);
|
||||||
|
Assert.Equal("someValue", entry.RawValue);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("prefix.Manufacturer.NullableIntProperty", kvp.Key);
|
||||||
|
var entry = kvp.Value;
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Equal("300", entry.AttemptedValue);
|
||||||
|
Assert.Equal("300", entry.RawValue);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("prefix.Manufacturer.OneCommaSeparatedString", kvp.Key);
|
||||||
|
var entry = kvp.Value;
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Equal("one, two, three", entry.AttemptedValue);
|
||||||
|
Assert.Equal("one, two, three", entry.RawValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelBindingTestContext GetModelBindingTestContext(
|
||||||
|
Action<HttpRequest> updateRequest = null,
|
||||||
|
Action<MvcOptions> updateOptions = null)
|
||||||
|
{
|
||||||
|
if (updateOptions == null)
|
||||||
|
{
|
||||||
|
updateOptions = o =>
|
||||||
|
{
|
||||||
|
o.AllowBindingHeaderValuesToNonStringModelTypes = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModelBindingTestHelper.GetTestContext(updateRequest, updateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Product
|
||||||
|
{
|
||||||
|
public Manufacturer Manufacturer { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Manufacturer
|
||||||
|
{
|
||||||
|
[FromHeader]
|
||||||
|
public string NoCommaString { get; set; }
|
||||||
|
|
||||||
|
[FromHeader(Name = "OneCommaSeparatedString")]
|
||||||
|
public string OneCommaSeparatedStringProperty { get; set; }
|
||||||
|
|
||||||
|
[FromHeader]
|
||||||
|
public int IntProperty { get; set; }
|
||||||
|
|
||||||
|
[FromHeader]
|
||||||
|
public int? NullableIntProperty { get; set; }
|
||||||
|
|
||||||
|
[FromHeader]
|
||||||
|
public long? NullableLongProperty { get; set; }
|
||||||
|
|
||||||
|
[FromHeader]
|
||||||
|
public string[] ArrayOfString { get; set; }
|
||||||
|
|
||||||
|
[FromHeader(Name = "EnumerableOfDouble")]
|
||||||
|
public IEnumerable<double> EnumerableOfDoubleProperty { get; set; }
|
||||||
|
|
||||||
|
[FromHeader]
|
||||||
|
public List<CarType> ListOfEnum { get; set; }
|
||||||
|
|
||||||
|
[FromHeader(Name = "ListOfOrderWithTypeConverter")]
|
||||||
|
public List<OrderWithTypeConverter> ListOfOrderWithTypeConverterProperty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CarType
|
||||||
|
{
|
||||||
|
Coupe,
|
||||||
|
Sedan
|
||||||
|
}
|
||||||
|
|
||||||
|
[TypeConverter(typeof(CanConvertFromStringConverter))]
|
||||||
|
private class OrderWithTypeConverter : IEquatable<OrderWithTypeConverter>
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public int ItemCount { get; set; }
|
||||||
|
|
||||||
|
public bool Equals(OrderWithTypeConverter other)
|
||||||
|
{
|
||||||
|
return Id == other.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CanConvertFromStringConverter : TypeConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
|
{
|
||||||
|
return sourceType == typeof(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||||
|
{
|
||||||
|
var id = value.ToString();
|
||||||
|
return new OrderWithTypeConverter() { Id = int.Parse(id) };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +60,18 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ParameterBinder GetParameterBinder(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var metadataProvider = serviceProvider.GetRequiredService<IModelMetadataProvider>();
|
||||||
|
var options = serviceProvider.GetRequiredService<IOptions<MvcOptions>>();
|
||||||
|
|
||||||
|
return new ParameterBinder(
|
||||||
|
metadataProvider,
|
||||||
|
new ModelBinderFactory(metadataProvider, options, serviceProvider),
|
||||||
|
new CompositeModelValidatorProvider(GetModelValidatorProviders(options)),
|
||||||
|
NullLoggerFactory.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
public static ParameterBinder GetParameterBinder(
|
public static ParameterBinder GetParameterBinder(
|
||||||
IModelMetadataProvider metadataProvider,
|
IModelMetadataProvider metadataProvider,
|
||||||
IModelBinderProvider binderProvider = null,
|
IModelBinderProvider binderProvider = null,
|
||||||
|
|
@ -77,7 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
|
|
||||||
return new ParameterBinder(
|
return new ParameterBinder(
|
||||||
metadataProvider,
|
metadataProvider,
|
||||||
GetModelBinderFactory(metadataProvider, options),
|
new ModelBinderFactory(metadataProvider, options, services),
|
||||||
new CompositeModelValidatorProvider(GetModelValidatorProviders(options)),
|
new CompositeModelValidatorProvider(GetModelValidatorProviders(options)),
|
||||||
NullLoggerFactory.Instance);
|
NullLoggerFactory.Instance);
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
return httpContext;
|
return httpContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IServiceProvider GetServices(Action<MvcOptions> updateOptions = null)
|
public static IServiceProvider GetServices(Action<MvcOptions> updateOptions = null)
|
||||||
{
|
{
|
||||||
var serviceCollection = new ServiceCollection();
|
var serviceCollection = new ServiceCollection();
|
||||||
serviceCollection.AddAuthorization();
|
serviceCollection.AddAuthorization();
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(mvcOptions.AllowCombiningAuthorizeFilters);
|
Assert.False(mvcOptions.AllowCombiningAuthorizeFilters);
|
||||||
|
Assert.False(mvcOptions.AllowBindingHeaderValuesToNonStringModelTypes);
|
||||||
Assert.False(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
Assert.False(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
||||||
Assert.Equal(InputFormatterExceptionPolicy.AllExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
Assert.Equal(InputFormatterExceptionPolicy.AllExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
||||||
Assert.False(jsonOptions.AllowInputFormatterExceptionMessages);
|
Assert.False(jsonOptions.AllowInputFormatterExceptionMessages);
|
||||||
|
|
@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
|
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
|
||||||
|
Assert.True(mvcOptions.AllowBindingHeaderValuesToNonStringModelTypes);
|
||||||
Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
||||||
Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
||||||
Assert.True(jsonOptions.AllowInputFormatterExceptionMessages);
|
Assert.True(jsonOptions.AllowInputFormatterExceptionMessages);
|
||||||
|
|
@ -80,6 +82,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
|
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
|
||||||
|
Assert.True(mvcOptions.AllowBindingHeaderValuesToNonStringModelTypes);
|
||||||
Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
||||||
Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
||||||
Assert.True(jsonOptions.AllowInputFormatterExceptionMessages);
|
Assert.True(jsonOptions.AllowInputFormatterExceptionMessages);
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,24 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
{
|
{
|
||||||
public class TestModelBinderFactory : ModelBinderFactory
|
public class TestModelBinderFactory : ModelBinderFactory
|
||||||
{
|
{
|
||||||
|
public static TestModelBinderFactory Create(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var options = serviceProvider.GetRequiredService<IOptions<MvcOptions>>();
|
||||||
|
return new TestModelBinderFactory(
|
||||||
|
TestModelMetadataProvider.CreateDefaultProvider(),
|
||||||
|
options,
|
||||||
|
serviceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
public static TestModelBinderFactory Create(params IModelBinderProvider[] providers)
|
public static TestModelBinderFactory Create(params IModelBinderProvider[] providers)
|
||||||
{
|
{
|
||||||
return Create(null, providers);
|
return Create(null, providers);
|
||||||
|
|
@ -58,14 +67,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
protected TestModelBinderFactory(IModelMetadataProvider metadataProvider, IOptions<MvcOptions> options)
|
protected TestModelBinderFactory(IModelMetadataProvider metadataProvider, IOptions<MvcOptions> options)
|
||||||
: base(metadataProvider, options, GetServices())
|
: this(metadataProvider, options, GetServices(options))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IServiceProvider GetServices()
|
protected TestModelBinderFactory(
|
||||||
|
IModelMetadataProvider metadataProvider,
|
||||||
|
IOptions<MvcOptions> options,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
|
: base(metadataProvider, options, serviceProvider)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceProvider GetServices(IOptions<MvcOptions> options)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||||
|
services.AddSingleton(options);
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue