diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBinderTypeProviderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBinderTypeProviderMetadata.cs
new file mode 100644
index 0000000000..4b1b680afa
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBinderTypeProviderMetadata.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Mvc.ModelBinding
+{
+ ///
+ /// Provides a which implements or
+ /// .
+ ///
+ public interface IBinderTypeProviderMetadata : IBinderMetadata
+ {
+ ///
+ /// A which implements either or
+ /// .
+ ///
+ Type BinderType { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/ModelBinderAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/ModelBinderAttribute.cs
new file mode 100644
index 0000000000..f8a3f848e6
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/ModelBinderAttribute.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Mvc.ModelBinding;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// An attribute that can specify a model name or type of or
+ /// to use for binding.
+ ///
+ [AttributeUsage(
+
+ // Support method parameters in actions.
+ AttributeTargets.Parameter |
+
+ // Support properties on model DTOs.
+ AttributeTargets.Property |
+
+ // Support model types.
+ AttributeTargets.Class |
+ AttributeTargets.Enum |
+ AttributeTargets.Struct,
+
+ AllowMultiple = false,
+ Inherited = true)]
+ public class ModelBinderAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata
+ {
+ private Type _binderType;
+
+ ///
+ public Type BinderType
+ {
+ get
+ {
+ return _binderType;
+ }
+ set
+ {
+ if (value != null)
+ {
+ if (!typeof(IModelBinder).IsAssignableFrom(value) &&
+ !typeof(IModelBinderProvider).IsAssignableFrom(value))
+ {
+ throw new InvalidOperationException(
+ Resources.FormatBinderType_MustBeIModelBinderOrIModelBinderProvider(
+ value.FullName,
+ typeof(IModelBinder).FullName,
+ typeof(IModelBinderProvider).FullName));
+ }
+ }
+
+ _binderType = value;
+ }
+ }
+
+ ///
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BinderTypeBasedModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BinderTypeBasedModelBinder.cs
new file mode 100644
index 0000000000..c34c3cfad2
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BinderTypeBasedModelBinder.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.Framework.DependencyInjection;
+
+namespace Microsoft.AspNet.Mvc.ModelBinding
+{
+ ///
+ /// An which can bind a model based on the value of
+ /// . The supplied
+ /// type or type will be used to bind the model.
+ ///
+ public class BinderTypeBasedModelBinder : IModelBinder
+ {
+ private readonly ITypeActivator _typeActivator;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The .
+ public BinderTypeBasedModelBinder([NotNull] ITypeActivator typeActivator)
+ {
+ _typeActivator = typeActivator;
+ }
+
+ public async Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ if (bindingContext.ModelMetadata.BinderType == null)
+ {
+ // Return false so that we are able to continue with the default set of model binders,
+ // if there is no specific model binder provided.
+ return false;
+ }
+
+ var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
+ var instance = _typeActivator.CreateInstance(requestServices, bindingContext.ModelMetadata.BinderType);
+
+ var modelBinder = instance as IModelBinder;
+ if (modelBinder == null)
+ {
+ var modelBinderProvider = instance as IModelBinderProvider;
+ if (modelBinderProvider != null)
+ {
+ modelBinder = new CompositeModelBinder(modelBinderProvider);
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ Resources.FormatBinderType_MustBeIModelBinderOrIModelBinderProvider(
+ bindingContext.ModelMetadata.BinderType.FullName,
+ typeof(IModelBinder).FullName,
+ typeof(IModelBinderProvider).FullName));
+ }
+ }
+
+ await modelBinder.BindModelAsync(bindingContext);
+
+ // return true here, because this binder will handle all cases where the model binder is
+ // specified by metadata.
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs
index 8e6e003179..03b1779f28 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs
@@ -23,6 +23,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
BinderMetadata = attributes.OfType().FirstOrDefault();
PropertyBindingInfo = attributes.OfType();
BinderModelNameProvider = attributes.OfType().FirstOrDefault();
+ BinderTypeProviders = attributes.OfType();
// Special case the [DisplayFormat] attribute hanging off an applied [DataType] attribute. This property is
// non-null for DataType.Currency, DataType.Date, DataType.Time, and potentially custom [DataType]
@@ -35,6 +36,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
+ ///
+ /// Gets (or sets in subclasses) found in collection passed
+ /// to the constructor, if any.
+ ///
+ public IEnumerable BinderTypeProviders { get; set; }
+
///
/// Gets (or sets in subclasses) found in collection passed to the
/// constructor, if any.
@@ -74,7 +81,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public HiddenInputAttribute HiddenInput { get; protected set; }
///
- /// Gets (or sets in subclasses) found in collection
+ /// Gets (or sets in subclasses) found in collection
/// passed to the constructor,
/// if any.
///
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs
index 7095d76b7a..e4148cd399 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs
@@ -35,6 +35,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
}
+ protected override Type ComputeBinderType()
+ {
+ if (PrototypeCache.BinderTypeProviders != null)
+ {
+ // The need for fallback here is to handle cases where a model binder is specified
+ // on a type and on a parameter to an action.
+ //
+ // We want to respect the value set by the parameter (if any), and use the value specifed
+ // on the type as a fallback.
+ //
+ // We generalize this process, in case someone adds ordered providers (with count > 2) through
+ // extensibility.
+ foreach (var provider in PrototypeCache.BinderTypeProviders)
+ {
+ if (provider.BinderType != null)
+ {
+ return provider.BinderType;
+ }
+ }
+ }
+
+ return base.ComputeBinderType();
+ }
+
protected override IBinderMetadata ComputeBinderMetadata()
{
return PrototypeCache.BinderMetadata != null
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs
index 6be9adfef4..c53daaed5e 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs
@@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private string _binderModelName;
private IReadOnlyList _binderIncludeProperties;
private IReadOnlyList _binderExcludeProperties;
+ private Type _binderType;
private bool _convertEmptyStringToNullComputed;
private bool _nullDisplayTextComputed;
@@ -55,6 +56,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _isBinderIncludePropertiesComputed;
private bool _isBinderModelNameComputed;
private bool _isBinderExcludePropertiesComputed;
+ private bool _isBinderTypeComputed;
// Constructor for creating real instances of the metadata class based on a prototype
protected CachedModelMetadata(CachedModelMetadata prototype, Func
public virtual IReadOnlyList BinderExcludeProperties { get; set; }
+ ///
+ /// The of an or an
+ /// of a model if specified explicitly using .
+ ///
+ public virtual Type BinderType { get; set; }
+
///
/// Gets or sets a binder metadata for this model.
///
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs
index 925624c0aa..a727041604 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs
@@ -442,6 +442,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("ModelStateDictionary_MaxModelStateErrors");
}
+ ///
+ /// The type '{0}' must implement either '{1}' or '{2}' to be used as a model binder.
+ ///
+ internal static string BinderType_MustBeIModelBinderOrIModelBinderProvider
+ {
+ get { return GetString("BinderType_MustBeIModelBinderOrIModelBinderProvider"); }
+ }
+
+ ///
+ /// The type '{0}' must implement either '{1}' or '{2}' to be used as a model binder.
+ ///
+ internal static string FormatBinderType_MustBeIModelBinderOrIModelBinderProvider(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("BinderType_MustBeIModelBinderOrIModelBinderProvider"), p0, p1, p2);
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx
index 81bc569163..b90c721ed0 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx
@@ -1,17 +1,17 @@
-
@@ -198,4 +198,7 @@
The maximum number of allowed model errors has been reached.
+
+ The type '{0}' must implement either '{1}' or '{2}' to be used as a model binder.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ParameterBinding/ModelBinderAttribute.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ParameterBinding/ModelBinderAttribute.cs
deleted file mode 100644
index 0b7303369f..0000000000
--- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ParameterBinding/ModelBinderAttribute.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using Microsoft.AspNet.Mvc.ModelBinding;
-
-namespace System.Web.Http
-{
- ///
- /// An attribute that specifies that the value can be bound by a model binder.
- ///
- [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
- public class ModelBinderAttribute : Attribute, IBinderMetadata
- {
- }
-}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
index 2be43e3715..dc5653e263 100644
--- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
+++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
@@ -27,6 +27,7 @@ namespace Microsoft.AspNet.Mvc
options.ViewEngines.Add(typeof(RazorViewEngine));
// Set up ModelBinding
+ options.ModelBinders.Add(typeof(BinderTypeBasedModelBinder));
options.ModelBinders.Add(typeof(ServicesModelBinder));
options.ModelBinders.Add(typeof(BodyModelBinder));
options.ModelBinders.Add(new TypeConverterModelBinder());
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingModelBinderAttributeTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingModelBinderAttributeTest.cs
new file mode 100644
index 0000000000..9bb00261a7
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingModelBinderAttributeTest.cs
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
+using System.Linq.Expressions;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.TestHost;
+using ModelBindingWebSite;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.FunctionalTests
+{
+ public class ModelBindingModelBinderAttributeTest
+ {
+ private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(ModelBindingWebSite));
+ private readonly Action _app = new ModelBindingWebSite.Startup().Configure;
+
+ [Fact]
+ public async Task ModelBinderAttribute_CustomModelPrefix()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // [ModelBinder(Name = "customPrefix")] is used to apply a prefix
+ var url =
+ "http://localhost/ModelBinderAttribute_Company/GetCompany?customPrefix.Employees[0].Name=somename";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var company = JsonConvert.DeserializeObject(body);
+
+ var employee = Assert.Single(company.Employees);
+ Assert.Equal("somename", employee.Name);
+ }
+
+ [Theory]
+ [InlineData("GetBinderType_UseModelBinderOnType")]
+ [InlineData("GetBinderType_UseModelBinderProviderOnType")]
+ public async Task ModelBinderAttribute_WithPrefixOnParameter(string action)
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // [ModelBinder(Name = "customPrefix")] is used to apply a prefix
+ var url =
+ "http://localhost/ModelBinderAttribute_Product/" +
+ action +
+ "?customPrefix.ProductId=5";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Equal(
+ "ModelBindingWebSite.Controllers.ModelBinderAttribute_ProductController+ProductModelBinder",
+ body);
+ }
+
+ [Theory]
+ [InlineData("GetBinderType_UseModelBinder")]
+ [InlineData("GetBinderType_UseModelBinderProvider")]
+ public async Task ModelBinderAttribute_WithBinderOnParameter(string action)
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url =
+ "http://localhost/ModelBinderAttribute_Product/" +
+ action +
+ "?model.productId=5";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Equal(
+ "ModelBindingWebSite.Controllers.ModelBinderAttribute_ProductController+ProductModelBinder",
+ body);
+ }
+
+ [Fact]
+ public async Task ModelBinderAttribute_WithBinderOnEnum()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url =
+ "http://localhost/ModelBinderAttribute_Product/" +
+ "ModelBinderAttribute_UseModelBinderOnEnum" +
+ "?status=Shipped";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Equal("StatusShipped", body);
+ }
+
+ private class Product
+ {
+ public int ProductId { get; set; }
+
+ public string BinderType { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs
index e1165a1842..de7c18dd4f 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs
@@ -64,8 +64,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.PostAsync("http://localhost/Home/GetCustomer?Id=1234", content);
-
- //Assert
+
+ // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var customer = JsonConvert.DeserializeObject(
await response.Content.ReadAsStringAsync());
diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BinderTypeBasedModelBinderModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BinderTypeBasedModelBinderModelBinderTest.cs
new file mode 100644
index 0000000000..8e3c85ba38
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BinderTypeBasedModelBinderModelBinderTest.cs
@@ -0,0 +1,199 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#if ASPNET50
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.Framework.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.ModelBinding.Test
+{
+ public class BinderTypeBasedModelBinderModelBinderTest
+ {
+ [Fact]
+ public async Task BindModel_ReturnsFalseIfNoBinderTypeIsSet()
+ {
+ // Arrange
+ var bindingContext = GetBindingContext(typeof(Person));
+
+ var binder = new BinderTypeBasedModelBinder(Mock.Of());
+
+ // Act
+ var binderResult = await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.False(binderResult);
+ }
+
+ [Fact]
+ public async Task BindModel_ReturnsTrueEvenIfSelectedBinderReturnsFalse()
+ {
+ // Arrange
+ var bindingContext = GetBindingContext(typeof(Person));
+ bindingContext.ModelMetadata.BinderType = typeof(FalseModelBinder);
+
+ var innerModelBinder = new FalseModelBinder();
+
+ var mockITypeActivator = new Mock();
+ mockITypeActivator
+ .Setup(o => o.CreateInstance(It.IsAny(), typeof(FalseModelBinder)))
+ .Returns(innerModelBinder);
+
+ var binder = new BinderTypeBasedModelBinder(mockITypeActivator.Object);
+
+ // Act
+ var binderResult = await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(binderResult);
+ }
+
+ [Fact]
+ public async Task BindModel_CallsBindAsync_OnProvidedModelBinder()
+ {
+ // Arrange
+ var bindingContext = GetBindingContext(typeof(Person));
+ bindingContext.ModelMetadata.BinderType = typeof(TrueModelBinder);
+
+ var model = new Person();
+ var innerModelBinder = new TrueModelBinder(model);
+
+ var mockITypeActivator = new Mock();
+ mockITypeActivator
+ .Setup(o => o.CreateInstance(It.IsAny(), typeof(TrueModelBinder)))
+ .Returns(innerModelBinder);
+
+ var binder = new BinderTypeBasedModelBinder(mockITypeActivator.Object);
+
+ // Act
+ var binderResult = await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(binderResult);
+ Assert.Same(model, bindingContext.Model);
+ }
+
+ [Fact]
+ public async Task BindModel_CallsBindAsync_OnProvidedModelBinderProvider()
+ {
+ // Arrange
+ var bindingContext = GetBindingContext(typeof(Person));
+ bindingContext.ModelMetadata.BinderType = typeof(ModelBinderProvider);
+
+ var model = new Person();
+ var innerModelBinder = new TrueModelBinder(model);
+
+ var provider = new ModelBinderProvider(innerModelBinder);
+
+ var mockITypeActivator = new Mock();
+ mockITypeActivator
+ .Setup(o => o.CreateInstance(It.IsAny(), typeof(ModelBinderProvider)))
+ .Returns(provider);
+
+ var binder = new BinderTypeBasedModelBinder(mockITypeActivator.Object);
+
+ // Act
+ var binderResult = await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(binderResult);
+ Assert.Same(model, bindingContext.Model);
+ }
+
+ [Fact]
+ public async Task BindModel_ForNonModelBinderAndModelBinderProviderTypes_Throws()
+ {
+ // Arrange
+ var bindingContext = GetBindingContext(typeof(Person));
+ bindingContext.ModelMetadata.BinderType = typeof(string);
+ var binder = new BinderTypeBasedModelBinder(Mock.Of());
+
+ var expected = "The type 'System.String' must implement either " +
+ "'Microsoft.AspNet.Mvc.ModelBinding.IModelBinder' or " +
+ "'Microsoft.AspNet.Mvc.ModelBinding.IModelBinderProvider' to be used as a model binder.";
+
+ // Act
+ var ex = await Assert.ThrowsAsync(
+ () => binder.BindModelAsync(bindingContext));
+
+ // Assert
+ Assert.Equal(expected, ex.Message);
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ var metadataProvider = new DataAnnotationsModelMetadataProvider();
+ var operationBindingContext = new OperationBindingContext
+ {
+ MetadataProvider = metadataProvider,
+ HttpContext = new DefaultHttpContext(),
+ ValidatorProvider = Mock.Of(),
+ };
+
+ var bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = metadataProvider.GetMetadataForType(null, modelType),
+ ModelName = "someName",
+ ValueProvider = Mock.Of(),
+ ModelState = new ModelStateDictionary(),
+ OperationBindingContext = operationBindingContext,
+ };
+
+ return bindingContext;
+ }
+
+ private class Person
+ {
+ public string Name { get; set; }
+
+ public int Age { get; set; }
+ }
+
+ private class FalseModelBinder : IModelBinder
+ {
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ return Task.FromResult(false);
+ }
+ }
+
+ private class TrueModelBinder : IModelBinder
+ {
+ private readonly object _model;
+
+ public TrueModelBinder(object model)
+ {
+ _model = model;
+ }
+
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ bindingContext.Model = _model;
+ return Task.FromResult(true);
+ }
+ }
+
+ private class ModelBinderProvider : IModelBinderProvider
+ {
+ private readonly IModelBinder _inner;
+
+ public ModelBinderProvider(IModelBinder inner)
+ {
+ _inner = inner;
+ }
+
+ public IReadOnlyList ModelBinders
+ {
+ get
+ {
+ return new List() { _inner, };
+ }
+ }
+ }
+ }
+}
+#endif
diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelBinderAttributeTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelBinderAttributeTest.cs
new file mode 100644
index 0000000000..612d9b3926
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelBinderAttributeTest.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.ModelBinding
+{
+ public class ModelBinderAttributeTest
+ {
+ [Fact]
+ public void InvalidBinderType_Throws()
+ {
+ // Arrange
+ var attribute = new ModelBinderAttribute();
+
+ var expected =
+ "The type 'System.String' must implement either " +
+ "'Microsoft.AspNet.Mvc.ModelBinding.IModelBinder' or " +
+ "'Microsoft.AspNet.Mvc.ModelBinding.IModelBinderProvider' to be used as a model binder.";
+
+ // Act
+ var ex = Assert.Throws(() => { attribute.BinderType = typeof(string); });
+
+ // Assert
+ Assert.Equal(expected, ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs
index 3e73de6703..694180aee6 100644
--- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs
@@ -39,7 +39,8 @@ namespace Microsoft.AspNet.Mvc
// Assert
var i = 0;
- Assert.Equal(9, mvcOptions.ModelBinders.Count);
+ Assert.Equal(10, mvcOptions.ModelBinders.Count);
+ Assert.Equal(typeof(BinderTypeBasedModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(ServicesModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(BodyModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(TypeConverterModelBinder), mvcOptions.ModelBinders[i++].OptionType);
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_CompanyController.cs b/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_CompanyController.cs
new file mode 100644
index 0000000000..0c022002cc
--- /dev/null
+++ b/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_CompanyController.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNet.Mvc;
+
+namespace ModelBindingWebSite.Controllers
+{
+ [Route("ModelBinderAttribute_Company/[action]")]
+ public class ModelBinderAttribute_CompanyController : Controller
+ {
+ // Uses Name to set a custom prefix
+ public Company GetCompany([ModelBinder(Name = "customPrefix")] Company company)
+ {
+ return company;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_ProductController.cs b/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_ProductController.cs
new file mode 100644
index 0000000000..180fb5de6a
--- /dev/null
+++ b/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_ProductController.cs
@@ -0,0 +1,141 @@
+// Copyright (c) Microsoft Open Technologies, Inc. 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.Tasks;
+using Microsoft.AspNet.Mvc;
+using Microsoft.AspNet.Mvc.ModelBinding;
+
+namespace ModelBindingWebSite.Controllers
+{
+ [Route("ModelBinderAttribute_Product/[action]")]
+ public class ModelBinderAttribute_ProductController : Controller
+ {
+ public string GetBinderType_UseModelBinderOnType(
+ [ModelBinder(Name = "customPrefix")] ProductWithBinderOnType model)
+ {
+ return model.BinderType.FullName;
+ }
+
+ public string GetBinderType_UseModelBinderProviderOnType(
+ [ModelBinder(Name = "customPrefix")] ProductWithBinderProviderOnType model)
+ {
+ return model.BinderType.FullName;
+ }
+
+ public string GetBinderType_UseModelBinder(
+ [ModelBinder(BinderType = typeof(ProductModelBinder))] Product model)
+ {
+ return model.BinderType.FullName;
+ }
+
+ public string GetBinderType_UseModelBinderProvider(
+ [ModelBinder(BinderType = typeof(ProductModelBinderProvider))] Product model)
+ {
+ return model.BinderType.FullName;
+ }
+
+ public string GetBinderType_UseModelBinderOnProperty(Order order)
+ {
+ return order.Product.BinderType.FullName;
+ }
+
+ public string ModelBinderAttribute_UseModelBinderOnEnum(OrderStatus status)
+ {
+ return status.ToString();
+ }
+
+ public class Product
+ {
+ public int ProductId { get; set; }
+
+ // Will be set by the binder
+ public Type BinderType { get; set; }
+ }
+
+ [ModelBinder(BinderType = typeof(ProductModelBinder))]
+ public class ProductWithBinderOnType : Product
+ {
+ }
+
+ [ModelBinder(BinderType = typeof(ProductModelBinderProvider))]
+ public class ProductWithBinderProviderOnType : Product
+ {
+ }
+
+ public class Order
+ {
+ [ModelBinder(BinderType = typeof(ProductModelBinder))]
+ public Product Product { get; set; }
+ }
+
+ [ModelBinder(BinderType = typeof(OrderStatusBinder))]
+ public enum OrderStatus
+ {
+ StatusOutOfStock,
+ StatusShipped,
+ StatusRecieved,
+ }
+
+ private class OrderStatusBinder : IModelBinder
+ {
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ if (typeof(OrderStatus).IsAssignableFrom(bindingContext.ModelType))
+ {
+ var request = bindingContext.OperationBindingContext.HttpContext.Request;
+
+ // Doing something slightly different here to make sure we don't get accidentally bound
+ // by the type converter binder.
+ OrderStatus model;
+ if (Enum.TryParse("Status" + request.Query.Get("status"), out model))
+ {
+ bindingContext.Model = model;
+ }
+
+ return Task.FromResult(true);
+ }
+
+ return Task.FromResult(false);
+ }
+ }
+
+ private class ProductModelBinder : IModelBinder
+ {
+ public async Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ if (typeof(Product).IsAssignableFrom(bindingContext.ModelType))
+ {
+ var model = (Product)Activator.CreateInstance(bindingContext.ModelType);
+
+ model.BinderType = GetType();
+
+ var key =
+ string.IsNullOrEmpty(bindingContext.ModelName) ?
+ "productId" :
+ bindingContext.ModelName + "." + "productId";
+
+ var value = await bindingContext.ValueProvider.GetValueAsync(key);
+ model.ProductId = (int)value.ConvertTo(typeof(int));
+
+ bindingContext.Model = model;
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ private class ProductModelBinderProvider : IModelBinderProvider
+ {
+ public IReadOnlyList ModelBinders
+ {
+ get
+ {
+ return new[] { new ProductModelBinder() };
+ }
+ }
+ }
+ }
+}
\ No newline at end of file