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 Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
/// <summary>
/// A key type which identifies a <see cref="ModelMetadata"/>.
/// </summary>
public struct ModelMetadataIdentity
public struct ModelMetadataIdentity : IEquatable<ModelMetadataIdentity>
{
/// <summary>
/// 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.
/// </summary>
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>
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>
/// Gets or sets a value which represents the <see cref="ModelBinding.BindingSource"/> associated with the
/// <see cref="Model"/>.
/// </summary>
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>
/// Gets or sets the name of the current field being bound.
/// </summary>

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// A metadata representation of a model type, property or parameter.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
public abstract class ModelMetadata
public abstract class ModelMetadata : IEquatable<ModelMetadata>
{
/// <summary>
/// The default value of <see cref="ModelMetadata.Order"/>.
@ -378,6 +378,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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()
{
Debug.Assert(ModelType != null);

View File

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

View File

@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc
{
private ControllerContext _controllerContext;
private IModelMetadataProvider _metadataProvider;
private IModelBinderFactory _modelBinderFactory;
private IObjectModelValidator _objectValidator;
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>
/// Gets or sets the <see cref="IUrlHelper"/>.
/// </summary>
@ -1139,7 +1165,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix,
ControllerContext,
MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders),
ModelBinderFactory,
valueProvider,
ControllerContext.InputFormatters,
ObjectValidator,
@ -1179,7 +1205,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix,
ControllerContext,
MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders),
ModelBinderFactory,
new CompositeValueProvider(ControllerContext.ValueProviders),
ControllerContext.InputFormatters,
ObjectValidator,
@ -1219,7 +1245,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix,
ControllerContext,
MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders),
ModelBinderFactory,
new CompositeValueProvider(ControllerContext.ValueProviders),
ControllerContext.InputFormatters,
ObjectValidator,
@ -1267,7 +1293,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix,
ControllerContext,
MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders),
ModelBinderFactory,
valueProvider,
ControllerContext.InputFormatters,
ObjectValidator,
@ -1314,7 +1340,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix,
ControllerContext,
MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders),
ModelBinderFactory,
valueProvider,
ControllerContext.InputFormatters,
ObjectValidator,
@ -1353,7 +1379,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix,
ControllerContext,
MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders),
ModelBinderFactory,
new CompositeValueProvider(ControllerContext.ValueProviders),
ControllerContext.InputFormatters,
ObjectValidator,
@ -1405,7 +1431,7 @@ namespace Microsoft.AspNetCore.Mvc
prefix,
ControllerContext,
MetadataProvider,
new CompositeModelBinder(ControllerContext.ModelBinders),
ModelBinderFactory,
valueProvider,
ControllerContext.InputFormatters,
ObjectValidator,

View File

@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Mvc
public class ControllerContext : ActionContext
{
private FormatterCollection<IInputFormatter> _inputFormatters;
private IList<IModelBinder> _modelBinders;
private IList<IModelValidatorProvider> _validatorProviders;
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>
/// Gets or sets the list of <see cref="IModelValidatorProvider"/> instances for the current request.
/// </summary>

View File

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

View File

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

View File

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

View File

@ -24,14 +24,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
typeof(ControllerArgumentBinder).GetTypeInfo().GetDeclaredMethod(
nameof(CallPropertyAddRange));
private readonly IModelBinderFactory _modelBinderFactory;
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IObjectModelValidator _validator;
public ControllerArgumentBinder(
IModelMetadataProvider modelMetadataProvider,
IModelBinderFactory modelBinderFactory,
IObjectModelValidator validator)
{
_modelMetadataProvider = modelMetadataProvider;
_modelBinderFactory = modelBinderFactory;
_validator = validator;
}
@ -118,7 +121,31 @@ namespace Microsoft.AspNetCore.Mvc.Internal
parameter.BindingInfo,
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;
if (modelBindingResult != null && modelBindingResult.Value.IsModelSet)
{
@ -241,7 +268,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
ActionContext = context,
InputFormatters = context.InputFormatters,
ModelBinder = new CompositeModelBinder(context.ModelBinders),
ValidatorProvider = new CompositeModelValidatorProvider(context.ValidatorProviders),
MetadataProvider = _modelMetadataProvider,
ValueProvider = new CompositeValueProvider(context.ValueProviders),

View File

@ -12,6 +12,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
public class DefaultModelBindingContext : ModelBindingContext
{
private OperationBindingContext _operationBindingContext;
private State _state;
private readonly Stack<State> _stack = new Stack<State>();
@ -57,17 +59,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyPredicateProvider =
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()
{
BinderModelName = binderModelName,
BindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource,
BinderType = bindingInfo?.BinderType ?? metadata.BinderType,
BindingSource = bindingSource,
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.
FieldName = binderModelName ?? modelName,
ModelName = binderModelName ?? modelName,
@ -76,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ModelMetadata = metadata,
ModelState = operationBindingContext.ActionContext.ModelState,
OperationBindingContext = operationBindingContext,
ValueProvider = operationBindingContext.ValueProvider,
ValueProvider = valueProvider,
ValidationState = new ValidationStateDictionary(),
};
@ -106,16 +110,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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;
ModelMetadata = modelMetadata;
ModelName = modelName;
FieldName = fieldName;
BinderModelName = modelMetadata.BinderModelName;
BinderType = modelMetadata.BinderType;
BindingSource = modelMetadata.BindingSource;
PropertyFilter = modelMetadata.PropertyBindingPredicateProvider?.PropertyFilter;
FallbackToEmptyPrefix = false;
IsTopLevelObject = false;
return scope;
@ -140,14 +149,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <inheritdoc />
public override OperationBindingContext OperationBindingContext
{
get { return _state.OperationBindingContext; }
get { return _operationBindingContext; }
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_state.OperationBindingContext = value;
_operationBindingContext = value;
}
}
@ -231,20 +240,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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 />
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
{
public OperationBindingContext OperationBindingContext;
public string FieldName;
public object Model;
public ModelMetadata ModelMetadata;
@ -313,8 +323,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public string BinderModelName;
public BindingSource BindingSource;
public Type BinderType;
public bool FallbackToEmptyPrefix;
public bool IsTopLevelObject;
public ModelBindingResult? Result;

View File

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

View File

@ -6,9 +6,8 @@ using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
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.Extensions.Options;
@ -37,17 +36,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
messageProvider.ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber;
// Set up ModelBinding
options.ModelBinders.Add(new BinderTypeBasedModelBinder());
options.ModelBinders.Add(new ServicesModelBinder());
options.ModelBinders.Add(new BodyModelBinder(readerFactory));
options.ModelBinders.Add(new HeaderModelBinder());
options.ModelBinders.Add(new SimpleTypeModelBinder());
options.ModelBinders.Add(new CancellationTokenModelBinder());
options.ModelBinders.Add(new ByteArrayModelBinder());
options.ModelBinders.Add(new FormFileModelBinder());
options.ModelBinders.Add(new FormCollectionModelBinder());
options.ModelBinders.Add(new GenericModelBinder());
options.ModelBinders.Add(new MutableObjectModelBinder());
options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
options.ModelBinderProviders.Add(new BodyModelBinderProvider(readerFactory));
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
options.ModelBinderProviders.Add(new FormFileModelBinderProvider());
options.ModelBinderProviders.Add(new FormCollectionModelBinderProvider());
options.ModelBinderProviders.Add(new KeyValuePairModelBinderProvider());
options.ModelBinderProviders.Add(new DictionaryModelBinderProvider());
options.ModelBinderProviders.Add(new ArrayModelBinderProvider());
options.ModelBinderProviders.Add(new CollectionModelBinderProvider());
options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
// Set up filters
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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// <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>
public class ArrayModelBinder<TElement> : CollectionModelBinder<TElement>
{
/// <inheritdoc />
public override Task BindModelAsync(ModelBindingContext bindingContext)
/// <summary>
/// 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 />
@ -62,8 +55,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
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
// 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.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// 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 />
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 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)
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
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 Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// ModelBinder to bind Byte Arrays.
/// ModelBinder to bind byte Arrays.
/// </summary>
public class ByteArrayModelBinder : IModelBinder
{
@ -20,16 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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.
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
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.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// <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));
}
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.
//
// DO NOT simplify this code by removing the cast.
var model = (object)bindingContext.OperationBindingContext.HttpContext.RequestAborted;
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
}
// 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;
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
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.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// <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>
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 />
public virtual async Task BindModelAsync(ModelBindingContext bindingContext)
{
@ -28,8 +47,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext));
}
ModelBindingHelper.ValidateBindingContext(bindingContext);
var model = bindingContext.Model;
if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
{
@ -163,7 +180,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelName: bindingContext.ModelName,
model: null))
{
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(bindingContext);
await ElementBinder.BindModelAsync(bindingContext);
if (bindingContext.Result != null && bindingContext.Result.Value.IsModelSet)
{
@ -224,7 +241,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelName: fullChildName,
model: null))
{
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(bindingContext);
await ElementBinder.BindModelAsync(bindingContext);
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.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// <see cref="IModelBinder"/> implementation for binding complex values.
/// <see cref="IModelBinder"/> implementation for binding complex types.
/// </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 />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
@ -21,12 +40,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext));
}
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!CanBindType(bindingContext.ModelMetadata))
{
return TaskCache.CompletedTask;
}
if (!CanCreateModel(bindingContext))
{
return TaskCache.CompletedTask;
@ -135,7 +148,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </returns>
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)
@ -284,22 +298,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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 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.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// <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>
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 />
public override async Task BindModelAsync(ModelBindingContext bindingContext)
{
@ -66,8 +84,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var valueMetadata = metadataProvider.GetMetadataForType(typeof(TValue));
var modelBinder = bindingContext.OperationBindingContext.ModelBinder;
var keyMappings = new Dictionary<string, TKey>(StringComparer.Ordinal);
foreach (var kvp in keys)
{
@ -81,7 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelName: kvp.Value,
model: null))
{
await modelBinder.BindModelAsync(bindingContext);
await _valueBinder.BindModelAsync(bindingContext);
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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// <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
{
/// <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>.
if (bindingContext.ModelType != typeof(IFormCollection))
{
return TaskCache.CompletedTask;
}
return BindModelCoreAsync(bindingContext);
}
private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
{
object model;
var request = bindingContext.OperationBindingContext.HttpContext.Request;
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
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// <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
{
/// <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 or async state machines.
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) &&
var createFileCollection =
bindingContext.ModelType == typeof(IFormFileCollection) &&
!bindingContext.ModelMetadata.IsReadOnly;
if (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection<IFormFile>(bindingContext))
{
// Silently fail and stop other model binders running if unable to create an instance or use the
// current instance.
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return TaskCache.CompletedTask;
return;
}
ICollection<IFormFile> postedFiles;
@ -59,13 +49,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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).
// 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.

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.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// 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));
}
// 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;
// Property name can be null if the model metadata represents a type (rather than a property or parameter).
var headerName = bindingContext.FieldName;
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;
}
else
{
var values = request.Headers.GetCommaSeparatedValues(headerName);
model = GetCompatibleCollection(bindingContext, values);
}
var value = request.Headers[headerName];
model = (string)value;
}
else if (ModelBindingHelper.CanGetCompatibleCollection<string>(bindingContext))
{
var values = request.Headers.GetCommaSeparatedValues(headerName);
model = GetCompatibleCollection(bindingContext, values);
}
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.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)
{
if (bindingContext == null)
@ -16,14 +46,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(bindingContext));
}
if (bindingContext.ModelType != typeof(KeyValuePair<TKey, TValue>))
{
// This binder does not apply.
return;
}
var keyResult = await TryBindStrongModel<TKey>(bindingContext, "Key");
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "Value");
var keyResult = await TryBindStrongModel<TKey>(bindingContext, _keyBinder, "Key");
var valueResult = await TryBindStrongModel<TValue>(bindingContext, _valueBinder, "Value");
if (keyResult.IsModelSet && valueResult.IsModelSet)
{
@ -70,6 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
internal async Task<ModelBindingResult> TryBindStrongModel<TModel>(
ModelBindingContext bindingContext,
IModelBinder binder,
string propertyName)
{
var propertyModelMetadata =
@ -83,9 +108,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelName: propertyModelName,
model: null))
{
await binder.BindModelAsync(bindingContext);
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(
bindingContext);
var result = bindingContext.Result;
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.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// 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));
}
// 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 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 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
{
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
@ -16,20 +20,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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);
if (valueProviderResult == ValueProviderResult.None)
{
// no entry
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
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
{
/// <summary>
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/>
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>.
/// 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 <paramref name="validatorProvider"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update and validate.</param>
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </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="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="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix,
ActionContext actionContext,
IModelMetadataProvider metadataProvider,
IModelBinder modelBinder,
IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator,
@ -72,9 +72,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider));
}
if (modelBinder == null)
if (modelBinderFactory == null)
{
throw new ArgumentNullException(nameof(modelBinder));
throw new ArgumentNullException(nameof(modelBinderFactory));
}
if (valueProvider == null)
@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix,
actionContext,
metadataProvider,
modelBinder,
modelBinderFactory,
valueProvider,
inputFormatters,
objectModelValidator,
@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
/// <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
/// <paramref name="validatorProvider"/>.
/// </summary>
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </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="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="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix,
ActionContext actionContext,
IModelMetadataProvider metadataProvider,
IModelBinder modelBinder,
IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator,
@ -168,9 +168,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider));
}
if (modelBinder == null)
if (modelBinderFactory == null)
{
throw new ArgumentNullException(nameof(modelBinder));
throw new ArgumentNullException(nameof(modelBinderFactory));
}
if (valueProvider == null)
@ -206,7 +206,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix,
actionContext,
metadataProvider,
modelBinder,
modelBinderFactory,
valueProvider,
inputFormatters,
objectModelValidator,
@ -215,7 +215,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
/// <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
/// <paramref name="validatorProvider"/>.
/// </summary>
@ -225,7 +225,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </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="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="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix,
ActionContext actionContext,
IModelMetadataProvider metadataProvider,
IModelBinder modelBinder,
IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator,
@ -270,9 +270,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider));
}
if (modelBinder == null)
if (modelBinderFactory == null)
{
throw new ArgumentNullException(nameof(modelBinder));
throw new ArgumentNullException(nameof(modelBinderFactory));
}
if (valueProvider == null)
@ -306,7 +306,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix,
actionContext,
metadataProvider,
modelBinder,
modelBinderFactory,
valueProvider,
inputFormatters,
objectModelValidator,
@ -315,7 +315,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
/// <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
/// <paramref name="validatorProvider"/>.
/// </summary>
@ -325,7 +325,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </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="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="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -341,7 +341,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix,
ActionContext actionContext,
IModelMetadataProvider metadataProvider,
IModelBinder modelBinder,
IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator,
@ -372,9 +372,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider));
}
if (modelBinder == null)
if (modelBinderFactory == null)
{
throw new ArgumentNullException(nameof(modelBinder));
throw new ArgumentNullException(nameof(modelBinderFactory));
}
if (valueProvider == null)
@ -404,7 +404,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
prefix,
actionContext,
metadataProvider,
modelBinder,
modelBinderFactory,
valueProvider,
inputFormatters,
objectModelValidator,
@ -413,7 +413,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
/// <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
/// <paramref name="validatorProvider"/>.
/// </summary>
@ -423,7 +423,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </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="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="inputFormatters">
/// The set of <see cref="IInputFormatter"/> instances for deserializing the body.
@ -441,7 +441,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
string prefix,
ActionContext actionContext,
IModelMetadataProvider metadataProvider,
IModelBinder modelBinder,
IModelBinderFactory modelBinderFactory,
IValueProvider valueProvider,
IList<IInputFormatter> inputFormatters,
IObjectModelValidator objectModelValidator,
@ -473,9 +473,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadataProvider));
}
if (modelBinder == null)
if (modelBinderFactory == null)
{
throw new ArgumentNullException(nameof(modelBinder));
throw new ArgumentNullException(nameof(modelBinderFactory));
}
if (valueProvider == null)
@ -517,7 +517,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var operationBindingContext = new OperationBindingContext
{
InputFormatters = inputFormatters,
ModelBinder = modelBinder,
ValidatorProvider = validatorProvider,
MetadataProvider = metadataProvider,
ActionContext = actionContext,
@ -532,7 +531,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelBindingContext.Model = model;
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;
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)
{
return (model is TModel) ? (TModel)model : default(TModel);

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc
FormatterMappings = new FormatterMappings();
InputFormatters = new FormatterCollection<IInputFormatter>();
OutputFormatters = new FormatterCollection<IOutputFormatter>();
ModelBinders = new List<IModelBinder>();
ModelBinderProviders = new List<IModelBinderProvider>();
ModelBindingMessageProvider = new ModelBindingMessageProvider();
ModelMetadataDetailsProviders = new List<IMetadataDetailsProvider>();
ModelValidatorProviders = new List<IModelValidatorProvider>();
@ -81,9 +81,9 @@ namespace Microsoft.AspNetCore.Mvc
}
/// <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>
public IList<IModelBinder> ModelBinders { get; }
public IList<IModelBinderProvider> ModelBinderProviders { get; }
/// <summary>
/// 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"));
}
/// <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)
{
var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -343,4 +343,7 @@
<data name="MustSpecifyAtLeastOneAuthenticationScheme" xml:space="preserve">
<value>At least one authentication scheme must be specified.</value>
</data>
<data name="CouldNotCreateIModelBinder" xml:space="preserve">
<value>Could not create a model binder for model object of type '{0}'.</value>
</data>
</root>

View File

@ -17,12 +17,9 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(HttpRequestMessage))
{
var model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage();
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
}
var model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage();
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
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());
// 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
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.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;

View File

@ -1638,7 +1638,6 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
var controllerContext = new ControllerContext()
{
HttpContext = httpContext,
ModelBinders = new[] { binder, },
ValueProviders = new[] { valueProvider, },
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()
{
ControllerContext = controllerContext,
MetadataProvider = metadataProvider,
ModelBinderFactory = binderFactory.Object,
ObjectValidator = new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
};

View File

@ -2057,7 +2057,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
actionDescriptor,
new IInputFormatter[0],
actionArgumentsBinder.Object,
new IModelBinder[0],
new IModelValidatorProvider[0],
new IValueProviderFactory[0],
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
@ -2090,9 +2089,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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>();
context.SetupGet(c => c.Items)
.Returns(new Dictionary<object, object>());
@ -2115,8 +2111,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new IInputFormatter[0],
new ControllerArgumentBinder(
metadataProvider,
TestModelBinderFactory.CreateDefault(metadataProvider),
new DefaultObjectValidator(metadataProvider, new ValidatorCache())),
new IModelBinder[] { binder.Object },
new IModelValidatorProvider[0],
new IValueProviderFactory[0],
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
@ -2241,7 +2237,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ControllerActionDescriptor descriptor,
IReadOnlyList<IInputFormatter> inputFormatters,
IControllerActionArgumentBinder controllerActionArgumentBinder,
IReadOnlyList<IModelBinder> modelBinders,
IReadOnlyList<IModelValidatorProvider> modelValidatorProviders,
IReadOnlyList<IValueProviderFactory> valueProviderFactories,
ILogger logger,
@ -2254,7 +2249,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
descriptor,
inputFormatters,
controllerActionArgumentBinder,
modelBinders,
modelValidatorProviders,
valueProviderFactories,
logger,

View File

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

View File

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
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.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class ArrayModelBinderTest
{
@ -22,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
};
var bindingContext = GetBindingContext(valueProvider);
var modelState = bindingContext.ModelState;
var binder = new ArrayModelBinder<int>();
var binder = new ArrayModelBinder<int>(new SimpleTypeModelBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -32,14 +31,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var array = Assert.IsType<int[]>(result.Model);
Assert.Equal(new[] { 42, 84 }, array);
Assert.True(modelState.IsValid);
}
[Fact]
public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{
// Arrange
var binder = new ArrayModelBinder<string>();
var binder = new ArrayModelBinder<string>(new SimpleTypeModelBinder());
var context = CreateContext();
context.IsTopLevelObject = true;
@ -69,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{
// Arrange
var binder = new ArrayModelBinder<string>();
var binder = new ArrayModelBinder<string>(new SimpleTypeModelBinder());
var context = CreateContext();
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.
[Theory]
[MemberData(nameof(ArrayModelData))]
@ -139,7 +115,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var bindingContext = GetBindingContext(valueProvider, isReadOnly: false);
var modelState = bindingContext.ModelState;
bindingContext.Model = model;
var binder = new ArrayModelBinder<int>();
var binder = new ArrayModelBinder<int>(new SimpleTypeModelBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -148,8 +125,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.Same(model, result.Model);
Assert.True(modelState.IsValid);
for (var i = 0; i < arrayLength; i++)
{
// Array should be unchanged.
@ -191,7 +167,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext
{
ModelBinder = CreateIntBinder(),
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 Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
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]
public async Task BindModel_ReturnsFailedResult_EvenIfSelectedBinderReturnsNull()
{
// Arrange
var bindingContext = GetBindingContext(typeof(Person), binderType: typeof(NullModelBinder));
var binder = new BinderTypeBasedModelBinder();
var binder = new BinderTypeModelBinder(typeof(NullModelBinder));
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -57,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
bindingContext.OperationBindingContext.HttpContext.RequestServices = serviceProvider;
var binder = new BinderTypeBasedModelBinder();
var binder = new BinderTypeModelBinder(typeof(NotNullModelBinder));
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -69,21 +55,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
}
[Fact]
public async Task BindModel_ForNonModelBinder_Throws()
public void BindModel_ForNonModelBinder_Throws()
{
// Arrange
var bindingContext = GetBindingContext(typeof(Person), binderType: typeof(Person));
var binder = new BinderTypeBasedModelBinder();
var expected = $"The type '{typeof(Person).FullName}' must implement " +
$"'{typeof(IModelBinder).FullName}' to be used as a model binder.";
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => binder.BindModelResultAsync(bindingContext));
// Assert
Assert.Equal(expected, ex.Message);
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => new BinderTypeModelBinder(typeof(Person)),
"binderType",
expected);
}
private static DefaultModelBindingContext GetBindingContext(Type modelType, Type binderType = null)
@ -108,7 +92,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
ValueProvider = Mock.Of<IValueProvider>(),
ModelState = new ModelStateDictionary(),
OperationBindingContext = operationBindingContext,
BinderType = binderType
};
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 Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class BodyModelBinderTests
{
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
new[] { inputFormatter },
metadataProvider: provider);
var binder = new BodyModelBinder(new TestHttpRequestStreamReaderFactory());
var binder = CreateBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder;
var binder = CreateBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider);
bindingContext.BinderModelName = "custom";
var binder = bindingContext.OperationBindingContext.ModelBinder;
var binder = CreateBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = GetBindingContext(typeof(Person), metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder;
var binder = CreateBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -125,44 +125,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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]
public async Task CustomFormatterDeserializationException_AddedToModelState()
{
@ -180,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
httpContext: httpContext,
metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder;
var binder = CreateBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -215,7 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
httpContext: httpContext,
metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder;
var binder = CreateBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -250,7 +212,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var provider = new TestModelMetadataProvider();
provider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), inputFormatters, metadataProvider: provider);
var binder = bindingContext.OperationBindingContext.ModelBinder;
var binder = CreateBinder();
// Act
var binderResult = await binder.BindModelResultAsync(bindingContext);
@ -288,7 +251,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
HttpContext = httpContext,
},
InputFormatters = inputFormatters.ToList(),
ModelBinder = new BodyModelBinder(new TestHttpRequestStreamReaderFactory()),
MetadataProvider = metadataProvider,
};
@ -307,6 +269,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return bindingContext;
}
private static BodyModelBinder CreateBinder()
{
return new BodyModelBinder(new TestHttpRequestStreamReaderFactory());
}
private class Person
{
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 Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class ByteArrayModelBinderTests
{
@ -106,20 +106,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
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)
{
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 Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class CancellationTokenModelBinderTests
{
@ -28,23 +28,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
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)
{
var metadataProvider = new EmptyModelMetadataProvider();
@ -59,7 +42,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{
HttpContext = new DefaultHttpContext(),
},
ModelBinder = new CancellationTokenModelBinder(),
MetadataProvider = metadataProvider,
},
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 Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Test;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class CollectionModelBinderTest
{
@ -26,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ "someName[baz]", "200" }
};
var bindingContext = GetModelBindingContext(valueProvider);
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act
var collectionResult = await binder.BindComplexCollectionFromIndexes(
@ -52,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ "someName[3]", "400" }
};
var bindingContext = GetModelBindingContext(valueProvider);
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act
var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null);
@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
};
var bindingContext = GetModelBindingContext(valueProvider, isReadOnly);
var modelState = bindingContext.ModelState;
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -111,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = bindingContext.ModelState;
var list = new List<int>();
bindingContext.Model = list;
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -138,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
};
var bindingContext = GetModelBindingContext(valueProvider, isReadOnly);
var modelState = bindingContext.ModelState;
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -165,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = bindingContext.ModelState;
var list = new List<int>();
bindingContext.Model = list;
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -182,7 +181,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task BindModelAsync_SimpleCollectionWithNullValue_Succeeds()
{
// Arrange
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
var valueProvider = new SimpleValueProvider
{
{ "someName", null },
@ -206,7 +205,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task BindSimpleCollection_RawValueIsEmptyCollection_ReturnsEmptyList()
{
// Arrange
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
var context = GetModelBindingContext(new SimpleValueProvider());
// Act
@ -221,7 +220,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{
// Arrange
var binder = new CollectionModelBinder<string>();
var binder = new CollectionModelBinder<string>(new StubModelBinder(result: null));
var context = CreateContext();
context.IsTopLevelObject = true;
@ -251,7 +250,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task CollectionModelBinder_DoesNotCreateEmptyCollection_IfModelNonNull()
{
// Arrange
var binder = new CollectionModelBinder<string>();
var binder = new CollectionModelBinder<string>(new StubModelBinder(result: null));
var context = CreateContext();
context.IsTopLevelObject = true;
@ -285,7 +284,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{
// Arrange
var binder = new CollectionModelBinder<string>();
var binder = new CollectionModelBinder<string>(new StubModelBinder(result: null));
var context = CreateContext();
context.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty");
@ -326,7 +325,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanCreateInstance_ReturnsExpectedValue(Type modelType, bool expectedResult)
{
// Arrange
var binder = new CollectionModelBinder<int>();
var binder = new CollectionModelBinder<int>(CreateIntBinder());
// Act
var result = binder.CanCreateInstance(modelType);
@ -342,13 +341,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var culture = new CultureInfo("fr-FR");
var bindingContext = GetModelBindingContext(new SimpleValueProvider());
bindingContext.OperationBindingContext.ModelBinder = new StubModelBinder(mbc =>
var elementBinder = new StubModelBinder(mbc =>
{
Assert.Equal("someName", mbc.ModelName);
mbc.Result = ModelBindingResult.Success(mbc.ModelName, 42);
});
var modelBinder = new CollectionModelBinder<int>();
var modelBinder = new CollectionModelBinder<int>(elementBinder);
// Act
var boundCollection = await modelBinder.BindSimpleCollection(
@ -379,7 +378,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext
{
ModelBinder = CreateIntBinder(),
MetadataProvider = metadataProvider
},
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.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.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.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
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();
@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(Person)));
bindingContext.IsTopLevelObject = isTopLevelObject;
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -49,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(modelMetadata);
bindingContext.IsTopLevelObject = false;
var binder = new MutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -64,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(Document)));
bindingContext.IsTopLevelObject = true;
var binder = new MutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -81,7 +82,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(HasAllGreedyProperties)));
bindingContext.IsTopLevelObject = isTopLevelObject;
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -109,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ValueProvider = valueProvider.Object;
bindingContext.OperationBindingContext.ValueProvider = valueProvider.Object;
var binder = new MutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -126,7 +127,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithNoProperties)));
bindingContext.IsTopLevelObject = false;
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -142,7 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithNoProperties)));
bindingContext.IsTopLevelObject = true;
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -172,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ValueProvider = valueProvider.Object;
bindingContext.OperationBindingContext.ValueProvider = valueProvider.Object;
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -207,7 +208,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ValueProvider = valueProvider.Object;
bindingContext.OperationBindingContext.ValueProvider = originalValueProvider.Object;
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -240,7 +241,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingContext.ValueProvider = valueProvider.Object;
bindingContext.OperationBindingContext.ValueProvider = originalValueProvider.Object;
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var canCreate = binder.CanCreateModel(bindingContext);
@ -269,7 +270,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext
{
ModelBinder = mockBinder,
MetadataProvider = _metadataProvider,
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
},
@ -278,7 +278,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var model = new Person();
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
var testableBinder = new Mock<TestableComplexTypeModelBinder> { CallBase = true };
testableBinder
.Setup(o => o.CreateModelPublic(bindingContext))
.Returns(model)
@ -311,7 +311,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = GetMetadataForProperty(typeof(MyModelTestingCanUpdateProperty), propertyName);
// Act
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
var canUpdate = ComplexTypeModelBinder.CanUpdatePropertyInternal(propertyMetadata);
// Assert
Assert.Equal(expected, canUpdate);
@ -331,7 +331,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var metadata = metadataProvider.GetMetadataForProperty(typeof(CollectionContainer), propertyName);
// Act
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(metadata);
var canUpdate = ComplexTypeModelBinder.CanUpdatePropertyInternal(metadata);
// Assert
Assert.Equal(expected, canUpdate);
@ -346,10 +346,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ModelMetadata = GetMetadataForType(typeof(Person))
};
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var model = testableBinder.CreateModelPublic(bindingContext);
var model = binder.CreateModelPublic(bindingContext);
// Assert
Assert.IsType<Person>(model);
@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), new Person());
var originalModel = bindingContext.Model;
var binder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
var binder = new Mock<TestableComplexTypeModelBinder>(){ CallBase = true };
binder
.Setup(b => b.CreateModelPublic(It.IsAny<ModelBindingContext>()))
.Verifiable();
@ -381,7 +381,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Arrange
var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), model: null);
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
var testableBinder = new Mock<TestableComplexTypeModelBinder> { CallBase = true };
testableBinder
.Setup(o => o.CreateModelPublic(bindingContext))
.Returns(new Person())
@ -402,10 +402,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_GetSetProperty(string property)
{
// Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
var context = new DefaultModelBindingContext()
var bindingContext = new DefaultModelBindingContext()
{
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
OperationBindingContext = new OperationBindingContext()
@ -420,8 +418,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
},
};
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var result = binder.CanBindPropertyPublic(context, metadata);
var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert
Assert.True(result);
@ -432,10 +432,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_GetOnlyProperty_WithBindNever(string property)
{
// Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
var context = new DefaultModelBindingContext()
var bindingContext = new DefaultModelBindingContext()
{
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
OperationBindingContext = new OperationBindingContext()
@ -450,8 +448,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
},
};
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var result = binder.CanBindPropertyPublic(context, metadata);
var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert
Assert.False(result);
@ -463,10 +463,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_GetSetProperty_WithBindNever(string property)
{
// Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
var context = new DefaultModelBindingContext()
var bindingContext = new DefaultModelBindingContext()
{
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
OperationBindingContext = new OperationBindingContext()
@ -481,8 +479,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
},
};
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var result = binder.CanBindPropertyPublic(context, metadata);
var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert
Assert.False(result);
@ -496,10 +496,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_WithPredicate(string property, bool expected)
{
// Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(TypeWithExcludedPropertiesUsingBindAttribute), property);
var context = new DefaultModelBindingContext()
var bindingContext = new DefaultModelBindingContext()
{
ModelMetadata = GetMetadataForType(typeof(TypeWithExcludedPropertiesUsingBindAttribute)),
OperationBindingContext = new OperationBindingContext()
@ -514,8 +512,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
},
};
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var result = binder.CanBindPropertyPublic(context, metadata);
var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert
Assert.Equal(expected, result);
@ -529,10 +529,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_WithBindInclude(string property, bool expected)
{
// Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(TypeWithIncludedPropertiesUsingBindAttribute), property);
var context = new DefaultModelBindingContext()
var bindingContext = new DefaultModelBindingContext()
{
ModelMetadata = GetMetadataForType(typeof(TypeWithIncludedPropertiesUsingBindAttribute)),
OperationBindingContext = new OperationBindingContext()
@ -547,8 +545,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
},
};
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var result = binder.CanBindPropertyPublic(context, metadata);
var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert
Assert.Equal(expected, result);
@ -561,10 +561,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CanBindProperty_BindingAttributes_OverridingBehavior(string property, bool expected)
{
// Arrange
var binder = new TestableMutableObjectModelBinder();
var metadata = GetMetadataForProperty(typeof(ModelWithMixedBindingBehaviors), property);
var context = new DefaultModelBindingContext()
var bindingContext = new DefaultModelBindingContext()
{
ModelMetadata = GetMetadataForType(typeof(ModelWithMixedBindingBehaviors)),
OperationBindingContext = new OperationBindingContext()
@ -579,8 +577,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
},
};
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
var result = binder.CanBindPropertyPublic(context, metadata);
var result = binder.CanBindPropertyPublic(bindingContext, metadata);
// Assert
Assert.Equal(expected, result);
@ -597,13 +597,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Age = -20
};
var binder = new TestableMutableObjectModelBinder();
var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithBindRequired.Age));
binder.Results[property] = ModelBindingResult.Failed("theModel.Age");
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
var binder = CreateBinder(bindingContext.ModelMetadata);
binder.Results[property] = ModelBindingResult.Failed("theModel.Age");
// Act
await binder.BindModelAsync(bindingContext);
@ -632,13 +632,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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));
binder.Results[property] = ModelBindingResult.Failed("theModel.Age");
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
// Act
await binder.BindModelAsync(bindingContext);
@ -667,15 +667,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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
// case because the property did have a result.
var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithBindRequired.Age));
binder.Results[property] = ModelBindingResult.Success("theModel.Age", model: null);
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
// Act
await binder.BindModelAsync(bindingContext);
@ -701,7 +701,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var model = new BindingOptionalProperty();
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), nameof(BindingOptionalProperty.ValueTypeRequired));
binder.Results[property] = ModelBindingResult.Failed("theModel.ValueTypeRequired");
@ -721,7 +721,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var model = new NullableValueTypeProperty();
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), nameof(NullableValueTypeProperty.NullableValueType));
binder.Results[property] = ModelBindingResult.Failed("theModel.NullableValueType");
@ -743,7 +743,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(containerMetadata, model);
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), 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 binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
var property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired));
binder.Results[property] = ModelBindingResult.Success(
@ -794,7 +794,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(containerMetadata, model);
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
foreach (var property in containerMetadata.Properties)
{
@ -833,10 +833,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = metadata.Properties[nameof(model.PropertyWithDefaultValue)];
var result = ModelBindingResult.Failed("foo");
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result);
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert
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.
var result = ModelBindingResult.Failed("foo");
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result);
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert
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.
var result = ModelBindingResult.Failed("foo");
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result);
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert
var person = Assert.IsType<Person>(bindingContext.Model);
@ -906,10 +906,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = metadata.Properties[nameof(model.NonUpdateableProperty)];
var result = ModelBindingResult.Failed("foo");
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result);
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert
// If didn't throw, success!
@ -953,10 +953,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
propertyName,
new Simple { Name = "Hanna" });
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result);
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert
Assert.Equal("Joe", propertyAccessor(model));
@ -977,7 +977,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var bindingContext = CreateContext(modelMetadata, model);
var result = ModelBindingResult.Success(propertyMetadata.PropertyName, new List<string>() { "hi" });
var binder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
@ -999,10 +999,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfBirth)];
var result = ModelBindingResult.Success("foo", new DateTime(2001, 1, 1));
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result);
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert
Assert.True(bindingContext.ModelState.IsValid);
@ -1026,10 +1026,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfDeath)];
var result = ModelBindingResult.Success("foo", new DateTime(1800, 1, 1));
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result);
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert
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 result = ModelBindingResult.Success("foo.NameNoAttribute", model: null);
var testableBinder = new TestableMutableObjectModelBinder();
var binder = CreateBinder(bindingContext.ModelMetadata);
// Act
testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, result);
binder.SetPropertyPublic(bindingContext, propertyMetadata, result);
// Assert
Assert.False(bindingContext.ModelState.IsValid);
@ -1064,6 +1064,30 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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)
{
var valueProvider = new TestValueProvider(new Dictionary<string, object>());
@ -1281,7 +1305,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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
@ -1333,10 +1360,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public IList<int> SettableList { get; set; } = new List<int> { 3, 9, 0 };
}
// Provides the ability to easily mock + call each of these APIs
public class TestableMutableObjectModelBinder : MutableObjectModelBinder
private class TestableComplexTypeModelBinderProvider : IModelBinderProvider
{
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>();
}

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.ModelBinding.Validation;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class DictionaryModelBinderTest
{
@ -23,25 +22,28 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task BindModel_Succeeds(bool isReadOnly)
{
// 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[1]", new KeyValuePair<int, string>(84, "eighty-four") },
{ "someName[0].Key", "42" },
{ "someName[0].Value", "forty-two" },
{ "someName[1].Key", "84" },
{ "someName[1].Value", "eighty-four" },
};
// Value Provider
var bindingContext = GetModelBindingContext(isReadOnly, values);
var modelState = bindingContext.ModelState;
var binder = new DictionaryModelBinder<int, string>();
bindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", values);
var binder = new DictionaryModelBinder<int, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
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.Equal(2, dictionary.Count);
Assert.Equal("forty-two", dictionary[42]);
@ -57,27 +59,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task BindModel_WithExistingModel_Succeeds(bool isReadOnly)
{
// 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[1]", new KeyValuePair<int, string>(84, "eighty-four") },
{ "someName[0].Key", "42" },
{ "someName[0].Value", "forty-two" },
{ "someName[1].Key", "84" },
{ "someName[1].Value", "eighty-four" },
};
var bindingContext = GetModelBindingContext(isReadOnly, values);
var modelState = bindingContext.ModelState;
bindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", values);
var dictionary = new Dictionary<int, string>();
bindingContext.Model = dictionary;
var binder = new DictionaryModelBinder<int, string>();
var binder = new DictionaryModelBinder<int, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.Same(dictionary, result.Model);
Assert.True(modelState.IsValid);
Assert.Same(dictionary, result.Model);
Assert.NotNull(dictionary);
Assert.Equal(2, dictionary.Count);
Assert.Equal("forty-two", dictionary[42]);
@ -123,10 +127,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
IDictionary<string, string> dictionary)
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();
var binder = new DictionaryModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext();
context.ModelName = modelName;
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
context.FieldName = modelName;
@ -140,7 +144,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var result = await binder.BindModelResultAsync(context);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.Equal(modelName, result.Key);
@ -160,10 +163,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ "three", "three" },
};
var binder = new DictionaryModelBinder<string, string>();
var binder = new DictionaryModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext();
context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateTestValueProvider("prefix[{0}]", dictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
context.FieldName = context.ModelName;
@ -210,10 +213,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{
// Arrange
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();
context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider =
CreateEnumerableValueProvider("prefix[{0}]", stringDictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
@ -252,10 +256,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ "prefix[27].Id", "98" },
{ "prefix[27].Name", "Fred" },
};
var binder = new DictionaryModelBinder<int, ModelWithProperties>();
var context = CreateContext();
context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", stringDictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
context.FieldName = context.ModelName;
@ -265,6 +268,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
typeof(ModelWithDictionaryProperties),
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
var result = await binder.BindModelResultAsync(context);
@ -298,10 +311,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{
// Arrange
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();
context.ModelName = modelName;
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
context.FieldName = context.ModelName;
@ -327,7 +341,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();
var binder = new DictionaryModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext();
context.IsTopLevelObject = true;
@ -357,7 +371,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();
var binder = new DictionaryModelBinder<int, int>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext();
context.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty");
@ -399,7 +413,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public void CanCreateInstance_ReturnsExpectedValue(Type modelType, bool expectedResult)
{
// Arrange
var binder = new DictionaryModelBinder<int, int>();
var binder = new DictionaryModelBinder<int, int>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
// Act
var result = binder.CanCreateInstance(modelType);
@ -427,17 +441,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
return modelBindingContext;
}
private static IModelBinder CreateCompositeBinder()
{
var binders = new IModelBinder[]
{
new SimpleTypeModelBinder(),
new MutableObjectModelBinder(),
};
return new CompositeModelBinder(binders);
}
private static IValueProvider CreateEnumerableValueProvider(
string keyFormat,
IDictionary<string, string> dictionary)
@ -468,7 +471,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
private static DefaultModelBindingContext GetModelBindingContext(
bool isReadOnly,
IDictionary<string, KeyValuePair<int, string>> values)
IDictionary<string, string> values = null)
{
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
@ -478,15 +481,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
typeof(ModelWithIDictionaryProperty),
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();
foreach (var kvp in values)
{
@ -500,7 +494,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext
{
ModelBinder = binder.Object,
MetadataProvider = metadataProvider,
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 Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class FormCollectionModelBinderTest
{
@ -47,47 +47,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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]
public async Task FormCollectionModelBinder_NoForm_BindSuccessful_ReturnsEmptyFormCollection()
{
@ -127,7 +86,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
HttpContext = httpContext,
},
ModelBinder = new FormCollectionModelBinder(),
MetadataProvider = metadataProvider,
},
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 Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class FormFileModelBinderTest
{
@ -108,24 +108,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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]
public async Task FormFileModelBinder_ReturnsFailedResult_WhenNoFilePosted()
{
@ -303,7 +285,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
HttpContext = httpContext,
},
ModelBinder = new FormFileModelBinder(),
MetadataProvider = metadataProvider,
},
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 Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class HeaderModelBinderTests
{
@ -101,50 +101,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
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]
public async Task HeaderBinder_ReturnsFailedResult_ForReadOnlyDestination()
{
@ -227,7 +183,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{
HttpContext = new DefaultHttpContext(),
},
ModelBinder = new HeaderModelBinder(),
MetadataProvider = metadataProvider,
},
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.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class KeyValuePairModelBinderTest
{
@ -22,8 +20,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var valueProvider = new SimpleValueProvider();
// Create string binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider, CreateStringBinder());
var binder = new KeyValuePairModelBinder<int, string>();
var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair<int, string>));
var binder = new KeyValuePairModelBinder<int, string>(CreateIntBinder(false), CreateStringBinder());
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -44,9 +42,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var valueProvider = new SimpleValueProvider();
// Create int binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider, CreateIntBinder());
var binder = new KeyValuePairModelBinder<int, string>();
var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair<int, string>));
var binder = new KeyValuePairModelBinder<int, string>(CreateIntBinder(), CreateStringBinder(false));
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -67,11 +65,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var valueProvider = new SimpleValueProvider();
// Create int binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider);
var mockBinder = new StubModelBinder();
bindingContext.OperationBindingContext.ModelBinder = mockBinder;
var binder = new KeyValuePairModelBinder<int, string>();
var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair<int, string>));
var binder = new KeyValuePairModelBinder<int, string>(CreateIntBinder(false), CreateStringBinder(false));
// Act
var result = await binder.BindModelResultAsync(bindingContext);
@ -86,11 +81,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task BindModel_SubBindingSucceeds()
{
// Arrange
var innerBinder = new CompositeModelBinder(new[] { CreateStringBinder(), CreateIntBinder() });
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
var result = await binder.BindModelResultAsync(bindingContext);
@ -124,12 +118,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Equal("someName.key", context.ModelName);
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
var result = await binder.TryBindStrongModel<int>(bindingContext, "key");
var result = await binder.TryBindStrongModel<int>(bindingContext, innerBinder, "key");
// Assert
Assert.Equal(innerResult.Value, result);
Assert.Empty(bindingContext.ModelState);
@ -139,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task KeyValuePairModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{
// Arrange
var binder = new KeyValuePairModelBinder<string, string>();
var binder = new KeyValuePairModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext();
context.IsTopLevelObject = true;
@ -170,7 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{
// Arrange
var binder = new KeyValuePairModelBinder<string, string>();
var binder = new KeyValuePairModelBinder<string, string>(new SimpleTypeModelBinder(), new SimpleTypeModelBinder());
var context = CreateContext();
context.ModelName = ModelNames.CreatePropertyModelName(prefix, "KeyValuePairProperty");
@ -200,7 +197,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
HttpContext = new DefaultHttpContext(),
},
MetadataProvider = new TestModelMetadataProvider(),
ModelBinder = new SimpleTypeModelBinder(),
},
ModelState = new ModelStateDictionary(),
};
@ -210,20 +206,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
private static DefaultModelBindingContext GetBindingContext(
IValueProvider valueProvider,
IModelBinder innerBinder = null,
Type keyValuePairType = null)
Type keyValuePairType)
{
var metataProvider = new EmptyModelMetadataProvider();
var bindingContext = new DefaultModelBindingContext
{
ModelMetadata = metataProvider.GetMetadataForType(
keyValuePairType ?? typeof(KeyValuePair<int, string>)),
ModelMetadata = metataProvider.GetMetadataForType(keyValuePairType),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext
{
ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider,
ValidatorProvider = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
@ -234,11 +227,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
return bindingContext;
}
private static IModelBinder CreateIntBinder()
private static IModelBinder CreateIntBinder(bool success = true)
{
var mockIntBinder = new StubModelBinder(mbc =>
{
if (mbc.ModelType == typeof(int))
if (mbc.ModelType == typeof(int) && success)
{
var model = 42;
return ModelBindingResult.Success(mbc.ModelName, model);
@ -248,11 +241,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
return mockIntBinder;
}
private static IModelBinder CreateStringBinder()
private static IModelBinder CreateStringBinder(bool success = true)
{
return new StubModelBinder(mbc =>
{
if (mbc.ModelType == typeof(string))
if (mbc.ModelType == typeof(string) && success)
{
var model = "some-value";
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 Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class ServicesModelBinderTest
{
@ -36,40 +36,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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)
{
var metadataProvider = new TestModelMetadataProvider();
@ -95,7 +61,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
RequestServices = services.BuildServiceProvider(),
}
},
ModelBinder = new HeaderModelBinder(),
MetadataProvider = metadataProvider,
},
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 Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
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
{
get
@ -134,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
}
[Fact]
public async Task BindModel_NullValueProviderResult_ReturnsNull()
public async Task BindModel_EmptyValueProviderResult_ReturnsFailed()
{
// Arrange
var bindingContext = GetBindingContext(typeof(int));
@ -144,7 +122,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
Assert.Equal(ModelBindingResult.Failed("theModelName"), result);
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.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
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;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
@ -37,7 +41,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Act
var originalBinderModelName = bindingContext.BinderModelName;
var originalBinderType = bindingContext.BinderType;
var originalBindingSource = bindingContext.BindingSource;
var originalModelState = bindingContext.ModelState;
var originalOperationBindingContext = bindingContext.OperationBindingContext;
@ -51,9 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert
Assert.Same(newModelMetadata.BinderModelName, bindingContext.BinderModelName);
Assert.Same(newModelMetadata.BinderType, bindingContext.BinderType);
Assert.Same(newModelMetadata.BindingSource, bindingContext.BindingSource);
Assert.False(bindingContext.FallbackToEmptyPrefix);
Assert.Equal("fieldName", bindingContext.FieldName);
Assert.False(bindingContext.IsTopLevelObject);
Assert.Null(bindingContext.Model);
@ -66,6 +67,98 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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]
public void ModelTypeAreFedFromModelMetadata()
{
@ -79,6 +172,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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
{
public Task BindModelAsync(ModelBindingContext bindingContext)

View File

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

View File

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

View File

@ -6,7 +6,7 @@ using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
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);
// Assert
Assert.Equal(null, modelBindingResult);
Assert.False(modelBindingResult.Value.IsModelSet);
// ModelState (not set unless inner binder sets it)
Assert.True(modelState.IsValid);
@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// Assert
// ModelBindingResult
Assert.Equal(null, modelBindingResult);
Assert.False(modelBindingResult.Value.IsModelSet);
// ModelState
Assert.True(modelState.IsValid);
@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// Assert
// ModelBindingResult
Assert.Equal(null, modelBindingResult);
Assert.False(modelBindingResult.Value.IsModelSet);
// ModelState
Assert.True(modelState.IsValid);

View File

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

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