-Issue #913 - Model-binding is being case-sensitive when binding Url data to Enum parameter.
Fix: Using TypeConverter solves this problem. -Issue #1123 - TypeConverterModelBinder cannot bind "byte" and "short". Fix: Modified code to use TypeConverter which can handle these scenarios. -Removing the GetConverterDelegate method and making the code similar to the WebApi.
This commit is contained in:
parent
a41b9dc983
commit
5fa8a91111
|
|
@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
private static bool CanBindType(Type modelType)
|
private static bool CanBindType(Type modelType)
|
||||||
{
|
{
|
||||||
// Simple types cannot use this binder
|
// Simple types cannot use this binder
|
||||||
var isComplexType = !ValueProviderResult.CanConvertFromString(modelType);
|
var isComplexType = !TypeHelper.HasStringConverter(modelType);
|
||||||
if (!isComplexType)
|
if (!isComplexType)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
{
|
{
|
||||||
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||||
|
|
||||||
if (!ValueProviderResult.CanConvertFromString(bindingContext.ModelType))
|
if (!TypeHelper.HasStringConverter(bindingContext.ModelType))
|
||||||
{
|
{
|
||||||
// this type cannot be converted
|
// this type cannot be converted
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
|
@ -18,5 +19,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
type.Equals(typeof(DateTimeOffset)) ||
|
type.Equals(typeof(DateTimeOffset)) ||
|
||||||
type.Equals(typeof(TimeSpan));
|
type.Equals(typeof(TimeSpan));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool HasStringConverter(Type type)
|
||||||
|
{
|
||||||
|
return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +107,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
|
||||||
public virtual bool IsComplexType
|
public virtual bool IsComplexType
|
||||||
{
|
{
|
||||||
get { return !ValueProviderResult.CanConvertFromString(ModelType); }
|
get { return !TypeHelper.HasStringConverter(ModelType); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsNullableValueType
|
public bool IsNullableValueType
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
|
@ -53,7 +54,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
if (value == null)
|
if (value == null)
|
||||||
{
|
{
|
||||||
// treat null route parameters as though they were the default value for the type
|
// treat null route parameters as though they were the default value for the type
|
||||||
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) :
|
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) :
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,34 +69,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
|
||||||
public static bool CanConvertFromString(Type destinationType)
|
public static bool CanConvertFromString(Type destinationType)
|
||||||
{
|
{
|
||||||
return GetConverterDelegate(destinationType) != null;
|
return TypeHelper.IsSimpleType(UnwrapNullableType(destinationType)) ||
|
||||||
}
|
TypeHelper.HasStringConverter(destinationType);
|
||||||
|
|
||||||
private object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
|
|
||||||
{
|
|
||||||
if (value == null || value.GetType().IsAssignableFrom(destinationType))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case of a Nullable object, we try again with its underlying type.
|
|
||||||
destinationType = UnwrapNullableType(destinationType);
|
|
||||||
|
|
||||||
// if this is a user-input value but the user didn't type anything, return no value
|
|
||||||
var valueAsString = value as string;
|
|
||||||
if (valueAsString != null && string.IsNullOrWhiteSpace(valueAsString))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var converter = GetConverterDelegate(destinationType);
|
|
||||||
if (converter == null)
|
|
||||||
{
|
|
||||||
var message = Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType);
|
|
||||||
throw new InvalidOperationException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return converter(value, culture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
|
private object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
|
||||||
|
|
@ -144,121 +119,57 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
return ConvertSimpleType(culture, value, destinationType);
|
return ConvertSimpleType(culture, value, destinationType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Func<object, CultureInfo, object> GetConverterDelegate(Type destinationType)
|
private object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
|
||||||
{
|
{
|
||||||
|
if (value == null || value.GetType().IsAssignableFrom(destinationType))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case of a Nullable object, we try again with its underlying type.
|
||||||
destinationType = UnwrapNullableType(destinationType);
|
destinationType = UnwrapNullableType(destinationType);
|
||||||
|
|
||||||
if (destinationType == typeof(string))
|
// if this is a user-input value but the user didn't type anything, return no value
|
||||||
|
var valueAsString = value as string;
|
||||||
|
if (valueAsString != null && string.IsNullOrWhiteSpace(valueAsString))
|
||||||
{
|
{
|
||||||
return (value, culture) => Convert.ToString(value, culture);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (destinationType == typeof(int))
|
var converter = TypeDescriptor.GetConverter(destinationType);
|
||||||
|
var canConvertFrom = converter.CanConvertFrom(value.GetType());
|
||||||
|
if (!canConvertFrom)
|
||||||
{
|
{
|
||||||
return (value, culture) => Convert.ToInt32(value, culture);
|
converter = TypeDescriptor.GetConverter(value.GetType());
|
||||||
}
|
}
|
||||||
|
if (!(canConvertFrom || converter.CanConvertTo(destinationType)))
|
||||||
if (destinationType == typeof(long))
|
|
||||||
{
|
{
|
||||||
return (value, culture) => Convert.ToInt64(value, culture);
|
// EnumConverter cannot convert integer, so we verify manually
|
||||||
}
|
if (destinationType.IsEnum() && (value is int))
|
||||||
|
|
||||||
if (destinationType == typeof(float))
|
|
||||||
{
|
|
||||||
return (value, culture) => Convert.ToSingle(value, culture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (destinationType == typeof(double))
|
|
||||||
{
|
|
||||||
return (value, culture) => Convert.ToDouble(value, culture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (destinationType == typeof(decimal))
|
|
||||||
{
|
|
||||||
return (value, culture) => Convert.ToDecimal(value, culture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (destinationType == typeof(bool))
|
|
||||||
{
|
|
||||||
return (value, culture) => Convert.ToBoolean(value, culture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (destinationType == typeof(DateTime))
|
|
||||||
{
|
|
||||||
return (value, culture) =>
|
|
||||||
{
|
{
|
||||||
ThrowIfNotStringType(value, destinationType);
|
return Enum.ToObject(destinationType, (int)value);
|
||||||
return DateTime.Parse((string)value, culture);
|
}
|
||||||
};
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (destinationType == typeof(DateTimeOffset))
|
try
|
||||||
{
|
{
|
||||||
return (value, culture) =>
|
return canConvertFrom
|
||||||
{
|
? converter.ConvertFrom(null, culture, value)
|
||||||
ThrowIfNotStringType(value, destinationType);
|
: converter.ConvertTo(null, culture, value, destinationType);
|
||||||
return DateTimeOffset.Parse((string)value, culture);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
if (destinationType == typeof(TimeSpan))
|
|
||||||
{
|
{
|
||||||
return (value, culture) =>
|
throw new InvalidOperationException(
|
||||||
{
|
Resources.FormatValueProviderResult_ConversionThrew(value.GetType(), destinationType), ex);
|
||||||
ThrowIfNotStringType(value, destinationType);
|
|
||||||
return TimeSpan.Parse((string)value, culture);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (destinationType == typeof(Guid))
|
|
||||||
{
|
|
||||||
return (value, culture) =>
|
|
||||||
{
|
|
||||||
ThrowIfNotStringType(value, destinationType);
|
|
||||||
return Guid.Parse((string)value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (destinationType.GetTypeInfo().IsEnum)
|
|
||||||
{
|
|
||||||
return (value, culture) =>
|
|
||||||
{
|
|
||||||
// EnumConverter cannot convert integer, so we verify manually
|
|
||||||
if ((value is int))
|
|
||||||
{
|
|
||||||
if (Enum.IsDefined(destinationType, value))
|
|
||||||
{
|
|
||||||
return Enum.ToObject(destinationType, (int)value);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FormatException(
|
|
||||||
Resources.FormatValueProviderResult_CannotConvertEnum(value,
|
|
||||||
destinationType));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ThrowIfNotStringType(value, destinationType);
|
|
||||||
return Enum.Parse(destinationType, (string)value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Type UnwrapNullableType(Type destinationType)
|
private static Type UnwrapNullableType(Type destinationType)
|
||||||
{
|
{
|
||||||
return Nullable.GetUnderlyingType(destinationType) ?? destinationType;
|
return Nullable.GetUnderlyingType(destinationType) ?? destinationType;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ThrowIfNotStringType(object value, Type destinationType)
|
|
||||||
{
|
|
||||||
var type = value.GetType();
|
|
||||||
if (type != typeof(string))
|
|
||||||
{
|
|
||||||
var message = Resources.FormatValueProviderResult_NoConverterExists(type, destinationType);
|
|
||||||
throw new InvalidOperationException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"System.Collections": "4.0.10-beta-*",
|
"System.Collections": "4.0.10-beta-*",
|
||||||
"System.Collections.Concurrent": "4.0.0-beta-*",
|
"System.Collections.Concurrent": "4.0.0-beta-*",
|
||||||
"System.ComponentModel": "4.0.0-beta-*",
|
"System.ComponentModel": "4.0.0-beta-*",
|
||||||
|
"System.ComponentModel.TypeConverter": "4.0.0-beta-*",
|
||||||
"System.Diagnostics.Contracts": "4.0.0-beta-*",
|
"System.Diagnostics.Contracts": "4.0.0-beta-*",
|
||||||
"System.Diagnostics.Debug": "4.0.10-beta-*",
|
"System.Diagnostics.Debug": "4.0.10-beta-*",
|
||||||
"System.Diagnostics.Tools": "4.0.0-beta-*",
|
"System.Diagnostics.Tools": "4.0.0-beta-*",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
[InlineData(typeof(byte))]
|
||||||
|
[InlineData(typeof(short))]
|
||||||
[InlineData(typeof(int))]
|
[InlineData(typeof(int))]
|
||||||
[InlineData(typeof(long))]
|
[InlineData(typeof(long))]
|
||||||
[InlineData(typeof(Guid))]
|
[InlineData(typeof(Guid))]
|
||||||
|
|
@ -62,8 +64,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
|
public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var message = TestPlatformHelper.IsMono ? "Input string was not in the correct format" :
|
var message = "The parameter conversion from type 'System.String' to type 'System.Int32' failed." +
|
||||||
"Input string was not in a correct format.";
|
" See the inner exception for more information.";
|
||||||
var bindingContext = GetBindingContext(typeof(int));
|
var bindingContext = GetBindingContext(typeof(int));
|
||||||
bindingContext.ValueProvider = new SimpleHttpValueProvider
|
bindingContext.ValueProvider = new SimpleHttpValueProvider
|
||||||
{
|
{
|
||||||
|
|
@ -78,7 +80,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(retVal);
|
Assert.True(retVal);
|
||||||
Assert.Null(bindingContext.Model);
|
Assert.Null(bindingContext.Model);
|
||||||
Assert.Equal(false, bindingContext.ModelState.IsValid);
|
Assert.False(bindingContext.ModelState.IsValid);
|
||||||
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
Assert.Equal(message, error.ErrorMessage);
|
Assert.Equal(message, error.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(new object[] { new[] { 1, 0 } })]
|
[InlineData(new object[] { new[] { 1, 0 } })]
|
||||||
[InlineData(new object[] { new[] { "Value1", "Value0" } })]
|
[InlineData(new object[] { new[] { "Value1", "Value0" } })]
|
||||||
|
[InlineData(new object[] { new[] { "Value1", "value0" } })]
|
||||||
public void ConvertTo_ConvertsEnumArrays(object value)
|
public void ConvertTo_ConvertsEnumArrays(object value)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -318,16 +319,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(int), typeof(FormatException))]
|
[InlineData(typeof(int), typeof(InvalidOperationException), typeof(Exception))]
|
||||||
[InlineData(typeof(double?), typeof(FormatException))]
|
[InlineData(typeof(double?), typeof(InvalidOperationException), typeof(Exception))]
|
||||||
[InlineData(typeof(MyEnum?), typeof(ArgumentException))]
|
[InlineData(typeof(MyEnum?), typeof(InvalidOperationException), typeof(FormatException))]
|
||||||
public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType)
|
public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType, Type innerExceptionType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var vpr = new ValueProviderResult("this-is-not-a-valid-value", null, CultureInfo.InvariantCulture);
|
var vpr = new ValueProviderResult("this-is-not-a-valid-value", null, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType));
|
var ex = Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType));
|
||||||
|
Assert.IsType(innerExceptionType, ex.InnerException);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -354,12 +356,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
var frCulture = new CultureInfo("fr-FR");
|
var frCulture = new CultureInfo("fr-FR");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cultureResult = (decimal)vpr.ConvertTo(typeof(decimal), frCulture);
|
var cultureResult = vpr.ConvertTo(typeof(decimal), frCulture);
|
||||||
var result = (decimal)vpr.ConvertTo(typeof(decimal));
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(12.5M, cultureResult);
|
Assert.Equal(12.5M, cultureResult);
|
||||||
Assert.Equal(125, result);
|
Assert.Throws<InvalidOperationException>(() => vpr.ConvertTo(typeof(decimal)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -387,14 +388,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
yield return new object[] { 42, 42M };
|
|
||||||
yield return new object[] { 42, 42L };
|
yield return new object[] { 42, 42L };
|
||||||
|
yield return new object[] { 42, (short)42 };
|
||||||
yield return new object[] { 42, (float)42.0 };
|
yield return new object[] { 42, (float)42.0 };
|
||||||
yield return new object[] { 42, (double)42.0 };
|
yield return new object[] { 42, (double)42.0 };
|
||||||
yield return new object[] { 42M, 42 };
|
yield return new object[] { 42M, 42 };
|
||||||
yield return new object[] { 42L, 42 };
|
yield return new object[] { 42L, 42 };
|
||||||
|
yield return new object[] { 42, (byte)42 };
|
||||||
|
yield return new object[] { (short)42, 42 };
|
||||||
yield return new object[] { (float)42.0, 42 };
|
yield return new object[] { (float)42.0, 42 };
|
||||||
yield return new object[] { (double)42.0, 42 };
|
yield return new object[] { (double)42.0, 42 };
|
||||||
|
yield return new object[] { (byte)42, 42 };
|
||||||
yield return new object[] { "2008-01-01", new DateTime(2008, 01, 01) };
|
yield return new object[] { "2008-01-01", new DateTime(2008, 01, 01) };
|
||||||
yield return new object[] { "00:00:20", TimeSpan.FromSeconds(20) };
|
yield return new object[] { "00:00:20", TimeSpan.FromSeconds(20) };
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue