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