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 modelAccessor) @@ -473,8 +475,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + /// + public sealed override Type BinderType + { + get + { + if (!_isBinderTypeComputed) + { + _binderType = ComputeBinderType(); + _isBinderTypeComputed = true; + } + return _binderType; + } + set + { + _binderType = value; + _isBinderTypeComputed = true; + } + } + protected TPrototypeCache PrototypeCache { get; set; } + protected virtual Type ComputeBinderType() + { + return base.BinderType; + } + protected virtual IBinderMetadata ComputeBinderMetadata() { return base.BinderMetadata; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs index d551fdd477..af7d9ad835 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } /// - /// Represents the name of a model if specified explicitly using . + /// The name of the model if specified explicitly using . /// public virtual string BinderModelName { get; set; } @@ -62,6 +62,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// 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