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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -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

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