Introducing ModelBinderFactory

This change separates model binding into IModelBinderProvider (decides
which binder to use) and IModelBinder (does binding). The
IModelBinderFactory is a new services with coordinates the creation of
model binders.
This commit is contained in:
Ryan Nowak 2016-03-16 14:38:36 -07:00
parent 7d43851952
commit fb81a5e11e
109 changed files with 3271 additions and 2032 deletions

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// Creates <see cref="IModelBinder"/> instances. Register <see cref="IModelBinderProvider"/>
/// instances in <c>MvcOptions</c>.
/// </summary>
public interface IModelBinderProvider
{
/// <summary>
/// Creates a <see cref="IModelBinder"/> based on <see cref="ModelBinderProviderContext"/>.
/// </summary>
/// <param name="context">The <see cref="ModelBinderProviderContext"/>.</param>
/// <returns>An <see cref="IModelBinder"/>.</returns>
IModelBinder GetBinder(ModelBinderProviderContext context);
}
}

View File

@ -3,13 +3,14 @@
using System; using System;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{ {
/// <summary> /// <summary>
/// A key type which identifies a <see cref="ModelMetadata"/>. /// A key type which identifies a <see cref="ModelMetadata"/>.
/// </summary> /// </summary>
public struct ModelMetadataIdentity public struct ModelMetadataIdentity : IEquatable<ModelMetadataIdentity>
{ {
/// <summary> /// <summary>
/// Creates a <see cref="ModelMetadataIdentity"/> for the provided model <see cref="Type"/>. /// Creates a <see cref="ModelMetadataIdentity"/> for the provided model <see cref="Type"/>.
@ -98,5 +99,31 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// the current instance represents a type. /// the current instance represents a type.
/// </summary> /// </summary>
public string Name { get; private set; } public string Name { get; private set; }
/// <inheritdoc />
public bool Equals(ModelMetadataIdentity other)
{
return
ContainerType == other.ContainerType &&
ModelType == other.ModelType &&
Name == other.Name;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
var other = obj as ModelMetadataIdentity?;
return other.HasValue && Equals(other.Value);
}
/// <inheritdoc />
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(ContainerType);
hash.Add(ModelType);
hash.Add(Name, StringComparer.Ordinal);
return hash;
}
} }
} }

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// A context object for <see cref="IModelBinderProvider.GetBinder"/>.
/// </summary>
public abstract class ModelBinderProviderContext
{
/// <summary>
/// Creates an <see cref="IModelBinder"/> for the given <paramref name="metadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="ModelMetadata"/> for the model.</param>
/// <returns>An <see cref="IModelBinder"/>.</returns>
public abstract IModelBinder CreateBinder(ModelMetadata metadata);
/// <summary>
/// Gets the <see cref="BindingInfo"/>. May be <c>null</c>.
/// </summary>
public abstract BindingInfo BindingInfo { get; }
/// <summary>
/// Gets the <see cref="ModelMetadata"/>.
/// </summary>
public abstract ModelMetadata Metadata { get; }
/// <summary>
/// Gets the <see cref="IModelMetadataProvider"/>.
/// </summary>
public abstract IModelMetadataProvider MetadataProvider { get; }
}
}

View File

@ -16,28 +16,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary> /// </summary>
public abstract string BinderModelName { get; set; } public abstract string BinderModelName { get; set; }
/// <summary>
/// Gets the <see cref="Type"/> of an <see cref="IModelBinder"/> associated with the
/// <see cref="Model"/>.
/// </summary>
public abstract Type BinderType { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value which represents the <see cref="ModelBinding.BindingSource"/> associated with the /// Gets or sets a value which represents the <see cref="ModelBinding.BindingSource"/> associated with the
/// <see cref="Model"/>. /// <see cref="Model"/>.
/// </summary> /// </summary>
public abstract BindingSource BindingSource { get; set; } public abstract BindingSource BindingSource { get; set; }
/// <summary>
/// Gets or sets a value that indicates whether the binder should use an empty prefix to look up
/// values in <see cref="IValueProvider"/> when no values are found using the <see cref="ModelName"/> prefix.
/// </summary>
/// <remarks>
/// Passed into the model binding system. Should not be <c>true</c> when <see cref="IsTopLevelObject"/> is
/// <c>false</c>.
/// </remarks>
public abstract bool FallbackToEmptyPrefix { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the current field being bound. /// Gets or sets the name of the current field being bound.
/// </summary> /// </summary>

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// A metadata representation of a model type, property or parameter. /// A metadata representation of a model type, property or parameter.
/// </summary> /// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")] [DebuggerDisplay("{DebuggerToString(),nq}")]
public abstract class ModelMetadata public abstract class ModelMetadata : IEquatable<ModelMetadata>
{ {
/// <summary> /// <summary>
/// The default value of <see cref="ModelMetadata.Order"/>. /// The default value of <see cref="ModelMetadata.Order"/>.
@ -378,6 +378,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return DisplayName ?? PropertyName ?? ModelType.Name; return DisplayName ?? PropertyName ?? ModelType.Name;
} }
/// <inheritdoc />
public bool Equals(ModelMetadata other)
{
if (object.ReferenceEquals(this, other))
{
return true;
}
if (other == null)
{
return false;
}
else
{
return Identity.Equals(other.Identity);
}
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return base.Equals(obj as ModelMetadata);
}
/// <inheritdoc />
public override int GetHashCode()
{
return Identity.GetHashCode();
}
private void InitializeTypeInformation() private void InitializeTypeInformation()
{ {
Debug.Assert(ModelType != null); Debug.Assert(ModelType != null);

View File

@ -35,11 +35,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary> /// </summary>
public IValueProvider ValueProvider { get; set; } public IValueProvider ValueProvider { get; set; }
/// <summary>
/// Gets or sets the <see cref="IModelBinder"/> associated with this context.
/// </summary>
public IModelBinder ModelBinder { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="IModelMetadataProvider"/> associated with this context. /// Gets or sets the <see cref="IModelMetadataProvider"/> associated with this context.
/// </summary> /// </summary>

View File

@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc
{ {
private ControllerContext _controllerContext; private ControllerContext _controllerContext;
private IModelMetadataProvider _metadataProvider; private IModelMetadataProvider _metadataProvider;
private IModelBinderFactory _modelBinderFactory;
private IObjectModelValidator _objectValidator; private IObjectModelValidator _objectValidator;
private IUrlHelper _url; private IUrlHelper _url;
@ -141,6 +142,31 @@ namespace Microsoft.AspNetCore.Mvc
} }
} }
/// <summary>
/// Gets or sets the <see cref="IModelBinderFactory"/>.
/// </summary>
public IModelBinderFactory ModelBinderFactory
{
get
{
if (_modelBinderFactory == null)
{
_modelBinderFactory = HttpContext?.RequestServices?.GetRequiredService<IModelBinderFactory>();
}
return _modelBinderFactory;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_modelBinderFactory = value;
}
}
/// <summary> /// <summary>
/// Gets or sets the <see cref="IUrlHelper"/>. /// Gets or sets the <see cref="IUrlHelper"/>.
/// </summary> /// </summary>
@ -1139,7 +1165,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix, prefix,
ControllerContext, ControllerContext,
MetadataProvider, MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders), ModelBinderFactory,
valueProvider, valueProvider,
ControllerContext.InputFormatters, ControllerContext.InputFormatters,
ObjectValidator, ObjectValidator,
@ -1179,7 +1205,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix, prefix,
ControllerContext, ControllerContext,
MetadataProvider, MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders), ModelBinderFactory,
new CompositeValueProvider(ControllerContext.ValueProviders), new CompositeValueProvider(ControllerContext.ValueProviders),
ControllerContext.InputFormatters, ControllerContext.InputFormatters,
ObjectValidator, ObjectValidator,
@ -1219,7 +1245,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix, prefix,
ControllerContext, ControllerContext,
MetadataProvider, MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders), ModelBinderFactory,
new CompositeValueProvider(ControllerContext.ValueProviders), new CompositeValueProvider(ControllerContext.ValueProviders),
ControllerContext.InputFormatters, ControllerContext.InputFormatters,
ObjectValidator, ObjectValidator,
@ -1267,7 +1293,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix, prefix,
ControllerContext, ControllerContext,
MetadataProvider, MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders), ModelBinderFactory,
valueProvider, valueProvider,
ControllerContext.InputFormatters, ControllerContext.InputFormatters,
ObjectValidator, ObjectValidator,
@ -1314,7 +1340,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix, prefix,
ControllerContext, ControllerContext,
MetadataProvider, MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders), ModelBinderFactory,
valueProvider, valueProvider,
ControllerContext.InputFormatters, ControllerContext.InputFormatters,
ObjectValidator, ObjectValidator,
@ -1353,7 +1379,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix, prefix,
ControllerContext, ControllerContext,
MetadataProvider, MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders), ModelBinderFactory,
new CompositeValueProvider(ControllerContext.ValueProviders), new CompositeValueProvider(ControllerContext.ValueProviders),
ControllerContext.InputFormatters, ControllerContext.InputFormatters,
ObjectValidator, ObjectValidator,
@ -1405,7 +1431,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix, prefix,
ControllerContext, ControllerContext,
MetadataProvider, MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders), ModelBinderFactory,
valueProvider, valueProvider,
ControllerContext.InputFormatters, ControllerContext.InputFormatters,
ObjectValidator, ObjectValidator,

View File

@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Mvc
public class ControllerContext : ActionContext public class ControllerContext : ActionContext
{ {
private FormatterCollection<IInputFormatter> _inputFormatters; private FormatterCollection<IInputFormatter> _inputFormatters;
private IList<IModelBinder> _modelBinders;
private IList<IModelValidatorProvider> _validatorProviders; private IList<IModelValidatorProvider> _validatorProviders;
private IList<IValueProvider> _valueProviders; private IList<IValueProvider> _valueProviders;
@ -81,31 +80,6 @@ namespace Microsoft.AspNetCore.Mvc
} }
} }
/// <summary>
/// Gets or sets the list of <see cref="IModelBinder"/> instances for the current request.
/// </summary>
public virtual IList<IModelBinder> ModelBinders
{
get
{
if (_modelBinders == null)
{
_modelBinders = new List<IModelBinder>();
}
return _modelBinders;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_modelBinders = value;
}
}
/// <summary> /// <summary>
/// Gets or sets the list of <see cref="IModelValidatorProvider"/> instances for the current request. /// Gets or sets the list of <see cref="IModelValidatorProvider"/> instances for the current request.
/// </summary> /// </summary>

View File

@ -143,6 +143,7 @@ namespace Microsoft.Extensions.DependencyInjection
var options = serviceProvider.GetRequiredService<IOptions<MvcOptions>>().Value; var options = serviceProvider.GetRequiredService<IOptions<MvcOptions>>().Value;
return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders); return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders);
})); }));
services.TryAddSingleton<IModelBinderFactory, ModelBinderFactory>();
services.TryAddSingleton<IObjectModelValidator, DefaultObjectValidator>(); services.TryAddSingleton<IObjectModelValidator, DefaultObjectValidator>();
services.TryAddSingleton<ValidatorCache>(); services.TryAddSingleton<ValidatorCache>();
services.TryAddSingleton<ClientValidatorCache>(); services.TryAddSingleton<ClientValidatorCache>();

View File

@ -32,7 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ControllerActionDescriptor descriptor, ControllerActionDescriptor descriptor,
IReadOnlyList<IInputFormatter> inputFormatters, IReadOnlyList<IInputFormatter> inputFormatters,
IControllerActionArgumentBinder argumentBinder, IControllerActionArgumentBinder argumentBinder,
IReadOnlyList<IModelBinder> modelBinders,
IReadOnlyList<IModelValidatorProvider> modelValidatorProviders, IReadOnlyList<IModelValidatorProvider> modelValidatorProviders,
IReadOnlyList<IValueProviderFactory> valueProviderFactories, IReadOnlyList<IValueProviderFactory> valueProviderFactories,
ILogger logger, ILogger logger,
@ -42,7 +41,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
actionContext, actionContext,
controllerActionInvokerCache, controllerActionInvokerCache,
inputFormatters, inputFormatters,
modelBinders,
modelValidatorProviders, modelValidatorProviders,
valueProviderFactories, valueProviderFactories,
logger, logger,

View File

@ -21,7 +21,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private readonly IControllerFactory _controllerFactory; private readonly IControllerFactory _controllerFactory;
private readonly ControllerActionInvokerCache _controllerActionInvokerCache; private readonly ControllerActionInvokerCache _controllerActionInvokerCache;
private readonly IReadOnlyList<IInputFormatter> _inputFormatters; private readonly IReadOnlyList<IInputFormatter> _inputFormatters;
private readonly IReadOnlyList<IModelBinder> _modelBinders;
private readonly IReadOnlyList<IModelValidatorProvider> _modelValidatorProviders; private readonly IReadOnlyList<IModelValidatorProvider> _modelValidatorProviders;
private readonly IReadOnlyList<IValueProviderFactory> _valueProviderFactories; private readonly IReadOnlyList<IValueProviderFactory> _valueProviderFactories;
private readonly int _maxModelValidationErrors; private readonly int _maxModelValidationErrors;
@ -40,7 +39,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
_controllerActionInvokerCache = controllerActionInvokerCache; _controllerActionInvokerCache = controllerActionInvokerCache;
_argumentBinder = argumentBinder; _argumentBinder = argumentBinder;
_inputFormatters = optionsAccessor.Value.InputFormatters.ToArray(); _inputFormatters = optionsAccessor.Value.InputFormatters.ToArray();
_modelBinders = optionsAccessor.Value.ModelBinders.ToArray();
_modelValidatorProviders = optionsAccessor.Value.ModelValidatorProviders.ToArray(); _modelValidatorProviders = optionsAccessor.Value.ModelValidatorProviders.ToArray();
_valueProviderFactories = optionsAccessor.Value.ValueProviderFactories.ToArray(); _valueProviderFactories = optionsAccessor.Value.ValueProviderFactories.ToArray();
_maxModelValidationErrors = optionsAccessor.Value.MaxModelValidationErrors; _maxModelValidationErrors = optionsAccessor.Value.MaxModelValidationErrors;
@ -72,7 +70,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
actionDescriptor, actionDescriptor,
_inputFormatters, _inputFormatters,
_argumentBinder, _argumentBinder,
_modelBinders,
_modelValidatorProviders, _modelValidatorProviders,
_valueProviderFactories, _valueProviderFactories,
_logger, _logger,

View File

@ -24,14 +24,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
typeof(ControllerArgumentBinder).GetTypeInfo().GetDeclaredMethod( typeof(ControllerArgumentBinder).GetTypeInfo().GetDeclaredMethod(
nameof(CallPropertyAddRange)); nameof(CallPropertyAddRange));
private readonly IModelBinderFactory _modelBinderFactory;
private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IObjectModelValidator _validator; private readonly IObjectModelValidator _validator;
public ControllerArgumentBinder( public ControllerArgumentBinder(
IModelMetadataProvider modelMetadataProvider, IModelMetadataProvider modelMetadataProvider,
IModelBinderFactory modelBinderFactory,
IObjectModelValidator validator) IObjectModelValidator validator)
{ {
_modelMetadataProvider = modelMetadataProvider; _modelMetadataProvider = modelMetadataProvider;
_modelBinderFactory = modelBinderFactory;
_validator = validator; _validator = validator;
} }
@ -118,7 +121,31 @@ namespace Microsoft.AspNetCore.Mvc.Internal
parameter.BindingInfo, parameter.BindingInfo,
parameter.Name); parameter.Name);
await operationContext.ModelBinder.BindModelAsync(modelBindingContext); if (parameter.BindingInfo?.BinderModelName != null)
{
// The name was set explicitly, always use that as the prefix.
modelBindingContext.ModelName = parameter.BindingInfo.BinderModelName;
}
else if (modelBindingContext.ValueProvider.ContainsPrefix(parameter.Name))
{
// We have a match for the parameter name, use that as that prefix.
modelBindingContext.ModelName = parameter.Name;
}
else
{
// No match, fallback to empty string as the prefix.
modelBindingContext.ModelName = string.Empty;
}
var binder = _modelBinderFactory.CreateBinder(new ModelBinderFactoryContext()
{
BindingInfo = parameter.BindingInfo,
Metadata = metadata,
CacheToken = parameter,
});
await binder.BindModelAsync(modelBindingContext);
var modelBindingResult = modelBindingContext.Result; var modelBindingResult = modelBindingContext.Result;
if (modelBindingResult != null && modelBindingResult.Value.IsModelSet) if (modelBindingResult != null && modelBindingResult.Value.IsModelSet)
{ {
@ -241,7 +268,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
ActionContext = context, ActionContext = context,
InputFormatters = context.InputFormatters, InputFormatters = context.InputFormatters,
ModelBinder = new CompositeModelBinder(context.ModelBinders),
ValidatorProvider = new CompositeModelValidatorProvider(context.ValidatorProviders), ValidatorProvider = new CompositeModelValidatorProvider(context.ValidatorProviders),
MetadataProvider = _modelMetadataProvider, MetadataProvider = _modelMetadataProvider,
ValueProvider = new CompositeValueProvider(context.ValueProviders), ValueProvider = new CompositeValueProvider(context.ValueProviders),

View File

@ -12,6 +12,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary> /// </summary>
public class DefaultModelBindingContext : ModelBindingContext public class DefaultModelBindingContext : ModelBindingContext
{ {
private OperationBindingContext _operationBindingContext;
private State _state; private State _state;
private readonly Stack<State> _stack = new Stack<State>(); private readonly Stack<State> _stack = new Stack<State>();
@ -57,17 +59,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyPredicateProvider = var propertyPredicateProvider =
bindingInfo?.PropertyBindingPredicateProvider ?? metadata.PropertyBindingPredicateProvider; bindingInfo?.PropertyBindingPredicateProvider ?? metadata.PropertyBindingPredicateProvider;
var valueProvider = operationBindingContext.ValueProvider;
var bindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource;
if (bindingSource != null && !bindingSource.IsGreedy)
{
valueProvider = FilterValueProvider(operationBindingContext.ValueProvider, bindingSource);
}
return new DefaultModelBindingContext() return new DefaultModelBindingContext()
{ {
BinderModelName = binderModelName, BinderModelName = binderModelName,
BindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource, BindingSource = bindingSource,
BinderType = bindingInfo?.BinderType ?? metadata.BinderType,
PropertyFilter = propertyPredicateProvider?.PropertyFilter, PropertyFilter = propertyPredicateProvider?.PropertyFilter,
// We only support fallback to empty prefix in cases where the model name is inferred from
// the parameter or property being bound.
FallbackToEmptyPrefix = binderModelName == null,
// Because this is the top-level context, FieldName and ModelName should be the same. // Because this is the top-level context, FieldName and ModelName should be the same.
FieldName = binderModelName ?? modelName, FieldName = binderModelName ?? modelName,
ModelName = binderModelName ?? modelName, ModelName = binderModelName ?? modelName,
@ -76,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ModelMetadata = metadata, ModelMetadata = metadata,
ModelState = operationBindingContext.ActionContext.ModelState, ModelState = operationBindingContext.ActionContext.ModelState,
OperationBindingContext = operationBindingContext, OperationBindingContext = operationBindingContext,
ValueProvider = operationBindingContext.ValueProvider, ValueProvider = valueProvider,
ValidationState = new ValidationStateDictionary(), ValidationState = new ValidationStateDictionary(),
}; };
@ -106,16 +110,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var scope = EnterNestedScope(); var scope = EnterNestedScope();
// Only filter if the new BindingSource affects the value providers. Otherwise we want
// to preserve the currrent state.
if (modelMetadata.BindingSource != null && !modelMetadata.BindingSource.IsGreedy)
{
ValueProvider = FilterValueProvider(_operationBindingContext.ValueProvider, modelMetadata.BindingSource);
}
Model = model; Model = model;
ModelMetadata = modelMetadata; ModelMetadata = modelMetadata;
ModelName = modelName; ModelName = modelName;
FieldName = fieldName; FieldName = fieldName;
BinderModelName = modelMetadata.BinderModelName; BinderModelName = modelMetadata.BinderModelName;
BinderType = modelMetadata.BinderType;
BindingSource = modelMetadata.BindingSource; BindingSource = modelMetadata.BindingSource;
PropertyFilter = modelMetadata.PropertyBindingPredicateProvider?.PropertyFilter; PropertyFilter = modelMetadata.PropertyBindingPredicateProvider?.PropertyFilter;
FallbackToEmptyPrefix = false;
IsTopLevelObject = false; IsTopLevelObject = false;
return scope; return scope;
@ -140,14 +149,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <inheritdoc /> /// <inheritdoc />
public override OperationBindingContext OperationBindingContext public override OperationBindingContext OperationBindingContext
{ {
get { return _state.OperationBindingContext; } get { return _operationBindingContext; }
set set
{ {
if (value == null) if (value == null)
{ {
throw new ArgumentNullException(nameof(value)); throw new ArgumentNullException(nameof(value));
} }
_state.OperationBindingContext = value; _operationBindingContext = value;
} }
} }
@ -231,20 +240,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
set { _state.BindingSource = value; } set { _state.BindingSource = value; }
} }
/// <inheritdoc />
public override Type BinderType
{
get { return _state.BinderType; }
set { _state.BinderType = value; }
}
/// <inheritdoc />
public override bool FallbackToEmptyPrefix
{
get { return _state.FallbackToEmptyPrefix; }
set { _state.FallbackToEmptyPrefix = value; }
}
/// <inheritdoc /> /// <inheritdoc />
public override bool IsTopLevelObject public override bool IsTopLevelObject
{ {
@ -298,9 +293,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
} }
private static IValueProvider FilterValueProvider(IValueProvider valueProvider, BindingSource bindingSource)
{
if (bindingSource == null || bindingSource.IsGreedy)
{
return valueProvider;
}
var bindingSourceValueProvider = valueProvider as IBindingSourceValueProvider;
if (bindingSourceValueProvider == null)
{
return valueProvider;
}
return bindingSourceValueProvider.Filter(bindingSource) ?? new CompositeValueProvider();
}
private struct State private struct State
{ {
public OperationBindingContext OperationBindingContext;
public string FieldName; public string FieldName;
public object Model; public object Model;
public ModelMetadata ModelMetadata; public ModelMetadata ModelMetadata;
@ -313,8 +323,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public string BinderModelName; public string BinderModelName;
public BindingSource BindingSource; public BindingSource BindingSource;
public Type BinderType;
public bool FallbackToEmptyPrefix;
public bool IsTopLevelObject; public bool IsTopLevelObject;
public ModelBindingResult? Result; public ModelBindingResult? Result;

View File

@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
private readonly ControllerActionInvokerCache _controllerActionInvokerCache; private readonly ControllerActionInvokerCache _controllerActionInvokerCache;
private readonly IReadOnlyList<IInputFormatter> _inputFormatters; private readonly IReadOnlyList<IInputFormatter> _inputFormatters;
private readonly IReadOnlyList<IModelBinder> _modelBinders;
private readonly IReadOnlyList<IModelValidatorProvider> _modelValidatorProviders; private readonly IReadOnlyList<IModelValidatorProvider> _modelValidatorProviders;
private readonly IReadOnlyList<IValueProviderFactory> _valueProviderFactories; private readonly IReadOnlyList<IValueProviderFactory> _valueProviderFactories;
private readonly DiagnosticSource _diagnosticSource; private readonly DiagnosticSource _diagnosticSource;
@ -47,7 +46,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ActionContext actionContext, ActionContext actionContext,
ControllerActionInvokerCache controllerActionInvokerCache, ControllerActionInvokerCache controllerActionInvokerCache,
IReadOnlyList<IInputFormatter> inputFormatters, IReadOnlyList<IInputFormatter> inputFormatters,
IReadOnlyList<IModelBinder> modelBinders,
IReadOnlyList<IModelValidatorProvider> modelValidatorProviders, IReadOnlyList<IModelValidatorProvider> modelValidatorProviders,
IReadOnlyList<IValueProviderFactory> valueProviderFactories, IReadOnlyList<IValueProviderFactory> valueProviderFactories,
ILogger logger, ILogger logger,
@ -69,11 +67,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(inputFormatters)); throw new ArgumentNullException(nameof(inputFormatters));
} }
if (modelBinders == null)
{
throw new ArgumentNullException(nameof(modelBinders));
}
if (modelValidatorProviders == null) if (modelValidatorProviders == null)
{ {
throw new ArgumentNullException(nameof(modelValidatorProviders)); throw new ArgumentNullException(nameof(modelValidatorProviders));
@ -98,7 +91,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
_controllerActionInvokerCache = controllerActionInvokerCache; _controllerActionInvokerCache = controllerActionInvokerCache;
_inputFormatters = inputFormatters; _inputFormatters = inputFormatters;
_modelBinders = modelBinders;
_modelValidatorProviders = modelValidatorProviders; _modelValidatorProviders = modelValidatorProviders;
_valueProviderFactories = valueProviderFactories; _valueProviderFactories = valueProviderFactories;
Logger = logger; Logger = logger;
@ -356,7 +348,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// binding. // binding.
Context.InputFormatters = new FormatterCollection<IInputFormatter>( Context.InputFormatters = new FormatterCollection<IInputFormatter>(
new CopyOnWriteList<IInputFormatter>(_inputFormatters)); new CopyOnWriteList<IInputFormatter>(_inputFormatters));
Context.ModelBinders = new CopyOnWriteList<IModelBinder>(_modelBinders);
Context.ValidatorProviders = new CopyOnWriteList<IModelValidatorProvider>(_modelValidatorProviders); Context.ValidatorProviders = new CopyOnWriteList<IModelValidatorProvider>(_modelValidatorProviders);
var valueProviders = new List<IValueProvider>(); var valueProviders = new List<IValueProvider>();

View File

@ -6,9 +6,8 @@ using System.Threading;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -37,17 +36,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
messageProvider.ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber; messageProvider.ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber;
// Set up ModelBinding // Set up ModelBinding
options.ModelBinders.Add(new BinderTypeBasedModelBinder()); options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
options.ModelBinders.Add(new ServicesModelBinder()); options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
options.ModelBinders.Add(new BodyModelBinder(readerFactory)); options.ModelBinderProviders.Add(new BodyModelBinderProvider(readerFactory));
options.ModelBinders.Add(new HeaderModelBinder()); options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
options.ModelBinders.Add(new SimpleTypeModelBinder()); options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
options.ModelBinders.Add(new CancellationTokenModelBinder()); options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
options.ModelBinders.Add(new ByteArrayModelBinder()); options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
options.ModelBinders.Add(new FormFileModelBinder()); options.ModelBinderProviders.Add(new FormFileModelBinderProvider());
options.ModelBinders.Add(new FormCollectionModelBinder()); options.ModelBinderProviders.Add(new FormCollectionModelBinderProvider());
options.ModelBinders.Add(new GenericModelBinder()); options.ModelBinderProviders.Add(new KeyValuePairModelBinderProvider());
options.ModelBinders.Add(new MutableObjectModelBinder()); options.ModelBinderProviders.Add(new DictionaryModelBinderProvider());
options.ModelBinderProviders.Add(new ArrayModelBinderProvider());
options.ModelBinderProviders.Add(new CollectionModelBinderProvider());
options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
// Set up filters // Set up filters
options.Filters.Add(new UnsupportedContentTypeFilter()); options.Filters.Add(new UnsupportedContentTypeFilter());

View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Internal
{
// Used as a placeholder to break cycles while building a tree of model binders in ModelBinderFactory.
//
// When a cycle is detected by a call to Create(...), we create an instance of this class and return it
// to break the cycle. Later when the 'real' binder is created we set Inner to point to that.
public class PlaceholderBinder : IModelBinder
{
public IModelBinder Inner { get; set; }
public Task BindModelAsync(ModelBindingContext bindingContext)
{
return Inner.BindModelAsync(bindingContext);
}
}
}

View File

@ -1,71 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// An <see cref="IModelBinder"/> which can bind a model based on the value of
/// <see cref="ModelMetadata.BinderType"/>. The supplied <see cref="IModelBinder"/>
/// type will be used to bind the model.
/// </summary>
public class BinderTypeBasedModelBinder : IModelBinder
{
private readonly Func<Type, ObjectFactory> _createFactory =
(t) => ActivatorUtilities.CreateFactory(t, Type.EmptyTypes);
private readonly ConcurrentDictionary<Type, ObjectFactory> _typeActivatorCache =
new ConcurrentDictionary<Type, ObjectFactory>();
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
if (bindingContext.BinderType == null)
{
// Return null so that we are able to continue with the default set of model binders,
// if there is no specific model binder provided.
return TaskCache.CompletedTask;
}
return BindModelCoreAsync(bindingContext);
}
private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
{
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var createFactory = _typeActivatorCache.GetOrAdd(bindingContext.BinderType, _createFactory);
var instance = createFactory(requestServices, arguments: null);
var modelBinder = instance as IModelBinder;
if (modelBinder == null)
{
throw new InvalidOperationException(
Resources.FormatBinderType_MustBeIModelBinder(
bindingContext.BinderType.FullName,
typeof(IModelBinder).FullName));
}
await modelBinder.BindModelAsync(bindingContext);
// A model binder was specified by metadata and this binder handles all such cases.
// Always tell the model binding system to skip other model binders i.e. return non-null.
if (bindingContext.Result == null)
{
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
}
}
}
}

View File

@ -5,10 +5,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// <see cref="IModelBinder"/> implementation for binding array values. /// <see cref="IModelBinder"/> implementation for binding array values.
@ -16,20 +14,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <typeparam name="TElement">Type of elements in the array.</typeparam> /// <typeparam name="TElement">Type of elements in the array.</typeparam>
public class ArrayModelBinder<TElement> : CollectionModelBinder<TElement> public class ArrayModelBinder<TElement> : CollectionModelBinder<TElement>
{ {
/// <inheritdoc /> /// <summary>
public override Task BindModelAsync(ModelBindingContext bindingContext) /// Creates a new <see cref="ArrayModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param>
public ArrayModelBinder(IModelBinder elementBinder)
: base(elementBinder)
{ {
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
if (bindingContext.ModelMetadata.IsReadOnly)
{
return TaskCache.CompletedTask;
}
return base.BindModelAsync(bindingContext);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -62,8 +55,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
throw new ArgumentNullException(nameof(target)); throw new ArgumentNullException(nameof(target));
} }
// Do not attempt to copy values into an array because an array's length is immutable. This choice is also // Do not attempt to copy values into an array because an array's length is immutable. This choice is also
// consistent with MutableObjectModelBinder's handling of a read-only array property. // consistent with our handling of a read-only array property.
} }
} }
} }

View File

@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for arrays.
/// </summary>
public class ArrayModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// We don't support binding readonly properties of arrays because we can't resize the
// existing value.
if (context.Metadata.ModelType.IsArray && !context.Metadata.IsReadOnly)
{
var elementType = context.Metadata.ElementMetadata.ModelType;
var elementBinder = context.CreateBinder(context.Metadata.ElementMetadata);
var binderType = typeof(ArrayModelBinder<>).MakeGenericType(elementType);
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder);
}
return null;
}
}
}

View File

@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinder"/> for models which specify an <see cref="IModelBinder"/> using
/// <see cref="BindingInfo.BinderType"/>.
/// </summary>
public class BinderTypeModelBinder : IModelBinder
{
private readonly ObjectFactory _factory;
/// <summary>
/// Creates a new <see cref="BinderTypeModelBinder"/>.
/// </summary>
/// <param name="binderType">The <see cref="Type"/> of the <see cref="IModelBinder"/>.</param>
public BinderTypeModelBinder(Type binderType)
{
if (binderType == null)
{
throw new ArgumentNullException(nameof(binderType));
}
if (!typeof(IModelBinder).GetTypeInfo().IsAssignableFrom(binderType.GetTypeInfo()))
{
throw new ArgumentException(
Resources.FormatBinderType_MustBeIModelBinder(
binderType.FullName,
typeof(IModelBinder).FullName),
nameof(binderType));
}
_factory = ActivatorUtilities.CreateFactory(binderType, Type.EmptyTypes);
}
/// <inheritdoc />
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var binder = (IModelBinder)_factory(requestServices, arguments: null);
await binder.BindModelAsync(bindingContext);
if (bindingContext.Result == null)
{
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
}
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for models which specify an <see cref="IModelBinder"/>
/// using <see cref="BindingInfo.BinderType"/>.
/// </summary>
public class BinderTypeModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.BindingInfo?.BinderType != null)
{
return new BinderTypeModelBinder(context.BindingInfo.BinderType);
}
return null;
}
}
}

View File

@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// An <see cref="IModelBinder"/> which binds models from the request body using an <see cref="IInputFormatter"/> /// An <see cref="IModelBinder"/> which binds models from the request body using an <see cref="IInputFormatter"/>
@ -33,37 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
/// <inheritdoc /> /// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext) public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null ||
!allowedBindingSource.CanAcceptDataFrom(BindingSource.Body))
{
// Formatters are opt-in. This model either didn't specify [FromBody] or specified something
// incompatible so let other binders run.
return TaskCache.CompletedTask;
}
return BindModelCoreAsync(bindingContext);
}
/// <summary>
/// Attempts to bind the model using formatters.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <returns>
/// A <see cref="Task{ModelBindingResult}"/> which when completed returns a <see cref="ModelBindingResult"/>.
/// </returns>
private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
{ {
if (bindingContext == null) if (bindingContext == null)
{ {

View File

@ -0,0 +1,47 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for deserializing the request body using a formatter.
/// </summary>
public class BodyModelBinderProvider : IModelBinderProvider
{
private readonly IHttpRequestStreamReaderFactory _readerFactory;
/// <summary>
/// Creates a new <see cref="BodyModelBinderProvider"/>.
/// </summary>
/// <param name="readerFactory">The <see cref="IHttpRequestStreamReaderFactory"/>.</param>
public BodyModelBinderProvider(IHttpRequestStreamReaderFactory readerFactory)
{
if (readerFactory == null)
{
throw new ArgumentNullException(nameof(readerFactory));
}
_readerFactory = readerFactory;
}
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.BindingInfo?.BindingSource != null &&
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body))
{
return new BodyModelBinder(_readerFactory);
}
return null;
}
}
}

View File

@ -5,10 +5,10 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// ModelBinder to bind Byte Arrays. /// ModelBinder to bind byte Arrays.
/// </summary> /// </summary>
public class ByteArrayModelBinder : IModelBinder public class ByteArrayModelBinder : IModelBinder
{ {
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext)); throw new ArgumentNullException(nameof(bindingContext));
} }
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
// Check if this binder applies.
if (bindingContext.ModelType != typeof(byte[]))
{
return TaskCache.CompletedTask;
}
// Check for missing data case 1: There was no <input ... /> element containing this data. // Check for missing data case 1: There was no <input ... /> element containing this data.
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None) if (valueProviderResult == ValueProviderResult.None)

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for binding base64 encoded byte arrays.
/// </summary>
public class ByteArrayModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(byte[]))
{
return new ByteArrayModelBinder();
}
return null;
}
}
}

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// <see cref="IModelBinder"/> implementation to bind models of type <see cref="CancellationToken"/>. /// <see cref="IModelBinder"/> implementation to bind models of type <see cref="CancellationToken"/>.
@ -22,16 +22,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext)); throw new ArgumentNullException(nameof(bindingContext));
} }
if (bindingContext.ModelType == typeof(CancellationToken)) // We need to force boxing now, so we can insert the same reference to the boxed CancellationToken
{ // in both the ValidationState and ModelBindingResult.
// We need to force boxing now, so we can insert the same reference to the boxed CancellationToken //
// in both the ValidationState and ModelBindingResult. // DO NOT simplify this code by removing the cast.
// var model = (object)bindingContext.OperationBindingContext.HttpContext.RequestAborted;
// DO NOT simplify this code by removing the cast. bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
var model = (object)bindingContext.OperationBindingContext.HttpContext.RequestAborted; bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
}
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for <see cref="CancellationToken"/>.
/// </summary>
public class CancellationTokenModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(CancellationToken))
{
return new CancellationTokenModelBinder();
}
return null;
}
}
}

View File

@ -12,7 +12,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// <see cref="IModelBinder"/> implementation for binding collection values. /// <see cref="IModelBinder"/> implementation for binding collection values.
@ -20,6 +20,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <typeparam name="TElement">Type of elements in the collection.</typeparam> /// <typeparam name="TElement">Type of elements in the collection.</typeparam>
public class CollectionModelBinder<TElement> : ICollectionModelBinder public class CollectionModelBinder<TElement> : ICollectionModelBinder
{ {
/// <summary>
/// Creates a new <see cref="CollectionModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">The <see cref="IModelBinder"/> for binding elements.</param>
public CollectionModelBinder(IModelBinder elementBinder)
{
if (elementBinder == null)
{
throw new ArgumentNullException(nameof(elementBinder));
}
ElementBinder = elementBinder;
}
/// <summary>
/// Gets the <see cref="IModelBinder"/> instances for binding collection elements.
/// </summary>
protected IModelBinder ElementBinder { get; }
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task BindModelAsync(ModelBindingContext bindingContext) public virtual async Task BindModelAsync(ModelBindingContext bindingContext)
{ {
@ -28,8 +47,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext)); throw new ArgumentNullException(nameof(bindingContext));
} }
ModelBindingHelper.ValidateBindingContext(bindingContext);
var model = bindingContext.Model; var model = bindingContext.Model;
if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
{ {
@ -163,7 +180,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelName: bindingContext.ModelName, modelName: bindingContext.ModelName,
model: null)) model: null))
{ {
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(bindingContext); await ElementBinder.BindModelAsync(bindingContext);
if (bindingContext.Result != null && bindingContext.Result.Value.IsModelSet) if (bindingContext.Result != null && bindingContext.Result.Value.IsModelSet)
{ {
@ -224,7 +241,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelName: fullChildName, modelName: fullChildName,
model: null)) model: null))
{ {
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(bindingContext); await ElementBinder.BindModelAsync(bindingContext);
result = bindingContext.Result; result = bindingContext.Result;
} }

View File

@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for <see cref="ICollection{T}"/>.
/// </summary>
public class CollectionModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var modelType = context.Metadata.ModelType;
// Arrays are handled by another binder.
if (modelType.IsArray)
{
return null;
}
// If the model type is ICollection<> then we can call its Add method, so we can always support it.
var collectionType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(ICollection<>));
if (collectionType != null)
{
var elementType = collectionType.GenericTypeArguments[0];
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(collectionType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder);
}
// If the model type is IEnumerable<> then we need to know if we can assign a List<> to it, since
// that's what we would create. (The cases handled here are IEnumerable<>, IReadOnlyColection<> and
// IReadOnlyList<>).
//
// We need to check IsReadOnly because we need to know if we can SET the property.
var enumerableType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(IEnumerable<>));
if (enumerableType != null && !context.Metadata.IsReadOnly)
{
var listType = typeof(List<>).MakeGenericType(enumerableType.GenericTypeArguments);
if (modelType.GetTypeInfo().IsAssignableFrom(listType.GetTypeInfo()))
{
var elementType = enumerableType.GenericTypeArguments[0];
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(enumerableType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder);
}
}
return null;
}
}
}

View File

@ -2,17 +2,36 @@
// 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.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// <see cref="IModelBinder"/> implementation for binding complex values. /// <see cref="IModelBinder"/> implementation for binding complex types.
/// </summary> /// </summary>
public class MutableObjectModelBinder : IModelBinder public class ComplexTypeModelBinder : IModelBinder
{ {
private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
/// <summary>
/// Creates a new <see cref="ComplexTypeModelBinder"/>.
/// </summary>
/// <param name="propertyBinders">
/// The <see cref="IDictionary{TKey, TValue}"/> of binders to use for binding properties.
/// </param>
public ComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
{
if (propertyBinders == null)
{
throw new ArgumentNullException(nameof(propertyBinders));
}
_propertyBinders = propertyBinders;
}
/// <inheritdoc /> /// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
@ -21,12 +40,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext)); throw new ArgumentNullException(nameof(bindingContext));
} }
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!CanBindType(bindingContext.ModelMetadata))
{
return TaskCache.CompletedTask;
}
if (!CanCreateModel(bindingContext)) if (!CanCreateModel(bindingContext))
{ {
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
@ -135,7 +148,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </returns> /// </returns>
protected virtual Task BindProperty(ModelBindingContext bindingContext) protected virtual Task BindProperty(ModelBindingContext bindingContext)
{ {
return bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(bindingContext); var binder = _propertyBinders[bindingContext.ModelMetadata];
return binder.BindModelAsync(bindingContext);
} }
internal bool CanCreateModel(ModelBindingContext bindingContext) internal bool CanCreateModel(ModelBindingContext bindingContext)
@ -284,22 +298,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return false; return false;
} }
private static bool CanBindType(ModelMetadata modelMetadata)
{
// Simple types cannot use this binder
if (!modelMetadata.IsComplexType)
{
return false;
}
if (modelMetadata.IsEnumerableType)
{
return false;
}
return true;
}
// Internal for tests // Internal for tests
internal static bool CanUpdatePropertyInternal(ModelMetadata propertyMetadata) internal static bool CanUpdatePropertyInternal(ModelMetadata propertyMetadata)
{ {

View File

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for complex types.
/// </summary>
public class ComplexTypeModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
foreach (var property in context.Metadata.Properties)
{
propertyBinders.Add(property, context.CreateBinder(property));
}
return new ComplexTypeModelBinder(propertyBinders);
}
return null;
}
}
}

View File

@ -12,7 +12,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// <see cref="IModelBinder"/> implementation for binding dictionary values. /// <see cref="IModelBinder"/> implementation for binding dictionary values.
@ -21,6 +21,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <typeparam name="TValue">Type of values in the dictionary.</typeparam> /// <typeparam name="TValue">Type of values in the dictionary.</typeparam>
public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>> public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>>
{ {
private readonly IModelBinder _valueBinder;
/// <summary>
/// Creates a new <see cref="DictionaryModelBinder{TKey, TValue}"/>.
/// </summary>
/// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param>
/// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param>
public DictionaryModelBinder(IModelBinder keyBinder, IModelBinder valueBinder)
: base(new KeyValuePairModelBinder<TKey, TValue>(keyBinder, valueBinder))
{
if (valueBinder == null)
{
throw new ArgumentNullException(nameof(valueBinder));
}
_valueBinder = valueBinder;
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task BindModelAsync(ModelBindingContext bindingContext) public override async Task BindModelAsync(ModelBindingContext bindingContext)
{ {
@ -66,8 +84,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var valueMetadata = metadataProvider.GetMetadataForType(typeof(TValue)); var valueMetadata = metadataProvider.GetMetadataForType(typeof(TValue));
var modelBinder = bindingContext.OperationBindingContext.ModelBinder;
var keyMappings = new Dictionary<string, TKey>(StringComparer.Ordinal); var keyMappings = new Dictionary<string, TKey>(StringComparer.Ordinal);
foreach (var kvp in keys) foreach (var kvp in keys)
{ {
@ -81,7 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelName: kvp.Value, modelName: kvp.Value,
model: null)) model: null))
{ {
await modelBinder.BindModelAsync(bindingContext); await _valueBinder.BindModelAsync(bindingContext);
var valueResult = bindingContext.Result; var valueResult = bindingContext.Result;

View File

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for binding <see cref="IDictionary{TKey, TValue}"/>.
/// </summary>
public class DictionaryModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var modelType = context.Metadata.ModelType;
var dictionaryType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(IDictionary<,>));
if (dictionaryType != null)
{
var keyType = dictionaryType.GenericTypeArguments[0];
var keyBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(keyType));
var valueType = dictionaryType.GenericTypeArguments[1];
var valueBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(valueType));
var binderType = typeof(DictionaryModelBinder<,>).MakeGenericType(dictionaryType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(binderType, keyBinder, valueBinder);
}
return null;
}
}
}

View File

@ -7,11 +7,10 @@ using System.Collections.Generic;
using System.Linq; 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.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// <see cref="IModelBinder"/> implementation to bind form values to <see cref="IFormCollection"/>. /// <see cref="IModelBinder"/> implementation to bind form values to <see cref="IFormCollection"/>.
@ -19,27 +18,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public class FormCollectionModelBinder : IModelBinder public class FormCollectionModelBinder : IModelBinder
{ {
/// <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));
} }
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
if (bindingContext.ModelType != typeof(IFormCollection))
{
return TaskCache.CompletedTask;
}
return BindModelCoreAsync(bindingContext);
}
private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
{
object model; object model;
var request = bindingContext.OperationBindingContext.HttpContext.Request; var request = bindingContext.OperationBindingContext.HttpContext.Request;
if (request.HasFormContentType) if (request.HasFormContentType)

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for <see cref="IFormCollection"/>.
/// </summary>
public class FormCollectionModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(IFormCollection))
{
return new FormCollectionModelBinder();
}
return null;
}
}
}

View File

@ -11,10 +11,9 @@ using System.Reflection;
#endif #endif
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.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// <see cref="IModelBinder"/> implementation to bind posted files to <see cref="IFormFile"/>. /// <see cref="IModelBinder"/> implementation to bind posted files to <see cref="IFormFile"/>.
@ -22,31 +21,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public class FormFileModelBinder : IModelBinder public class FormFileModelBinder : IModelBinder
{ {
/// <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));
} }
// This method is optimized to use cached tasks when possible and avoid allocating var createFileCollection =
// using Task.FromResult or async state machines. bindingContext.ModelType == typeof(IFormFileCollection) &&
var modelType = bindingContext.ModelType;
if (modelType != typeof(IFormFile) && !typeof(IEnumerable<IFormFile>).IsAssignableFrom(modelType))
{
// Not a type this model binder supports. Let other binders run.
return TaskCache.CompletedTask;
}
var createFileCollection = modelType == typeof(IFormFileCollection) &&
!bindingContext.ModelMetadata.IsReadOnly; !bindingContext.ModelMetadata.IsReadOnly;
if (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection<IFormFile>(bindingContext)) if (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection<IFormFile>(bindingContext))
{ {
// Silently fail and stop other model binders running if unable to create an instance or use the // Silently fail and stop other model binders running if unable to create an instance or use the
// current instance. // current instance.
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName); bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return TaskCache.CompletedTask; return;
} }
ICollection<IFormFile> postedFiles; ICollection<IFormFile> postedFiles;
@ -59,13 +49,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
postedFiles = ModelBindingHelper.GetCompatibleCollection<IFormFile>(bindingContext); postedFiles = ModelBindingHelper.GetCompatibleCollection<IFormFile>(bindingContext);
} }
return BindModelCoreAsync(bindingContext, postedFiles);
}
private async Task BindModelCoreAsync(ModelBindingContext bindingContext, ICollection<IFormFile> postedFiles)
{
Debug.Assert(postedFiles != null);
// If we're at the top level, then use the FieldName (parameter or property name). // If we're at the top level, then use the FieldName (parameter or property name).
// This handles the fact that there will be nothing in the ValueProviders for this parameter // This handles the fact that there will be nothing in the ValueProviders for this parameter
// and so we'll do the right thing even though we 'fell-back' to the empty prefix. // and so we'll do the right thing even though we 'fell-back' to the empty prefix.

View File

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for <see cref="IFormFile"/>, collections
/// of <see cref="IFormFile"/>, and <see cref="IFormFileCollection"/>.
/// </summary>
public class FormFileModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var modelType = context.Metadata.ModelType;
if (modelType == typeof(IFormFile) ||
modelType == typeof(IFormFileCollection) ||
typeof(IEnumerable<IFormFile>).GetTypeInfo().IsAssignableFrom(modelType.GetTypeInfo()))
{
return new FormFileModelBinder();
}
return null;
}
}
}

View File

@ -9,7 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// An <see cref="IModelBinder"/> which binds models from the request headers when a model /// An <see cref="IModelBinder"/> which binds models from the request headers when a model
@ -25,36 +25,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext)); throw new ArgumentNullException(nameof(bindingContext));
} }
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult or async state machines.
var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null ||
!allowedBindingSource.CanAcceptDataFrom(BindingSource.Header))
{
// Headers are opt-in. This model either didn't specify [FromHeader] or specified something
// incompatible so let other binders run.
return TaskCache.CompletedTask;
}
var request = bindingContext.OperationBindingContext.HttpContext.Request; var request = bindingContext.OperationBindingContext.HttpContext.Request;
// 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;
object model; object model;
if (ModelBindingHelper.CanGetCompatibleCollection<string>(bindingContext)) if (bindingContext.ModelType == typeof(string))
{ {
if (bindingContext.ModelType == typeof(string)) var value = request.Headers[headerName];
{ model = (string)value;
var value = request.Headers[headerName]; }
model = (string)value; else if (ModelBindingHelper.CanGetCompatibleCollection<string>(bindingContext))
} {
else var values = request.Headers.GetCommaSeparatedValues(headerName);
{ model = GetCompatibleCollection(bindingContext, values);
var values = request.Headers.GetCommaSeparatedValues(headerName);
model = GetCompatibleCollection(bindingContext, values);
}
} }
else else
{ {

View File

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for binding header values.
/// </summary>
public class HeaderModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.BindingInfo?.BindingSource != null &&
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
{
// 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))
{
return new HeaderModelBinder();
}
}
return null;
}
}
}

View File

@ -5,10 +5,40 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public sealed class KeyValuePairModelBinder<TKey, TValue> : IModelBinder /// <summary>
/// An <see cref="IModelBinder"/> for <see cref="KeyValuePair{TKey, TValue}"/>.
/// </summary>
/// <typeparam name="TKey">The key type.</typeparam>
/// <typeparam name="TValue">The value type.</typeparam>
public class KeyValuePairModelBinder<TKey, TValue> : IModelBinder
{ {
private readonly IModelBinder _keyBinder;
private readonly IModelBinder _valueBinder;
/// <summary>
/// Creates a new <see cref="KeyValuePair{TKey, TValue}"/>.
/// </summary>
/// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param>
/// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param>
public KeyValuePairModelBinder(IModelBinder keyBinder, IModelBinder valueBinder)
{
if (keyBinder == null)
{
throw new ArgumentNullException(nameof(keyBinder));
}
if (valueBinder == null)
{
throw new ArgumentNullException(nameof(valueBinder));
}
_keyBinder = keyBinder;
_valueBinder = valueBinder;
}
/// <inheritdoc />
public async Task BindModelAsync(ModelBindingContext bindingContext) public async Task BindModelAsync(ModelBindingContext bindingContext)
{ {
if (bindingContext == null) if (bindingContext == null)
@ -16,14 +46,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext)); throw new ArgumentNullException(nameof(bindingContext));
} }
if (bindingContext.ModelType != typeof(KeyValuePair<TKey, TValue>)) var keyResult = await TryBindStrongModel<TKey>(bindingContext, _keyBinder, "Key");
{ var valueResult = await TryBindStrongModel<TValue>(bindingContext, _valueBinder, "Value");
// This binder does not apply.
return;
}
var keyResult = await TryBindStrongModel<TKey>(bindingContext, "Key");
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "Value");
if (keyResult.IsModelSet && valueResult.IsModelSet) if (keyResult.IsModelSet && valueResult.IsModelSet)
{ {
@ -70,6 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
internal async Task<ModelBindingResult> TryBindStrongModel<TModel>( internal async Task<ModelBindingResult> TryBindStrongModel<TModel>(
ModelBindingContext bindingContext, ModelBindingContext bindingContext,
IModelBinder binder,
string propertyName) string propertyName)
{ {
var propertyModelMetadata = var propertyModelMetadata =
@ -83,9 +108,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelName: propertyModelName, modelName: propertyModelName,
model: null)) model: null))
{ {
await binder.BindModelAsync(bindingContext);
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(
bindingContext);
var result = bindingContext.Result; var result = bindingContext.Result;
if (result != null && result.Value.IsModelSet) if (result != null && result.Value.IsModelSet)
{ {

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for <see cref="KeyValuePair{TKey, TValue}"/>.
/// </summary>
public class KeyValuePairModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var modelTypeInfo = context.Metadata.ModelType.GetTypeInfo();
if (modelTypeInfo.IsGenericType &&
modelTypeInfo.GetGenericTypeDefinition().GetTypeInfo() == typeof(KeyValuePair<,>).GetTypeInfo())
{
var typeArguments = modelTypeInfo.GenericTypeArguments;
var keyMetadata = context.MetadataProvider.GetMetadataForType(typeArguments[0]);
var keyBinder = context.CreateBinder(keyMetadata);
var valueMetadata = context.MetadataProvider.GetMetadataForType(typeArguments[1]);
var valueBinder = context.CreateBinder(valueMetadata);
var binderType = typeof(KeyValuePairModelBinder<,>).MakeGenericType(typeArguments);
return (IModelBinder)Activator.CreateInstance(binderType, keyBinder, valueBinder);
}
return null;
}
}
}

View File

@ -2,13 +2,12 @@
// 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.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary> /// <summary>
/// An <see cref="IModelBinder"/> which binds models from the request services when a model /// An <see cref="IModelBinder"/> which binds models from the request services when a model
@ -24,19 +23,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext)); throw new ArgumentNullException(nameof(bindingContext));
} }
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null ||
!allowedBindingSource.CanAcceptDataFrom(BindingSource.Services))
{
// Services are opt-in. This model either didn't specify [FromService] or specified something
// incompatible so let other binders run.
return TaskCache.CompletedTask;
}
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices; var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var model = requestServices.GetRequiredService(bindingContext.ModelType); var model = requestServices.GetRequiredService(bindingContext.ModelType);

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for binding from the <see cref="IServiceProvider"/>.
/// </summary>
public class ServicesModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.BindingInfo?.BindingSource != null &&
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Services))
{
return new ServicesModelBinder();
}
return null;
}
}
}

View File

@ -5,10 +5,14 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
/// <summary>
/// An <see cref="IModelBinder"/> for simple types.
/// </summary>
public class SimpleTypeModelBinder : IModelBinder public class SimpleTypeModelBinder : IModelBinder
{ {
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
if (bindingContext == null) if (bindingContext == null)
@ -16,20 +20,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext)); throw new ArgumentNullException(nameof(bindingContext));
} }
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
if (bindingContext.ModelMetadata.IsComplexType)
{
// this type cannot be converted
return TaskCache.CompletedTask;
}
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None) if (valueProviderResult == ValueProviderResult.None)
{ {
// no entry // no entry
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for binding simple data types.
/// </summary>
public class SimpleTypeModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.Metadata.IsComplexType)
{
return new SimpleTypeModelBinder();
}
return null;
}
}
}

View File

@ -1,171 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// Represents an <see cref="IModelBinder"/> that delegates to one of a collection of <see cref="IModelBinder"/>
/// instances.
/// </summary>
/// <remarks>
/// If no binder is available and the <see cref="ModelBindingContext"/> allows it,
/// this class tries to find a binder using an empty prefix.
/// </remarks>
public class CompositeModelBinder : ICompositeModelBinder
{
/// <summary>
/// Initializes a new instance of the CompositeModelBinder class.
/// </summary>
/// <param name="modelBinders">A collection of <see cref="IModelBinder"/> instances.</param>
public CompositeModelBinder(IList<IModelBinder> modelBinders)
{
if (modelBinders == null)
{
throw new ArgumentNullException(nameof(modelBinders));
}
ModelBinders = modelBinders;
}
/// <inheritdoc />
public IList<IModelBinder> ModelBinders { get; }
public virtual Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
return RunModelBinders(bindingContext);
}
private async Task RunModelBinders(ModelBindingContext bindingContext)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
ModelBindingResult? overallResult = null;
try
{
using (bindingContext.EnterNestedScope())
{
if (PrepareBindingContext(bindingContext))
{
// Perf: Avoid allocations
for (var i = 0; i < ModelBinders.Count; i++)
{
var binder = ModelBinders[i];
await binder.BindModelAsync(bindingContext);
if (bindingContext.Result != null)
{
var result = bindingContext.Result.Value;
// This condition is necessary because the ModelState entry would never be validated if
// caller fell back to the empty prefix, leading to an possibly-incorrect !IsValid. In most
// (hopefully all) cases, the ModelState entry exists because some binders add errors before
// returning a result with !IsModelSet. Those binders often cannot run twice anyhow.
if (result.IsModelSet ||
bindingContext.ModelState.ContainsKey(bindingContext.ModelName))
{
if (bindingContext.IsTopLevelObject && result.Model != null)
{
ValidationStateEntry entry;
if (!bindingContext.ValidationState.TryGetValue(result.Model, out entry))
{
entry = new ValidationStateEntry()
{
Key = result.Key,
Metadata = bindingContext.ModelMetadata,
};
bindingContext.ValidationState.Add(result.Model, entry);
}
}
overallResult = bindingContext.Result;
return;
}
// Current binder should have been able to bind value but found nothing. Exit loop in a way that
// tells caller to fall back to the empty prefix, if appropriate. Do not return result because it
// means only "other binders are not applicable".
// overallResult MUST still be null at this return statement.
return;
}
}
}
}
}
finally
{
bindingContext.Result = overallResult;
}
}
private static bool PrepareBindingContext(ModelBindingContext bindingContext)
{
// If the property has a specified data binding sources, we need to filter the set of value providers
// to just those that match. We can skip filtering when IsGreedy == true, because that can't use
// value providers.
//
// We also want to base this filtering on the - top-level value provider in case the data source
// on this property doesn't intersect with the ambient data source.
//
// Ex:
//
// public class Person
// {
// [FromQuery]
// public int Id { get; set; }
// }
//
// public IActionResult UpdatePerson([FromForm] Person person) { }
//
// In this example, [FromQuery] overrides the ambient data source (form).
var valueProvider = bindingContext.ValueProvider;
var bindingSource = bindingContext.BindingSource;
var modelName = bindingContext.ModelName;
var fallbackToEmptyPrefix = bindingContext.FallbackToEmptyPrefix;
if (bindingSource != null && !bindingSource.IsGreedy)
{
var bindingSourceValueProvider = valueProvider as IBindingSourceValueProvider;
if (bindingSourceValueProvider != null)
{
valueProvider = bindingSourceValueProvider.Filter(bindingSource);
if (valueProvider == null)
{
// Unable to find a value provider for this binding source.
return false;
}
}
}
if (bindingSource != null && bindingSource.IsGreedy)
{
bindingContext.ModelName = modelName;
}
else if (
!fallbackToEmptyPrefix ||
valueProvider.ContainsPrefix(bindingContext.ModelName))
{
bindingContext.ModelName = modelName;
}
else
{
bindingContext.ModelName = string.Empty;
}
bindingContext.ValueProvider = valueProvider;
bindingContext.FallbackToEmptyPrefix = false;
return true;
}
}
}

View File

@ -1,175 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
#if NETSTANDARD1_5
using System.Reflection;
#endif
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class GenericModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
var binderType = ResolveBinderType(bindingContext);
if (binderType == null)
{
return TaskCache.CompletedTask;
}
var binder = (IModelBinder)Activator.CreateInstance(binderType);
var collectionBinder = binder as ICollectionModelBinder;
if (collectionBinder != null &&
bindingContext.Model == null &&
!collectionBinder.CanCreateInstance(bindingContext.ModelType))
{
// Able to resolve a binder type but need a new model instance and that binder cannot create it.
return TaskCache.CompletedTask;
}
return BindModelCoreAsync(bindingContext, binder);
}
private async Task BindModelCoreAsync(ModelBindingContext bindingContext, IModelBinder binder)
{
Debug.Assert(binder != null);
await binder.BindModelAsync(bindingContext);
if (bindingContext.Result == null)
{
// Always tell the model binding system to skip other model binders.
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
}
}
private static Type ResolveBinderType(ModelBindingContext context)
{
var modelType = context.ModelType;
return GetArrayBinder(modelType) ??
GetDictionaryBinder(modelType) ??
GetCollectionBinder(modelType) ??
GetEnumerableBinder(context) ??
GetKeyValuePairBinder(modelType);
}
private static Type GetArrayBinder(Type modelType)
{
if (modelType.IsArray)
{
var elementType = modelType.GetElementType();
return typeof(ArrayModelBinder<>).MakeGenericType(elementType);
}
return null;
}
private static Type GetCollectionBinder(Type modelType)
{
return GetGenericBinderType(
typeof(ICollection<>),
typeof(CollectionModelBinder<>),
modelType);
}
private static Type GetDictionaryBinder(Type modelType)
{
return GetGenericBinderType(
typeof(IDictionary<,>),
typeof(DictionaryModelBinder<,>),
modelType);
}
private static Type GetEnumerableBinder(ModelBindingContext context)
{
var modelTypeArguments = GetGenericBinderTypeArgs(typeof(IEnumerable<>), context.ModelType);
if (modelTypeArguments == null)
{
return null;
}
if (context.Model == null)
{
// GetCollectionBinder has already confirmed modelType is not compatible with ICollection<T>. Can a
// List<T> (the default CollectionModelBinder type) instance be used instead of that exact type?
// Likely this will succeed only if the property type is exactly IEnumerable<T>.
var closedListType = typeof(List<>).MakeGenericType(modelTypeArguments);
if (!context.ModelType.IsAssignableFrom(closedListType))
{
return null;
}
}
else
{
// A non-null instance must be updated in-place. For that the instance must also implement
// ICollection<T>. For example an IEnumerable<T> property may have a List<T> default value. Do not use
// IsAssignableFrom() because that does not handle explicit interface implementations and binders all
// perform explicit casts.
if (!context.ModelMetadata.IsCollectionType)
{
return null;
}
}
return typeof(CollectionModelBinder<>).MakeGenericType(modelTypeArguments);
}
private static Type GetKeyValuePairBinder(Type modelType)
{
Debug.Assert(modelType != null);
// Since KeyValuePair is a value type, ExtractGenericInterface() succeeds only on an exact match.
var closedGenericType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(KeyValuePair<,>));
if (closedGenericType != null)
{
return typeof(KeyValuePairModelBinder<,>).MakeGenericType(modelType.GenericTypeArguments);
}
return null;
}
// Example: GetGenericBinderType(typeof(IList<T>), typeof(ListBinder<T>), ...) means that the ListBinder<T>
// type can update models that implement IList<T>. This method will return
// ListBinder<T> or null, depending on whether the type checks succeed.
private static Type GetGenericBinderType(Type supportedInterfaceType, Type openBinderType, Type modelType)
{
Debug.Assert(openBinderType != null);
var modelTypeArguments = GetGenericBinderTypeArgs(supportedInterfaceType, modelType);
if (modelTypeArguments == null)
{
return null;
}
return openBinderType.MakeGenericType(modelTypeArguments);
}
// Get the generic arguments for the binder, based on the model type. Or null if not compatible.
private static Type[] GetGenericBinderTypeArgs(Type supportedInterfaceType, Type modelType)
{
Debug.Assert(supportedInterfaceType != null);
Debug.Assert(modelType != null);
var closedGenericInterface =
ClosedGenericMatcher.ExtractGenericInterface(modelType, supportedInterfaceType);
return closedGenericInterface?.GenericTypeArguments;
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// A factory abstraction for creating <see cref="IModelBinder"/> instances.
/// </summary>
public interface IModelBinderFactory
{
/// <summary>
/// Creates a new <see cref="IModelBinder"/>.
/// </summary>
/// <param name="context">The <see cref="ModelBinderFactoryContext"/>.</param>
/// <returns>An <see cref="IModelBinder"/> instance.</returns>
IModelBinder CreateBinder(ModelBinderFactoryContext context);
}
}

View File

@ -0,0 +1,233 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// A factory for <see cref="IModelBinder"/> instances.
/// </summary>
public class ModelBinderFactory : IModelBinderFactory
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly IModelBinderProvider[] _providers;
private readonly ConcurrentDictionary<object, IModelBinder> _cache;
/// <summary>
/// Creates a new <see cref="ModelBinderFactory"/>.
/// </summary>
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="options">The <see cref="IOptions{TOptions}"/> for <see cref="MvcOptions"/>.</param>
public ModelBinderFactory(IModelMetadataProvider metadataProvider, IOptions<MvcOptions> options)
{
_metadataProvider = metadataProvider;
_providers = options.Value.ModelBinderProviders.ToArray();
_cache = new ConcurrentDictionary<object, IModelBinder>(ReferenceEqualityComparer.Instance);
}
/// <inheritdoc />
public IModelBinder CreateBinder(ModelBinderFactoryContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// We perform caching in CreateBinder (not in CreateBinderCore) because we only want to
// cache the top-level binder.
IModelBinder binder;
if (context.CacheToken != null && _cache.TryGetValue(context.CacheToken, out binder))
{
return binder;
}
var providerContext = new DefaultModelBinderProviderContext(this, context);
binder = CreateBinderCore(providerContext, context.CacheToken);
if (binder == null)
{
var message = Resources.FormatCouldNotCreateIModelBinder(providerContext.Metadata.ModelType);
throw new InvalidOperationException(message);
}
if (context.CacheToken != null)
{
_cache.TryAdd(context.CacheToken, binder);
}
return binder;
}
private IModelBinder CreateBinderCore(DefaultModelBinderProviderContext providerContext, object token)
{
// A non-null token will usually be passed in at the the top level (ParameterDescriptor likely).
// This prevents us from treating a parameter the same as a collection-element - which could
// happen looking at just model metadata.
var key = new Key(providerContext.Metadata, token);
// If we're currently recursively building a binder for this type, just return
// a PlaceholderBinder. We'll fix it up later to point to the 'real' binder
// when the stack unwinds.
var stack = providerContext.Stack;
for (var i = 0; i < stack.Count; i++)
{
var entry = stack[i];
if (key.Equals(entry.Key))
{
if (entry.Value == null)
{
// Recursion detected, create a DelegatingBinder.
var binder = new PlaceholderBinder();
stack[i] = new KeyValuePair<Key, PlaceholderBinder>(entry.Key, binder);
return binder;
}
else
{
return entry.Value;
}
}
}
// OK this isn't a recursive case (yet) so "push" an entry on the stack and then ask the providers
// to create the binder.
stack.Add(new KeyValuePair<Key, PlaceholderBinder>(key, null));
IModelBinder result = null;
for (var i = 0; i < _providers.Length; i++)
{
var provider = _providers[i];
result = provider.GetBinder(providerContext);
if (result != null)
{
break;
}
}
if (result == null && stack.Count > 1)
{
// Use a no-op binder if we're below the top level. At the top level, we throw.
result = NoOpBinder.Instance;
}
// "pop"
Debug.Assert(stack.Count > 0);
var delegatingBinder = stack[stack.Count - 1].Value;
stack.RemoveAt(stack.Count - 1);
// If the DelegatingBinder was created, then it means we recursed. Hook it up to the 'real' binder.
if (delegatingBinder != null)
{
delegatingBinder.Inner = result;
}
return result;
}
private class DefaultModelBinderProviderContext : ModelBinderProviderContext
{
private readonly ModelBinderFactory _factory;
public DefaultModelBinderProviderContext(
ModelBinderFactory factory,
ModelBinderFactoryContext factoryContext)
{
_factory = factory;
Metadata = factoryContext.Metadata;
BindingInfo = factoryContext.BindingInfo;
MetadataProvider = _factory._metadataProvider;
Stack = new List<KeyValuePair<Key, PlaceholderBinder>>();
}
private DefaultModelBinderProviderContext(
DefaultModelBinderProviderContext parent,
ModelMetadata metadata)
{
Metadata = metadata;
_factory = parent._factory;
MetadataProvider = parent.MetadataProvider;
Stack = parent.Stack;
BindingInfo = new BindingInfo()
{
BinderModelName = metadata.BinderModelName,
BinderType = metadata.BinderType,
BindingSource = metadata.BindingSource,
PropertyBindingPredicateProvider = metadata.PropertyBindingPredicateProvider,
};
}
public override BindingInfo BindingInfo { get; }
public override ModelMetadata Metadata { get; }
public override IModelMetadataProvider MetadataProvider { get; }
// Not using a 'real' Stack<> because we want random access to modify the entries.
public List<KeyValuePair<Key, PlaceholderBinder>> Stack { get; }
public override IModelBinder CreateBinder(ModelMetadata metadata)
{
var nestedContext = new DefaultModelBinderProviderContext(this, metadata);
return _factory.CreateBinderCore(nestedContext, token: null);
}
}
private class NoOpBinder : IModelBinder
{
public static readonly IModelBinder Instance = new NoOpBinder();
public Task BindModelAsync(ModelBindingContext bindingContext)
{
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return TaskCache.CompletedTask;
}
}
private struct Key : IEquatable<Key>
{
private readonly ModelMetadata _metadata;
private readonly object _token; // Explicitly using ReferenceEquality for tokens.
public Key(ModelMetadata metadata, object token)
{
_metadata = metadata;
_token = token;
}
public bool Equals(Key other)
{
return _metadata.Equals(other._metadata) && object.ReferenceEquals(_token, other._token);
}
public override bool Equals(object obj)
{
var other = obj as Key?;
return other.HasValue && Equals(other.Value);
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(_metadata);
hash.Add(RuntimeHelpers.GetHashCode(_token));
return hash;
}
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// A context object for <see cref="ModelBinderFactory.CreateBinder"/>.
/// </summary>
public class ModelBinderFactoryContext
{
/// <summary>
/// Gets or sets the <see cref="ModelBinding.BindingInfo"/>.
/// </summary>
public BindingInfo BindingInfo { get; set; }
/// <summary>
/// Gets or sets the <see cref="ModelMetadata"/>.
/// </summary>
public ModelMetadata Metadata { get; set; }
/// <summary>
/// Gets or sets the cache token. If <c>non-null</c> the resulting <see cref="IModelBinder"/>
/// will be cached.
/// </summary>
public object CacheToken { get; set; }
}
}

View File

@ -20,9 +20,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public static class ModelBindingHelper public static class ModelBindingHelper
{ {
/// <summary> /// <summary>
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/> /// Updates the specified <paramref name="model"/> instance using the specified
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified /// <paramref name="modelBinderFactory"/> and the specified <paramref name="valueProvider"/> and executes
/// <paramref name="validatorProvider"/>. /// validation using the specified <paramref name="validatorProvider"/>.
/// </summary> /// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam> /// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update and validate.</param> /// <param name="model">The model instance to update and validate.</param>
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </param> /// </param>
/// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param> /// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param> /// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param> /// <param name="modelBinderFactory">The <see cref="IModelBinderFactory"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param> /// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="inputFormatters"> /// <param name="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body. /// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix, string prefix,
ActionContext actionContext, ActionContext actionContext,
IModelMetadataProvider metadataProvider, IModelMetadataProvider metadataProvider,
IModelBinder modelBinder, IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider, IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters, IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator, IObjectModelValidator objectModelValidator,
@ -72,9 +72,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider)); throw new ArgumentNullException(nameof(metadataProvider));
} }
if (modelBinder == null) if (modelBinderFactory == null)
{ {
throw new ArgumentNullException(nameof(modelBinder)); throw new ArgumentNullException(nameof(modelBinderFactory));
} }
if (valueProvider == null) if (valueProvider == null)
@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix, prefix,
actionContext, actionContext,
metadataProvider, metadataProvider,
modelBinder, modelBinderFactory,
valueProvider, valueProvider,
inputFormatters, inputFormatters,
objectModelValidator, objectModelValidator,
@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
/// <summary> /// <summary>
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/> /// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinderFactory"/>
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified /// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>. /// <paramref name="validatorProvider"/>.
/// </summary> /// </summary>
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </param> /// </param>
/// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param> /// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param> /// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param> /// <param name="modelBinderFactory">The <see cref="IModelBinderFactory"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param> /// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="inputFormatters"> /// <param name="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body. /// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix, string prefix,
ActionContext actionContext, ActionContext actionContext,
IModelMetadataProvider metadataProvider, IModelMetadataProvider metadataProvider,
IModelBinder modelBinder, IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider, IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters, IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator, IObjectModelValidator objectModelValidator,
@ -168,9 +168,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider)); throw new ArgumentNullException(nameof(metadataProvider));
} }
if (modelBinder == null) if (modelBinderFactory == null)
{ {
throw new ArgumentNullException(nameof(modelBinder)); throw new ArgumentNullException(nameof(modelBinderFactory));
} }
if (valueProvider == null) if (valueProvider == null)
@ -206,7 +206,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix, prefix,
actionContext, actionContext,
metadataProvider, metadataProvider,
modelBinder, modelBinderFactory,
valueProvider, valueProvider,
inputFormatters, inputFormatters,
objectModelValidator, objectModelValidator,
@ -215,7 +215,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
/// <summary> /// <summary>
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/> /// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinderFactory"/>
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified /// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>. /// <paramref name="validatorProvider"/>.
/// </summary> /// </summary>
@ -225,7 +225,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </param> /// </param>
/// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param> /// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param> /// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param> /// <param name="modelBinderFactory">The <see cref="IModelBinderFactory"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param> /// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="inputFormatters"> /// <param name="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body. /// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix, string prefix,
ActionContext actionContext, ActionContext actionContext,
IModelMetadataProvider metadataProvider, IModelMetadataProvider metadataProvider,
IModelBinder modelBinder, IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider, IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters, IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator, IObjectModelValidator objectModelValidator,
@ -270,9 +270,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider)); throw new ArgumentNullException(nameof(metadataProvider));
} }
if (modelBinder == null) if (modelBinderFactory == null)
{ {
throw new ArgumentNullException(nameof(modelBinder)); throw new ArgumentNullException(nameof(modelBinderFactory));
} }
if (valueProvider == null) if (valueProvider == null)
@ -306,7 +306,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix, prefix,
actionContext, actionContext,
metadataProvider, metadataProvider,
modelBinder, modelBinderFactory,
valueProvider, valueProvider,
inputFormatters, inputFormatters,
objectModelValidator, objectModelValidator,
@ -315,7 +315,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
/// <summary> /// <summary>
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/> /// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinderFactory"/>
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified /// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>. /// <paramref name="validatorProvider"/>.
/// </summary> /// </summary>
@ -325,7 +325,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </param> /// </param>
/// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param> /// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param> /// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param> /// <param name="modelBinderFactory">The <see cref="IModelBinderFactory"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param> /// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="inputFormatters"> /// <param name="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body. /// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -341,7 +341,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix, string prefix,
ActionContext actionContext, ActionContext actionContext,
IModelMetadataProvider metadataProvider, IModelMetadataProvider metadataProvider,
IModelBinder modelBinder, IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider, IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters, IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator, IObjectModelValidator objectModelValidator,
@ -372,9 +372,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider)); throw new ArgumentNullException(nameof(metadataProvider));
} }
if (modelBinder == null) if (modelBinderFactory == null)
{ {
throw new ArgumentNullException(nameof(modelBinder)); throw new ArgumentNullException(nameof(modelBinderFactory));
} }
if (valueProvider == null) if (valueProvider == null)
@ -404,7 +404,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix, prefix,
actionContext, actionContext,
metadataProvider, metadataProvider,
modelBinder, modelBinderFactory,
valueProvider, valueProvider,
inputFormatters, inputFormatters,
objectModelValidator, objectModelValidator,
@ -413,7 +413,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
/// <summary> /// <summary>
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/> /// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinderFactory"/>
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified /// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>. /// <paramref name="validatorProvider"/>.
/// </summary> /// </summary>
@ -423,7 +423,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </param> /// </param>
/// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param> /// <param name="actionContext">The <see cref="ActionContext"/> for the current executing request.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param> /// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param> /// <param name="modelBinderFactory">The <see cref="IModelBinderFactory"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param> /// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="inputFormatters"> /// <param name="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body. /// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -441,7 +441,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix, string prefix,
ActionContext actionContext, ActionContext actionContext,
IModelMetadataProvider metadataProvider, IModelMetadataProvider metadataProvider,
IModelBinder modelBinder, IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider, IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters, IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator, IObjectModelValidator objectModelValidator,
@ -473,9 +473,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider)); throw new ArgumentNullException(nameof(metadataProvider));
} }
if (modelBinder == null) if (modelBinderFactory == null)
{ {
throw new ArgumentNullException(nameof(modelBinder)); throw new ArgumentNullException(nameof(modelBinderFactory));
} }
if (valueProvider == null) if (valueProvider == null)
@ -517,7 +517,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var operationBindingContext = new OperationBindingContext var operationBindingContext = new OperationBindingContext
{ {
InputFormatters = inputFormatters, InputFormatters = inputFormatters,
ModelBinder = modelBinder,
ValidatorProvider = validatorProvider, ValidatorProvider = validatorProvider,
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
ActionContext = actionContext, ActionContext = actionContext,
@ -532,7 +531,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelBindingContext.Model = model; modelBindingContext.Model = model;
modelBindingContext.PropertyFilter = predicate; modelBindingContext.PropertyFilter = predicate;
await modelBinder.BindModelAsync(modelBindingContext); var factoryContext = new ModelBinderFactoryContext()
{
Metadata = modelMetadata,
BindingInfo = new BindingInfo()
{
BinderModelName = modelMetadata.BinderModelName,
BinderType = modelMetadata.BinderType,
BindingSource = modelMetadata.BindingSource,
PropertyBindingPredicateProvider = modelMetadata.PropertyBindingPredicateProvider,
},
// We're using the model metadata as the cache token here so that TryUpdateModelAsync calls
// for the same model type can share a binder. This won't overlap with normal model binding
// operations because they use the ParameterDescriptor for the token.
CacheToken = modelMetadata,
};
var binder = modelBinderFactory.CreateBinder(factoryContext);
await binder.BindModelAsync(modelBindingContext);
var modelBindingResult = modelBindingContext.Result; var modelBindingResult = modelBindingContext.Result;
if (modelBindingResult != null && modelBindingResult.Value.IsModelSet) if (modelBindingResult != null && modelBindingResult.Value.IsModelSet)
{ {
@ -730,19 +747,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
} }
internal static void ValidateBindingContext(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
if (bindingContext.ModelMetadata == null)
{
throw new ArgumentException(Resources.ModelBinderUtil_ModelMetadataCannotBeNull, nameof(bindingContext));
}
}
internal static TModel CastOrDefault<TModel>(object model) internal static TModel CastOrDefault<TModel>(object model)
{ {
return (model is TModel) ? (TModel)model : default(TModel); return (model is TModel) ? (TModel)model : default(TModel);

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc
FormatterMappings = new FormatterMappings(); FormatterMappings = new FormatterMappings();
InputFormatters = new FormatterCollection<IInputFormatter>(); InputFormatters = new FormatterCollection<IInputFormatter>();
OutputFormatters = new FormatterCollection<IOutputFormatter>(); OutputFormatters = new FormatterCollection<IOutputFormatter>();
ModelBinders = new List<IModelBinder>(); ModelBinderProviders = new List<IModelBinderProvider>();
ModelBindingMessageProvider = new ModelBindingMessageProvider(); ModelBindingMessageProvider = new ModelBindingMessageProvider();
ModelMetadataDetailsProviders = new List<IMetadataDetailsProvider>(); ModelMetadataDetailsProviders = new List<IMetadataDetailsProvider>();
ModelValidatorProviders = new List<IModelValidatorProvider>(); ModelValidatorProviders = new List<IModelValidatorProvider>();
@ -81,9 +81,9 @@ namespace Microsoft.AspNetCore.Mvc
} }
/// <summary> /// <summary>
/// Gets a list of <see cref="IModelBinder"/>s used by this application. /// Gets a list of <see cref="IModelBinderProvider"/>s used by this application.
/// </summary> /// </summary>
public IList<IModelBinder> ModelBinders { get; } public IList<IModelBinderProvider> ModelBinderProviders { get; }
/// <summary> /// <summary>
/// Gets the default <see cref="IModelBindingMessageProvider"/>. Changes here are copied to the /// Gets the default <see cref="IModelBindingMessageProvider"/>. Changes here are copied to the

View File

@ -1162,6 +1162,22 @@ namespace Microsoft.AspNetCore.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("MustSpecifyAtLeastOneAuthenticationScheme")); return string.Format(CultureInfo.CurrentCulture, GetString("MustSpecifyAtLeastOneAuthenticationScheme"));
} }
/// <summary>
/// Could not create a model binder for model object of type '{0}'.
/// </summary>
internal static string CouldNotCreateIModelBinder
{
get { return GetString("CouldNotCreateIModelBinder"); }
}
/// <summary>
/// Could not create a model binder for model object of type '{0}'.
/// </summary>
internal static string FormatCouldNotCreateIModelBinder(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("CouldNotCreateIModelBinder"), p0);
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -343,4 +343,7 @@
<data name="MustSpecifyAtLeastOneAuthenticationScheme" xml:space="preserve"> <data name="MustSpecifyAtLeastOneAuthenticationScheme" xml:space="preserve">
<value>At least one authentication scheme must be specified.</value> <value>At least one authentication scheme must be specified.</value>
</data> </data>
<data name="CouldNotCreateIModelBinder" xml:space="preserve">
<value>Could not create a model binder for model object of type '{0}'.</value>
</data>
</root> </root>

View File

@ -17,12 +17,9 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
/// <inheritdoc /> /// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
if (bindingContext.ModelType == typeof(HttpRequestMessage)) var model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage();
{ bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
var model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage(); bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
}
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Net.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
{
/// <summary>
/// <see cref="IModelBinderProvider"/> implementation to bind models of type <see cref="HttpRequestMessage"/>.
/// </summary>
public class HttpRequestMessageModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(HttpRequestMessage))
{
return new HttpRequestMessageModelBinder();
}
return null;
}
}
}

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
options.Filters.Add(new HttpResponseExceptionActionFilter()); options.Filters.Add(new HttpResponseExceptionActionFilter());
// Add a model binder to be able to bind HttpRequestMessage // Add a model binder to be able to bind HttpRequestMessage
options.ModelBinders.Insert(0, new HttpRequestMessageModelBinder()); options.ModelBinderProviders.Insert(0, new HttpRequestMessageModelBinderProvider());
// Add a formatter to write out an HttpResponseMessage to the response // Add a formatter to write out an HttpResponseMessage to the response
options.OutputFormatters.Insert(0, new HttpResponseMessageOutputFormatter()); options.OutputFormatters.Insert(0, new HttpResponseMessageOutputFormatter());

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Constraints;

View File

@ -1638,7 +1638,6 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
var controllerContext = new ControllerContext() var controllerContext = new ControllerContext()
{ {
HttpContext = httpContext, HttpContext = httpContext,
ModelBinders = new[] { binder, },
ValueProviders = new[] { valueProvider, }, ValueProviders = new[] { valueProvider, },
ValidatorProviders = new[] ValidatorProviders = new[]
{ {
@ -1649,10 +1648,16 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
}, },
}; };
var binderFactory = new Mock<IModelBinderFactory>();
binderFactory
.Setup(f => f.CreateBinder(It.IsAny<ModelBinderFactoryContext>()))
.Returns(binder);
var controller = new TestableController() var controller = new TestableController()
{ {
ControllerContext = controllerContext, ControllerContext = controllerContext,
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
ModelBinderFactory = binderFactory.Object,
ObjectValidator = new DefaultObjectValidator(metadataProvider, new ValidatorCache()), ObjectValidator = new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
}; };

View File

@ -2057,7 +2057,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
actionDescriptor, actionDescriptor,
new IInputFormatter[0], new IInputFormatter[0],
actionArgumentsBinder.Object, actionArgumentsBinder.Object,
new IModelBinder[0],
new IModelValidatorProvider[0], new IModelValidatorProvider[0],
new IValueProviderFactory[0], new IValueProviderFactory[0],
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(), new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
@ -2090,9 +2089,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
FilterDescriptors = new List<FilterDescriptor>() FilterDescriptors = new List<FilterDescriptor>()
}; };
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(TaskCache.CompletedTask);
var context = new Mock<HttpContext>(); var context = new Mock<HttpContext>();
context.SetupGet(c => c.Items) context.SetupGet(c => c.Items)
.Returns(new Dictionary<object, object>()); .Returns(new Dictionary<object, object>());
@ -2115,8 +2111,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new IInputFormatter[0], new IInputFormatter[0],
new ControllerArgumentBinder( new ControllerArgumentBinder(
metadataProvider, metadataProvider,
TestModelBinderFactory.CreateDefault(metadataProvider),
new DefaultObjectValidator(metadataProvider, new ValidatorCache())), new DefaultObjectValidator(metadataProvider, new ValidatorCache())),
new IModelBinder[] { binder.Object },
new IModelValidatorProvider[0], new IModelValidatorProvider[0],
new IValueProviderFactory[0], new IValueProviderFactory[0],
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(), new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
@ -2241,7 +2237,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ControllerActionDescriptor descriptor, ControllerActionDescriptor descriptor,
IReadOnlyList<IInputFormatter> inputFormatters, IReadOnlyList<IInputFormatter> inputFormatters,
IControllerActionArgumentBinder controllerActionArgumentBinder, IControllerActionArgumentBinder controllerActionArgumentBinder,
IReadOnlyList<IModelBinder> modelBinders,
IReadOnlyList<IModelValidatorProvider> modelValidatorProviders, IReadOnlyList<IModelValidatorProvider> modelValidatorProviders,
IReadOnlyList<IValueProviderFactory> valueProviderFactories, IReadOnlyList<IValueProviderFactory> valueProviderFactories,
ILogger logger, ILogger logger,
@ -2254,7 +2249,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
descriptor, descriptor,
inputFormatters, inputFormatters,
controllerActionArgumentBinder, controllerActionArgumentBinder,
modelBinders,
modelValidatorProviders, modelValidatorProviders,
valueProviderFactories, valueProviderFactories,
logger, logger,

View File

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Test;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Moq; using Moq;
@ -36,17 +35,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
binder binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>())) .Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(TaskCache.CompletedTask); .Returns(TaskCache.CompletedTask);
var factory = GetModelBinderFactory(binder.Object);
var argumentBinder = GetArgumentBinder(factory);
var controllerContext = GetControllerContext(actionDescriptor); var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ModelBinders.Add(binder.Object);
controllerContext.ValueProviders.Add(new SimpleValueProvider());
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var argumentBinder = GetArgumentBinder();
// Act // Act
var result = await argumentBinder var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
.BindActionArgumentsAsync(controllerContext, new TestController());
// Assert // Assert
Assert.Empty(result); Assert.Empty(result);
@ -69,17 +64,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
binder binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>())) .Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(TaskCache.CompletedTask); .Returns(TaskCache.CompletedTask);
var factory = GetModelBinderFactory(binder.Object);
var argumentBinder = GetArgumentBinder(factory);
var controllerContext = GetControllerContext(actionDescriptor); var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ModelBinders.Add(binder.Object);
controllerContext.ValueProviders.Add(new SimpleValueProvider());
var argumentBinder = GetArgumentBinder();
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
// Act // Act
var result = await argumentBinder var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
.BindActionArgumentsAsync(controllerContext, new TestController());
// Assert // Assert
Assert.Empty(result); Assert.Empty(result);
@ -111,16 +102,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
context.Result = ModelBindingResult.Success(string.Empty, value); context.Result = ModelBindingResult.Success(string.Empty, value);
}) })
.Returns(TaskCache.CompletedTask); .Returns(TaskCache.CompletedTask);
var factory = GetModelBinderFactory(binder.Object);
var argumentBinder = GetArgumentBinder(factory);
var controllerContext = GetControllerContext(actionDescriptor); var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ModelBinders.Add(binder.Object);
controllerContext.ValueProviders.Add(new SimpleValueProvider());
var argumentBinder = GetArgumentBinder();
// Act // Act
var result = await argumentBinder var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
.BindActionArgumentsAsync(controllerContext, new TestController());
// Assert // Assert
Assert.Equal(1, result.Count); Assert.Equal(1, result.Count);
@ -139,7 +127,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ParameterType = typeof(object), ParameterType = typeof(object),
}); });
var controllerContext = GetControllerContext(actionDescriptor, "Hello"); var controllerContext = GetControllerContext(actionDescriptor);
var factory = GetModelBinderFactory("Hello");
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict); var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator mockValidator
@ -150,7 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<object>())); It.IsAny<object>()));
var argumentBinder = GetArgumentBinder(mockValidator.Object); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object);
// Act // Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
@ -180,15 +170,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
BindingInfo = new BindingInfo(), BindingInfo = new BindingInfo(),
}); });
var controllerContext = GetControllerContext(actionDescriptor);
var binder = new Mock<IModelBinder>(); var binder = new Mock<IModelBinder>();
binder binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>())) .Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(TaskCache.CompletedTask); .Returns(TaskCache.CompletedTask);
var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ModelBinders.Add(binder.Object);
controllerContext.ValueProviders.Add(new SimpleValueProvider());
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict); var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator mockValidator
.Setup(o => o.Validate( .Setup(o => o.Validate(
@ -198,7 +186,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<object>())); It.IsAny<object>()));
var argumentBinder = GetArgumentBinder(mockValidator.Object); var factory = GetModelBinderFactory(binder.Object);
var argumentBinder = GetArgumentBinder(factory, mockValidator.Object);
// Act // Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
@ -226,7 +215,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ParameterType = typeof(string), ParameterType = typeof(string),
}); });
var controllerContext = GetControllerContext(actionDescriptor, "Hello"); var controllerContext = GetControllerContext(actionDescriptor);
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict); var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator mockValidator
@ -237,7 +226,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<object>())); It.IsAny<object>()));
var argumentBinder = GetArgumentBinder(mockValidator.Object); var factory = GetModelBinderFactory("Hello");
var argumentBinder = GetArgumentBinder(factory, mockValidator.Object);
// Act // Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
@ -266,15 +256,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ParameterType = typeof(string), ParameterType = typeof(string),
}); });
var controllerContext = GetControllerContext(actionDescriptor);
var binder = new Mock<IModelBinder>(); var binder = new Mock<IModelBinder>();
binder binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>())) .Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(TaskCache.CompletedTask); .Returns(TaskCache.CompletedTask);
var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ModelBinders.Add(binder.Object);
controllerContext.ValueProviders.Add(new SimpleValueProvider());
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict); var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator mockValidator
.Setup(o => o.Validate( .Setup(o => o.Validate(
@ -284,7 +272,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<object>())); It.IsAny<object>()));
var argumentBinder = GetArgumentBinder(mockValidator.Object); var factory = GetModelBinderFactory(binder.Object);
var argumentBinder = GetArgumentBinder(factory, mockValidator.Object);
// Act // Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
@ -313,8 +302,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ParameterType = typeof(string) ParameterType = typeof(string)
}); });
var controllerContext = GetControllerContext(actionDescriptor, "Hello"); var controllerContext = GetControllerContext(actionDescriptor);
var argumentBinder = GetArgumentBinder();
var factory = GetModelBinderFactory("Hello");
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController(); var controller = new TestController();
// Act // Act
@ -339,10 +331,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ParameterType = typeof(ICollection<string>), ParameterType = typeof(ICollection<string>),
}); });
var expected = new List<string> { "Hello", "World", "!!" }; var controllerContext = GetControllerContext(actionDescriptor);
var controllerContext = GetControllerContext(actionDescriptor, expected);
var expected = new List<string> { "Hello", "World", "!!" };
var factory = GetModelBinderFactory(expected);
var argumentBinder = GetArgumentBinder(factory);
var argumentBinder = GetArgumentBinder();
var controller = new TestController(); var controller = new TestController();
// Act // Act
@ -369,13 +363,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ParameterType = typeof(int) ParameterType = typeof(int)
}); });
var binder = new StubModelBinder(ModelBindingResult.Success(string.Empty, model: null));
var controllerContext = GetControllerContext(actionDescriptor); var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ModelBinders.Add(binder);
controllerContext.ValueProviders.Add(new SimpleValueProvider());
var argumentBinder = GetArgumentBinder(); var binder = new StubModelBinder(ModelBindingResult.Success(string.Empty, model: null));
var factory = GetModelBinderFactory(binder);
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController(); var controller = new TestController();
// Some non default value. // Some non default value.
@ -401,13 +394,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ParameterType = typeof(int?) ParameterType = typeof(int?)
}); });
var binder = new StubModelBinder(ModelBindingResult.Success(key: string.Empty, model: null));
var controllerContext = GetControllerContext(actionDescriptor); var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ModelBinders.Add(binder);
controllerContext.ValueProviders.Add(new SimpleValueProvider());
var argumentBinder = GetArgumentBinder(); var binder = new StubModelBinder(ModelBindingResult.Success(key: string.Empty, model: null));
var factory = GetModelBinderFactory(binder);
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController(); var controller = new TestController();
// Some non default value. // Some non default value.
@ -478,8 +470,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ParameterType = propertyType, ParameterType = propertyType,
}); });
var controllerContext = GetControllerContext(actionDescriptor, inputValue); var controllerContext = GetControllerContext(actionDescriptor);
var argumentBinder = GetArgumentBinder();
var factory = GetModelBinderFactory(inputValue);
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController(); var controller = new TestController();
// Act // Act
@ -533,8 +528,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
var controllerContext = GetControllerContext(actionDescriptor); var controllerContext = GetControllerContext(actionDescriptor);
var argumentBinder = GetArgumentBinder();
var controller = new TestController();
var binder = new StubModelBinder(bindingContext => var binder = new StubModelBinder(bindingContext =>
{ {
@ -550,9 +543,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName); bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
} }
}); });
controllerContext.ModelBinders.Add(binder.Object);
var factory = GetModelBinderFactory(binder);
controllerContext.ValueProviders.Add(new SimpleValueProvider()); controllerContext.ValueProviders.Add(new SimpleValueProvider());
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController();
// Act // Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller); var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller);
@ -574,6 +571,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
RouteData = new RouteData(), RouteData = new RouteData(),
}; };
context.ValueProviders.Add(new SimpleValueProvider());
return context; return context;
} }
@ -589,37 +587,47 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}; };
} }
private static ControllerContext GetControllerContext(ControllerActionDescriptor descriptor = null, object model = null) private static ModelBinderFactory GetModelBinderFactory(object model = null)
{ {
var context = new ControllerContext()
{
ActionDescriptor = descriptor ?? GetActionDescriptor(),
HttpContext = new DefaultHttpContext(),
RouteData = new RouteData(),
};
var binder = new Mock<IModelBinder>(); var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>())) binder
.Returns<DefaultModelBindingContext>(mbc => .Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
{ .Returns<DefaultModelBindingContext>(mbc =>
mbc.Result = ModelBindingResult.Success(string.Empty, model); {
return TaskCache.CompletedTask; mbc.Result = ModelBindingResult.Success(string.Empty, model);
}); return TaskCache.CompletedTask;
});
context.ModelBinders.Add(binder.Object); return GetModelBinderFactory(binder.Object);
context.ValueProviders.Add(new SimpleValueProvider());
return context;
} }
private static ControllerArgumentBinder GetArgumentBinder(IObjectModelValidator validator = null) private static ModelBinderFactory GetModelBinderFactory(IModelBinder binder)
{
var provider = new Mock<IModelBinderProvider>();
provider
.Setup(p => p.GetBinder(It.IsAny<ModelBinderProviderContext>()))
.Returns(binder);
return TestModelBinderFactory.Create(provider.Object);
}
private static ControllerArgumentBinder GetArgumentBinder(
IModelBinderFactory factory = null,
IObjectModelValidator validator = null)
{ {
if (validator == null) if (validator == null)
{ {
validator = CreateMockValidator(); validator = CreateMockValidator();
} }
if (factory == null)
{
factory = TestModelBinderFactory.CreateDefault();
}
return new ControllerArgumentBinder( return new ControllerArgumentBinder(
TestModelMetadataProvider.CreateDefaultProvider(), TestModelMetadataProvider.CreateDefaultProvider(),
factory,
validator); validator);
} }

View File

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Core;
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 Xunit; using Xunit;

View File

@ -0,0 +1,92 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class ArrayModelBinderProviderTest
{
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(TestClass))]
[InlineData(typeof(IList<int>))]
public void Create_ForNonArrayTypes_ReturnsNull(Type modelType)
{
// Arrange
var provider = new ArrayModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData(typeof(byte[]))]
[InlineData(typeof(string[]))]
[InlineData(typeof(TestClass[]))]
[InlineData(typeof(DateTime?[]))]
public void Create_ForArrayTypes_ReturnsBinder(Type modelType)
{
// Arrange
var provider = new ArrayModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
context.OnCreatingBinder((m) =>
{
// Expect to be called with the element type to create a binder for elements.
Assert.Equal(modelType.GetElementType(), m.ModelType);
return Mock.Of<IModelBinder>();
});
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType(typeof(ArrayModelBinder<>).MakeGenericType(modelType.GetElementType()), result);
}
[Fact]
public void Create_ForModelMetadataReadOnly_ReturnsNull()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForProperty(
typeof(ModelWithIntArrayProperty),
nameof(ModelWithIntArrayProperty.ArrayProperty)).BindingDetails(bd => bd.IsReadOnly = true);
var modelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithIntArrayProperty),
nameof(ModelWithIntArrayProperty.ArrayProperty));
var provider = new ArrayModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(int[]));
context.OnCreatingBinder((m) =>
{
// Expect to be called with the element type to create a binder for elements.
Assert.Equal(typeof(int), m.ModelType);
return Mock.Of<IModelBinder>();
});
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<ArrayModelBinder<int>>(result);
}
private class TestClass
{
}
private class ModelWithIntArrayProperty
{
public int[] ArrayProperty { get; set; }
}
}
}

View File

@ -4,10 +4,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Internal;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class ArrayModelBinderTest public class ArrayModelBinderTest
{ {
@ -22,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
}; };
var bindingContext = GetBindingContext(valueProvider); var bindingContext = GetBindingContext(valueProvider);
var modelState = bindingContext.ModelState; var modelState = bindingContext.ModelState;
var binder = new ArrayModelBinder<int>(); var binder = new ArrayModelBinder<int>(new SimpleTypeModelBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -32,14 +31,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var array = Assert.IsType<int[]>(result.Model); var array = Assert.IsType<int[]>(result.Model);
Assert.Equal(new[] { 42, 84 }, array); Assert.Equal(new[] { 42, 84 }, array);
Assert.True(modelState.IsValid);
} }
[Fact] [Fact]
public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject() public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{ {
// Arrange // Arrange
var binder = new ArrayModelBinder<string>(); var binder = new ArrayModelBinder<string>(new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.IsTopLevelObject = true; context.IsTopLevelObject = true;
@ -69,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix) public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new ArrayModelBinder<string>(); var binder = new ArrayModelBinder<string>(new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.ModelName = ModelNames.CreatePropertyModelName(prefix, "ArrayProperty"); context.ModelName = ModelNames.CreatePropertyModelName(prefix, "ArrayProperty");
@ -101,28 +99,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
} }
} }
[Theory]
[InlineData(null)]
[MemberData(nameof(ArrayModelData))]
public async Task BindModelAsync_ModelMetadataReadOnly_ReturnsNull(int[] model)
{
// Arrange
var valueProvider = new SimpleValueProvider
{
{ "someName[0]", "42" },
{ "someName[1]", "84" },
};
var bindingContext = GetBindingContext(valueProvider, isReadOnly: true);
bindingContext.Model = model;
var binder = new ArrayModelBinder<int>();
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
// Here "fails silently" means the call does not update the array but also does not throw or set an error. // Here "fails silently" means the call does not update the array but also does not throw or set an error.
[Theory] [Theory]
[MemberData(nameof(ArrayModelData))] [MemberData(nameof(ArrayModelData))]
@ -139,7 +115,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var bindingContext = GetBindingContext(valueProvider, isReadOnly: false); var bindingContext = GetBindingContext(valueProvider, isReadOnly: false);
var modelState = bindingContext.ModelState; var modelState = bindingContext.ModelState;
bindingContext.Model = model; bindingContext.Model = model;
var binder = new ArrayModelBinder<int>();
var binder = new ArrayModelBinder<int>(new SimpleTypeModelBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -149,7 +126,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Same(model, result.Model); Assert.Same(model, result.Model);
Assert.True(modelState.IsValid);
for (var i = 0; i < arrayLength; i++) for (var i = 0; i < arrayLength; i++)
{ {
// Array should be unchanged. // Array should be unchanged.
@ -191,7 +167,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
ValueProvider = valueProvider, ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext OperationBindingContext = new OperationBindingContext
{ {
ModelBinder = CreateIntBinder(),
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
}, },
}; };

View File

@ -0,0 +1,58 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class BinderTypeModelBinderProviderTest
{
[Fact]
public void Create_WhenBinderTypeIsNull_ReturnsNull()
{
// Arrange
var provider = new BinderTypeModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Person));
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_WhenBinderTypeIsSet_ReturnsBinder()
{
// Arrange
var provider = new BinderTypeModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Person));
context.BindingInfo.BinderType = typeof(NullModelBinder);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<BinderTypeModelBinder>(result);
}
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
private class NullModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
return Task.FromResult(0);
}
}
}
}

View File

@ -5,36 +5,22 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class BinderTypeBasedModelBinderModelBinderTest public class BinderTypeModelBinderTest
{ {
[Fact]
public async Task BindModel_ReturnsNothing_IfNoBinderTypeIsSet()
{
// Arrange
var bindingContext = GetBindingContext(typeof(Person));
var binder = new BinderTypeBasedModelBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), binderResult);
}
[Fact] [Fact]
public async Task BindModel_ReturnsFailedResult_EvenIfSelectedBinderReturnsNull() public async Task BindModel_ReturnsFailedResult_EvenIfSelectedBinderReturnsNull()
{ {
// Arrange // Arrange
var bindingContext = GetBindingContext(typeof(Person), binderType: typeof(NullModelBinder)); var bindingContext = GetBindingContext(typeof(Person), binderType: typeof(NullModelBinder));
var binder = new BinderTypeBasedModelBinder(); var binder = new BinderTypeModelBinder(typeof(NullModelBinder));
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -57,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
bindingContext.OperationBindingContext.HttpContext.RequestServices = serviceProvider; bindingContext.OperationBindingContext.HttpContext.RequestServices = serviceProvider;
var binder = new BinderTypeBasedModelBinder(); var binder = new BinderTypeModelBinder(typeof(NotNullModelBinder));
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -69,21 +55,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task BindModel_ForNonModelBinder_Throws() public void BindModel_ForNonModelBinder_Throws()
{ {
// Arrange // Arrange
var bindingContext = GetBindingContext(typeof(Person), binderType: typeof(Person)); var bindingContext = GetBindingContext(typeof(Person), binderType: typeof(Person));
var binder = new BinderTypeBasedModelBinder();
var expected = $"The type '{typeof(Person).FullName}' must implement " + var expected = $"The type '{typeof(Person).FullName}' must implement " +
$"'{typeof(IModelBinder).FullName}' to be used as a model binder."; $"'{typeof(IModelBinder).FullName}' to be used as a model binder.";
// Act // Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>( ExceptionAssert.ThrowsArgument(
() => binder.BindModelResultAsync(bindingContext)); () => new BinderTypeModelBinder(typeof(Person)),
"binderType",
// Assert expected);
Assert.Equal(expected, ex.Message);
} }
private static DefaultModelBindingContext GetBindingContext(Type modelType, Type binderType = null) private static DefaultModelBindingContext GetBindingContext(Type modelType, Type binderType = null)
@ -108,7 +92,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
ValueProvider = Mock.Of<IValueProvider>(), ValueProvider = Mock.Of<IValueProvider>(),
ModelState = new ModelStateDictionary(), ModelState = new ModelStateDictionary(),
OperationBindingContext = operationBindingContext, OperationBindingContext = operationBindingContext,
BinderType = binderType
}; };
return bindingContext; return bindingContext;

View File

@ -0,0 +1,63 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class BodyModelBinderProviderTest
{
public static TheoryData<BindingSource> NonBodyBindingSources
{
get
{
return new TheoryData<BindingSource>()
{
BindingSource.Header,
BindingSource.Form,
null,
};
}
}
[Theory]
[MemberData(nameof(NonBodyBindingSources))]
public void Create_WhenBindingSourceIsNotFromBody_ReturnsNull(BindingSource source)
{
// Arrange
var provider = new BodyModelBinderProvider(new TestHttpRequestStreamReaderFactory());
var context = new TestModelBinderProviderContext(typeof(Person));
context.BindingInfo.BindingSource = source;
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_WhenBindingSourceIsFromBody_ReturnsBinder()
{
// Arrange
var provider = new BodyModelBinderProvider(new TestHttpRequestStreamReaderFactory());
var context = new TestModelBinderProviderContext(typeof(Person));
context.BindingInfo.BindingSource = BindingSource.Body;
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<BodyModelBinder>(result);
}
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
}

View File

@ -14,7 +14,7 @@ using Microsoft.Net.Http.Headers;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class BodyModelBinderTests public class BodyModelBinderTests
{ {
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
new[] { inputFormatter }, new[] { inputFormatter },
metadataProvider: provider); metadataProvider: provider);
var binder = new BodyModelBinder(new TestHttpRequestStreamReaderFactory()); var binder = CreateBinder();
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider); var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder; var binder = CreateBinder();
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider); var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider);
bindingContext.BinderModelName = "custom"; bindingContext.BinderModelName = "custom";
var binder = bindingContext.OperationBindingContext.ModelBinder; var binder = CreateBinder();
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider); var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder; var binder = CreateBinder();
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -125,44 +125,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
} }
[Fact]
public async Task BindModel_IsGreedy_IgnoresWrongSource()
{
// Arrange
var provider = new TestModelMetadataProvider();
provider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Header);
var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider);
bindingContext.BindingSource = BindingSource.Header;
var binder = bindingContext.OperationBindingContext.ModelBinder;
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), binderResult);
}
[Fact]
public async Task BindModel_IsGreedy_IgnoresMetadataWithNoSource()
{
// Arrange
var provider = new TestModelMetadataProvider();
provider.ForType<Person>().BindingDetails(d => d.BindingSource = null);
var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider);
bindingContext.BindingSource = null;
var binder = bindingContext.OperationBindingContext.ModelBinder;
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), binderResult);
}
[Fact] [Fact]
public async Task CustomFormatterDeserializationException_AddedToModelState() public async Task CustomFormatterDeserializationException_AddedToModelState()
{ {
@ -180,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
httpContext: httpContext, httpContext: httpContext,
metadataProvider: provider); metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder; var binder = CreateBinder();
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -215,7 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
httpContext: httpContext, httpContext: httpContext,
metadataProvider: provider); metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder; var binder = CreateBinder();
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -250,7 +212,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var provider = new TestModelMetadataProvider(); var provider = new TestModelMetadataProvider();
provider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body); provider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), inputFormatters, metadataProvider: provider); var bindingContext = GetBindingContext(typeof(Person), inputFormatters, metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder;
var binder = CreateBinder();
// Act // Act
var binderResult = await binder.BindModelResultAsync(bindingContext); var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -288,7 +251,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
HttpContext = httpContext, HttpContext = httpContext,
}, },
InputFormatters = inputFormatters.ToList(), InputFormatters = inputFormatters.ToList(),
ModelBinder = new BodyModelBinder(new TestHttpRequestStreamReaderFactory()),
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
}; };
@ -307,6 +269,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return bindingContext; return bindingContext;
} }
private static BodyModelBinder CreateBinder()
{
return new BodyModelBinder(new TestHttpRequestStreamReaderFactory());
}
private class Person private class Person
{ {
public string Name { get; set; } public string Name { get; set; }

View File

@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class ByteArrayModelBinderProviderTest
{
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(TestClass))]
[InlineData(typeof(IList<byte>))]
[InlineData(typeof(int[]))]
public void Create_ForNonByteArrayTypes_ReturnsNull(Type modelType)
{
// Arrange
var provider = new ByteArrayModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_ForByteArray_ReturnsBinder()
{
// Arrange
var provider = new ByteArrayModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(byte[]));
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<ByteArrayModelBinder>(result);
}
private class TestClass
{
}
}
}

View File

@ -5,7 +5,7 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class ByteArrayModelBinderTests public class ByteArrayModelBinderTests
{ {
@ -106,20 +106,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Empty(bindingContext.ModelState); // No submitted data for "foo". Assert.Empty(bindingContext.ModelState); // No submitted data for "foo".
} }
[Fact]
public async Task BindModel_ReturnsNull_ForOtherTypes()
{
// Arrange
var bindingContext = GetBindingContext(new SimpleValueProvider(), typeof(int[]));
var binder = new ByteArrayModelBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), binderResult);
}
private static DefaultModelBindingContext GetBindingContext(IValueProvider valueProvider, Type modelType) private static DefaultModelBindingContext GetBindingContext(IValueProvider valueProvider, Type modelType)
{ {
var metadataProvider = new EmptyModelMetadataProvider(); var metadataProvider = new EmptyModelMetadataProvider();

View File

@ -0,0 +1,49 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class CancellationTokenModelBinderProviderTest
{
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(TestClass))]
[InlineData(typeof(IList<int>))]
[InlineData(typeof(int[]))]
public void Create_ForNonCancellationTokenTypes_ReturnsNull(Type modelType)
{
// Arrange
var provider = new CancellationTokenModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_ForCancellationToken_ReturnsBinder()
{
// Arrange
var provider = new CancellationTokenModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(CancellationToken));
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<CancellationTokenModelBinder>(result);
}
private class TestClass
{
}
}
}

View File

@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class CancellationTokenModelBinderTests public class CancellationTokenModelBinderTests
{ {
@ -28,23 +28,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Equal(bindingContext.OperationBindingContext.HttpContext.RequestAborted, result.Model); Assert.Equal(bindingContext.OperationBindingContext.HttpContext.RequestAborted, result.Model);
} }
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(object))]
[InlineData(typeof(CancellationTokenModelBinderTests))]
public async Task CancellationTokenModelBinder_ReturnsNull_ForNonCancellationTokenType(Type t)
{
// Arrange
var bindingContext = GetBindingContext(t);
var binder = new CancellationTokenModelBinder();
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
private static DefaultModelBindingContext GetBindingContext(Type modelType) private static DefaultModelBindingContext GetBindingContext(Type modelType)
{ {
var metadataProvider = new EmptyModelMetadataProvider(); var metadataProvider = new EmptyModelMetadataProvider();
@ -59,7 +42,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ {
HttpContext = new DefaultHttpContext(), HttpContext = new DefaultHttpContext(),
}, },
ModelBinder = new CancellationTokenModelBinder(),
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
}, },
ValidationState = new ValidationStateDictionary(), ValidationState = new ValidationStateDictionary(),

View File

@ -0,0 +1,111 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class CollectionModelBinderProviderTest
{
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(int))]
[InlineData(typeof(Person))]
[InlineData(typeof(int[]))]
public void Create_ForNonSupportedTypes_ReturnsNull(Type modelType)
{
// Arrange
var provider = new CollectionModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Theory]
// These aren't ICollection<> - we can handle them by creating a List<>
[InlineData(typeof(IEnumerable<int>))]
[InlineData(typeof(IReadOnlyCollection<int>))]
[InlineData(typeof(IReadOnlyList<int>))]
// These are ICollection<> - we can handle them by adding items to the existing collection or
// creating a new one.
[InlineData(typeof(ICollection<int>))]
[InlineData(typeof(IList<int>))]
[InlineData(typeof(List<int>))]
[InlineData(typeof(Collection<int>))]
public void Create_ForSupportedTypes_ReturnsBinder(Type modelType)
{
// Arrange
var provider = new CollectionModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
Type elementType = null;
context.OnCreatingBinder(m =>
{
Assert.Equal(typeof(int), m.ModelType);
elementType = m.ModelType;
return Mock.Of<IModelBinder>();
});
// Act
var result = provider.GetBinder(context);
// Assert
Assert.NotNull(elementType);
Assert.IsType<CollectionModelBinder<int>>(result);
}
// These aren't ICollection<> - we can handle them by creating a List<> - but in this case
// we can't set the property so we can't bind.
[Theory]
[InlineData(nameof(ReadOnlyProperties.Enumerable))]
[InlineData(nameof(ReadOnlyProperties.ReadOnlyCollection))]
[InlineData(nameof(ReadOnlyProperties.ReadOnlyList))]
public void Create_ForNonICollectionTypes_ReadOnlyProperty_ReturnsNull(string propertyName)
{
// Arrange
var provider = new CollectionModelBinderProvider();
var metadataProvider = TestModelBinderProviderContext.CachedMetadataProvider;
var metadata = metadataProvider.GetMetadataForProperty(typeof(ReadOnlyProperties), propertyName);
Assert.NotNull(metadata);
Assert.True(metadata.IsReadOnly);
var context = new TestModelBinderProviderContext(metadata, bindingInfo: null);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
private class ReadOnlyProperties
{
public IEnumerable<int> Enumerable { get; }
public IReadOnlyCollection<int> ReadOnlyCollection { get; }
public IReadOnlyList<int> ReadOnlyList { get; }
}
}
}

View File

@ -8,11 +8,10 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Test;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class CollectionModelBinderTest public class CollectionModelBinderTest
{ {
@ -26,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ "someName[baz]", "200" } { "someName[baz]", "200" }
}; };
var bindingContext = GetModelBindingContext(valueProvider); var bindingContext = GetModelBindingContext(valueProvider);
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act // Act
var collectionResult = await binder.BindComplexCollectionFromIndexes( var collectionResult = await binder.BindComplexCollectionFromIndexes(
@ -52,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ "someName[3]", "400" } { "someName[3]", "400" }
}; };
var bindingContext = GetModelBindingContext(valueProvider); var bindingContext = GetModelBindingContext(valueProvider);
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act // Act
var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null); var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null);
@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}; };
var bindingContext = GetModelBindingContext(valueProvider, isReadOnly); var bindingContext = GetModelBindingContext(valueProvider, isReadOnly);
var modelState = bindingContext.ModelState; var modelState = bindingContext.ModelState;
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -111,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = bindingContext.ModelState; var modelState = bindingContext.ModelState;
var list = new List<int>(); var list = new List<int>();
bindingContext.Model = list; bindingContext.Model = list;
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -138,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}; };
var bindingContext = GetModelBindingContext(valueProvider, isReadOnly); var bindingContext = GetModelBindingContext(valueProvider, isReadOnly);
var modelState = bindingContext.ModelState; var modelState = bindingContext.ModelState;
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -165,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = bindingContext.ModelState; var modelState = bindingContext.ModelState;
var list = new List<int>(); var list = new List<int>();
bindingContext.Model = list; bindingContext.Model = list;
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -182,7 +181,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task BindModelAsync_SimpleCollectionWithNullValue_Succeeds() public async Task BindModelAsync_SimpleCollectionWithNullValue_Succeeds()
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
var valueProvider = new SimpleValueProvider var valueProvider = new SimpleValueProvider
{ {
{ "someName", null }, { "someName", null },
@ -206,7 +205,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task BindSimpleCollection_RawValueIsEmptyCollection_ReturnsEmptyList() public async Task BindSimpleCollection_RawValueIsEmptyCollection_ReturnsEmptyList()
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
var context = GetModelBindingContext(new SimpleValueProvider()); var context = GetModelBindingContext(new SimpleValueProvider());
// Act // Act
@ -221,7 +220,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject() public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>(); var binder = new CollectionModelBinder<string>(new StubModelBinder(result: null));
var context = CreateContext(); var context = CreateContext();
context.IsTopLevelObject = true; context.IsTopLevelObject = true;
@ -251,7 +250,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task CollectionModelBinder_DoesNotCreateEmptyCollection_IfModelNonNull() public async Task CollectionModelBinder_DoesNotCreateEmptyCollection_IfModelNonNull()
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>(); var binder = new CollectionModelBinder<string>(new StubModelBinder(result: null));
var context = CreateContext(); var context = CreateContext();
context.IsTopLevelObject = true; context.IsTopLevelObject = true;
@ -285,7 +284,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix) public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>(); var binder = new CollectionModelBinder<string>(new StubModelBinder(result: null));
var context = CreateContext(); var context = CreateContext();
context.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty"); context.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty");
@ -326,7 +325,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanCreateInstance_ReturnsExpectedValue(Type modelType, bool expectedResult) public void CanCreateInstance_ReturnsExpectedValue(Type modelType, bool expectedResult)
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act // Act
var result = binder.CanCreateInstance(modelType); var result = binder.CanCreateInstance(modelType);
@ -342,13 +341,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var culture = new CultureInfo("fr-FR"); var culture = new CultureInfo("fr-FR");
var bindingContext = GetModelBindingContext(new SimpleValueProvider()); var bindingContext = GetModelBindingContext(new SimpleValueProvider());
bindingContext.OperationBindingContext.ModelBinder = new StubModelBinder(mbc => var elementBinder = new StubModelBinder(mbc =>
{ {
Assert.Equal("someName", mbc.ModelName); Assert.Equal("someName", mbc.ModelName);
mbc.Result = ModelBindingResult.Success(mbc.ModelName, 42); mbc.Result = ModelBindingResult.Success(mbc.ModelName, 42);
}); });
var modelBinder = new CollectionModelBinder<int>(); var modelBinder = new CollectionModelBinder<int>(elementBinder);
// Act // Act
var boundCollection = await modelBinder.BindSimpleCollection( var boundCollection = await modelBinder.BindSimpleCollection(
@ -379,7 +378,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ValueProvider = valueProvider, ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext OperationBindingContext = new OperationBindingContext
{ {
ModelBinder = CreateIntBinder(),
MetadataProvider = metadataProvider MetadataProvider = metadataProvider
}, },
ValidationState = new ValidationStateDictionary(), ValidationState = new ValidationStateDictionary(),

View File

@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class ComplexTypeModelBinderProviderTest
{
[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(int))]
[InlineData(typeof(List<int>))]
public void Create_ForNonComplexType_ReturnsNull(Type modelType)
{
// Arrange
var provider = new ComplexTypeModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_ForSupportedTypes_ReturnsBinder()
{
// Arrange
var provider = new ComplexTypeModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Person));
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(int) || m.ModelType == typeof(string))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<ComplexTypeModelBinder>(result);
}
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
}

View File

@ -5,20 +5,21 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Test; using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class MutableObjectModelBinderTest public class ComplexTypeModelBinderTest
{ {
private static readonly IModelMetadataProvider _metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); private static readonly IModelMetadataProvider _metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(Person))); var bindingContext = CreateContext(GetMetadataForType(typeof(Person)));
bindingContext.IsTopLevelObject = isTopLevelObject; bindingContext.IsTopLevelObject = isTopLevelObject;
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -49,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(modelMetadata); var bindingContext = CreateContext(modelMetadata);
bindingContext.IsTopLevelObject = false; bindingContext.IsTopLevelObject = false;
var binder = new MutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -64,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(Document))); var bindingContext = CreateContext(GetMetadataForType(typeof(Document)));
bindingContext.IsTopLevelObject = true; bindingContext.IsTopLevelObject = true;
var binder = new MutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -81,7 +82,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(HasAllGreedyProperties))); var bindingContext = CreateContext(GetMetadataForType(typeof(HasAllGreedyProperties)));
bindingContext.IsTopLevelObject = isTopLevelObject; bindingContext.IsTopLevelObject = isTopLevelObject;
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -109,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ValueProvider = valueProvider.Object; bindingContext.ValueProvider = valueProvider.Object;
bindingContext.OperationBindingContext.ValueProvider = valueProvider.Object; bindingContext.OperationBindingContext.ValueProvider = valueProvider.Object;
var binder = new MutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -126,7 +127,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithNoProperties))); var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithNoProperties)));
bindingContext.IsTopLevelObject = false; bindingContext.IsTopLevelObject = false;
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -142,7 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithNoProperties))); var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithNoProperties)));
bindingContext.IsTopLevelObject = true; bindingContext.IsTopLevelObject = true;
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -172,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ValueProvider = valueProvider.Object; bindingContext.ValueProvider = valueProvider.Object;
bindingContext.OperationBindingContext.ValueProvider = valueProvider.Object; bindingContext.OperationBindingContext.ValueProvider = valueProvider.Object;
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -207,7 +208,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ValueProvider = valueProvider.Object; bindingContext.ValueProvider = valueProvider.Object;
bindingContext.OperationBindingContext.ValueProvider = originalValueProvider.Object; bindingContext.OperationBindingContext.ValueProvider = originalValueProvider.Object;
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -240,7 +241,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ValueProvider = valueProvider.Object; bindingContext.ValueProvider = valueProvider.Object;
bindingContext.OperationBindingContext.ValueProvider = originalValueProvider.Object; bindingContext.OperationBindingContext.ValueProvider = originalValueProvider.Object;
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var canCreate = binder.CanCreateModel(bindingContext); var canCreate = binder.CanCreateModel(bindingContext);
@ -269,7 +270,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ValueProvider = mockValueProvider.Object, ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext OperationBindingContext = new OperationBindingContext
{ {
ModelBinder = mockBinder,
MetadataProvider = _metadataProvider, MetadataProvider = _metadataProvider,
ValidatorProvider = Mock.Of<IModelValidatorProvider>() ValidatorProvider = Mock.Of<IModelValidatorProvider>()
}, },
@ -278,7 +278,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var model = new Person(); var model = new Person();
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true }; var testableBinder = new Mock<TestableComplexTypeModelBinder> { CallBase = true };
testableBinder testableBinder
.Setup(o => o.CreateModelPublic(bindingContext)) .Setup(o => o.CreateModelPublic(bindingContext))
.Returns(model) .Returns(model)
@ -311,7 +311,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = GetMetadataForProperty(typeof(MyModelTestingCanUpdateProperty), propertyName); var propertyMetadata = GetMetadataForProperty(typeof(MyModelTestingCanUpdateProperty), propertyName);
// Act // Act
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata); var canUpdate = ComplexTypeModelBinder.CanUpdatePropertyInternal(propertyMetadata);
// Assert // Assert
Assert.Equal(expected, canUpdate); Assert.Equal(expected, canUpdate);
@ -331,7 +331,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var metadata = metadataProvider.GetMetadataForProperty(typeof(CollectionContainer), propertyName); var metadata = metadataProvider.GetMetadataForProperty(typeof(CollectionContainer), propertyName);
// Act // Act
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(metadata); var canUpdate = ComplexTypeModelBinder.CanUpdatePropertyInternal(metadata);
// Assert // Assert
Assert.Equal(expected, canUpdate); Assert.Equal(expected, canUpdate);
@ -346,10 +346,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ModelMetadata = GetMetadataForType(typeof(Person)) ModelMetadata = GetMetadataForType(typeof(Person))
}; };
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var model = testableBinder.CreateModelPublic(bindingContext); var model = binder.CreateModelPublic(bindingContext);
// Assert // Assert
Assert.IsType<Person>(model); Assert.IsType<Person>(model);
@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), new Person()); var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), new Person());
var originalModel = bindingContext.Model; var originalModel = bindingContext.Model;
var binder = new Mock<TestableMutableObjectModelBinder> { CallBase = true }; var binder = new Mock<TestableComplexTypeModelBinder>(){ CallBase = true };
binder binder
.Setup(b => b.CreateModelPublic(It.IsAny<ModelBindingContext>())) .Setup(b => b.CreateModelPublic(It.IsAny<ModelBindingContext>()))
.Verifiable(); .Verifiable();
@ -381,7 +381,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Arrange // Arrange
var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), model: null); var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), model: null);
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true }; var testableBinder = new Mock<TestableComplexTypeModelBinder> { CallBase = true };
testableBinder testableBinder
.Setup(o => o.CreateModelPublic(bindingContext)) .Setup(o => o.CreateModelPublic(bindingContext))
.Returns(new Person()) .Returns(new Person())
@ -402,10 +402,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_GetSetProperty(string property) public void CanBindProperty_GetSetProperty(string property)
{ {
// Arrange // Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property); var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
var context = new DefaultModelBindingContext() var bindingContext = new DefaultModelBindingContext()
{ {
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)), ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
OperationBindingContext = new OperationBindingContext() OperationBindingContext = new OperationBindingContext()
@ -420,8 +418,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}, },
}; };
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var result = binder.CanBindPropertyPublic(context, metadata); var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -432,10 +432,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_GetOnlyProperty_WithBindNever(string property) public void CanBindProperty_GetOnlyProperty_WithBindNever(string property)
{ {
// Arrange // Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property); var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
var context = new DefaultModelBindingContext() var bindingContext = new DefaultModelBindingContext()
{ {
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)), ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
OperationBindingContext = new OperationBindingContext() OperationBindingContext = new OperationBindingContext()
@ -450,8 +448,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}, },
}; };
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var result = binder.CanBindPropertyPublic(context, metadata); var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert // Assert
Assert.False(result); Assert.False(result);
@ -463,10 +463,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_GetSetProperty_WithBindNever(string property) public void CanBindProperty_GetSetProperty_WithBindNever(string property)
{ {
// Arrange // Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property); var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
var context = new DefaultModelBindingContext() var bindingContext = new DefaultModelBindingContext()
{ {
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)), ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
OperationBindingContext = new OperationBindingContext() OperationBindingContext = new OperationBindingContext()
@ -481,8 +479,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}, },
}; };
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var result = binder.CanBindPropertyPublic(context, metadata); var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert // Assert
Assert.False(result); Assert.False(result);
@ -496,10 +496,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_WithPredicate(string property, bool expected) public void CanBindProperty_WithPredicate(string property, bool expected)
{ {
// Arrange // Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(TypeWithExcludedPropertiesUsingBindAttribute), property); var metadata = GetMetadataForProperty(typeof(TypeWithExcludedPropertiesUsingBindAttribute), property);
var context = new DefaultModelBindingContext() var bindingContext = new DefaultModelBindingContext()
{ {
ModelMetadata = GetMetadataForType(typeof(TypeWithExcludedPropertiesUsingBindAttribute)), ModelMetadata = GetMetadataForType(typeof(TypeWithExcludedPropertiesUsingBindAttribute)),
OperationBindingContext = new OperationBindingContext() OperationBindingContext = new OperationBindingContext()
@ -514,8 +512,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}, },
}; };
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var result = binder.CanBindPropertyPublic(context, metadata); var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert // Assert
Assert.Equal(expected, result); Assert.Equal(expected, result);
@ -529,10 +529,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_WithBindInclude(string property, bool expected) public void CanBindProperty_WithBindInclude(string property, bool expected)
{ {
// Arrange // Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(TypeWithIncludedPropertiesUsingBindAttribute), property); var metadata = GetMetadataForProperty(typeof(TypeWithIncludedPropertiesUsingBindAttribute), property);
var context = new DefaultModelBindingContext() var bindingContext = new DefaultModelBindingContext()
{ {
ModelMetadata = GetMetadataForType(typeof(TypeWithIncludedPropertiesUsingBindAttribute)), ModelMetadata = GetMetadataForType(typeof(TypeWithIncludedPropertiesUsingBindAttribute)),
OperationBindingContext = new OperationBindingContext() OperationBindingContext = new OperationBindingContext()
@ -547,8 +545,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}, },
}; };
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var result = binder.CanBindPropertyPublic(context, metadata); var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert // Assert
Assert.Equal(expected, result); Assert.Equal(expected, result);
@ -561,10 +561,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_BindingAttributes_OverridingBehavior(string property, bool expected) public void CanBindProperty_BindingAttributes_OverridingBehavior(string property, bool expected)
{ {
// Arrange // Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(ModelWithMixedBindingBehaviors), property); var metadata = GetMetadataForProperty(typeof(ModelWithMixedBindingBehaviors), property);
var context = new DefaultModelBindingContext() var bindingContext = new DefaultModelBindingContext()
{ {
ModelMetadata = GetMetadataForType(typeof(ModelWithMixedBindingBehaviors)), ModelMetadata = GetMetadataForType(typeof(ModelWithMixedBindingBehaviors)),
OperationBindingContext = new OperationBindingContext() OperationBindingContext = new OperationBindingContext()
@ -579,8 +577,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}, },
}; };
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
var result = binder.CanBindPropertyPublic(context, metadata); var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert // Assert
Assert.Equal(expected, result); Assert.Equal(expected, result);
@ -597,13 +597,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Age = -20 Age = -20
}; };
var binder = new TestableMutableObjectModelBinder();
var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithBindRequired.Age)); var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithBindRequired.Age));
binder.Results[property] = ModelBindingResult.Failed("theModel.Age");
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
var binder = CreateBinder(bindingContext.ModelMetadata);
binder.Results[property] = ModelBindingResult.Failed("theModel.Age");
// Act // Act
await binder.BindModelAsync(bindingContext); await binder.BindModelAsync(bindingContext);
@ -632,13 +632,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Age = -20 Age = -20
}; };
var binder = new TestableMutableObjectModelBinder(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithDataMemberIsRequired.Age)); var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithDataMemberIsRequired.Age));
binder.Results[property] = ModelBindingResult.Failed("theModel.Age"); binder.Results[property] = ModelBindingResult.Failed("theModel.Age");
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
// Act // Act
await binder.BindModelAsync(bindingContext); await binder.BindModelAsync(bindingContext);
@ -667,15 +667,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Age = -20 Age = -20
}; };
var binder = new TestableMutableObjectModelBinder(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
var binder = CreateBinder(bindingContext.ModelMetadata);
// Attempt to set non-Nullable property to null. BindRequiredAttribute should not be relevant in this // Attempt to set non-Nullable property to null. BindRequiredAttribute should not be relevant in this
// case because the property did have a result. // case because the property did have a result.
var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithBindRequired.Age)); var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithBindRequired.Age));
binder.Results[property] = ModelBindingResult.Success("theModel.Age", model: null); binder.Results[property] = ModelBindingResult.Success("theModel.Age", model: null);
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
// Act // Act
await binder.BindModelAsync(bindingContext); await binder.BindModelAsync(bindingContext);
@ -701,7 +701,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var model = new BindingOptionalProperty(); var model = new BindingOptionalProperty();
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), nameof(BindingOptionalProperty.ValueTypeRequired)); var property = GetMetadataForProperty(model.GetType(), nameof(BindingOptionalProperty.ValueTypeRequired));
binder.Results[property] = ModelBindingResult.Failed("theModel.ValueTypeRequired"); binder.Results[property] = ModelBindingResult.Failed("theModel.ValueTypeRequired");
@ -721,7 +721,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var model = new NullableValueTypeProperty(); var model = new NullableValueTypeProperty();
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), nameof(NullableValueTypeProperty.NullableValueType)); var property = GetMetadataForProperty(model.GetType(), nameof(NullableValueTypeProperty.NullableValueType));
binder.Results[property] = ModelBindingResult.Failed("theModel.NullableValueType"); binder.Results[property] = ModelBindingResult.Failed("theModel.NullableValueType");
@ -743,7 +743,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(containerMetadata, model); var bindingContext = CreateContext(containerMetadata, model);
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired)); var property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired));
binder.Results[property] = ModelBindingResult.Failed("theModel." + nameof(Person.ValueTypeRequired)); binder.Results[property] = ModelBindingResult.Failed("theModel." + nameof(Person.ValueTypeRequired));
@ -765,7 +765,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(containerMetadata, model); var bindingContext = CreateContext(containerMetadata, model);
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired)); var property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired));
binder.Results[property] = ModelBindingResult.Success( binder.Results[property] = ModelBindingResult.Success(
@ -794,7 +794,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(containerMetadata, model); var bindingContext = CreateContext(containerMetadata, model);
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
foreach (var property in containerMetadata.Properties) foreach (var property in containerMetadata.Properties)
{ {
@ -833,10 +833,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = metadata.Properties[nameof(model.PropertyWithDefaultValue)]; var propertyMetadata = metadata.Properties[nameof(model.PropertyWithDefaultValue)];
var result = ModelBindingResult.Failed("foo"); var result = ModelBindingResult.Failed("foo");
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert // Assert
var person = Assert.IsType<Person>(bindingContext.Model); var person = Assert.IsType<Person>(bindingContext.Model);
@ -858,10 +858,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// The null model value won't be used because IsModelBound = false. // The null model value won't be used because IsModelBound = false.
var result = ModelBindingResult.Failed("foo"); var result = ModelBindingResult.Failed("foo");
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert // Assert
var person = Assert.IsType<Person>(bindingContext.Model); var person = Assert.IsType<Person>(bindingContext.Model);
@ -883,10 +883,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// The null model value won't be used because IsModelBound = false. // The null model value won't be used because IsModelBound = false.
var result = ModelBindingResult.Failed("foo"); var result = ModelBindingResult.Failed("foo");
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert // Assert
var person = Assert.IsType<Person>(bindingContext.Model); var person = Assert.IsType<Person>(bindingContext.Model);
@ -906,10 +906,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = metadata.Properties[nameof(model.NonUpdateableProperty)]; var propertyMetadata = metadata.Properties[nameof(model.NonUpdateableProperty)];
var result = ModelBindingResult.Failed("foo"); var result = ModelBindingResult.Failed("foo");
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert // Assert
// If didn't throw, success! // If didn't throw, success!
@ -953,10 +953,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
propertyName, propertyName,
new Simple { Name = "Hanna" }); new Simple { Name = "Hanna" });
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert // Assert
Assert.Equal("Joe", propertyAccessor(model)); Assert.Equal("Joe", propertyAccessor(model));
@ -977,7 +977,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(modelMetadata, model); var bindingContext = CreateContext(modelMetadata, model);
var result = ModelBindingResult.Success(propertyMetadata.PropertyName, new List<string>() { "hi" }); var result = ModelBindingResult.Success(propertyMetadata.PropertyName, new List<string>() { "hi" });
var binder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
binder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
@ -999,10 +999,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfBirth)]; var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfBirth)];
var result = ModelBindingResult.Success("foo", new DateTime(2001, 1, 1)); var result = ModelBindingResult.Success("foo", new DateTime(2001, 1, 1));
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert // Assert
Assert.True(bindingContext.ModelState.IsValid); Assert.True(bindingContext.ModelState.IsValid);
@ -1026,10 +1026,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfDeath)]; var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfDeath)];
var result = ModelBindingResult.Success("foo", new DateTime(1800, 1, 1)); var result = ModelBindingResult.Success("foo", new DateTime(1800, 1, 1));
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert // Assert
Assert.Equal("Date of death can't be before date of birth." + Environment.NewLine Assert.Equal("Date of death can't be before date of birth." + Environment.NewLine
@ -1051,10 +1051,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.NameNoAttribute)]; var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.NameNoAttribute)];
var result = ModelBindingResult.Success("foo.NameNoAttribute", model: null); var result = ModelBindingResult.Success("foo.NameNoAttribute", model: null);
var testableBinder = new TestableMutableObjectModelBinder(); var binder = CreateBinder(bindingContext.ModelMetadata);
// Act // Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result); binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert // Assert
Assert.False(bindingContext.ModelState.IsValid); Assert.False(bindingContext.ModelState.IsValid);
@ -1064,6 +1064,30 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message); bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message);
} }
private static TestableComplexTypeModelBinder CreateBinder(ModelMetadata metadata)
{
var options = new TestOptionsManager<MvcOptions>();
MvcCoreMvcOptionsSetup.ConfigureMvc(options.Value, new TestHttpRequestStreamReaderFactory());
var lastIndex = options.Value.ModelBinderProviders.Count - 1;
Assert.IsType<ComplexTypeModelBinderProvider>(options.Value.ModelBinderProviders[lastIndex]);
options.Value.ModelBinderProviders.RemoveAt(lastIndex);
options.Value.ModelBinderProviders.Add(new TestableComplexTypeModelBinderProvider());
var factory = TestModelBinderFactory.Create(options.Value.ModelBinderProviders.ToArray());
return (TestableComplexTypeModelBinder)factory.CreateBinder(new ModelBinderFactoryContext()
{
Metadata = metadata,
BindingInfo = new BindingInfo()
{
BinderModelName = metadata.BinderModelName,
BinderType = metadata.BinderType,
BindingSource = metadata.BindingSource,
PropertyBindingPredicateProvider = metadata.PropertyBindingPredicateProvider,
},
});
}
private static DefaultModelBindingContext CreateContext(ModelMetadata metadata, object model = null) private static DefaultModelBindingContext CreateContext(ModelMetadata metadata, object model = null)
{ {
var valueProvider = new TestValueProvider(new Dictionary<string, object>()); var valueProvider = new TestValueProvider(new Dictionary<string, object>());
@ -1281,7 +1305,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
private class NonValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata private class NonValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata
{ {
public BindingSource BindingSource { get { return BindingSource.Body; } } public BindingSource BindingSource
{
get { return new BindingSource("Special", string.Empty, isGreedy: true, isFromRequest: true); }
}
} }
private class ValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata private class ValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata
@ -1333,10 +1360,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public IList<int> SettableList { get; set; } = new List<int> { 3, 9, 0 }; public IList<int> SettableList { get; set; } = new List<int> { 3, 9, 0 };
} }
// Provides the ability to easily mock + call each of these APIs private class TestableComplexTypeModelBinderProvider : IModelBinderProvider
public class TestableMutableObjectModelBinder : MutableObjectModelBinder
{ {
public TestableMutableObjectModelBinder() public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.IsComplexType)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
foreach (var property in context.Metadata.Properties)
{
propertyBinders.Add(property, context.CreateBinder(property));
}
return new TestableComplexTypeModelBinder(propertyBinders);
}
return null;
}
}
// Provides the ability to easily mock + call each of these APIs
public class TestableComplexTypeModelBinder : ComplexTypeModelBinder
{
public TestableComplexTypeModelBinder()
: this(new Dictionary<ModelMetadata, IModelBinder>())
{
}
public TestableComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
: base(propertyBinders)
{ {
Results = new Dictionary<ModelMetadata, ModelBindingResult>(); Results = new Dictionary<ModelMetadata, ModelBindingResult>();
} }

View File

@ -0,0 +1,70 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class DictionaryModelBinderProviderTest
{
[Theory]
[InlineData(typeof(Person))]
[InlineData(typeof(string))]
[InlineData(typeof(IEnumerable<KeyValuePair<string, int>>))]
[InlineData(typeof(ICollection<string>))]
public void Create_ForNonDictionaryType_ReturnsNull(Type modelType)
{
// Arrange
var provider = new DictionaryModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData(typeof(IDictionary<string, int>))]
[InlineData(typeof(Dictionary<string, int>))]
public void Create_ForDictionaryType_ReturnsBinder(Type modelType)
{
// Arrange
var provider = new DictionaryModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(KeyValuePair<string, int>) ||
m.ModelType == typeof(int) ||
m.ModelType == typeof(string))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<DictionaryModelBinder<string, int>>(result);
}
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
}

View File

@ -10,10 +10,9 @@ using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class DictionaryModelBinderTest public class DictionaryModelBinderTest
{ {
@ -23,25 +22,28 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task BindModel_Succeeds(bool isReadOnly) public async Task BindModel_Succeeds(bool isReadOnly)
{ {
// Arrange // Arrange
var values = new Dictionary<string, KeyValuePair<int, string>>() var values = new Dictionary<string, string>()
{ {
{ "someName[0]", new KeyValuePair<int, string>(42, "forty-two") }, { "someName[0].Key", "42" },
{ "someName[1]", new KeyValuePair<int, string>(84, "eighty-four") }, { "someName[0].Value", "forty-two" },
{ "someName[1].Key", "84" },
{ "someName[1].Value", "eighty-four" },
}; };
// Value Provider
var bindingContext = GetModelBindingContext(isReadOnly, values); var bindingContext = GetModelBindingContext(isReadOnly, values);
var modelState = bindingContext.ModelState; bindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", values);
var binder = new DictionaryModelBinder<int, string>();
var binder = new DictionaryModelBinder<int, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
// Assert // Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(result.Model);
Assert.True(modelState.IsValid);
var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(result.Model);
Assert.NotNull(dictionary); Assert.NotNull(dictionary);
Assert.Equal(2, dictionary.Count); Assert.Equal(2, dictionary.Count);
Assert.Equal("forty-two", dictionary[42]); Assert.Equal("forty-two", dictionary[42]);
@ -57,27 +59,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task BindModel_WithExistingModel_Succeeds(bool isReadOnly) public async Task BindModel_WithExistingModel_Succeeds(bool isReadOnly)
{ {
// Arrange // Arrange
var values = new Dictionary<string, KeyValuePair<int, string>>() var values = new Dictionary<string, string>()
{ {
{ "someName[0]", new KeyValuePair<int, string>(42, "forty-two") }, { "someName[0].Key", "42" },
{ "someName[1]", new KeyValuePair<int, string>(84, "eighty-four") }, { "someName[0].Value", "forty-two" },
{ "someName[1].Key", "84" },
{ "someName[1].Value", "eighty-four" },
}; };
var bindingContext = GetModelBindingContext(isReadOnly, values); var bindingContext = GetModelBindingContext(isReadOnly, values);
var modelState = bindingContext.ModelState; bindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", values);
var dictionary = new Dictionary<int, string>(); var dictionary = new Dictionary<int, string>();
bindingContext.Model = dictionary; bindingContext.Model = dictionary;
var binder = new DictionaryModelBinder<int, string>();
var binder = new DictionaryModelBinder<int, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
// Assert // Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Same(dictionary, result.Model);
Assert.True(modelState.IsValid);
Assert.Same(dictionary, result.Model);
Assert.NotNull(dictionary); Assert.NotNull(dictionary);
Assert.Equal(2, dictionary.Count); Assert.Equal(2, dictionary.Count);
Assert.Equal("forty-two", dictionary[42]); Assert.Equal("forty-two", dictionary[42]);
@ -123,10 +127,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
IDictionary<string, string> dictionary) IDictionary<string, string> dictionary)
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.ModelName = modelName; context.ModelName = modelName;
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary); context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider; context.ValueProvider = context.OperationBindingContext.ValueProvider;
context.FieldName = modelName; context.FieldName = modelName;
@ -140,7 +144,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var result = await binder.BindModelResultAsync(context); var result = await binder.BindModelResultAsync(context);
// Assert // Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Equal(modelName, result.Key); Assert.Equal(modelName, result.Key);
@ -160,10 +163,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ "three", "three" }, { "three", "three" },
}; };
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.ModelName = "prefix"; context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateTestValueProvider("prefix[{0}]", dictionary); context.OperationBindingContext.ValueProvider = CreateTestValueProvider("prefix[{0}]", dictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider; context.ValueProvider = context.OperationBindingContext.ValueProvider;
context.FieldName = context.ModelName; context.FieldName = context.ModelName;
@ -210,10 +213,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ {
// Arrange // Arrange
var stringDictionary = dictionary.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.ToString()); var stringDictionary = dictionary.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.ToString());
var binder = new DictionaryModelBinder<long, int>();
var binder = new DictionaryModelBinder<long, int>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.ModelName = "prefix"; context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = context.OperationBindingContext.ValueProvider =
CreateEnumerableValueProvider("prefix[{0}]", stringDictionary); CreateEnumerableValueProvider("prefix[{0}]", stringDictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider; context.ValueProvider = context.OperationBindingContext.ValueProvider;
@ -252,10 +256,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ "prefix[27].Id", "98" }, { "prefix[27].Id", "98" },
{ "prefix[27].Name", "Fred" }, { "prefix[27].Name", "Fred" },
}; };
var binder = new DictionaryModelBinder<int, ModelWithProperties>();
var context = CreateContext(); var context = CreateContext();
context.ModelName = "prefix"; context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", stringDictionary); context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", stringDictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider; context.ValueProvider = context.OperationBindingContext.ValueProvider;
context.FieldName = context.ModelName; context.FieldName = context.ModelName;
@ -265,6 +268,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
typeof(ModelWithDictionaryProperties), typeof(ModelWithDictionaryProperties),
nameof(ModelWithDictionaryProperties.DictionaryWithComplexValuesProperty)); nameof(ModelWithDictionaryProperties.DictionaryWithComplexValuesProperty));
var valueMetadata = metadataProvider.GetMetadataForType(typeof(ModelWithProperties));
var binder = new DictionaryModelBinder<int, ModelWithProperties>(
new SimpleTypeModelBinder(),
new ComplexTypeModelBinder(new Dictionary<ModelMetadata, IModelBinder>()
{
{ valueMetadata.Properties["Id"], new SimpleTypeModelBinder() },
{ valueMetadata.Properties["Name"], new SimpleTypeModelBinder() },
}));
// Act // Act
var result = await binder.BindModelResultAsync(context); var result = await binder.BindModelResultAsync(context);
@ -298,10 +311,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ {
// Arrange // Arrange
var expectedDictionary = new SortedDictionary<string, string>(dictionary); var expectedDictionary = new SortedDictionary<string, string>(dictionary);
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.ModelName = modelName; context.ModelName = modelName;
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary); context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider; context.ValueProvider = context.OperationBindingContext.ValueProvider;
context.FieldName = context.ModelName; context.FieldName = context.ModelName;
@ -327,7 +341,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObject() public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.IsTopLevelObject = true; context.IsTopLevelObject = true;
@ -357,7 +371,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix) public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<int, int>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty"); context.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty");
@ -399,7 +413,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public void CanCreateInstance_ReturnsExpectedValue(Type modelType, bool expectedResult) public void CanCreateInstance_ReturnsExpectedValue(Type modelType, bool expectedResult)
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<int, int>(); var binder = new DictionaryModelBinder<int, int>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
// Act // Act
var result = binder.CanCreateInstance(modelType); var result = binder.CanCreateInstance(modelType);
@ -427,17 +441,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
return modelBindingContext; return modelBindingContext;
} }
private static IModelBinder CreateCompositeBinder()
{
var binders = new IModelBinder[]
{
new SimpleTypeModelBinder(),
new MutableObjectModelBinder(),
};
return new CompositeModelBinder(binders);
}
private static IValueProvider CreateEnumerableValueProvider( private static IValueProvider CreateEnumerableValueProvider(
string keyFormat, string keyFormat,
IDictionary<string, string> dictionary) IDictionary<string, string> dictionary)
@ -468,7 +471,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
private static DefaultModelBindingContext GetModelBindingContext( private static DefaultModelBindingContext GetModelBindingContext(
bool isReadOnly, bool isReadOnly,
IDictionary<string, KeyValuePair<int, string>> values) IDictionary<string, string> values = null)
{ {
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
metadataProvider metadataProvider
@ -478,15 +481,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
typeof(ModelWithIDictionaryProperty), typeof(ModelWithIDictionaryProperty),
nameof(ModelWithIDictionaryProperty.DictionaryProperty)); nameof(ModelWithIDictionaryProperty.DictionaryProperty));
var binder = new StubModelBinder(mbc =>
{
KeyValuePair<int, string> value;
if (values.TryGetValue(mbc.ModelName, out value))
{
mbc.Result = ModelBindingResult.Success(mbc.ModelName, value);
}
});
var valueProvider = new SimpleValueProvider(); var valueProvider = new SimpleValueProvider();
foreach (var kvp in values) foreach (var kvp in values)
{ {
@ -500,7 +494,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
ModelState = new ModelStateDictionary(), ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext OperationBindingContext = new OperationBindingContext
{ {
ModelBinder = binder.Object,
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
ValueProvider = valueProvider, ValueProvider = valueProvider,
}, },

View File

@ -0,0 +1,50 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class FormCollectionModelBinderProviderTest
{
[Theory]
[InlineData(typeof(FormCollection))]
[InlineData(typeof(TestClass))]
[InlineData(typeof(IList<int>))]
[InlineData(typeof(int[]))]
public void Create_ForNonFormCollectionTypes_ReturnsNull(Type modelType)
{
// Arrange
var provider = new FormCollectionModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_ForFormCollectionToken_ReturnsBinder()
{
// Arrange
var provider = new FormCollectionModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(IFormCollection));
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<FormCollectionModelBinder>(result);
}
private class TestClass
{
}
}
}

View File

@ -12,7 +12,7 @@ using Microsoft.Extensions.Primitives;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class FormCollectionModelBinderTest public class FormCollectionModelBinderTest
{ {
@ -47,47 +47,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal("value2", form["field2"]); Assert.Equal("value2", form["field2"]);
} }
[Fact]
public async Task FormCollectionModelBinder_InvalidType_BindFails()
{
// Arrange
var formCollection = new FormCollection(new Dictionary<string, StringValues>
{
{ "field1", "value1" },
{ "field2", "value2" }
});
var httpContext = GetMockHttpContext(formCollection);
var bindingContext = GetBindingContext(typeof(string), httpContext);
var binder = new FormCollectionModelBinder();
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
// We only support IFormCollection here. Using the concrete type won't work.
[Fact]
public async Task FormCollectionModelBinder_FormCollectionConcreteType_BindFails()
{
// Arrange
var formCollection = new FormCollection(new Dictionary<string, StringValues>
{
{ "field1", "value1" },
{ "field2", new string[] { "value2" } }
});
var httpContext = GetMockHttpContext(formCollection);
var bindingContext = GetBindingContext(typeof(FormCollection), httpContext);
var binder = new FormCollectionModelBinder();
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact] [Fact]
public async Task FormCollectionModelBinder_NoForm_BindSuccessful_ReturnsEmptyFormCollection() public async Task FormCollectionModelBinder_NoForm_BindSuccessful_ReturnsEmptyFormCollection()
{ {
@ -127,7 +86,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
HttpContext = httpContext, HttpContext = httpContext,
}, },
ModelBinder = new FormCollectionModelBinder(),
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
}, },
ValidationState = new ValidationStateDictionary(), ValidationState = new ValidationStateDictionary(),

View File

@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class FormFileModelBinderProviderTest
{
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(IFormCollection))]
[InlineData(typeof(TestClass))]
[InlineData(typeof(IList<int>))]
public void Create_ForUnsupportedTypes_ReturnsNull(Type modelType)
{
// Arrange
var provider = new FormFileModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData(typeof(IFormFile))]
[InlineData(typeof(IFormFile[]))]
[InlineData(typeof(IFormFileCollection))]
[InlineData(typeof(IEnumerable<IFormFile>))]
[InlineData(typeof(Collection<IFormFile>))]
public void Create_ForSupportedTypes_ReturnsBinder(Type modelType)
{
// Arrange
var provider = new FormFileModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<FormFileModelBinder>(result);
}
private class TestClass
{
}
}
}

View File

@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class FormFileModelBinderTest public class FormFileModelBinderTest
{ {
@ -108,24 +108,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal("file1.txt", file.FileName); Assert.Equal("file1.txt", file.FileName);
} }
[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(IEnumerable<string>))]
public async Task FormFileModelBinder_ReturnsNothing_ForUnsupportedDestinationTypes(Type destinationType)
{
// Arrange
var formFiles = GetTwoFiles();
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
var bindingContext = GetBindingContext(destinationType, httpContext);
var binder = new FormFileModelBinder();
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact] [Fact]
public async Task FormFileModelBinder_ReturnsFailedResult_WhenNoFilePosted() public async Task FormFileModelBinder_ReturnsFailedResult_WhenNoFilePosted()
{ {
@ -303,7 +285,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
HttpContext = httpContext, HttpContext = httpContext,
}, },
ModelBinder = new FormFileModelBinder(),
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
}, },
ValidationState = new ValidationStateDictionary(), ValidationState = new ValidationStateDictionary(),

View File

@ -0,0 +1,105 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class HeaderModelBinderProviderTest
{
public static TheoryData<BindingSource> NonHeaderBindingSources
{
get
{
return new TheoryData<BindingSource>()
{
BindingSource.Body,
BindingSource.Form,
null,
};
}
}
[Theory]
[MemberData(nameof(NonHeaderBindingSources))]
public void Create_WhenBindingSourceIsNotFromHeader_ReturnsNull(BindingSource source)
{
// Arrange
var provider = new HeaderModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(string));
context.BindingInfo.BindingSource = source;
// Act
var result = provider.GetBinder(context);
// Assert
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)
{
// Arrange
var provider = new HeaderModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
context.BindingInfo.BindingSource = BindingSource.Header;
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<HeaderModelBinder>(result);
}
[Theory]
[InlineData(typeof(Dictionary<int, string>))]
[InlineData(typeof(Collection<int>))]
[InlineData(typeof(Person))]
public void Create_WhenModelTypeIsUnsupportedType_ReturnsNull(Type modelType)
{
// Arrange
var provider = new HeaderModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
context.BindingInfo.BindingSource = BindingSource.Header;
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
}

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class HeaderModelBinderTests public class HeaderModelBinderTests
{ {
@ -101,50 +101,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Equal(headerValue.Split(','), result.Model as IEnumerable<string>); Assert.Equal(headerValue.Split(','), result.Model as IEnumerable<string>);
} }
[Fact]
public async Task HeaderBinder_ReturnsNothing_ForNullBindingSource()
{
// Arrange
var type = typeof(string);
var header = "User-Agent";
var headerValue = "UnitTest";
var binder = new HeaderModelBinder();
var modelBindingContext = GetBindingContext(type);
modelBindingContext.BindingSource = null;
modelBindingContext.FieldName = header;
modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
// Act
var result = await binder.BindModelResultAsync(modelBindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact]
public async Task HeaderBinder_ReturnsNothing_ForNonHeaderBindingSource()
{
// Arrange
var type = typeof(string);
var header = "User-Agent";
var headerValue = "UnitTest";
var binder = new HeaderModelBinder();
var modelBindingContext = GetBindingContext(type);
modelBindingContext.BindingSource = BindingSource.Body;
modelBindingContext.FieldName = header;
modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
// Act
var result = await binder.BindModelResultAsync(modelBindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact] [Fact]
public async Task HeaderBinder_ReturnsFailedResult_ForReadOnlyDestination() public async Task HeaderBinder_ReturnsFailedResult_ForReadOnlyDestination()
{ {
@ -227,7 +183,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ {
HttpContext = new DefaultHttpContext(), HttpContext = new DefaultHttpContext(),
}, },
ModelBinder = new HeaderModelBinder(),
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
}, },
BinderModelName = modelMetadata.BinderModelName, BinderModelName = modelMetadata.BinderModelName,

View File

@ -0,0 +1,66 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class KeyValuePairModelBinderProviderTest
{
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(Person))]
[InlineData(typeof(KeyValuePair<string, int>?))]
[InlineData(typeof(KeyValuePair<string, int>[]))]
public void Create_ForNonKeyValuePair_ReturnsNull(Type modelType)
{
// Arrange
var provider = new KeyValuePairModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_ForKeyValuePair_ReturnsBinder()
{
// Arrange
var provider = new KeyValuePairModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(KeyValuePair<string, int>));
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(string) || m.ModelType == typeof(int))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<KeyValuePairModelBinder<string, int>>(result);
}
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
}

View File

@ -7,11 +7,9 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class KeyValuePairModelBinderTest public class KeyValuePairModelBinderTest
{ {
@ -22,8 +20,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var valueProvider = new SimpleValueProvider(); var valueProvider = new SimpleValueProvider();
// Create string binder to create the value but not the key. // Create string binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider, CreateStringBinder()); var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair<int, string>));
var binder = new KeyValuePairModelBinder<int, string>(); var binder = new KeyValuePairModelBinder<int, string>(CreateIntBinder(false), CreateStringBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -44,8 +42,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var valueProvider = new SimpleValueProvider(); var valueProvider = new SimpleValueProvider();
// Create int binder to create the value but not the key. // Create int binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider, CreateIntBinder()); var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair<int, string>));
var binder = new KeyValuePairModelBinder<int, string>(); var binder = new KeyValuePairModelBinder<int, string>(CreateIntBinder(), CreateStringBinder(false));
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -67,11 +65,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var valueProvider = new SimpleValueProvider(); var valueProvider = new SimpleValueProvider();
// Create int binder to create the value but not the key. // Create int binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider); var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair<int, string>));
var mockBinder = new StubModelBinder(); var binder = new KeyValuePairModelBinder<int, string>(CreateIntBinder(false), CreateStringBinder(false));
bindingContext.OperationBindingContext.ModelBinder = mockBinder;
var binder = new KeyValuePairModelBinder<int, string>();
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -86,11 +81,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task BindModel_SubBindingSucceeds() public async Task BindModel_SubBindingSucceeds()
{ {
// Arrange // Arrange
var innerBinder = new CompositeModelBinder(new[] { CreateStringBinder(), CreateIntBinder() });
var valueProvider = new SimpleValueProvider(); var valueProvider = new SimpleValueProvider();
var bindingContext = GetBindingContext(valueProvider, innerBinder);
var binder = new KeyValuePairModelBinder<int, string>(); var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair<int, string>));
var binder = new KeyValuePairModelBinder<int, string>(CreateIntBinder(), CreateStringBinder());
// Act // Act
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
@ -124,12 +118,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Equal("someName.key", context.ModelName); Assert.Equal("someName.key", context.ModelName);
return innerResult; return innerResult;
}); });
var bindingContext = GetBindingContext(new SimpleValueProvider(), innerBinder);
var binder = new KeyValuePairModelBinder<int, string>(); var valueProvider = new SimpleValueProvider();
var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair<int, string>));
var binder = new KeyValuePairModelBinder<int, string>(innerBinder, innerBinder);
// Act // Act
var result = await binder.TryBindStrongModel<int>(bindingContext, "key"); var result = await binder.TryBindStrongModel<int>(bindingContext, innerBinder, "key");
// Assert // Assert
Assert.Equal(innerResult.Value, result); Assert.Equal(innerResult.Value, result);
Assert.Empty(bindingContext.ModelState); Assert.Empty(bindingContext.ModelState);
@ -139,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task KeyValuePairModelBinder_CreatesEmptyCollection_IfIsTopLevelObject() public async Task KeyValuePairModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{ {
// Arrange // Arrange
var binder = new KeyValuePairModelBinder<string, string>(); var binder = new KeyValuePairModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.IsTopLevelObject = true; context.IsTopLevelObject = true;
@ -170,7 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix) public async Task KeyValuePairModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new KeyValuePairModelBinder<string, string>(); var binder = new KeyValuePairModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext(); var context = CreateContext();
context.ModelName = ModelNames.CreatePropertyModelName(prefix, "KeyValuePairProperty"); context.ModelName = ModelNames.CreatePropertyModelName(prefix, "KeyValuePairProperty");
@ -200,7 +197,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
HttpContext = new DefaultHttpContext(), HttpContext = new DefaultHttpContext(),
}, },
MetadataProvider = new TestModelMetadataProvider(), MetadataProvider = new TestModelMetadataProvider(),
ModelBinder = new SimpleTypeModelBinder(),
}, },
ModelState = new ModelStateDictionary(), ModelState = new ModelStateDictionary(),
}; };
@ -210,20 +206,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
private static DefaultModelBindingContext GetBindingContext( private static DefaultModelBindingContext GetBindingContext(
IValueProvider valueProvider, IValueProvider valueProvider,
IModelBinder innerBinder = null, Type keyValuePairType)
Type keyValuePairType = null)
{ {
var metataProvider = new EmptyModelMetadataProvider(); var metataProvider = new EmptyModelMetadataProvider();
var bindingContext = new DefaultModelBindingContext var bindingContext = new DefaultModelBindingContext
{ {
ModelMetadata = metataProvider.GetMetadataForType( ModelMetadata = metataProvider.GetMetadataForType(keyValuePairType),
keyValuePairType ?? typeof(KeyValuePair<int, string>)),
ModelName = "someName", ModelName = "someName",
ModelState = new ModelStateDictionary(), ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider, ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext OperationBindingContext = new OperationBindingContext
{ {
ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider, MetadataProvider = metataProvider,
ValidatorProvider = new DataAnnotationsModelValidatorProvider( ValidatorProvider = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(), new ValidationAttributeAdapterProvider(),
@ -234,11 +227,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
return bindingContext; return bindingContext;
} }
private static IModelBinder CreateIntBinder() private static IModelBinder CreateIntBinder(bool success = true)
{ {
var mockIntBinder = new StubModelBinder(mbc => var mockIntBinder = new StubModelBinder(mbc =>
{ {
if (mbc.ModelType == typeof(int)) if (mbc.ModelType == typeof(int) && success)
{ {
var model = 42; var model = 42;
return ModelBindingResult.Success(mbc.ModelName, model); return ModelBindingResult.Success(mbc.ModelName, model);
@ -248,11 +241,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
return mockIntBinder; return mockIntBinder;
} }
private static IModelBinder CreateStringBinder() private static IModelBinder CreateStringBinder(bool success = true)
{ {
return new StubModelBinder(mbc => return new StubModelBinder(mbc =>
{ {
if (mbc.ModelType == typeof(string)) if (mbc.ModelType == typeof(string) && success)
{ {
var model = "some-value"; var model = "some-value";
return ModelBindingResult.Success(mbc.ModelName, model); return ModelBindingResult.Success(mbc.ModelName, model);

View File

@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class ServicesModelBinderProviderTest
{
public static TheoryData<BindingSource> NonServicesBindingSources
{
get
{
return new TheoryData<BindingSource>()
{
BindingSource.Header,
BindingSource.Form,
null,
};
}
}
[Theory]
[MemberData(nameof(NonServicesBindingSources))]
public void Create_WhenBindingSourceIsNotFromServices_ReturnsNull(BindingSource source)
{
// Arrange
var provider = new ServicesModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(IPersonService));
context.BindingInfo.BindingSource = source;
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_WhenBindingSourceIsFromServices_ReturnsBinder()
{
// Arrange
var provider = new ServicesModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(IPersonService));
context.BindingInfo.BindingSource = BindingSource.Services;
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<ServicesModelBinder>(result);
}
private class IPersonService
{
}
}
}

View File

@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class ServicesModelBinderTest public class ServicesModelBinderTest
{ {
@ -36,40 +36,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Null(entry.Metadata); Assert.Null(entry.Metadata);
} }
[Fact]
public async Task ServiceModelBinder_ReturnsNothing_ForNullBindingSource()
{
// Arrange
var type = typeof(IService);
var binder = new ServicesModelBinder();
var modelBindingContext = GetBindingContext(type);
modelBindingContext.BindingSource = null;
// Act
var result = await binder.BindModelResultAsync(modelBindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact]
public async Task ServiceModelBinder_ReturnsNothing_ForNonServiceBindingSource()
{
// Arrange
var type = typeof(IService);
var binder = new ServicesModelBinder();
var modelBindingContext = GetBindingContext(type);
modelBindingContext.BindingSource = BindingSource.Body;
// Act
var result = await binder.BindModelResultAsync(modelBindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
private static DefaultModelBindingContext GetBindingContext(Type modelType) private static DefaultModelBindingContext GetBindingContext(Type modelType)
{ {
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
@ -95,7 +61,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
RequestServices = services.BuildServiceProvider(), RequestServices = services.BuildServiceProvider(),
} }
}, },
ModelBinder = new HeaderModelBinder(),
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
}, },
BinderModelName = modelMetadata.BinderModelName, BinderModelName = modelMetadata.BinderModelName,

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class SimpleTypeModelBinderProviderTest
{
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(Calendar))]
[InlineData(typeof(TestClass))]
[InlineData(typeof(List<int>))]
public void Create_ForCollectionOrComplexTypes_ReturnsNull(Type modelType)
{
// Arrange
var provider = new SimpleTypeModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(string))]
[InlineData(typeof(DateTime))]
[InlineData(typeof(DateTime?))]
public void Create_ForSimpleTypes_ReturnsBinder(Type modelType)
{
// Arrange
var provider = new SimpleTypeModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<SimpleTypeModelBinder>(result);
}
private class TestClass
{
}
}
}

View File

@ -7,32 +7,10 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
public class SimpleTypeModelBinderTest public class SimpleTypeModelBinderTest
{ {
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(Calendar))]
[InlineData(typeof(TestClass))]
public async Task BindModel_ReturnsNothing_IfTypeCannotBeConverted(Type destinationType)
{
// Arrange
var bindingContext = GetBindingContext(destinationType);
bindingContext.ValueProvider = new SimpleValueProvider
{
{ "theModelName", "some-value" }
};
var binder = new SimpleTypeModelBinder();
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
public static TheoryData<Type> ConvertableTypeData public static TheoryData<Type> ConvertableTypeData
{ {
get get
@ -134,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task BindModel_NullValueProviderResult_ReturnsNull() public async Task BindModel_EmptyValueProviderResult_ReturnsFailed()
{ {
// Arrange // Arrange
var bindingContext = GetBindingContext(typeof(int)); var bindingContext = GetBindingContext(typeof(int));
@ -144,7 +122,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var result = await binder.BindModelResultAsync(bindingContext); var result = await binder.BindModelResultAsync(bindingContext);
// Assert // Assert
Assert.Equal(default(ModelBindingResult), result); Assert.Equal(ModelBindingResult.Failed("theModelName"), result);
Assert.Empty(bindingContext.ModelState); Assert.Empty(bindingContext.ModelState);
} }

View File

@ -1,532 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{
public class CompositeModelBinderTest
{
[Fact]
public async Task BindModel_SuccessfulBind_ReturnsModel()
{
// Arrange
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someName", "dummyValue" }
},
ValidationState = new ValidationStateDictionary(),
FieldName = "someName",
};
var mockIntBinder = new StubModelBinder(context =>
{
Assert.Same(bindingContext.ModelMetadata, context.ModelMetadata);
Assert.Equal("someName", context.ModelName);
Assert.Same(bindingContext.ValueProvider, context.ValueProvider);
context.Result = ModelBindingResult.Success("someName", 42);
});
var shimBinder = CreateCompositeBinder(mockIntBinder);
// Act
var result = await shimBinder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.Equal(42, result.Model);
}
[Fact]
public async Task BindModel_SuccessfulBind_SetsValidationStateAtTopLevel()
{
// Arrange
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = true,
IsTopLevelObject = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someName", "dummyValue" }
},
ValidationState = new ValidationStateDictionary(),
FieldName = "someName",
};
var mockIntBinder = new StubModelBinder(context =>
{
Assert.Same(bindingContext.ModelMetadata, context.ModelMetadata);
Assert.Equal("someName", context.ModelName);
Assert.Same(bindingContext.ValueProvider, context.ValueProvider);
context.Result = ModelBindingResult.Success("someName", 42);
});
var shimBinder = CreateCompositeBinder(mockIntBinder);
// Act
var result = await shimBinder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.Equal(42, result.Model);
Assert.Contains(result.Model, bindingContext.ValidationState.Keys);
var entry = bindingContext.ValidationState[result.Model];
Assert.Equal("someName", entry.Key);
Assert.Same(bindingContext.ModelMetadata, entry.Metadata);
}
[Fact]
public async Task BindModel_SuccessfulBind_DoesNotSetValidationState_WhenNotTopLevel()
{
// Arrange
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someName", "dummyValue" }
},
ValidationState = new ValidationStateDictionary(),
FieldName = "someName",
};
var mockIntBinder = new StubModelBinder(context =>
{
Assert.Same(bindingContext.ModelMetadata, context.ModelMetadata);
Assert.Equal("someName", context.ModelName);
Assert.Same(bindingContext.ValueProvider, context.ValueProvider);
context.Result = ModelBindingResult.Success("someName", 42);
});
var shimBinder = CreateCompositeBinder(mockIntBinder);
// Act
var result = await shimBinder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.Equal(42, result.Model);
Assert.Empty(bindingContext.ValidationState);
}
[Fact]
public async Task BindModel_SuccessfulBind_ComplexTypeFallback_ReturnsModel()
{
// Arrange
var expectedModel = new List<int> { 1, 2, 3, 4, 5 };
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = true,
IsTopLevelObject = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someOtherName", "dummyValue" }
},
ValidationState = new ValidationStateDictionary(),
FieldName = "someName",
};
var mockIntBinder = new StubModelBinder(mbc =>
{
if (!string.IsNullOrEmpty(mbc.ModelName))
{
return;
}
Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata);
Assert.Equal("", mbc.ModelName);
Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
mbc.Result = ModelBindingResult.Success(string.Empty, expectedModel);
});
var shimBinder = CreateCompositeBinder(mockIntBinder);
// Act
var result = await shimBinder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.Equal(string.Empty, result.Key);
Assert.Equal(expectedModel, result.Model);
}
[Fact]
public async Task ModelBinder_ReturnsNothing_IfBinderMatchesButDoesNotSetModel()
{
// Arrange
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someOtherName", "dummyValue" }
},
FieldName = "someName",
};
var mockIntBinder = new StubModelBinder(context =>
{
context.Result = ModelBindingResult.Failed("someName");
});
var composite = CreateCompositeBinder(mockIntBinder);
// Act
var result = await composite.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact]
public async Task ModelBinder_DoesNotFallBackToEmpty_IfFallbackToEmptyPrefixFalse()
{
// Arrange
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = false,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someOtherName", "dummyValue" }
},
FieldName = "someName",
};
var mockIntBinder = new StubModelBinder(context =>
{
Assert.Equal("someName", context.ModelName);
context.Result = ModelBindingResult.Failed("someName");
});
var composite = CreateCompositeBinder(mockIntBinder);
// Act & Assert
var result = await composite.BindModelResultAsync(bindingContext);
Assert.Equal(1, mockIntBinder.BindModelCount);
}
[Fact]
public async Task ModelBinder_DoesNotFallBackToEmpty_IfErrorsAreAdded()
{
// Arrange
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = false,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someOtherName", "dummyValue" }
},
FieldName = "someName",
};
var mockIntBinder = new StubModelBinder(context =>
{
Assert.Equal("someName", context.ModelName);
context.ModelState.AddModelError(context.ModelName, "this is an error message");
context.Result = ModelBindingResult.Failed("someName");
});
var composite = CreateCompositeBinder(mockIntBinder);
// Act & Assert
var result = await composite.BindModelResultAsync(bindingContext);
Assert.Equal(1, mockIntBinder.BindModelCount);
}
[Fact]
public async Task ModelBinder_ReturnsNonEmptyResult_SetsNullValue_SetsModelStateKey()
{
// Arrange
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someOtherName", "dummyValue" }
},
FieldName = "someName",
};
var mockIntBinder = new StubModelBinder(context =>
{
context.Result = ModelBindingResult.Success("someName", model: null);
});
var composite = CreateCompositeBinder(mockIntBinder);
// Act
var result = await composite.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.Equal("someName", result.Key);
Assert.Null(result.Model);
}
[Fact]
public async Task BindModel_UnsuccessfulBind_SimpleTypeNoFallback_ReturnsNothing()
{
// Arrange
var shimBinder = CreateCompositeBinder(new StubModelBinder());
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)),
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider(),
FieldName = "test-field",
};
// Act
var result = await shimBinder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact]
public async Task BindModel_WithDefaultBinders_BindsSimpleType()
{
// Arrange
var binder = CreateBinderWithDefaults();
var valueProvider = new SimpleValueProvider
{
{ "firstName", "firstName-value"},
{ "lastName", "lastName-value"}
};
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
var model = Assert.IsType<SimplePropertiesModel>(result.Model);
Assert.Equal("firstName-value", model.FirstName);
Assert.Equal("lastName-value", model.LastName);
}
[Fact]
public async Task BindModel_WithDefaultBinders_BindsComplexType()
{
// Arrange
var binder = CreateBinderWithDefaults();
var valueProvider = new SimpleValueProvider
{
{ "firstName", "firstName-value"},
{ "lastName", "lastName-value"},
{ "friends[0].firstName", "first-friend"},
{ "friends[0].age", "40"},
{ "friends[0].friends[0].firstname", "nested friend"},
{ "friends[1].firstName", "some other"},
{ "friends[1].lastName", "name"},
{ "resume", "4+mFeTp3tPF=" }
};
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(Person));
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
var model = Assert.IsType<Person>(result.Model);
Assert.Equal("firstName-value", model.FirstName);
Assert.Equal("lastName-value", model.LastName);
Assert.Equal(2, model.Friends.Count);
Assert.Equal("first-friend", model.Friends[0].FirstName);
Assert.Equal(40, model.Friends[0].Age);
var nestedFriend = Assert.Single(model.Friends[0].Friends);
Assert.Equal("nested friend", nestedFriend.FirstName);
Assert.Equal("some other", model.Friends[1].FirstName);
Assert.Equal("name", model.Friends[1].LastName);
Assert.Equal(new byte[] { 227, 233, 133, 121, 58, 119, 180, 241 }, model.Resume);
}
[Fact]
public async Task BindModel_DoesNotAddAValidationNode_IfModelIsNotSet()
{
// Arrange
var valueProvider = new SimpleValueProvider();
var mockBinder = new StubModelBinder(ModelBindingResult.Failed("someName"));
var binder = CreateCompositeBinder(mockBinder);
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact]
public async Task BindModel_DoesNotAddAValidationNode_IfModelBindingResultIsNothing()
{
// Arrange
var mockBinder = new StubModelBinder();
var binder = CreateCompositeBinder(mockBinder);
var valueProvider = new SimpleValueProvider();
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact]
public async Task BindModel_UsesTheValidationNodeOnModelBindingResult_IfPresent()
{
// Arrange
var valueProvider = new SimpleValueProvider();
var mockBinder = new StubModelBinder(ModelBindingResult.Success("someName", 42));
var binder = CreateCompositeBinder(mockBinder);
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
}
private static DefaultModelBindingContext CreateBindingContext(
IModelBinder binder,
IValueProvider valueProvider,
Type type)
{
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var bindingContext = new DefaultModelBindingContext
{
FallbackToEmptyPrefix = true,
IsTopLevelObject = true,
ModelMetadata = metadataProvider.GetMetadataForType(type),
ModelName = "parameter",
FieldName = "parameter",
ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext
{
MetadataProvider = metadataProvider,
ModelBinder = binder,
},
ValidationState = new ValidationStateDictionary(),
};
return bindingContext;
}
private static CompositeModelBinder CreateBinderWithDefaults()
{
var binders = new IModelBinder[]
{
new ByteArrayModelBinder(),
new GenericModelBinder(),
new SimpleTypeModelBinder(),
new MutableObjectModelBinder()
};
var binder = new CompositeModelBinder(binders);
return binder;
}
private static CompositeModelBinder CreateCompositeBinder(IModelBinder mockIntBinder)
{
var shimBinder = new CompositeModelBinder(new[] { mockIntBinder });
return shimBinder;
}
private class SimplePropertiesModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
private sealed class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public List<Person> Friends { get; set; }
public byte[] Resume { get; set; }
}
private class User : IValidatableObject
{
public string Password { get; set; }
[Compare("Password")]
public string ConfirmPassword { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Password == "password")
{
yield return new ValidationResult("Password does not meet complexity requirements.");
}
}
}
}
}

View File

@ -2,9 +2,13 @@
// 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.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding.Test; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding
@ -37,7 +41,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Act // Act
var originalBinderModelName = bindingContext.BinderModelName; var originalBinderModelName = bindingContext.BinderModelName;
var originalBinderType = bindingContext.BinderType;
var originalBindingSource = bindingContext.BindingSource; var originalBindingSource = bindingContext.BindingSource;
var originalModelState = bindingContext.ModelState; var originalModelState = bindingContext.ModelState;
var originalOperationBindingContext = bindingContext.OperationBindingContext; var originalOperationBindingContext = bindingContext.OperationBindingContext;
@ -51,9 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert // Assert
Assert.Same(newModelMetadata.BinderModelName, bindingContext.BinderModelName); Assert.Same(newModelMetadata.BinderModelName, bindingContext.BinderModelName);
Assert.Same(newModelMetadata.BinderType, bindingContext.BinderType);
Assert.Same(newModelMetadata.BindingSource, bindingContext.BindingSource); Assert.Same(newModelMetadata.BindingSource, bindingContext.BindingSource);
Assert.False(bindingContext.FallbackToEmptyPrefix);
Assert.Equal("fieldName", bindingContext.FieldName); Assert.Equal("fieldName", bindingContext.FieldName);
Assert.False(bindingContext.IsTopLevelObject); Assert.False(bindingContext.IsTopLevelObject);
Assert.Null(bindingContext.Model); Assert.Null(bindingContext.Model);
@ -66,6 +67,98 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
disposable.Dispose(); disposable.Dispose();
} }
[Fact]
public void CreateBindingContext_FiltersValueProviders_ForValueProviderSource()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var original = CreateDefaultValueProvider();
var operationBindingContext = new OperationBindingContext()
{
ActionContext = new ActionContext(),
ValueProvider = original,
};
// Act
var context = DefaultModelBindingContext.CreateBindingContext(
operationBindingContext,
metadataProvider.GetMetadataForType(typeof(object)),
new BindingInfo() { BindingSource = BindingSource.Query },
"model");
// Assert
Assert.Collection(
Assert.IsType<CompositeValueProvider>(context.ValueProvider),
vp => Assert.Same(original[1], vp));
}
[Fact]
public void EnterNestedScope_FiltersValueProviders_ForValueProviderSource()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(typeof(string), nameof(string.Length))
.BindingDetails(b => b.BindingSource = BindingSource.Query);
var original = CreateDefaultValueProvider();
var operationBindingContext = new OperationBindingContext()
{
ActionContext = new ActionContext(),
ValueProvider = original,
};
var context = DefaultModelBindingContext.CreateBindingContext(
operationBindingContext,
metadataProvider.GetMetadataForType(typeof(string)),
new BindingInfo(),
"model");
var propertyMetadata = metadataProvider.GetMetadataForProperty(typeof(string), nameof(string.Length));
// Act
context.EnterNestedScope(propertyMetadata, "Length", "Length", model: null);
// Assert
Assert.Collection(
Assert.IsType<CompositeValueProvider>(context.ValueProvider),
vp => Assert.Same(original[1], vp));
}
[Fact]
public void EnterNestedScope_FiltersValueProviders_BasedOnTopLevelValueProviders()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(typeof(string), nameof(string.Length))
.BindingDetails(b => b.BindingSource = BindingSource.Form);
var original = CreateDefaultValueProvider();
var operationBindingContext = new OperationBindingContext()
{
ActionContext = new ActionContext(),
ValueProvider = original,
};
var context = DefaultModelBindingContext.CreateBindingContext(
operationBindingContext,
metadataProvider.GetMetadataForType(typeof(string)),
new BindingInfo() { BindingSource = BindingSource.Query },
"model");
var propertyMetadata = metadataProvider.GetMetadataForProperty(typeof(string), nameof(string.Length));
// Act
context.EnterNestedScope(propertyMetadata, "Length", "Length", model: null);
// Assert
Assert.Collection(
Assert.IsType<CompositeValueProvider>(context.ValueProvider),
vp => Assert.Same(original[2], vp));
}
[Fact] [Fact]
public void ModelTypeAreFedFromModelMetadata() public void ModelTypeAreFedFromModelMetadata()
{ {
@ -79,6 +172,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(typeof(int), bindingContext.ModelType); Assert.Equal(typeof(int), bindingContext.ModelType);
} }
private static CompositeValueProvider CreateDefaultValueProvider()
{
var result = new CompositeValueProvider();
result.Add(new RouteValueProvider(BindingSource.Path, new RouteValueDictionary()));
result.Add(new QueryStringValueProvider(
BindingSource.Query,
new QueryCollection(),
CultureInfo.InvariantCulture));
result.Add(new FormValueProvider(
BindingSource.Form,
new FormCollection(new Dictionary<string, StringValues>()),
CultureInfo.CurrentCulture));
return result;
}
private class TestModelBinder : IModelBinder private class TestModelBinder : IModelBinder
{ {
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding

View File

@ -0,0 +1,228 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class ModelBinderFactoryTest
{
// No providers => can't create a binder
[Fact]
public void CreateBinder_Throws_WhenBinderNotCreated()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = new TestOptionsManager<MvcOptions>();
var factory = new ModelBinderFactory(metadataProvider, options);
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(string)),
};
// Act
var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateBinder(context));
// Assert
Assert.Equal(
$"Could not create a model binder for model object of type '{typeof(string).FullName}'.",
exception.Message);
}
[Fact]
public void CreateBinder_CreatesNoOpBinder_WhenPropertyDoesntHaveABinder()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
// There isn't a provider that can handle WidgetId.
var options = new TestOptionsManager<MvcOptions>();
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(Widget))
{
Assert.NotNull(c.CreateBinder(c.Metadata.Properties[nameof(Widget.Id)]));
return Mock.Of<IModelBinder>();
}
return null;
}));
var factory = new ModelBinderFactory(metadataProvider, options);
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Widget)),
};
// Act
var result = factory.CreateBinder(context);
// Assert
Assert.NotNull(result);
}
[Fact]
public void CreateBinder_NestedProperties()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = new TestOptionsManager<MvcOptions>();
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(Widget))
{
Assert.NotNull(c.CreateBinder(c.Metadata.Properties[nameof(Widget.Id)]));
return Mock.Of<IModelBinder>();
}
else if (c.Metadata.ModelType == typeof(WidgetId))
{
return Mock.Of<IModelBinder>();
}
return null;
}));
var factory = new ModelBinderFactory(metadataProvider, options);
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Widget)),
};
// Act
var result = factory.CreateBinder(context);
// Assert
Assert.NotNull(result);
}
[Fact]
public void CreateBinder_BreaksCycles()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var callCount = 0;
var options = new TestOptionsManager<MvcOptions>();
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
var currentCallCount = ++callCount;
Assert.Equal(typeof(Employee), c.Metadata.ModelType);
var binder = c.CreateBinder(c.Metadata.Properties[nameof(Employee.Manager)]);
if (currentCallCount == 2)
{
Assert.IsType<PlaceholderBinder>(binder);
}
return Mock.Of<IModelBinder>();
}));
var factory = new ModelBinderFactory(metadataProvider, options);
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Employee)),
};
// Act
var result = factory.CreateBinder(context);
// Assert
Assert.NotNull(result);
}
[Fact]
public void CreateBinder_DoesNotCache_WhenTokenIsNull()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = new TestOptionsManager<MvcOptions>();
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
Assert.Equal(typeof(Employee), c.Metadata.ModelType);
return Mock.Of<IModelBinder>();
}));
var factory = new ModelBinderFactory(metadataProvider, options);
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Employee)),
};
// Act
var result1 = factory.CreateBinder(context);
var result2 = factory.CreateBinder(context);
// Assert
Assert.NotSame(result1, result2);
}
[Fact]
public void CreateBinder_Caches_WhenTokenIsNotNull()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = new TestOptionsManager<MvcOptions>();
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
Assert.Equal(typeof(Employee), c.Metadata.ModelType);
return Mock.Of<IModelBinder>();
}));
var factory = new ModelBinderFactory(metadataProvider, options);
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Employee)),
CacheToken = new object(),
};
// Act
var result1 = factory.CreateBinder(context);
var result2 = factory.CreateBinder(context);
// Assert
Assert.Same(result1, result2);
}
private class Widget
{
public WidgetId Id { get; set; }
}
private class WidgetId
{
}
private class Employee
{
public Employee Manager { get; set; }
}
private class TestModelBinderProvider : IModelBinderProvider
{
private readonly Func<ModelBinderProviderContext, IModelBinder> _factory;
public TestModelBinderProvider(Func<ModelBinderProviderContext, IModelBinder> factory)
{
_factory = factory;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
return _factory(context);
}
}
}
}

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Test; using Microsoft.AspNetCore.Mvc.ModelBinding.Test;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Moq; using Moq;
@ -49,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string.Empty, string.Empty,
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider, metadataProvider,
GetCompositeBinder(binder), GetModelBinderFactory(binder),
Mock.Of<IValueProvider>(), Mock.Of<IValueProvider>(),
new List<IInputFormatter>(), new List<IInputFormatter>(),
new Mock<IObjectModelValidator>(MockBehavior.Strict).Object, new Mock<IObjectModelValidator>(MockBehavior.Strict).Object,
@ -66,10 +67,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Arrange // Arrange
// Mono issue - https://github.com/aspnet/External/issues/19 // Mono issue - https://github.com/aspnet/External/issues/19
var expectedMessage = PlatformNormalizer.NormalizeContent("The MyProperty field is required."); var expectedMessage = PlatformNormalizer.NormalizeContent("The MyProperty field is required.");
var binders = new IModelBinder[] var binderProviders = new IModelBinderProvider[]
{ {
new SimpleTypeModelBinder(), new SimpleTypeModelBinderProvider(),
new MutableObjectModelBinder() new ComplexTypeModelBinderProvider(),
}; };
var validator = new DataAnnotationsModelValidatorProvider( var validator = new DataAnnotationsModelValidatorProvider(
@ -94,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
"", "",
actionContext, actionContext,
modelMetadataProvider, modelMetadataProvider,
GetCompositeBinder(binders), GetModelBinderFactory(binderProviders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(modelMetadataProvider, new ValidatorCache()), new DefaultObjectValidator(modelMetadataProvider, new ValidatorCache()),
@ -110,10 +111,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task TryUpdateModel_ReturnsTrue_IfModelBindsAndValidatesSuccessfully() public async Task TryUpdateModel_ReturnsTrue_IfModelBindsAndValidatesSuccessfully()
{ {
// Arrange // Arrange
var binders = new IModelBinder[] var binderProviders = new IModelBinderProvider[]
{ {
new SimpleTypeModelBinder(), new SimpleTypeModelBinderProvider(),
new MutableObjectModelBinder() new ComplexTypeModelBinderProvider(),
}; };
var validator = new DataAnnotationsModelValidatorProvider( var validator = new DataAnnotationsModelValidatorProvider(
@ -136,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
"", "",
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider, metadataProvider,
GetCompositeBinder(binders), GetModelBinderFactory(binderProviders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider, new ValidatorCache()), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
@ -164,7 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string.Empty, string.Empty,
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider, metadataProvider,
GetCompositeBinder(binder), GetModelBinderFactory(binder),
Mock.Of<IValueProvider>(), Mock.Of<IValueProvider>(),
new List<IInputFormatter>(), new List<IInputFormatter>(),
new Mock<IObjectModelValidator>(MockBehavior.Strict).Object, new Mock<IObjectModelValidator>(MockBehavior.Strict).Object,
@ -182,10 +183,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task TryUpdateModel_UsingIncludePredicateOverload_ReturnsTrue_ModelBindsAndValidatesSuccessfully() public async Task TryUpdateModel_UsingIncludePredicateOverload_ReturnsTrue_ModelBindsAndValidatesSuccessfully()
{ {
// Arrange // Arrange
var binders = new IModelBinder[] var binderProviders = new IModelBinderProvider[]
{ {
new SimpleTypeModelBinder(), new SimpleTypeModelBinderProvider(),
new MutableObjectModelBinder() new ComplexTypeModelBinderProvider(),
}; };
var validator = new DataAnnotationsModelValidatorProvider( var validator = new DataAnnotationsModelValidatorProvider(
@ -220,7 +221,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
"", "",
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider, metadataProvider,
GetCompositeBinder(binders), GetModelBinderFactory(binderProviders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider, new ValidatorCache()), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
@ -250,7 +251,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string.Empty, string.Empty,
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider, metadataProvider,
GetCompositeBinder(binder), GetModelBinderFactory(binder),
Mock.Of<IValueProvider>(), Mock.Of<IValueProvider>(),
new List<IInputFormatter>(), new List<IInputFormatter>(),
new Mock<IObjectModelValidator>(MockBehavior.Strict).Object, new Mock<IObjectModelValidator>(MockBehavior.Strict).Object,
@ -268,10 +269,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task TryUpdateModel_UsingIncludeExpressionOverload_ReturnsTrue_ModelBindsAndValidatesSuccessfully() public async Task TryUpdateModel_UsingIncludeExpressionOverload_ReturnsTrue_ModelBindsAndValidatesSuccessfully()
{ {
// Arrange // Arrange
var binders = new IModelBinder[] var binderProviders = new IModelBinderProvider[]
{ {
new SimpleTypeModelBinder(), new SimpleTypeModelBinderProvider(),
new MutableObjectModelBinder() new ComplexTypeModelBinderProvider(),
}; };
var validator = new DataAnnotationsModelValidatorProvider( var validator = new DataAnnotationsModelValidatorProvider(
@ -302,7 +303,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
"", "",
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
TestModelMetadataProvider.CreateDefaultProvider(), TestModelMetadataProvider.CreateDefaultProvider(),
GetCompositeBinder(binders), GetModelBinderFactory(binderProviders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider, new ValidatorCache()), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
@ -321,10 +322,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task TryUpdateModel_UsingDefaultIncludeOverload_IncludesAllProperties() public async Task TryUpdateModel_UsingDefaultIncludeOverload_IncludesAllProperties()
{ {
// Arrange // Arrange
var binders = new IModelBinder[] var binderProviders = new IModelBinderProvider[]
{ {
new SimpleTypeModelBinder(), new SimpleTypeModelBinderProvider(),
new MutableObjectModelBinder() new ComplexTypeModelBinderProvider(),
}; };
var validator = new DataAnnotationsModelValidatorProvider( var validator = new DataAnnotationsModelValidatorProvider(
@ -355,7 +356,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
"", "",
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider, metadataProvider,
GetCompositeBinder(binders), GetModelBinderFactory(binderProviders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider, new ValidatorCache()), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
@ -506,7 +507,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix: "", prefix: "",
actionContext: new ActionContext() { HttpContext = new DefaultHttpContext() }, actionContext: new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider: metadataProvider, metadataProvider: metadataProvider,
modelBinder: GetCompositeBinder(binder), modelBinderFactory: GetModelBinderFactory(binder),
valueProvider: Mock.Of<IValueProvider>(), valueProvider: Mock.Of<IValueProvider>(),
inputFormatters: new List<IInputFormatter>(), inputFormatters: new List<IInputFormatter>(),
objectModelValidator: new Mock<IObjectModelValidator>(MockBehavior.Strict).Object, objectModelValidator: new Mock<IObjectModelValidator>(MockBehavior.Strict).Object,
@ -524,10 +525,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task TryUpdateModelNonGeneric_PredicateOverload_ReturnsTrue_ModelBindsAndValidatesSuccessfully() public async Task TryUpdateModelNonGeneric_PredicateOverload_ReturnsTrue_ModelBindsAndValidatesSuccessfully()
{ {
// Arrange // Arrange
var binders = new IModelBinder[] var binderProviders = new IModelBinderProvider[]
{ {
new SimpleTypeModelBinder(), new SimpleTypeModelBinderProvider(),
new MutableObjectModelBinder() new ComplexTypeModelBinderProvider(),
}; };
var validator = new DataAnnotationsModelValidatorProvider( var validator = new DataAnnotationsModelValidatorProvider(
@ -563,7 +564,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
"", "",
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider, metadataProvider,
GetCompositeBinder(binders), GetModelBinderFactory(binderProviders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider, new ValidatorCache()), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
@ -595,7 +596,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix: "", prefix: "",
actionContext: new ActionContext() { HttpContext = new DefaultHttpContext() }, actionContext: new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider: metadataProvider, metadataProvider: metadataProvider,
modelBinder: GetCompositeBinder(binder.Object), modelBinderFactory: GetModelBinderFactory(binder.Object),
valueProvider: Mock.Of<IValueProvider>(), valueProvider: Mock.Of<IValueProvider>(),
inputFormatters: new List<IInputFormatter>(), inputFormatters: new List<IInputFormatter>(),
objectModelValidator: new Mock<IObjectModelValidator>(MockBehavior.Strict).Object, objectModelValidator: new Mock<IObjectModelValidator>(MockBehavior.Strict).Object,
@ -610,10 +611,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task TryUpdateModelNonGeneric_ModelTypeOverload_ReturnsTrue_IfModelBindsAndValidatesSuccessfully() public async Task TryUpdateModelNonGeneric_ModelTypeOverload_ReturnsTrue_IfModelBindsAndValidatesSuccessfully()
{ {
// Arrange // Arrange
var binders = new IModelBinder[] var binderProviders = new IModelBinderProvider[]
{ {
new SimpleTypeModelBinder(), new SimpleTypeModelBinderProvider(),
new MutableObjectModelBinder() new ComplexTypeModelBinderProvider(),
}; };
var validator = new DataAnnotationsModelValidatorProvider( var validator = new DataAnnotationsModelValidatorProvider(
@ -637,7 +638,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
"", "",
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
TestModelMetadataProvider.CreateDefaultProvider(), TestModelMetadataProvider.CreateDefaultProvider(),
GetCompositeBinder(binders), GetModelBinderFactory(binderProviders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider, new ValidatorCache()), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
@ -666,7 +667,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
"", "",
new ActionContext() { HttpContext = new DefaultHttpContext() }, new ActionContext() { HttpContext = new DefaultHttpContext() },
metadataProvider, metadataProvider,
GetCompositeBinder(binder.Object), GetModelBinderFactory(binder.Object),
Mock.Of<IValueProvider>(), Mock.Of<IValueProvider>(),
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider, new ValidatorCache()), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
@ -816,9 +817,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
} }
private static IModelBinder GetCompositeBinder(params IModelBinder[] binders) public static ModelBinderFactory GetModelBinderFactory(IModelBinder binder)
{ {
return new CompositeModelBinder(binders); var binderProvider = new Mock<IModelBinderProvider>();
binderProvider
.Setup(p => p.GetBinder(It.IsAny<ModelBinderProviderContext>()))
.Returns(binder);
return TestModelBinderFactory.Create(binderProvider.Object);
}
private static ModelBinderFactory GetModelBinderFactory(params IModelBinderProvider[] providers)
{
return TestModelBinderFactory.CreateDefault(providers);
} }
public class User public class User

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
public sealed class SimpleValueProvider : Dictionary<string, object>, IValueProvider public sealed class SimpleValueProvider : Dictionary<string, object>, IValueProvider
{ {

View File

@ -6,7 +6,7 @@ using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
public class StubModelBinder : IModelBinder public class StubModelBinder : IModelBinder
{ {

View File

@ -0,0 +1,69 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class TestModelBinderProviderContext : ModelBinderProviderContext
{
// Has to be internal because TestModelMetadataProvider is 'shared' code.
internal static readonly TestModelMetadataProvider CachedMetadataProvider = new TestModelMetadataProvider();
private readonly List<Func<ModelMetadata, IModelBinder>> _binderCreators =
new List<Func<ModelMetadata, IModelBinder>>();
public TestModelBinderProviderContext(Type modelType)
{
Metadata = CachedMetadataProvider.GetMetadataForType(modelType);
MetadataProvider = CachedMetadataProvider;
BindingInfo = new BindingInfo()
{
BinderModelName = Metadata.BinderModelName,
BinderType = Metadata.BinderType,
BindingSource = Metadata.BindingSource,
PropertyBindingPredicateProvider = Metadata.PropertyBindingPredicateProvider,
};
}
public TestModelBinderProviderContext(ModelMetadata metadata, BindingInfo bindingInfo)
{
Metadata = metadata;
BindingInfo = bindingInfo;
MetadataProvider = CachedMetadataProvider;
}
public override BindingInfo BindingInfo { get; }
public override ModelMetadata Metadata { get; }
public override IModelMetadataProvider MetadataProvider { get; }
public override IModelBinder CreateBinder(ModelMetadata metadata)
{
foreach (var creator in _binderCreators)
{
var result = creator(metadata);
if (result != null)
{
return result;
}
}
return null;
}
public void OnCreatingBinder(Func<ModelMetadata, IModelBinder> binderCreator)
{
_binderCreators.Add(binderCreator);
}
public void OnCreatingBinder(ModelMetadata metadata, Func<IModelBinder> binderCreator)
{
_binderCreators.Add((m) => m.Equals(metadata) ? binderCreator() : null);
}
}
}

View File

@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext); var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext);
// Assert // Assert
Assert.Equal(null, modelBindingResult); Assert.False(modelBindingResult.Value.IsModelSet);
// ModelState (not set unless inner binder sets it) // ModelState (not set unless inner binder sets it)
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// Assert // Assert
// ModelBindingResult // ModelBindingResult
Assert.Equal(null, modelBindingResult); Assert.False(modelBindingResult.Value.IsModelSet);
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// Assert // Assert
// ModelBindingResult // ModelBindingResult
Assert.Equal(null, modelBindingResult); Assert.False(modelBindingResult.Value.IsModelSet);
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);

View File

@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// Assert // Assert
// ModelBindingResult // ModelBindingResult
Assert.Equal(default(ModelBindingResult), modelBindingResult); Assert.False(modelBindingResult.IsModelSet);
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);

Some files were not shown because too many files have changed in this diff Show More