Adding support for ModelMetadataAttribute in core.
This commit is contained in:
parent
ba8cf3ca46
commit
e9bcc3f0e8
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue