Modify TypeConverterModelBinder to use ValueProviderResult.CanConvertFromString to determine if it can convert a
value * Adding support for extra type conversions
This commit is contained in:
parent
a5899584a2
commit
cc00d8cff7
|
|
@ -100,22 +100,6 @@ namespace Microsoft.AspNet.Mvc
|
|||
return (!type.GetTypeInfo().IsValueType || IsNullableValueType(type));
|
||||
}
|
||||
|
||||
public static bool HasStringConverter([NotNull] this Type type)
|
||||
{
|
||||
var typeInfo = type.GetTypeInfo();
|
||||
if (typeInfo.IsPrimitive || type == typeof(string))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (IsNullableValueType(type) && HasStringConverter(type.GenericTypeArguments[0]))
|
||||
{
|
||||
// Nullable<T> where T is a primitive type or has a type converter
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Type[] GetTypeArgumentsIfMatch([NotNull] Type closedType, Type matchingOpenType)
|
||||
{
|
||||
var closedTypeInfo = closedType.GetTypeInfo();
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
private static bool CanBindType(Type modelType)
|
||||
{
|
||||
// Simple types cannot use this binder
|
||||
var isComplexType = !modelType.HasStringConverter();
|
||||
var isComplexType = !ValueProviderResult.CanConvertFromString(modelType);
|
||||
if (!isComplexType)
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -13,26 +13,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
{
|
||||
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||
|
||||
if (!bindingContext.ModelType.HasStringConverter())
|
||||
if (!ValueProviderResult.CanConvertFromString(bindingContext.ModelType))
|
||||
{
|
||||
// this type cannot be converted
|
||||
return false;
|
||||
}
|
||||
|
||||
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||
if (valueProviderResult == null)
|
||||
{
|
||||
return false; // no entry
|
||||
}
|
||||
|
||||
// Provider should have verified this before creating
|
||||
Contract.Assert(bindingContext.ModelType.HasStringConverter());
|
||||
|
||||
object newModel;
|
||||
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
|
||||
try
|
||||
{
|
||||
newModel = valueProviderResult.ConvertTo(bindingContext.ModelType);
|
||||
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel);
|
||||
bindingContext.Model = newModel;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -45,11 +44,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
{
|
||||
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel);
|
||||
bindingContext.Model = newModel;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
public virtual bool IsComplexType
|
||||
{
|
||||
get { return !ModelType.HasStringConverter(); }
|
||||
get { return !ValueProviderResult.CanConvertFromString(ModelType); }
|
||||
}
|
||||
|
||||
public bool IsNullableValueType
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
using System.Collections;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
|
|
@ -64,7 +63,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
return UnwrapPossibleArrayType(cultureToUse, value, type);
|
||||
}
|
||||
|
||||
private static object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
|
||||
public static bool CanConvertFromString(Type destinationType)
|
||||
{
|
||||
return GetConverterDelegate(destinationType) != null;
|
||||
}
|
||||
|
||||
private object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
|
||||
{
|
||||
if (value == null || value.GetType().IsAssignableFrom(destinationType))
|
||||
{
|
||||
|
|
@ -72,80 +76,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
// In case of a Nullable object, we try again with its underlying type.
|
||||
var underlyingType = Nullable.GetUnderlyingType(destinationType);
|
||||
if (underlyingType != null)
|
||||
{
|
||||
destinationType = underlyingType;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (destinationType == typeof(string))
|
||||
var converter = GetConverterDelegate(destinationType);
|
||||
if (converter == null)
|
||||
{
|
||||
return Convert.ToString(value, culture);
|
||||
var message = Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(int))
|
||||
{
|
||||
return Convert.ToInt32(value, culture);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(long))
|
||||
{
|
||||
return Convert.ToInt64(value, culture);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(float))
|
||||
{
|
||||
return Convert.ToSingle(value, culture);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(double))
|
||||
{
|
||||
return Convert.ToDouble(value, culture);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(decimal))
|
||||
{
|
||||
return Convert.ToDecimal(value, culture);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(bool))
|
||||
{
|
||||
return Convert.ToBoolean(value, culture);
|
||||
}
|
||||
|
||||
if (destinationType.GetTypeInfo().IsEnum)
|
||||
{
|
||||
// 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
|
||||
{
|
||||
return Enum.Parse(destinationType, valueAsString);
|
||||
}
|
||||
}
|
||||
|
||||
var message = Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType);
|
||||
throw new InvalidOperationException(message);
|
||||
return converter(value, culture);
|
||||
}
|
||||
|
||||
private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
|
||||
private object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
|
||||
{
|
||||
// array conversion results in four cases, as below
|
||||
var valueAsArray = value as Array;
|
||||
|
|
@ -189,5 +139,122 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
// case 4: both destination + source type are single elements, so convert
|
||||
return ConvertSimpleType(culture, value, destinationType);
|
||||
}
|
||||
|
||||
private static Func<object, CultureInfo, object> GetConverterDelegate(Type destinationType)
|
||||
{
|
||||
destinationType = UnwrapNullableType(destinationType);
|
||||
|
||||
if (destinationType == typeof(string))
|
||||
{
|
||||
return (value, culture) => Convert.ToString(value, culture);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(int))
|
||||
{
|
||||
return (value, culture) => Convert.ToInt32(value, culture);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(long))
|
||||
{
|
||||
return (value, culture) => Convert.ToInt64(value, culture);
|
||||
}
|
||||
|
||||
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 DateTime.Parse((string)value, culture);
|
||||
};
|
||||
}
|
||||
|
||||
if (destinationType == typeof(DateTimeOffset))
|
||||
{
|
||||
return (value, culture) =>
|
||||
{
|
||||
ThrowIfNotStringType(value, destinationType);
|
||||
return DateTimeOffset.Parse((string)value, culture);
|
||||
};
|
||||
}
|
||||
|
||||
if (destinationType == typeof(TimeSpan))
|
||||
{
|
||||
return (value, culture) =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
return Nullable.GetUnderlyingType(destinationType) ?? destinationType;
|
||||
}
|
||||
|
||||
private static void ThrowIfNotStringType(object value, Type destinationType)
|
||||
{
|
||||
var type = value.GetType();
|
||||
if (type != typeof(string))
|
||||
{
|
||||
string message = Resources.FormatValueProviderResult_NoConverterExists(type, destinationType);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,52 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
{
|
||||
public class TypeConverterModelBinderTest
|
||||
{
|
||||
// private static readonly ModelBinderErrorMessageProvider = (modelMetadata, incomingValue) => null;
|
||||
[Theory]
|
||||
[InlineData(typeof(object))]
|
||||
[InlineData(typeof(Calendar))]
|
||||
[InlineData(typeof(TestClass))]
|
||||
public void BindModel_ReturnsFalse_IfTypeCannotBeConverted(Type destinationType)
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = GetBindingContext(destinationType);
|
||||
bindingContext.ValueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "theModelName", "some-value" }
|
||||
};
|
||||
|
||||
var binder = new TypeConverterModelBinder();
|
||||
|
||||
// Act
|
||||
var retVal = binder.BindModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(retVal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(int))]
|
||||
[InlineData(typeof(long))]
|
||||
[InlineData(typeof(Guid))]
|
||||
[InlineData(typeof(DateTimeOffset))]
|
||||
[InlineData(typeof(double))]
|
||||
[InlineData(typeof(DayOfWeek))]
|
||||
public void BindModel_ReturnsTrue_IfTypeCanBeConverted(Type destinationType)
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = GetBindingContext(destinationType);
|
||||
bindingContext.ValueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "theModelName", "some-value" }
|
||||
};
|
||||
|
||||
var binder = new TypeConverterModelBinder();
|
||||
|
||||
// Act
|
||||
var retVal = binder.BindModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(retVal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
|
||||
|
|
@ -25,7 +70,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
bool retVal = binder.BindModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(retVal);
|
||||
Assert.True(retVal);
|
||||
Assert.Null(bindingContext.Model);
|
||||
Assert.Equal(false, bindingContext.ModelState.IsValid);
|
||||
Assert.Equal("Input string was not in a correct format.", bindingContext.ModelState["theModelName"].Errors[0].ErrorMessage);
|
||||
|
|
@ -98,5 +143,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
ValueProvider = new SimpleHttpValueProvider() // empty
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TestClass
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -390,9 +390,52 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
yield return new object[] { 42L, 42 };
|
||||
yield return new object[] { (float)42.0, 42 };
|
||||
yield return new object[] { (double)42.0, 42 };
|
||||
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[] { "c6687d3a-51f9-4159-8771-a66d2b7d7038",
|
||||
Guid.Parse("c6687d3a-51f9-4159-8771-a66d2b7d7038") };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TimeSpan))]
|
||||
[InlineData(typeof(DateTime))]
|
||||
[InlineData(typeof(DateTimeOffset))]
|
||||
[InlineData(typeof(Guid))]
|
||||
[InlineData(typeof(MyEnum))]
|
||||
public void ConvertTo_Throws_IfValueIsNotStringData(Type destinationType)
|
||||
{
|
||||
// Arrange
|
||||
var result = new ValueProviderResult(new MyClassWithoutConverter(), "", CultureInfo.InvariantCulture);
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => result.ConvertTo(destinationType));
|
||||
|
||||
// Assert
|
||||
var expectedMessage = string.Format("The parameter conversion from type '{0}' to type '{1}' " +
|
||||
"failed because no type converter can convert between these types.",
|
||||
typeof(MyClassWithoutConverter), destinationType);
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertTo_Throws_IfDestinationTypeIsNotConvertible()
|
||||
{
|
||||
// Arrange
|
||||
var value = "Hello world";
|
||||
var destinationType = typeof(MyClassWithoutConverter);
|
||||
var result = new ValueProviderResult(value, "", CultureInfo.InvariantCulture);
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => result.ConvertTo(destinationType));
|
||||
|
||||
// Assert
|
||||
var expectedMessage = string.Format("The parameter conversion from type '{0}' to type '{1}' " +
|
||||
"failed because no type converter can convert between these types.",
|
||||
value.GetType(), typeof(MyClassWithoutConverter));
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
private class MyClassWithoutConverter
|
||||
{
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue