Adding support for ModelMetadataAttribute in core.

This commit is contained in:
Harsh Gupta 2014-11-19 14:13:18 -08:00 committed by Ryan Nowak
parent ba8cf3ca46
commit e9bcc3f0e8
18 changed files with 771 additions and 47 deletions

View File

@ -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
{
/// <summary>
/// Provides a <see cref="Type"/> which implements <see cref="IModelBinder"/> or
/// <see cref="IModelBinderProvider"/>.
/// </summary>
public interface IBinderTypeProviderMetadata : IBinderMetadata
{
/// <summary>
/// A <see cref="Type"/> which implements either <see cref="IModelBinder"/> or
/// <see cref="IModelBinderProvider"/>.
/// </summary>
Type BinderType { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// An attribute that can specify a model name or type of <see cref="IModelBinder"/> or
/// <see cref="IModelBinderProvider"/> to use for binding.
/// </summary>
[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;
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
public string Name { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="IModelBinder"/> which can bind a model based on the value of
/// <see cref="ModelMetadata.BinderType"/>. The supplied <see cref="IModelBinder"/>
/// type or <see cref="IModelBinderProvider"/> type will be used to bind the model.
/// </summary>
public class BinderTypeBasedModelBinder : IModelBinder
{
private readonly ITypeActivator _typeActivator;
/// <summary>
/// Creates a new instance of <see cref="BinderTypeBasedModelBinder"/>.
/// </summary>
/// <param name="typeActivator">The <see cref="ITypeActivator"/>.</param>
public BinderTypeBasedModelBinder([NotNull] ITypeActivator typeActivator)
{
_typeActivator = typeActivator;
}
public async Task<bool> 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;
}
}
}

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
BinderMetadata = attributes.OfType<IBinderMetadata>().FirstOrDefault();
PropertyBindingInfo = attributes.OfType<IPropertyBindingInfo>();
BinderModelNameProvider = attributes.OfType<IModelNameProvider>().FirstOrDefault();
BinderTypeProviders = attributes.OfType<IBinderTypeProviderMetadata>();
// 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
}
}
/// <summary>
/// Gets (or sets in subclasses) <see cref="IEnumerable{IBinderTypeProviderMetadata}"/> found in collection passed
/// to the <see cref="CachedDataAnnotationsMetadataAttributes(IEnumerable{object})"/> constructor, if any.
/// </summary>
public IEnumerable<IBinderTypeProviderMetadata> BinderTypeProviders { get; set; }
/// <summary>
/// Gets (or sets in subclasses) <see cref="IBinderMetadata"/> found in collection passed to the
/// <see cref="CachedDataAnnotationsMetadataAttributes(IEnumerable{object})"/> constructor, if any.
@ -74,7 +81,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public HiddenInputAttribute HiddenInput { get; protected set; }
/// <summary>
/// Gets (or sets in subclasses) <see cref="IEnumerable{IModelPropertyBindingInfo}"/> found in collection
/// Gets (or sets in subclasses) <see cref="IEnumerable{IPropertyBindingInfo}"/> found in collection
/// passed to the <see cref="CachedDataAnnotationsMetadataAttributes(IEnumerable{object})"/> constructor,
/// if any.
/// </summary>

View File

@ -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

View File

@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private string _binderModelName;
private IReadOnlyList<string> _binderIncludeProperties;
private IReadOnlyList<string> _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<TPrototypeCache> prototype, Func<object> modelAccessor)
@ -473,8 +475,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
/// <inheritdoc />
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;

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
/// <summary>
/// Represents the name of a model if specified explicitly using <see cref="IModelNameProvider"/>.
/// The name of the model if specified explicitly using <see cref="IModelNameProvider"/>.
/// </summary>
public virtual string BinderModelName { get; set; }
@ -62,6 +62,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public virtual IReadOnlyList<string> BinderExcludeProperties { get; set; }
/// <summary>
/// The <see cref="Type"/> of an <see cref="IModelBinder"/> or an <see cref="IModelBinderProvider"/>
/// of a model if specified explicitly using <see cref="IBinderTypeProviderMetadata"/>.
/// </summary>
public virtual Type BinderType { get; set; }
/// <summary>
/// Gets or sets a binder metadata for this model.
/// </summary>

View File

@ -442,6 +442,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("ModelStateDictionary_MaxModelStateErrors");
}
/// <summary>
/// The type '{0}' must implement either '{1}' or '{2}' to be used as a model binder.
/// </summary>
internal static string BinderType_MustBeIModelBinderOrIModelBinderProvider
{
get { return GetString("BinderType_MustBeIModelBinderOrIModelBinderProvider"); }
}
/// <summary>
/// The type '{0}' must implement either '{1}' or '{2}' to be used as a model binder.
/// </summary>
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);

View File

@ -198,4 +198,7 @@
<data name="ModelStateDictionary_MaxModelStateErrors" xml:space="preserve">
<value>The maximum number of allowed model errors has been reached.</value>
</data>
<data name="BinderType_MustBeIModelBinderOrIModelBinderProvider" xml:space="preserve">
<value>The type '{0}' must implement either '{1}' or '{2}' to be used as a model binder.</value>
</data>
</root>

View File

@ -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
{
/// <summary>
/// An attribute that specifies that the value can be bound by a model binder.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class ModelBinderAttribute : Attribute, IBinderMetadata
{
}
}

View File

@ -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());

View File

@ -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<IApplicationBuilder> _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<Company>(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; }
}
}
}

View File

@ -65,7 +65,7 @@ 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<Customer>(
await response.Content.ReadAsStringAsync());

View File

@ -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<ITypeActivator>());
// 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<ITypeActivator>();
mockITypeActivator
.Setup(o => o.CreateInstance(It.IsAny<IServiceProvider>(), 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<ITypeActivator>();
mockITypeActivator
.Setup(o => o.CreateInstance(It.IsAny<IServiceProvider>(), 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<ITypeActivator>();
mockITypeActivator
.Setup(o => o.CreateInstance(It.IsAny<IServiceProvider>(), 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<ITypeActivator>());
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<InvalidOperationException>(
() => 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<IModelValidatorProvider>(),
};
var bindingContext = new ModelBindingContext
{
ModelMetadata = metadataProvider.GetMetadataForType(null, modelType),
ModelName = "someName",
ValueProvider = Mock.Of<IValueProvider>(),
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<bool> BindModelAsync(ModelBindingContext bindingContext)
{
return Task.FromResult(false);
}
}
private class TrueModelBinder : IModelBinder
{
private readonly object _model;
public TrueModelBinder(object model)
{
_model = model;
}
public Task<bool> 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<IModelBinder> ModelBinders
{
get
{
return new List<IModelBinder>() { _inner, };
}
}
}
}
}
#endif

View File

@ -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<InvalidOperationException>(() => { attribute.BinderType = typeof(string); });
// Assert
Assert.Equal(expected, ex.Message);
}
}
}

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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<bool> 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<OrderStatus>("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<bool> 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<IModelBinder> ModelBinders
{
get
{
return new[] { new ProductModelBinder() };
}
}
}
}
}