[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>
|
||||
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>
|
||||
/// Gets the <see cref="BindingInfo"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
if (Version >= CompatibilityVersion.Version_2_1)
|
||||
{
|
||||
values[nameof(MvcOptions.AllowCombiningAuthorizeFilters)] = true;
|
||||
values[nameof(MvcOptions.AllowBindingHeaderValuesToNonStringModelTypes)] = true;
|
||||
values[nameof(MvcOptions.InputFormatterExceptionPolicy)] = InputFormatterExceptionPolicy.MalformedInputExceptions;
|
||||
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, string, Type, Exception> _cannotBindToFilesCollectionDueToUnsupportedContentType;
|
||||
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, string, string, Exception> _noNonIndexBasedFormatFoundForCollection;
|
||||
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>(
|
||||
LogLevel.Debug,
|
||||
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(
|
||||
LogLevel.Debug,
|
||||
|
|
@ -597,6 +598,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
LogLevel.Debug,
|
||||
42,
|
||||
"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)
|
||||
|
|
@ -1169,6 +1175,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
_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)
|
||||
{
|
||||
_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.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
|
|
@ -21,11 +23,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
|
||||
/// <summary>
|
||||
/// <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>
|
||||
/// </summary>
|
||||
[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()
|
||||
: this(NullLoggerFactory.Instance)
|
||||
{
|
||||
|
|
@ -36,35 +38,117 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
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)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
if (innerModelBinder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(innerModelBinder));
|
||||
}
|
||||
|
||||
_logger = loggerFactory.CreateLogger<HeaderModelBinder>();
|
||||
InnerModelBinder = innerModelBinder;
|
||||
}
|
||||
|
||||
// to enable unit testing
|
||||
internal IModelBinder InnerModelBinder { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext == null)
|
||||
{
|
||||
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).
|
||||
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))
|
||||
{
|
||||
_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;
|
||||
if (bindingContext.ModelType == typeof(string))
|
||||
{
|
||||
|
|
@ -101,7 +185,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
}
|
||||
|
||||
_logger.DoneAttemptingToBindModel(bindingContext);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static object GetCompatibleCollection(ModelBindingContext bindingContext, string[] values)
|
||||
|
|
@ -126,5 +209,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
|
||||
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.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||
{
|
||||
|
|
@ -21,26 +22,65 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (context.BindingInfo.BindingSource != null &&
|
||||
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
|
||||
var bindingInfo = context.BindingInfo;
|
||||
if (bindingInfo.BindingSource == null ||
|
||||
!bindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
|
||||
{
|
||||
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger<HeaderModelBinderProvider>();
|
||||
return null;
|
||||
}
|
||||
|
||||
// We only support strings and collections of strings. Some cases can fail
|
||||
// at runtime due to collections we can't modify.
|
||||
if (context.Metadata.ModelType == typeof(string) ||
|
||||
context.Metadata.ElementType == typeof(string))
|
||||
var modelMetadata = context.Metadata;
|
||||
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger<HeaderModelBinderProvider>();
|
||||
|
||||
var options = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
|
||||
if (!options.AllowBindingHeaderValuesToNonStringModelTypes)
|
||||
{
|
||||
if (modelMetadata.ModelType == typeof(string) ||
|
||||
modelMetadata.ElementType == typeof(string))
|
||||
{
|
||||
return new HeaderModelBinder(loggerFactory);
|
||||
}
|
||||
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(
|
||||
DefaultModelBinderProviderContext parent,
|
||||
ModelMetadata metadata)
|
||||
ModelMetadata metadata,
|
||||
BindingInfo bindingInfo)
|
||||
{
|
||||
Metadata = metadata;
|
||||
|
||||
_factory = parent._factory;
|
||||
MetadataProvider = parent.MetadataProvider;
|
||||
Visited = parent.Visited;
|
||||
|
||||
BindingInfo = new BindingInfo()
|
||||
{
|
||||
BinderModelName = metadata.BinderModelName,
|
||||
BinderType = metadata.BinderType,
|
||||
BindingSource = metadata.BindingSource,
|
||||
PropertyFilterProvider = metadata.PropertyFilterProvider,
|
||||
};
|
||||
BindingInfo = bindingInfo;
|
||||
}
|
||||
|
||||
public override BindingInfo BindingInfo { get; }
|
||||
|
|
@ -280,18 +274,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
public override IServiceProvider Services => _factory._serviceProvider;
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
// 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.
|
||||
var token = metadata;
|
||||
|
||||
var nestedContext = new DefaultModelBinderProviderContext(this, metadata);
|
||||
var nestedContext = new DefaultModelBinderProviderContext(this, metadata, bindingInfo);
|
||||
return _factory.CreateBinderCoreCached(nestedContext, token);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
|
|||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors;
|
||||
|
||||
// See CompatibilitySwitch.cs for guide on how to implement these.
|
||||
private readonly CompatibilitySwitch<bool> _allowBindingHeaderValuesToNonStringModelTypes;
|
||||
private readonly CompatibilitySwitch<bool> _allowCombiningAuthorizeFilters;
|
||||
private readonly CompatibilitySwitch<InputFormatterExceptionPolicy> _inputFormatterExceptionPolicy;
|
||||
private readonly CompatibilitySwitch<bool> _suppressBindingUndefinedValueToEnumType;
|
||||
|
|
@ -47,12 +49,14 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
ValueProviderFactories = new List<IValueProviderFactory>();
|
||||
|
||||
_allowCombiningAuthorizeFilters = new CompatibilitySwitch<bool>(nameof(AllowCombiningAuthorizeFilters));
|
||||
_allowBindingHeaderValuesToNonStringModelTypes = new CompatibilitySwitch<bool>(nameof(AllowBindingHeaderValuesToNonStringModelTypes));
|
||||
_inputFormatterExceptionPolicy = new CompatibilitySwitch<InputFormatterExceptionPolicy>(nameof(InputFormatterExceptionPolicy), InputFormatterExceptionPolicy.AllExceptions);
|
||||
_suppressBindingUndefinedValueToEnumType = new CompatibilitySwitch<bool>(nameof(SuppressBindingUndefinedValueToEnumType));
|
||||
|
||||
|
||||
_switches = new ICompatibilitySwitch[]
|
||||
{
|
||||
_allowCombiningAuthorizeFilters,
|
||||
_allowCombiningAuthorizeFilters,
|
||||
_allowBindingHeaderValuesToNonStringModelTypes,
|
||||
_inputFormatterExceptionPolicy,
|
||||
_suppressBindingUndefinedValueToEnumType,
|
||||
};
|
||||
|
|
@ -108,6 +112,38 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
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>
|
||||
/// Gets a Dictionary of CacheProfile Names, <see cref="CacheProfile"/> which are pre-defined settings for
|
||||
/// response caching.
|
||||
|
|
|
|||
|
|
@ -4,12 +4,65 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||
{
|
||||
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
|
||||
{
|
||||
get
|
||||
|
|
@ -29,8 +82,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
{
|
||||
// Arrange
|
||||
var provider = new HeaderModelBinderProvider();
|
||||
|
||||
var context = new TestModelBinderProviderContext(typeof(string));
|
||||
var testBinder = Mock.Of<IModelBinder>();
|
||||
var context = GetTestModelBinderProviderContext(typeof(string));
|
||||
context.OnCreatingBinder(modelMetadata => testBinder);
|
||||
context.BindingInfo.BindingSource = source;
|
||||
|
||||
// Act
|
||||
|
|
@ -40,52 +94,91 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
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]
|
||||
[InlineData(typeof(string))]
|
||||
[InlineData(typeof(IEnumerable<string>))]
|
||||
[InlineData(typeof(string[]))]
|
||||
[InlineData(typeof(Collection<string>))]
|
||||
public void Create_WhenModelTypeIsSupportedType_ReturnsBinder(Type modelType)
|
||||
[InlineData(typeof(bool))]
|
||||
[InlineData(typeof(int))]
|
||||
[InlineData(typeof(DateTime))]
|
||||
[InlineData(typeof(double))]
|
||||
[InlineData(typeof(CarEnumType))]
|
||||
public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForSimpleTypes(Type modelType)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new HeaderModelBinderProvider();
|
||||
|
||||
var context = new TestModelBinderProviderContext(modelType);
|
||||
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
|
||||
Assert.IsType<HeaderModelBinder>(result);
|
||||
var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
|
||||
Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(Dictionary<int, string>))]
|
||||
[InlineData(typeof(Collection<int>))]
|
||||
[InlineData(typeof(Person))]
|
||||
public void Create_WhenModelTypeIsUnsupportedType_ReturnsNull(Type modelType)
|
||||
[InlineData(typeof(bool?))]
|
||||
[InlineData(typeof(int?))]
|
||||
[InlineData(typeof(DateTime?))]
|
||||
[InlineData(typeof(double?))]
|
||||
[InlineData(typeof(CarEnumType?))]
|
||||
public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForNullableSimpleTypes(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;
|
||||
|
||||
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;
|
||||
|
||||
// Act
|
||||
|
|
@ -95,11 +188,89 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
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
|
||||
{
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
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.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||
{
|
||||
public class HeaderModelBinderTests
|
||||
{
|
||||
[Theory]
|
||||
[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)
|
||||
public static TheoryData<HeaderModelBinder> HeaderModelBinderWithoutInnerBinderData
|
||||
{
|
||||
// Arrange
|
||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
||||
var bindingContext = GetBindingContext(type);
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(bindingContext.Result.IsModelSet);
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<HeaderModelBinder>();
|
||||
#pragma warning disable CS0618
|
||||
data.Add(new HeaderModelBinder());
|
||||
#pragma warning restore CS0618
|
||||
data.Add(new HeaderModelBinder(NullLoggerFactory.Instance));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeaderBinder_BindsHeaders_ToStringCollection()
|
||||
[Theory]
|
||||
[MemberData(nameof(HeaderModelBinderWithoutInnerBinderData))]
|
||||
public async Task HeaderBinder_BindsHeaders_ToStringCollection_WithoutInnerModelBinder(
|
||||
HeaderModelBinder binder)
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(string[]);
|
||||
var header = "Accept";
|
||||
var headerValue = "application/json,text/json";
|
||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
||||
var bindingContext = GetBindingContext(type);
|
||||
|
||||
var bindingContext = CreateContext(type);
|
||||
|
||||
bindingContext.FieldName = header;
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeaderBinder_BindsHeaders_ToStringType()
|
||||
[Theory]
|
||||
[MemberData(nameof(HeaderModelBinderWithoutInnerBinderData))]
|
||||
public async Task HeaderBinder_BindsHeaders_ToStringType_WithoutInnerModelBinder(HeaderModelBinder binder)
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(string);
|
||||
var header = "User-Agent";
|
||||
var headerValue = "UnitTest";
|
||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
||||
var bindingContext = GetBindingContext(type);
|
||||
var bindingContext = CreateContext(type);
|
||||
|
||||
bindingContext.FieldName = header;
|
||||
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
||||
|
|
@ -81,13 +83,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
[InlineData(typeof(List<string>))]
|
||||
[InlineData(typeof(LinkedList<string>))]
|
||||
[InlineData(typeof(StringList))]
|
||||
public async Task HeaderBinder_BindsHeaders_ForCollectionsItCanCreate(Type destinationType)
|
||||
public async Task HeaderBinder_BindsHeaders_ForCollectionsItCanCreate_WithoutInnerModelBinder(
|
||||
Type destinationType)
|
||||
{
|
||||
// Arrange
|
||||
var header = "Accept";
|
||||
var headerValue = "application/json,text/json";
|
||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
||||
var bindingContext = GetBindingContext(destinationType);
|
||||
#pragma warning disable CS0618
|
||||
var binder = new HeaderModelBinder();
|
||||
#pragma warning restore CS0618
|
||||
var bindingContext = CreateContext(destinationType);
|
||||
|
||||
bindingContext.FieldName = header;
|
||||
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>);
|
||||
}
|
||||
|
||||
[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]
|
||||
public async Task HeaderBinder_ReturnsResult_ForReadOnlyDestination()
|
||||
{
|
||||
// Arrange
|
||||
var header = "Accept";
|
||||
var headerValue = "application/json,text/json";
|
||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
||||
var bindingContext = GetBindingContextForReadOnlyArray();
|
||||
|
||||
bindingContext.FieldName = header;
|
||||
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
||||
var bindingContext = CreateContext(GetMetadataForReadOnlyArray());
|
||||
var binder = CreateBinder(bindingContext);
|
||||
bindingContext.HttpContext.Request.Headers.Add("Header", "application/json,text/json");
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
|
@ -122,66 +256,186 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeaderBinder_ReturnsFailedResult_ForCollectionsItCannotCreate()
|
||||
public async Task HeaderBinder_ResetsTheBindingScope_GivingOriginalValueProvider()
|
||||
{
|
||||
// Arrange
|
||||
var header = "Accept";
|
||||
var headerValue = "application/json,text/json";
|
||||
var binder = new HeaderModelBinder(NullLoggerFactory.Instance);
|
||||
var bindingContext = GetBindingContext(typeof(ISet<string>));
|
||||
|
||||
bindingContext.FieldName = header;
|
||||
bindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
|
||||
var expectedValueProvider = Mock.Of<IValueProvider>();
|
||||
var bindingContext = CreateContext(GetMetadataForType(typeof(string)), expectedValueProvider);
|
||||
var binder = CreateBinder(bindingContext);
|
||||
bindingContext.HttpContext.Request.Headers.Add("Header", "application/json,text/json");
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(bindingContext.Result.IsModelSet);
|
||||
Assert.Null(bindingContext.Result.Model);
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
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();
|
||||
metadataProvider.ForType(modelType).BindingDetails(d => d.BindingSource = BindingSource.Header);
|
||||
var modelMetadata = metadataProvider.GetMetadataForType(modelType);
|
||||
|
||||
return GetBindingContext(metadataProvider, modelMetadata);
|
||||
return metadataProvider.GetMetadataForType(modelType);
|
||||
}
|
||||
|
||||
private static DefaultModelBindingContext GetBindingContextForReadOnlyArray()
|
||||
private static ModelMetadata GetMetadataForReadOnlyArray()
|
||||
{
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
metadataProvider
|
||||
.ForProperty<ModelWithReadOnlyArray>(nameof(ModelWithReadOnlyArray.ArrayProperty))
|
||||
.BindingDetails(bd => bd.BindingSource = BindingSource.Header);
|
||||
var modelMetadata = metadataProvider.GetMetadataForProperty(
|
||||
return metadataProvider.GetMetadataForProperty(
|
||||
typeof(ModelWithReadOnlyArray),
|
||||
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
|
||||
|
|
@ -192,5 +446,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
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.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
public class TestModelBinderProviderContext : ModelBinderProviderContext
|
||||
{
|
||||
private BindingInfo _bindingInfo;
|
||||
|
||||
// Has to be internal because TestModelMetadataProvider is 'shared' code.
|
||||
internal static readonly TestModelMetadataProvider CachedMetadataProvider = new TestModelMetadataProvider();
|
||||
|
||||
|
|
@ -18,10 +21,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
new List<Func<ModelMetadata, IModelBinder>>();
|
||||
|
||||
public TestModelBinderProviderContext(Type modelType)
|
||||
: this(modelType, bindingInfo: null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestModelBinderProviderContext(Type modelType, BindingInfo bindingInfo)
|
||||
{
|
||||
Metadata = CachedMetadataProvider.GetMetadataForType(modelType);
|
||||
MetadataProvider = CachedMetadataProvider;
|
||||
BindingInfo = new BindingInfo()
|
||||
_bindingInfo = bindingInfo ?? new BindingInfo
|
||||
{
|
||||
BinderModelName = Metadata.BinderModelName,
|
||||
BinderType = Metadata.BinderType,
|
||||
|
|
@ -31,22 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
Services = GetServices();
|
||||
}
|
||||
|
||||
public TestModelBinderProviderContext(ModelMetadata metadata, 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 BindingInfo BindingInfo => _bindingInfo;
|
||||
|
||||
public override ModelMetadata Metadata { get; }
|
||||
|
||||
|
|
@ -68,6 +61,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
return null;
|
||||
}
|
||||
|
||||
public override IModelBinder CreateBinder(ModelMetadata metadata, BindingInfo bindingInfo)
|
||||
{
|
||||
_bindingInfo = bindingInfo;
|
||||
return this.CreateBinder(metadata);
|
||||
}
|
||||
|
||||
public void OnCreatingBinder(Func<ModelMetadata, IModelBinder> binderCreator)
|
||||
{
|
||||
_binderCreators.Add(binderCreator);
|
||||
|
|
@ -82,6 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||
services.AddSingleton(Options.Create(new MvcOptions()));
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -31,7 +33,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public async Task BindPropertyFromHeader_NoData_UsesFullPathAsKeyForModelStateErrors()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -43,7 +44,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
};
|
||||
|
||||
// Do not add any headers.
|
||||
var testContext = ModelBindingTestHelper.GetTestContext();
|
||||
var testContext = GetModelBindingTestContext();
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
|
|
@ -70,7 +72,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public async Task BindPropertyFromHeader_WithPrefix_GetsBound()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -81,8 +82,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
ParameterType = typeof(Person)
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
var testContext = GetModelBindingTestContext(
|
||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
|
|
@ -106,7 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Empty(entry.Value.Errors);
|
||||
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
|
||||
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,
|
||||
|
|
@ -115,7 +117,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public async Task BindPropertyFromHeader_WithData_WithEmptyPrefix_GetsBound()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -123,8 +124,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
ParameterType = typeof(Person)
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
var testContext = GetModelBindingTestContext(
|
||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
|
|
@ -148,7 +150,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Empty(entry.Value.Errors);
|
||||
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
|
||||
Assert.Equal("someValue", entry.Value.AttemptedValue);
|
||||
Assert.Equal(new string[] { "someValue" }, entry.Value.RawValue);
|
||||
Assert.Equal("someValue", entry.Value.RawValue);
|
||||
}
|
||||
|
||||
private class ListContainer1
|
||||
|
|
@ -161,7 +163,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public async Task BindCollectionPropertyFromHeader_WithData_IsBound()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -169,8 +170,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
ParameterType = typeof(ListContainer1),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
var testContext = GetModelBindingTestContext(
|
||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
|
|
@ -195,7 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Empty(modelStateEntry.Errors);
|
||||
Assert.Equal(ModelValidationState.Valid, modelStateEntry.ValidationState);
|
||||
Assert.Equal("someValue", modelStateEntry.AttemptedValue);
|
||||
Assert.Equal(new[] { "someValue" }, modelStateEntry.RawValue);
|
||||
Assert.Equal("someValue", modelStateEntry.RawValue);
|
||||
}
|
||||
|
||||
private class ListContainer2
|
||||
|
|
@ -208,7 +210,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public async Task BindReadOnlyCollectionPropertyFromHeader_WithData_IsBound()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -216,8 +217,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
ParameterType = typeof(ListContainer2),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
var testContext = GetModelBindingTestContext(
|
||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
|
|
@ -242,7 +244,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Empty(modelStateEntry.Errors);
|
||||
Assert.Equal(ModelValidationState.Valid, modelStateEntry.ValidationState);
|
||||
Assert.Equal("someValue", modelStateEntry.AttemptedValue);
|
||||
Assert.Equal(new[] { "someValue" }, modelStateEntry.RawValue);
|
||||
Assert.Equal("someValue", modelStateEntry.RawValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -251,20 +253,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public async Task BindParameterFromHeader_WithData_WithPrefix_ModelGetsBound(Type modelType, string value)
|
||||
{
|
||||
// Arrange
|
||||
object expectedValue;
|
||||
string expectedAttemptedValue;
|
||||
object expectedRawValue;
|
||||
if (modelType == typeof(string))
|
||||
{
|
||||
expectedValue = value;
|
||||
expectedRawValue = new string[] { value };
|
||||
expectedAttemptedValue = value;
|
||||
expectedRawValue = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
expectedValue = value.Split(',').Select(v => v.Trim()).ToArray();
|
||||
expectedRawValue = expectedValue;
|
||||
expectedAttemptedValue = value.Replace(" ", "");
|
||||
expectedRawValue = value.Split(',').Select(v => v.Trim()).ToArray();
|
||||
}
|
||||
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -276,8 +277,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
ParameterType = modelType
|
||||
};
|
||||
|
||||
Action<HttpRequest> action = r => r.Headers.Add("CustomParameter", new[] { value });
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(action);
|
||||
Action<HttpRequest> action = r => r.Headers.Add("CustomParameter", new[] { expectedAttemptedValue });
|
||||
var testContext = GetModelBindingTestContext(action);
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||
|
||||
// Do not add any headers.
|
||||
var httpContext = testContext.HttpContext;
|
||||
|
|
@ -301,8 +303,221 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Equal("CustomParameter", entry.Key);
|
||||
Assert.Empty(entry.Value.Errors);
|
||||
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);
|
||||
}
|
||||
|
||||
[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(
|
||||
IModelMetadataProvider metadataProvider,
|
||||
IModelBinderProvider binderProvider = null,
|
||||
|
|
@ -77,7 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
return new ParameterBinder(
|
||||
metadataProvider,
|
||||
GetModelBinderFactory(metadataProvider, options),
|
||||
new ModelBinderFactory(metadataProvider, options, services),
|
||||
new CompositeModelValidatorProvider(GetModelValidatorProviders(options)),
|
||||
NullLoggerFactory.Instance);
|
||||
}
|
||||
|
|
@ -130,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
return httpContext;
|
||||
}
|
||||
|
||||
private static IServiceProvider GetServices(Action<MvcOptions> updateOptions = null)
|
||||
public static IServiceProvider GetServices(Action<MvcOptions> updateOptions = null)
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddAuthorization();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
|||
|
||||
// Assert
|
||||
Assert.False(mvcOptions.AllowCombiningAuthorizeFilters);
|
||||
Assert.False(mvcOptions.AllowBindingHeaderValuesToNonStringModelTypes);
|
||||
Assert.False(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
||||
Assert.Equal(InputFormatterExceptionPolicy.AllExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
||||
Assert.False(jsonOptions.AllowInputFormatterExceptionMessages);
|
||||
|
|
@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
|||
|
||||
// Assert
|
||||
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
|
||||
Assert.True(mvcOptions.AllowBindingHeaderValuesToNonStringModelTypes);
|
||||
Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
||||
Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
||||
Assert.True(jsonOptions.AllowInputFormatterExceptionMessages);
|
||||
|
|
@ -80,6 +82,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
|||
|
||||
// Assert
|
||||
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
|
||||
Assert.True(mvcOptions.AllowBindingHeaderValuesToNonStringModelTypes);
|
||||
Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType);
|
||||
Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy);
|
||||
Assert.True(jsonOptions.AllowInputFormatterExceptionMessages);
|
||||
|
|
|
|||
|
|
@ -3,15 +3,24 @@
|
|||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
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)
|
||||
{
|
||||
return Create(null, providers);
|
||||
|
|
@ -58,14 +67,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
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();
|
||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||
services.AddSingleton(options);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue