diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/IModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/IModelBinderProvider.cs new file mode 100644 index 0000000000..397fa7e637 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/IModelBinderProvider.cs @@ -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 +{ + /// + /// Creates instances. Register + /// instances in MvcOptions. + /// + public interface IModelBinderProvider + { + /// + /// Creates a based on . + /// + /// The . + /// An . + IModelBinder GetBinder(ModelBinderProviderContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs index a45aa8e5a8..bdc03ca456 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs @@ -3,13 +3,14 @@ using System; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata { /// /// A key type which identifies a . /// - public struct ModelMetadataIdentity + public struct ModelMetadataIdentity : IEquatable { /// /// Creates a for the provided model . @@ -98,5 +99,31 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata /// the current instance represents a type. /// public string Name { get; private set; } + + /// + public bool Equals(ModelMetadataIdentity other) + { + return + ContainerType == other.ContainerType && + ModelType == other.ModelType && + Name == other.Name; + } + + /// + public override bool Equals(object obj) + { + var other = obj as ModelMetadataIdentity?; + return other.HasValue && Equals(other.Value); + } + + /// + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + hash.Add(ContainerType); + hash.Add(ModelType); + hash.Add(Name, StringComparer.Ordinal); + return hash; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBinderProviderContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBinderProviderContext.cs new file mode 100644 index 0000000000..aa7f4a91a7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBinderProviderContext.cs @@ -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 +{ + /// + /// A context object for . + /// + public abstract class ModelBinderProviderContext + { + /// + /// Creates an for the given . + /// + /// The for the model. + /// An . + public abstract IModelBinder CreateBinder(ModelMetadata metadata); + + /// + /// Gets the . May be null. + /// + public abstract BindingInfo BindingInfo { get; } + + /// + /// Gets the . + /// + public abstract ModelMetadata Metadata { get; } + + /// + /// Gets the . + /// + public abstract IModelMetadataProvider MetadataProvider { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBindingContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBindingContext.cs index 46bba22316..a6fc24614e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBindingContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBindingContext.cs @@ -16,28 +16,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// public abstract string BinderModelName { get; set; } - /// - /// Gets the of an associated with the - /// . - /// - public abstract Type BinderType { get; set; } - /// /// Gets or sets a value which represents the associated with the /// . /// public abstract BindingSource BindingSource { get; set; } - /// - /// Gets or sets a value that indicates whether the binder should use an empty prefix to look up - /// values in when no values are found using the prefix. - /// - /// - /// Passed into the model binding system. Should not be true when is - /// false. - /// - public abstract bool FallbackToEmptyPrefix { get; set; } - /// /// Gets or sets the name of the current field being bound. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs index 53c0b827d7..de480a0dbc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// A metadata representation of a model type, property or parameter. /// [DebuggerDisplay("{DebuggerToString(),nq}")] - public abstract class ModelMetadata + public abstract class ModelMetadata : IEquatable { /// /// The default value of . @@ -378,6 +378,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding return DisplayName ?? PropertyName ?? ModelType.Name; } + /// + public bool Equals(ModelMetadata other) + { + if (object.ReferenceEquals(this, other)) + { + return true; + } + + if (other == null) + { + return false; + } + else + { + return Identity.Equals(other.Identity); + } + } + + /// + public override bool Equals(object obj) + { + return base.Equals(obj as ModelMetadata); + } + + /// + public override int GetHashCode() + { + return Identity.GetHashCode(); + } + private void InitializeTypeInformation() { Debug.Assert(ModelType != null); diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/OperationBindingContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/OperationBindingContext.cs index 59ea7ce3b8..e5a21af2de 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/OperationBindingContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/OperationBindingContext.cs @@ -35,11 +35,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// public IValueProvider ValueProvider { get; set; } - /// - /// Gets or sets the associated with this context. - /// - public IModelBinder ModelBinder { get; set; } - /// /// Gets or sets the associated with this context. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs index e39eb9cabf..eee27e65f9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs @@ -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 } } + /// + /// Gets or sets the . + /// + public IModelBinderFactory ModelBinderFactory + { + get + { + if (_modelBinderFactory == null) + { + _modelBinderFactory = HttpContext?.RequestServices?.GetRequiredService(); + } + + return _modelBinderFactory; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _modelBinderFactory = value; + } + } + /// /// Gets or sets the . /// @@ -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, diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerContext.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerContext.cs index ec21744763..a63c612f63 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerContext.cs @@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Mvc public class ControllerContext : ActionContext { private FormatterCollection _inputFormatters; - private IList _modelBinders; private IList _validatorProviders; private IList _valueProviders; @@ -81,31 +80,6 @@ namespace Microsoft.AspNetCore.Mvc } } - /// - /// Gets or sets the list of instances for the current request. - /// - public virtual IList ModelBinders - { - get - { - if (_modelBinders == null) - { - _modelBinders = new List(); - } - - return _modelBinders; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _modelBinders = value; - } - } - /// /// Gets or sets the list of instances for the current request. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index e226f08472..36d978c428 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -143,6 +143,7 @@ namespace Microsoft.Extensions.DependencyInjection var options = serviceProvider.GetRequiredService>().Value; return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders); })); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs index fb9c0352b4..21367a6e52 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs @@ -32,7 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal ControllerActionDescriptor descriptor, IReadOnlyList inputFormatters, IControllerActionArgumentBinder argumentBinder, - IReadOnlyList modelBinders, IReadOnlyList modelValidatorProviders, IReadOnlyList valueProviderFactories, ILogger logger, @@ -42,7 +41,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal actionContext, controllerActionInvokerCache, inputFormatters, - modelBinders, modelValidatorProviders, valueProviderFactories, logger, diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs index f5ce2d1810..adab7bde3f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs @@ -21,7 +21,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal private readonly IControllerFactory _controllerFactory; private readonly ControllerActionInvokerCache _controllerActionInvokerCache; private readonly IReadOnlyList _inputFormatters; - private readonly IReadOnlyList _modelBinders; private readonly IReadOnlyList _modelValidatorProviders; private readonly IReadOnlyList _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, diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs index 89408ec0e1..dba8f14fb8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs @@ -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), diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelBindingContext.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelBindingContext.cs index 5a2027bfb2..017596e4b0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelBindingContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelBindingContext.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// public class DefaultModelBindingContext : ModelBindingContext { + private OperationBindingContext _operationBindingContext; + private State _state; private readonly Stack _stack = new Stack(); @@ -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 /// 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; } } - /// - public override Type BinderType - { - get { return _state.BinderType; } - set { _state.BinderType = value; } - } - - /// - public override bool FallbackToEmptyPrefix - { - get { return _state.FallbackToEmptyPrefix; } - set { _state.FallbackToEmptyPrefix = value; } - } - /// 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; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterActionInvoker.cs index a81f0febce..af924afce9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterActionInvoker.cs @@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal { private readonly ControllerActionInvokerCache _controllerActionInvokerCache; private readonly IReadOnlyList _inputFormatters; - private readonly IReadOnlyList _modelBinders; private readonly IReadOnlyList _modelValidatorProviders; private readonly IReadOnlyList _valueProviderFactories; private readonly DiagnosticSource _diagnosticSource; @@ -47,7 +46,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal ActionContext actionContext, ControllerActionInvokerCache controllerActionInvokerCache, IReadOnlyList inputFormatters, - IReadOnlyList modelBinders, IReadOnlyList modelValidatorProviders, IReadOnlyList 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( new CopyOnWriteList(_inputFormatters)); - Context.ModelBinders = new CopyOnWriteList(_modelBinders); Context.ValidatorProviders = new CopyOnWriteList(_modelValidatorProviders); var valueProviders = new List(); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs index 2cda9e1203..44ddbea572 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs @@ -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()); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/PlaceholderBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/PlaceholderBinder.cs new file mode 100644 index 0000000000..b1e28f1563 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/PlaceholderBinder.cs @@ -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); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/BinderTypeBasedModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/BinderTypeBasedModelBinder.cs deleted file mode 100644 index 3e2676931c..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/BinderTypeBasedModelBinder.cs +++ /dev/null @@ -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 -{ - /// - /// An which can bind a model based on the value of - /// . The supplied - /// type will be used to bind the model. - /// - public class BinderTypeBasedModelBinder : IModelBinder - { - private readonly Func _createFactory = - (t) => ActivatorUtilities.CreateFactory(t, Type.EmptyTypes); - private readonly ConcurrentDictionary _typeActivatorCache = - new ConcurrentDictionary(); - - 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. - - 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); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ArrayModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinder.cs similarity index 73% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ArrayModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinder.cs index a0d092a2b3..584cc32b0c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ArrayModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinder.cs @@ -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 { /// /// implementation for binding array values. @@ -16,20 +14,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// Type of elements in the array. public class ArrayModelBinder : CollectionModelBinder { - /// - public override Task BindModelAsync(ModelBindingContext bindingContext) + /// + /// Creates a new . + /// + /// + /// The for binding . + /// + 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); } /// @@ -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. } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinderProvider.cs new file mode 100644 index 0000000000..a6f087883b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinderProvider.cs @@ -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 +{ + /// + /// An for arrays. + /// + public class ArrayModelBinderProvider : IModelBinderProvider + { + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinder.cs new file mode 100644 index 0000000000..4de5f164f4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinder.cs @@ -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 +{ + /// + /// An for models which specify an using + /// . + /// + public class BinderTypeModelBinder : IModelBinder + { + private readonly ObjectFactory _factory; + + /// + /// Creates a new . + /// + /// The of the . + 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); + } + + /// + 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); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinderProvider.cs new file mode 100644 index 0000000000..22a0c1bac7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinderProvider.cs @@ -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 +{ + /// + /// An for models which specify an + /// using . + /// + public class BinderTypeModelBinderProvider : IModelBinderProvider + { + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/BodyModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinder.cs similarity index 75% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/BodyModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinder.cs index e9b673cad7..5e1a4c636f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/BodyModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinder.cs @@ -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 { /// /// An which binds models from the request body using an @@ -33,37 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } /// - 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. - - 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); - } - - /// - /// Attempts to bind the model using formatters. - /// - /// The . - /// - /// A which when completed returns a . - /// - private async Task BindModelCoreAsync(ModelBindingContext bindingContext) + public async Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs new file mode 100644 index 0000000000..07b933b4bd --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs @@ -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 +{ + /// + /// An for deserializing the request body using a formatter. + /// + public class BodyModelBinderProvider : IModelBinderProvider + { + private readonly IHttpRequestStreamReaderFactory _readerFactory; + + /// + /// Creates a new . + /// + /// The . + public BodyModelBinderProvider(IHttpRequestStreamReaderFactory readerFactory) + { + if (readerFactory == null) + { + throw new ArgumentNullException(nameof(readerFactory)); + } + + _readerFactory = readerFactory; + } + + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ByteArrayModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ByteArrayModelBinder.cs similarity index 81% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ByteArrayModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ByteArrayModelBinder.cs index ad22b699e7..3547582763 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ByteArrayModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ByteArrayModelBinder.cs @@ -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 { /// - /// ModelBinder to bind Byte Arrays. + /// ModelBinder to bind byte Arrays. /// 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. - - // Check if this binder applies. - if (bindingContext.ModelType != typeof(byte[])) - { - return TaskCache.CompletedTask; - } - // Check for missing data case 1: There was no element containing this data. var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult == ValueProviderResult.None) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ByteArrayModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ByteArrayModelBinderProvider.cs new file mode 100644 index 0000000000..1253b44705 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ByteArrayModelBinderProvider.cs @@ -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 +{ + /// + /// An for binding base64 encoded byte arrays. + /// + public class ByteArrayModelBinderProvider : IModelBinderProvider + { + /// + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Metadata.ModelType == typeof(byte[])) + { + return new ByteArrayModelBinder(); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CancellationTokenModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CancellationTokenModelBinder.cs similarity index 53% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CancellationTokenModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CancellationTokenModelBinder.cs index 560a6831ab..5ee4975da0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CancellationTokenModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CancellationTokenModelBinder.cs @@ -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 { /// /// implementation to bind models of type . @@ -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; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CancellationTokenModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CancellationTokenModelBinderProvider.cs new file mode 100644 index 0000000000..b3ee6f7361 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CancellationTokenModelBinderProvider.cs @@ -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 +{ + /// + /// An for . + /// + public class CancellationTokenModelBinderProvider : IModelBinderProvider + { + /// + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Metadata.ModelType == typeof(CancellationToken)) + { + return new CancellationTokenModelBinder(); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CollectionModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinder.cs similarity index 94% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CollectionModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinder.cs index 53b4122c86..23fc5e83d7 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CollectionModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinder.cs @@ -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 { /// /// implementation for binding collection values. @@ -20,6 +20,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// Type of elements in the collection. public class CollectionModelBinder : ICollectionModelBinder { + /// + /// Creates a new . + /// + /// The for binding elements. + public CollectionModelBinder(IModelBinder elementBinder) + { + if (elementBinder == null) + { + throw new ArgumentNullException(nameof(elementBinder)); + } + + ElementBinder = elementBinder; + } + + /// + /// Gets the instances for binding collection elements. + /// + protected IModelBinder ElementBinder { get; } + /// 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; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinderProvider.cs new file mode 100644 index 0000000000..17c5e0daf8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinderProvider.cs @@ -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 +{ + /// + /// An for . + /// + public class CollectionModelBinderProvider : IModelBinderProvider + { + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/MutableObjectModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinder.cs similarity index 94% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/MutableObjectModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinder.cs index f7ab2ae686..402dc2ec65 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinder.cs @@ -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 { /// - /// implementation for binding complex values. + /// implementation for binding complex types. /// - public class MutableObjectModelBinder : IModelBinder + public class ComplexTypeModelBinder : IModelBinder { + private readonly IDictionary _propertyBinders; + + /// + /// Creates a new . + /// + /// + /// The of binders to use for binding properties. + /// + public ComplexTypeModelBinder(IDictionary propertyBinders) + { + if (propertyBinders == null) + { + throw new ArgumentNullException(nameof(propertyBinders)); + } + + _propertyBinders = propertyBinders; + } + /// 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 /// 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) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinderProvider.cs new file mode 100644 index 0000000000..580b45f8fe --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinderProvider.cs @@ -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 +{ + /// + /// An for complex types. + /// + public class ComplexTypeModelBinderProvider : IModelBinderProvider + { + /// + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType) + { + var propertyBinders = new Dictionary(); + foreach (var property in context.Metadata.Properties) + { + propertyBinders.Add(property, context.CreateBinder(property)); + } + + return new ComplexTypeModelBinder(propertyBinders); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/DictionaryModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinder.cs similarity index 85% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/DictionaryModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinder.cs index b6acd20735..931b364ee2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/DictionaryModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinder.cs @@ -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 { /// /// implementation for binding dictionary values. @@ -21,6 +21,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// Type of values in the dictionary. public class DictionaryModelBinder : CollectionModelBinder> { + private readonly IModelBinder _valueBinder; + + /// + /// Creates a new . + /// + /// The for . + /// The for . + public DictionaryModelBinder(IModelBinder keyBinder, IModelBinder valueBinder) + : base(new KeyValuePairModelBinder(keyBinder, valueBinder)) + { + if (valueBinder == null) + { + throw new ArgumentNullException(nameof(valueBinder)); + } + + _valueBinder = valueBinder; + } + /// 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(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; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinderProvider.cs new file mode 100644 index 0000000000..87166f8cb8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinderProvider.cs @@ -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 +{ + /// + /// An for binding . + /// + public class DictionaryModelBinderProvider : IModelBinderProvider + { + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/FormCollectionModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinder.cs similarity index 82% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/FormCollectionModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinder.cs index 2feaef2527..94e4f34359 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/FormCollectionModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinder.cs @@ -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 { /// /// implementation to bind form values to . @@ -19,27 +18,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public class FormCollectionModelBinder : IModelBinder { /// - 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. - - 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) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinderProvider.cs new file mode 100644 index 0000000000..f87bcd352d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinderProvider.cs @@ -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 +{ + /// + /// An for . + /// + public class FormCollectionModelBinderProvider : IModelBinderProvider + { + /// + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Metadata.ModelType == typeof(IFormCollection)) + { + return new FormCollectionModelBinder(); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/FormFileModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinder.cs similarity index 86% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/FormFileModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinder.cs index 2261104e4e..a2572f581b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/FormFileModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinder.cs @@ -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 { /// /// implementation to bind posted files to . @@ -22,31 +21,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public class FormFileModelBinder : IModelBinder { /// - 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).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(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 postedFiles; @@ -59,13 +49,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding postedFiles = ModelBindingHelper.GetCompatibleCollection(bindingContext); } - return BindModelCoreAsync(bindingContext, postedFiles); - } - - private async Task BindModelCoreAsync(ModelBindingContext bindingContext, ICollection 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. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinderProvider.cs new file mode 100644 index 0000000000..1b181f6fb4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinderProvider.cs @@ -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 +{ + /// + /// An for , collections + /// of , and . + /// + public class FormFileModelBinderProvider : IModelBinderProvider + { + /// + 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).GetTypeInfo().IsAssignableFrom(modelType.GetTypeInfo())) + { + return new FormFileModelBinder(); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/HeaderModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinder.cs similarity index 73% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/HeaderModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinder.cs index 467ae469be..8aa1e2d42c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/HeaderModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinder.cs @@ -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 { /// /// An 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(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(bindingContext)) + { + var values = request.Headers.GetCommaSeparatedValues(headerName); + model = GetCompatibleCollection(bindingContext, values); } else { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinderProvider.cs new file mode 100644 index 0000000000..8da75e8da2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinderProvider.cs @@ -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 +{ + /// + /// An for binding header values. + /// + public class HeaderModelBinderProvider : IModelBinderProvider + { + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/KeyValuePairModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/KeyValuePairModelBinder.cs similarity index 71% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/KeyValuePairModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/KeyValuePairModelBinder.cs index 7f6122089c..4732fd9dd8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/KeyValuePairModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/KeyValuePairModelBinder.cs @@ -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 : IModelBinder + /// + /// An for . + /// + /// The key type. + /// The value type. + public class KeyValuePairModelBinder : IModelBinder { + private readonly IModelBinder _keyBinder; + private readonly IModelBinder _valueBinder; + + /// + /// Creates a new . + /// + /// The for . + /// The for . + 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; + } + + /// 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)) - { - // This binder does not apply. - return; - } - - var keyResult = await TryBindStrongModel(bindingContext, "Key"); - var valueResult = await TryBindStrongModel(bindingContext, "Value"); + var keyResult = await TryBindStrongModel(bindingContext, _keyBinder, "Key"); + var valueResult = await TryBindStrongModel(bindingContext, _valueBinder, "Value"); if (keyResult.IsModelSet && valueResult.IsModelSet) { @@ -70,6 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding internal async Task TryBindStrongModel( 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) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/KeyValuePairModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/KeyValuePairModelBinderProvider.cs new file mode 100644 index 0000000000..fb9e706b8a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/KeyValuePairModelBinderProvider.cs @@ -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 +{ + /// + /// An for . + /// + public class KeyValuePairModelBinderProvider : IModelBinderProvider + { + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ServicesModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinder.cs similarity index 63% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ServicesModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinder.cs index 26020e9090..5ccc928917 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ServicesModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinder.cs @@ -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 { /// /// An 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. - - 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); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinderProvider.cs new file mode 100644 index 0000000000..c543a043d2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinderProvider.cs @@ -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 +{ + /// + /// An for binding from the . + /// + public class ServicesModelBinderProvider : IModelBinderProvider + { + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/SimpleTypeModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinder.cs similarity index 86% rename from src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/SimpleTypeModelBinder.cs rename to src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinder.cs index 8e683d5726..1d6836fdce 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/SimpleTypeModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinder.cs @@ -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 { + /// + /// An for simple types. + /// public class SimpleTypeModelBinder : IModelBinder { + /// 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. - - 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; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinderProvider.cs new file mode 100644 index 0000000000..16869d81ad --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinderProvider.cs @@ -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 +{ + /// + /// An for binding simple data types. + /// + public class SimpleTypeModelBinderProvider : IModelBinderProvider + { + /// + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Metadata.IsComplexType) + { + return new SimpleTypeModelBinder(); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CompositeModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CompositeModelBinder.cs deleted file mode 100644 index aa9b2b51f1..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CompositeModelBinder.cs +++ /dev/null @@ -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 -{ - /// - /// Represents an that delegates to one of a collection of - /// instances. - /// - /// - /// If no binder is available and the allows it, - /// this class tries to find a binder using an empty prefix. - /// - public class CompositeModelBinder : ICompositeModelBinder - { - /// - /// Initializes a new instance of the CompositeModelBinder class. - /// - /// A collection of instances. - public CompositeModelBinder(IList modelBinders) - { - if (modelBinders == null) - { - throw new ArgumentNullException(nameof(modelBinders)); - } - - ModelBinders = modelBinders; - } - - /// - public IList 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; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/GenericModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/GenericModelBinder.cs deleted file mode 100644 index 81fffd95db..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/GenericModelBinder.cs +++ /dev/null @@ -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. - - 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. Can a - // List (the default CollectionModelBinder type) instance be used instead of that exact type? - // Likely this will succeed only if the property type is exactly IEnumerable. - 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. For example an IEnumerable property may have a List 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), typeof(ListBinder), ...) means that the ListBinder - // type can update models that implement IList. This method will return - // ListBinder 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; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/IModelBinderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/IModelBinderFactory.cs new file mode 100644 index 0000000000..310183424a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/IModelBinderFactory.cs @@ -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 +{ + /// + /// A factory abstraction for creating instances. + /// + public interface IModelBinderFactory + { + /// + /// Creates a new . + /// + /// The . + /// An instance. + IModelBinder CreateBinder(ModelBinderFactoryContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs new file mode 100644 index 0000000000..179ef3202d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs @@ -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 +{ + /// + /// A factory for instances. + /// + public class ModelBinderFactory : IModelBinderFactory + { + private readonly IModelMetadataProvider _metadataProvider; + private readonly IModelBinderProvider[] _providers; + + private readonly ConcurrentDictionary _cache; + + /// + /// Creates a new . + /// + /// The . + /// The for . + public ModelBinderFactory(IModelMetadataProvider metadataProvider, IOptions options) + { + _metadataProvider = metadataProvider; + _providers = options.Value.ModelBinderProviders.ToArray(); + + _cache = new ConcurrentDictionary(ReferenceEqualityComparer.Instance); + } + + /// + 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(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, 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>(); + } + + 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> 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 + { + 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; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactoryContext.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactoryContext.cs new file mode 100644 index 0000000000..3ed2b371a4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactoryContext.cs @@ -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 +{ + /// + /// A context object for . + /// + public class ModelBinderFactoryContext + { + /// + /// Gets or sets the . + /// + public BindingInfo BindingInfo { get; set; } + + /// + /// Gets or sets the . + /// + public ModelMetadata Metadata { get; set; } + + /// + /// Gets or sets the cache token. If non-null the resulting + /// will be cached. + /// + public object CacheToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBindingHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBindingHelper.cs index 9e1a376389..a72483cc5f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBindingHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBindingHelper.cs @@ -20,9 +20,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public static class ModelBindingHelper { /// - /// Updates the specified instance using the specified - /// and the specified and executes validation using the specified - /// . + /// Updates the specified instance using the specified + /// and the specified and executes + /// validation using the specified . /// /// The type of the model object. /// The model instance to update and validate. @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// The for the current executing request. /// The provider used for reading metadata for the model type. - /// The used for binding. + /// The used for binding. /// The used for looking up values. /// /// The set of 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 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 } /// - /// Updates the specified instance using the specified + /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// The for the current executing request. /// The provider used for reading metadata for the model type. - /// The used for binding. + /// The used for binding. /// The used for looking up values. /// /// The set of 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 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 } /// - /// Updates the specified instance using the specified + /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// @@ -225,7 +225,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// The for the current executing request. /// The provider used for reading metadata for the model type. - /// The used for binding. + /// The used for binding. /// The used for looking up values. /// /// The set of 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 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 } /// - /// Updates the specified instance using the specified + /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// @@ -325,7 +325,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// The for the current executing request. /// The provider used for reading metadata for the model type. - /// The used for binding. + /// The used for binding. /// The used for looking up values. /// /// The set of 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 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 } /// - /// Updates the specified instance using the specified + /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// @@ -423,7 +423,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// The for the current executing request. /// The provider used for reading metadata for the model type. - /// The used for binding. + /// The used for binding. /// The used for looking up values. /// /// The set of 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 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(object model) { return (model is TModel) ? (TModel)model : default(TModel); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs index d1703527f2..2d0768fb72 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc FormatterMappings = new FormatterMappings(); InputFormatters = new FormatterCollection(); OutputFormatters = new FormatterCollection(); - ModelBinders = new List(); + ModelBinderProviders = new List(); ModelBindingMessageProvider = new ModelBindingMessageProvider(); ModelMetadataDetailsProviders = new List(); ModelValidatorProviders = new List(); @@ -81,9 +81,9 @@ namespace Microsoft.AspNetCore.Mvc } /// - /// Gets a list of s used by this application. + /// Gets a list of s used by this application. /// - public IList ModelBinders { get; } + public IList ModelBinderProviders { get; } /// /// Gets the default . Changes here are copied to the diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index 3f100b7465..8859b10fbb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -1162,6 +1162,22 @@ namespace Microsoft.AspNetCore.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("MustSpecifyAtLeastOneAuthenticationScheme")); } + /// + /// Could not create a model binder for model object of type '{0}'. + /// + internal static string CouldNotCreateIModelBinder + { + get { return GetString("CouldNotCreateIModelBinder"); } + } + + /// + /// Could not create a model binder for model object of type '{0}'. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx index 7d1d3c176b..40506b3759 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx @@ -1,17 +1,17 @@  - @@ -343,4 +343,7 @@ At least one authentication scheme must be specified. + + Could not create a model binder for model object of type '{0}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs index 3e738b8a62..2cc20278ff 100644 --- a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs @@ -17,12 +17,9 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim /// 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; } diff --git a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinderProvider.cs new file mode 100644 index 0000000000..bb06a4d243 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinderProvider.cs @@ -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 +{ + /// + /// implementation to bind models of type . + /// + public class HttpRequestMessageModelBinderProvider : IModelBinderProvider + { + /// + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context.Metadata.ModelType == typeof(HttpRequestMessage)) + { + return new HttpRequestMessageModelBinder(); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs index f5eb8c4cc4..e75fae9159 100644 --- a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs @@ -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()); diff --git a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs index eeaeabe56d..581f141b36 100644 --- a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs index 75437ad81d..b88a9a1bb9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs @@ -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(); + binderFactory + .Setup(f => f.CreateBinder(It.IsAny())) + .Returns(binder); + var controller = new TestableController() { ControllerContext = controllerContext, MetadataProvider = metadataProvider, + ModelBinderFactory = binderFactory.Object, ObjectValidator = new DefaultObjectValidator(metadataProvider, new ValidatorCache()), }; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs index 512bad203b..33e527ab8c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs @@ -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(), @@ -2090,9 +2089,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal FilterDescriptors = new List() }; - var binder = new Mock(); - binder.Setup(b => b.BindModelAsync(It.IsAny())) - .Returns(TaskCache.CompletedTask); var context = new Mock(); context.SetupGet(c => c.Items) .Returns(new Dictionary()); @@ -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(), @@ -2241,7 +2237,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal ControllerActionDescriptor descriptor, IReadOnlyList inputFormatters, IControllerActionArgumentBinder controllerActionArgumentBinder, - IReadOnlyList modelBinders, IReadOnlyList modelValidatorProviders, IReadOnlyList valueProviderFactories, ILogger logger, @@ -2254,7 +2249,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal descriptor, inputFormatters, controllerActionArgumentBinder, - modelBinders, modelValidatorProviders, valueProviderFactories, logger, diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerArgumentBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerArgumentBinderTests.cs index 366140c95a..875d695486 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerArgumentBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerArgumentBinderTests.cs @@ -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())) .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())) .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(MockBehavior.Strict); mockValidator @@ -150,7 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal It.IsAny(), It.IsAny())); - 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(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(TaskCache.CompletedTask); - var controllerContext = GetControllerContext(actionDescriptor); - controllerContext.ModelBinders.Add(binder.Object); - controllerContext.ValueProviders.Add(new SimpleValueProvider()); - var mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( @@ -198,7 +186,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal It.IsAny(), It.IsAny())); - 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(MockBehavior.Strict); mockValidator @@ -237,7 +226,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal It.IsAny(), It.IsAny())); - 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(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(TaskCache.CompletedTask); - var controllerContext = GetControllerContext(actionDescriptor); - controllerContext.ModelBinders.Add(binder.Object); - controllerContext.ValueProviders.Add(new SimpleValueProvider()); - var mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( @@ -284,7 +272,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal It.IsAny(), It.IsAny())); - 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), }); - var expected = new List { "Hello", "World", "!!" }; - var controllerContext = GetControllerContext(actionDescriptor, expected); + var controllerContext = GetControllerContext(actionDescriptor); + + var expected = new List { "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(); - binder.Setup(b => b.BindModelAsync(It.IsAny())) - .Returns(mbc => - { - mbc.Result = ModelBindingResult.Success(string.Empty, model); - return TaskCache.CompletedTask; - }); + binder + .Setup(b => b.BindModelAsync(It.IsAny())) + .Returns(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(); + provider + .Setup(p => p.GetBinder(It.IsAny())) + .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); } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultBindingMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultBindingMetadataProviderTest.cs index c6396828d4..85f8fca32c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultBindingMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultBindingMetadataProviderTest.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ArrayModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ArrayModelBinderProviderTest.cs new file mode 100644 index 0000000000..83a96daa9a --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ArrayModelBinderProviderTest.cs @@ -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))] + 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(); + }); + + // 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(); + }); + + // Act + var result = provider.GetBinder(context); + + // Assert + Assert.IsType>(result); + } + + private class TestClass + { + } + + private class ModelWithIntArrayProperty + { + public int[] ArrayProperty { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ArrayModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ArrayModelBinderTest.cs similarity index 84% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ArrayModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ArrayModelBinderTest.cs index d66d9a1a74..29c522c195 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ArrayModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ArrayModelBinderTest.cs @@ -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(); + var binder = new ArrayModelBinder(new SimpleTypeModelBinder()); // Act var result = await binder.BindModelResultAsync(bindingContext); @@ -32,14 +31,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test var array = Assert.IsType(result.Model); Assert.Equal(new[] { 42, 84 }, array); - Assert.True(modelState.IsValid); } [Fact] public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject() { // Arrange - var binder = new ArrayModelBinder(); + var binder = new ArrayModelBinder(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(); + var binder = new ArrayModelBinder(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(); - - // 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(); + + var binder = new ArrayModelBinder(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, }, }; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BinderTypeModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BinderTypeModelBinderProviderTest.cs new file mode 100644 index 0000000000..98c72b2918 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BinderTypeModelBinderProviderTest.cs @@ -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(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); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BinderTypeBasedModelBinderModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BinderTypeModelBinderTest.cs similarity index 78% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BinderTypeBasedModelBinderModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BinderTypeModelBinderTest.cs index 9ceca93e1a..9e587e6833 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BinderTypeBasedModelBinderModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BinderTypeModelBinderTest.cs @@ -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( - () => 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(), ModelState = new ModelStateDictionary(), OperationBindingContext = operationBindingContext, - BinderType = binderType }; return bindingContext; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderProviderTest.cs new file mode 100644 index 0000000000..3b40faa9ff --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderProviderTest.cs @@ -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 NonBodyBindingSources + { + get + { + return new TheoryData() + { + 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(result); + } + + private class Person + { + public string Name { get; set; } + + public int Age { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs similarity index 84% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs index 1b72b07940..c79e308389 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs @@ -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().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().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().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; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderProviderTest.cs new file mode 100644 index 0000000000..e7694592ae --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderProviderTest.cs @@ -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))] + [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(result); + } + + private class TestClass + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ByteArrayModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs similarity index 89% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ByteArrayModelBinderTests.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs index c7c6c49296..54b556921b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ByteArrayModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs @@ -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(); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CancellationTokenModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CancellationTokenModelBinderProviderTest.cs new file mode 100644 index 0000000000..676485a8ae --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CancellationTokenModelBinderProviderTest.cs @@ -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))] + [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(result); + } + + private class TestClass + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CancellationTokenModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CancellationTokenModelBinderTests.cs similarity index 73% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CancellationTokenModelBinderTests.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CancellationTokenModelBinderTests.cs index 05918d63de..a0d4b08148 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CancellationTokenModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CancellationTokenModelBinderTests.cs @@ -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(), diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs new file mode 100644 index 0000000000..644e4b456c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs @@ -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))] + [InlineData(typeof(IReadOnlyCollection))] + [InlineData(typeof(IReadOnlyList))] + + // These are ICollection<> - we can handle them by adding items to the existing collection or + // creating a new one. + [InlineData(typeof(ICollection))] + [InlineData(typeof(IList))] + [InlineData(typeof(List))] + [InlineData(typeof(Collection))] + 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(); + }); + + // Act + var result = provider.GetBinder(context); + + // Assert + Assert.NotNull(elementType); + Assert.IsType>(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 Enumerable { get; } + + public IReadOnlyCollection ReadOnlyCollection { get; } + + public IReadOnlyList ReadOnlyList { get; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CollectionModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderTest.cs similarity index 92% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CollectionModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderTest.cs index b234ae2cd6..2a0d477780 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CollectionModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderTest.cs @@ -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(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); bindingContext.Model = list; - var binder = new CollectionModelBinder(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); bindingContext.Model = list; - var binder = new CollectionModelBinder(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); + var binder = new CollectionModelBinder(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(); + var modelBinder = new CollectionModelBinder(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(), diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderProviderTest.cs new file mode 100644 index 0000000000..9dc044f814 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderProviderTest.cs @@ -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))] + 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(); + } + else + { + Assert.False(true, "Not the right model type"); + return null; + } + }); + + // Act + var result = provider.GetBinder(context); + + // Assert + Assert.IsType(result); + } + + private class Person + { + public string Name { get; set; } + + public int Age { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/MutableObjectModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs similarity index 87% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/MutableObjectModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs index d24be63fb8..cc8ee08b06 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/MutableObjectModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs @@ -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() }, @@ -278,7 +278,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var model = new Person(); - var testableBinder = new Mock { CallBase = true }; + var testableBinder = new Mock { 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(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 { CallBase = true }; + var binder = new Mock(){ CallBase = true }; binder .Setup(b => b.CreateModelPublic(It.IsAny())) .Verifiable(); @@ -381,7 +381,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding // Arrange var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), model: null); - var testableBinder = new Mock { CallBase = true }; + var testableBinder = new Mock { 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(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(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(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() { "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(); + MvcCoreMvcOptionsSetup.ConfigureMvc(options.Value, new TestHttpRequestStreamReaderFactory()); + + var lastIndex = options.Value.ModelBinderProviders.Count - 1; + Assert.IsType(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()); @@ -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 SettableList { get; set; } = new List { 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(); + 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()) + { + } + + public TestableComplexTypeModelBinder(IDictionary propertyBinders) + : base(propertyBinders) { Results = new Dictionary(); } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderProviderTest.cs new file mode 100644 index 0000000000..d60c3b2628 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderProviderTest.cs @@ -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>))] + [InlineData(typeof(ICollection))] + 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))] + [InlineData(typeof(Dictionary))] + 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) || + m.ModelType == typeof(int) || + m.ModelType == typeof(string)) + { + return Mock.Of(); + } + else + { + Assert.False(true, "Not the right model type"); + return null; + } + }); + + // Act + var result = provider.GetBinder(context); + + // Assert + Assert.IsType>(result); + } + + private class Person + { + public string Name { get; set; } + + public int Age { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs similarity index 90% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs index c582e13c0d..73ffa7cc89 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs @@ -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>() + var values = new Dictionary() { - { "someName[0]", new KeyValuePair(42, "forty-two") }, - { "someName[1]", new KeyValuePair(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(); + bindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", values); + + var binder = new DictionaryModelBinder(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>(result.Model); - Assert.True(modelState.IsValid); + var dictionary = Assert.IsAssignableFrom>(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>() + var values = new Dictionary() { - { "someName[0]", new KeyValuePair(42, "forty-two") }, - { "someName[1]", new KeyValuePair(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(); bindingContext.Model = dictionary; - var binder = new DictionaryModelBinder(); + + var binder = new DictionaryModelBinder(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 dictionary) { // Arrange - var binder = new DictionaryModelBinder(); + var binder = new DictionaryModelBinder(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(); + var binder = new DictionaryModelBinder(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(); + + var binder = new DictionaryModelBinder(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(); + 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( + new SimpleTypeModelBinder(), + new ComplexTypeModelBinder(new Dictionary() + { + { 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(dictionary); - var binder = new DictionaryModelBinder(); + var binder = new DictionaryModelBinder(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(); + var binder = new DictionaryModelBinder(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(); + var binder = new DictionaryModelBinder(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(); + var binder = new DictionaryModelBinder(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 dictionary) @@ -468,7 +471,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test private static DefaultModelBindingContext GetModelBindingContext( bool isReadOnly, - IDictionary> values) + IDictionary 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 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, }, diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormCollectionModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormCollectionModelBinderProviderTest.cs new file mode 100644 index 0000000000..e285a64175 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormCollectionModelBinderProviderTest.cs @@ -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))] + [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(result); + } + + private class TestClass + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/FormCollectionModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormCollectionModelBinderTest.cs similarity index 68% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/FormCollectionModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormCollectionModelBinderTest.cs index ceb925883a..c1833789c4 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/FormCollectionModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormCollectionModelBinderTest.cs @@ -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 - { - { "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 - { - { "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(), diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderProviderTest.cs new file mode 100644 index 0000000000..7c726c129b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderProviderTest.cs @@ -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))] + 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))] + [InlineData(typeof(Collection))] + 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(result); + } + + private class TestClass + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/FormFileModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderTest.cs similarity index 94% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/FormFileModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderTest.cs index 13c818ce11..cd99808e0e 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/FormFileModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderTest.cs @@ -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))] - 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(), diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderProviderTest.cs new file mode 100644 index 0000000000..34b4dd5222 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderProviderTest.cs @@ -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 NonHeaderBindingSources + { + get + { + return new TheoryData() + { + 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(result); + } + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(string[]))] + [InlineData(typeof(Collection))] + 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(result); + } + + [Theory] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(Collection))] + [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; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/HeaderModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderTests.cs similarity index 82% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/HeaderModelBinderTests.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderTests.cs index 5383e2be71..e03a1a0d10 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/HeaderModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderTests.cs @@ -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); } - [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, diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/KeyValuePairModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/KeyValuePairModelBinderProviderTest.cs new file mode 100644 index 0000000000..8f4587301e --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/KeyValuePairModelBinderProviderTest.cs @@ -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?))] + [InlineData(typeof(KeyValuePair[]))] + 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)); + context.OnCreatingBinder(m => + { + if (m.ModelType == typeof(string) || m.ModelType == typeof(int)) + { + return Mock.Of(); + } + else + { + Assert.False(true, "Not the right model type"); + return null; + } + }); + + // Act + var result = provider.GetBinder(context); + + // Assert + Assert.IsType>(result); + } + + private class Person + { + public string Name { get; set; } + + public int Age { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/KeyValuePairModelBinderTest.cs similarity index 89% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/KeyValuePairModelBinderTest.cs index 854701fde7..1a2bedf317 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/KeyValuePairModelBinderTest.cs @@ -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(); + var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair)); + var binder = new KeyValuePairModelBinder(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(); - + var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair)); + var binder = new KeyValuePairModelBinder(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(); + var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair)); + var binder = new KeyValuePairModelBinder(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(); + var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair)); + var binder = new KeyValuePairModelBinder(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(); + var valueProvider = new SimpleValueProvider(); + + var bindingContext = GetBindingContext(valueProvider, typeof(KeyValuePair)); + var binder = new KeyValuePairModelBinder(innerBinder, innerBinder); // Act - var result = await binder.TryBindStrongModel(bindingContext, "key"); + var result = await binder.TryBindStrongModel(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(); + var binder = new KeyValuePairModelBinder(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(); + var binder = new KeyValuePairModelBinder(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)), + 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); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ServicesModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ServicesModelBinderProviderTest.cs new file mode 100644 index 0000000000..e3a46b57bf --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ServicesModelBinderProviderTest.cs @@ -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 NonServicesBindingSources + { + get + { + return new TheoryData() + { + 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(result); + } + + private class IPersonService + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ServicesModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ServicesModelBinderTest.cs similarity index 69% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ServicesModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ServicesModelBinderTest.cs index ab7e6079c7..fefb721a9b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ServicesModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ServicesModelBinderTest.cs @@ -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, diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderProviderTest.cs new file mode 100644 index 0000000000..8c7329c6e5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderProviderTest.cs @@ -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))] + 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(result); + } + + private class TestClass + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/SimpleTypeModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs similarity index 88% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/SimpleTypeModelBinderTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs index bcdf9c767a..eec2c8cf4a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/SimpleTypeModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs @@ -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 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); } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeModelBinderTest.cs deleted file mode 100644 index a00d9f4c6e..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeModelBinderTest.cs +++ /dev/null @@ -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 { 1, 2, 3, 4, 5 }; - - var bindingContext = new DefaultModelBindingContext - { - FallbackToEmptyPrefix = true, - IsTopLevelObject = true, - ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), - 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)), - 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)), - 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)), - 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)), - 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(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(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 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 Validate(ValidationContext validationContext) - { - if (Password == "password") - { - yield return new ValidationResult("Password does not meet complexity requirements."); - } - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/DefaultModelBindingContextTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/DefaultModelBindingContextTest.cs index 2d0eb5d238..912cebc402 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/DefaultModelBindingContextTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/DefaultModelBindingContextTest.cs @@ -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(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(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(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()), + CultureInfo.CurrentCulture)); + return result; + } + private class TestModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelBinderAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelBinderAttributeTest.cs index 906b717837..34fafaec5a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelBinderAttributeTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelBinderAttributeTest.cs @@ -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 diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBinderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBinderFactoryTest.cs new file mode 100644 index 0000000000..d26440a439 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBinderFactoryTest.cs @@ -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(); + var factory = new ModelBinderFactory(metadataProvider, options); + + var context = new ModelBinderFactoryContext() + { + Metadata = metadataProvider.GetMetadataForType(typeof(string)), + }; + + // Act + var exception = Assert.Throws(() => 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(); + 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(); + } + + 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(); + 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(); + } + else if (c.Metadata.ModelType == typeof(WidgetId)) + { + return Mock.Of(); + } + + 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(); + 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(binder); + } + + return Mock.Of(); + })); + + 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(); + options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c => + { + Assert.Equal(typeof(Employee), c.Metadata.ModelType); + return Mock.Of(); + })); + + 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(); + options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c => + { + Assert.Equal(typeof(Employee), c.Metadata.ModelType); + return Mock.Of(); + })); + + 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 _factory; + + public TestModelBinderProvider(Func factory) + { + _factory = factory; + } + + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + return _factory(context); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs index 87e71e1a13..015750bed9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs @@ -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(), new List(), new Mock(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(), 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(), 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(), new List(), new Mock(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(), 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(), new List(), new Mock(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(), 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(), 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(), inputFormatters: new List(), objectModelValidator: new Mock(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(), 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(), inputFormatters: new List(), objectModelValidator: new Mock(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(), 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(), new List(), 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(); + binderProvider + .Setup(p => p.GetBinder(It.IsAny())) + .Returns(binder); + + return TestModelBinderFactory.Create(binderProvider.Object); + } + + private static ModelBinderFactory GetModelBinderFactory(params IModelBinderProvider[] providers) + { + return TestModelBinderFactory.CreateDefault(providers); } public class User diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/SimpleValueProvider.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/SimpleValueProvider.cs index 0fc396d21e..a5e4fac206 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/SimpleValueProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/SimpleValueProvider.cs @@ -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, IValueProvider { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/StubModelBinder.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/StubModelBinder.cs index b61e0738d0..a0953351fd 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/StubModelBinder.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/StubModelBinder.cs @@ -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 { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/TestModelBinderProviderContext.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/TestModelBinderProviderContext.cs new file mode 100644 index 0000000000..1a78730ca0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/TestModelBinderProviderContext.cs @@ -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> _binderCreators = + new List>(); + + 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 binderCreator) + { + _binderCreators.Add(binderCreator); + } + + public void OnCreatingBinder(ModelMetadata metadata, Func binderCreator) + { + _binderCreators.Add((m) => m.Equals(metadata) ? binderCreator() : null); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs index f24111d874..2a073eebe2 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs @@ -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); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ByteArrayModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ByteArrayModelBinderIntegrationTest.cs index 950c0ca7dd..bf09ff0232 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ByteArrayModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ByteArrayModelBinderIntegrationTest.cs @@ -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); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs index 578c912213..3186996089 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs @@ -293,11 +293,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests var modelState = operationContext.ActionContext.ModelState; // Act - var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? - default(ModelBindingResult); + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext); // Assert - Assert.Equal(default(ModelBindingResult), modelBindingResult); + Assert.False(modelBindingResult.Value.IsModelSet); // ModelState Assert.True(modelState.IsValid); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs index 081bab15e1..bfe2ad5b25 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs @@ -150,6 +150,22 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } } + private class AddressBinderProvider : IModelBinderProvider + { + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + var allowedBindingSource = context.BindingInfo?.BindingSource; + if (allowedBindingSource?.CanAcceptDataFrom(BindAddressAttribute.Source) == true) + { + // Binding Sources are opt-in. This model either didn't specify one or specified something + // incompatible so let other binders run. + return new AddressBinder(); + } + + return null; + } + } + private class AddressBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) @@ -158,6 +174,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests { throw new ArgumentNullException(nameof(bindingContext)); } + Debug.Assert(bindingContext.Result == null); var allowedBindingSource = bindingContext.BindingSource; @@ -180,7 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests public async Task GenericModelBinder_BindsCollection_ElementTypeUsesGreedyModelBinder_WithPrefix_Success() { // Arrange - var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(binderProvider: new AddressBinderProvider()); var parameter = new ParameterDescriptor() { Name = "parameter", @@ -189,8 +206,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // Need to have a key here so that the GenericModelBinder will recurse to bind elements. var operationContext = ModelBindingTestHelper.GetOperationBindingContext( - request => request.QueryString = new QueryString("?parameter.index=0"), - options => options.ModelBinders.Add(new AddressBinder())); + request => request.QueryString = new QueryString("?parameter.index=0")); var modelState = operationContext.ActionContext.ModelState; diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs index 601a29abb6..c82de145b8 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -40,28 +40,40 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), ValidatorProvider = new CompositeModelValidatorProvider(controllerContext.ValidatorProviders), ValueProvider = new CompositeValueProvider(controllerContext.ValueProviders), - ModelBinder = new CompositeModelBinder(controllerContext.ModelBinders), }; } - public static ControllerArgumentBinder GetArgumentBinder(MvcOptions options = null) + public static ControllerArgumentBinder GetArgumentBinder( + MvcOptions options = null, + IModelBinderProvider binderProvider = null) { if (options == null) { var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - return GetArgumentBinder(metadataProvider); + return GetArgumentBinder(metadataProvider, binderProvider); } else { var metadataProvider = TestModelMetadataProvider.CreateProvider(options.ModelMetadataDetailsProviders); - return GetArgumentBinder(metadataProvider); + return GetArgumentBinder(metadataProvider, binderProvider); } } - public static ControllerArgumentBinder GetArgumentBinder(IModelMetadataProvider metadataProvider) + public static ControllerArgumentBinder GetArgumentBinder( + IModelMetadataProvider metadataProvider, + IModelBinderProvider binderProvider = null) { + var services = GetServices(); + var options = services.GetRequiredService>(); + + if (binderProvider != null) + { + options.Value.ModelBinderProviders.Insert(0, binderProvider); + } + return new ControllerArgumentBinder( metadataProvider, + new ModelBinderFactory(metadataProvider, options), GetObjectValidator(metadataProvider)); } @@ -81,6 +93,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests updateRequest(httpContext.Request); } + httpContext.RequestServices = GetServices(updateOptions); + return httpContext; + } + + private static IServiceProvider GetServices(Action updateOptions = null) + { var serviceCollection = new ServiceCollection(); serviceCollection.AddMvc(); serviceCollection @@ -92,8 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests serviceCollection.Configure(updateOptions); } - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - return httpContext; + return serviceCollection.BuildServiceProvider(); } private static ControllerContext GetControllerContext(MvcOptions options, ActionContext context) @@ -108,7 +125,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests { InputFormatters = options.InputFormatters, ValidatorProviders = options.ModelValidatorProviders, - ModelBinders = options.ModelBinders, ValueProviders = valueProviderFactoryContext.ValueProviders }; } diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs index 5e83e1869a..778f689ebe 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs @@ -440,7 +440,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } [Fact] - public async Task BindParameter_NoData_DoesNotGetBound() + public async Task BindParameter_NoData_Fails() { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); @@ -463,7 +463,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // Assert // ModelBindingResult - Assert.Equal(default(ModelBindingResult), modelBindingResult); + Assert.Equal(ModelBindingResult.Failed(string.Empty), modelBindingResult); // ModelState Assert.True(modelState.IsValid); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs index 05cee32164..02602539bc 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs @@ -1091,17 +1091,17 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests string prefix, OperationBindingContext operationContext) { - return ModelBindingHelper.TryUpdateModelAsync( - model, - model.GetType(), - prefix, - operationContext.ActionContext, - operationContext.MetadataProvider, - operationContext.ModelBinder, - operationContext.ValueProvider, - operationContext.InputFormatters, - ModelBindingTestHelper.GetObjectValidator(operationContext.MetadataProvider), - operationContext.ValidatorProvider); + return ModelBindingHelper.TryUpdateModelAsync( + model, + model.GetType(), + prefix, + operationContext.ActionContext, + operationContext.MetadataProvider, + TestModelBinderFactory.CreateDefault(), + operationContext.ValueProvider, + operationContext.InputFormatters, + ModelBindingTestHelper.GetObjectValidator(operationContext.MetadataProvider), + operationContext.ValidatorProvider); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs index 19a93a5993..a270bbc19e 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; 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.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Razor; @@ -41,24 +42,28 @@ namespace Microsoft.AspNetCore.Mvc } [Fact] - public void Setup_SetsUpModelBinders() + public void Setup_SetsUpModelBinderProviders() { // Arrange & Act var options = GetOptions(); // Assert - Assert.Collection(options.ModelBinders, - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder), - binder => Assert.IsType(binder)); + Assert.Collection( + options.ModelBinderProviders, + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder), + binder => Assert.IsType(binder)); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelBinderFactory.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelBinderFactory.cs new file mode 100644 index 0000000000..78451aed0d --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelBinderFactory.cs @@ -0,0 +1,61 @@ +// 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.Internal; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + public class TestModelBinderFactory : ModelBinderFactory + { + public static TestModelBinderFactory Create(params IModelBinderProvider[] providers) + { + return Create(null, providers); + } + + public static TestModelBinderFactory Create( + IModelMetadataProvider metadataProvider, + params IModelBinderProvider[] providers) + { + if (metadataProvider == null) + { + metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + } + + var options = new TestOptionsManager(); + foreach (var provider in providers) + { + options.Value.ModelBinderProviders.Add(provider); + } + return new TestModelBinderFactory(metadataProvider, options); + } + + public static TestModelBinderFactory CreateDefault(params IModelBinderProvider[] providers) + { + return CreateDefault(null, providers); + } + + public static TestModelBinderFactory CreateDefault( + IModelMetadataProvider metadataProvider, + params IModelBinderProvider[] providers) + { + if (metadataProvider == null) + { + metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + } + + var options = new TestOptionsManager(); + foreach (var provider in providers) + { + options.Value.ModelBinderProviders.Add(provider); + } + new MvcCoreMvcOptionsSetup(new TestHttpRequestStreamReaderFactory()).Configure(options.Value); + return new TestModelBinderFactory(metadataProvider, options); + } + + protected TestModelBinderFactory(IModelMetadataProvider metadataProvider, IOptions options) + : base(metadataProvider, options) + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs index 1add42a3c5..736573d1e1 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs @@ -287,7 +287,6 @@ namespace Microsoft.AspNetCore.Mvc.Test var controllerContext = new ControllerContext() { HttpContext = httpContext, - ModelBinders = new[] { binder, }, ValueProviders = new[] { valueProvider, }, ValidatorProviders = new[] { diff --git a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageModelBinderTest.cs index 10261297e8..49d35e09b1 100644 --- a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageModelBinderTest.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim public class HttpRequestMessageModelBinderTest { [Fact] - public async Task BindModelAsync_ReturnsNonEmptyResult_ForHttpRequestMessageType() + public async Task BindModelAsync_BindsHttpRequestMessage() { // Arrange var binder = new HttpRequestMessageModelBinder(); @@ -37,23 +37,6 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim Assert.Null(entry.Metadata); } - [Theory] - [InlineData(typeof(int))] - [InlineData(typeof(object))] - [InlineData(typeof(HttpRequestMessageModelBinderTest))] - public async Task BindModelAsync_ReturnsNull_ForNonHttpRequestMessageType(Type type) - { - // Arrange - var binder = new HttpRequestMessageModelBinder(); - var bindingContext = GetBindingContext(type); - - // Act - await binder.BindModelAsync(bindingContext); - - // Assert - Assert.False(bindingContext.Result.HasValue); - } - private static DefaultModelBindingContext GetBindingContext(Type modelType) { var metadataProvider = new EmptyModelMetadataProvider();