diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 1aeea1c7a7..5a88d51bd2 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private static bool CanBindType(Type modelType) { // Simple types cannot use this binder - var isComplexType = !ValueProviderResult.CanConvertFromString(modelType); + var isComplexType = !TypeHelper.HasStringConverter(modelType); if (!isComplexType) { return false; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs index 7e2edc9f9a..800f14567f 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { ModelBindingHelper.ValidateBindingContext(bindingContext); - if (!ValueProviderResult.CanConvertFromString(bindingContext.ModelType)) + if (!TypeHelper.HasStringConverter(bindingContext.ModelType)) { // this type cannot be converted return false; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs index 47bcdecbf1..760382cb51 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.ComponentModel; using System.Reflection; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -18,5 +19,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding type.Equals(typeof(DateTimeOffset)) || type.Equals(typeof(TimeSpan)); } + + internal static bool HasStringConverter(Type type) + { + return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string)); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs index 7faf1015c1..ee8faf91c2 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs @@ -107,7 +107,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public virtual bool IsComplexType { - get { return !ValueProviderResult.CanConvertFromString(ModelType); } + get { return !TypeHelper.HasStringConverter(ModelType); } } public bool IsNullableValueType diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs index a0d2ddbf04..b2d25afe12 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.ComponentModel; using System.Globalization; using System.Reflection; @@ -53,7 +54,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding if (value == null) { // 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; } @@ -68,34 +69,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding 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)) - { - 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); + return TypeHelper.IsSimpleType(UnwrapNullableType(destinationType)) || + TypeHelper.HasStringConverter(destinationType); } private object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType) @@ -144,121 +119,57 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return ConvertSimpleType(culture, value, destinationType); } - private static Func 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); - 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 (destinationType == typeof(long)) + if (!(canConvertFrom || converter.CanConvertTo(destinationType))) { - 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) => + // EnumConverter cannot convert integer, so we verify manually + if (destinationType.IsEnum() && (value is int)) { - ThrowIfNotStringType(value, destinationType); - return DateTime.Parse((string)value, culture); - }; + return Enum.ToObject(destinationType, (int)value); + } + + throw new InvalidOperationException( + Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType)); } - if (destinationType == typeof(DateTimeOffset)) + try { - return (value, culture) => - { - ThrowIfNotStringType(value, destinationType); - return DateTimeOffset.Parse((string)value, culture); - }; + return canConvertFrom + ? converter.ConvertFrom(null, culture, value) + : converter.ConvertTo(null, culture, value, destinationType); } - - if (destinationType == typeof(TimeSpan)) + catch (Exception ex) { - return (value, culture) => - { - ThrowIfNotStringType(value, destinationType); - return TimeSpan.Parse((string)value, culture); - }; + throw new InvalidOperationException( + Resources.FormatValueProviderResult_ConversionThrew(value.GetType(), destinationType), ex); } - - 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)) - { - var message = Resources.FormatValueProviderResult_NoConverterExists(type, destinationType); - throw new InvalidOperationException(message); - } - } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json index f2f1271d2a..664d517169 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json @@ -24,6 +24,7 @@ "System.Collections": "4.0.10-beta-*", "System.Collections.Concurrent": "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.Debug": "4.0.10-beta-*", "System.Diagnostics.Tools": "4.0.0-beta-*", diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs index 4ed7d233c7..7c16625922 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs @@ -34,6 +34,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test } [Theory] + [InlineData(typeof(byte))] + [InlineData(typeof(short))] [InlineData(typeof(int))] [InlineData(typeof(long))] [InlineData(typeof(Guid))] @@ -62,8 +64,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState() { // Arrange - var message = TestPlatformHelper.IsMono ? "Input string was not in the correct format" : - "Input string was not in a correct format."; + var message = "The parameter conversion from type 'System.String' to type 'System.Int32' failed." + + " See the inner exception for more information."; var bindingContext = GetBindingContext(typeof(int)); bindingContext.ValueProvider = new SimpleHttpValueProvider { @@ -78,7 +80,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test // Assert Assert.True(retVal); Assert.Null(bindingContext.Model); - Assert.Equal(false, bindingContext.ModelState.IsValid); + Assert.False(bindingContext.ModelState.IsValid); var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors); Assert.Equal(message, error.ErrorMessage); } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs index 649adb7919..712317a8b7 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs @@ -289,6 +289,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding [Theory] [InlineData(new object[] { new[] { 1, 0 } })] [InlineData(new object[] { new[] { "Value1", "Value0" } })] + [InlineData(new object[] { new[] { "Value1", "value0" } })] public void ConvertTo_ConvertsEnumArrays(object value) { // Arrange @@ -318,16 +319,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } [Theory] - [InlineData(typeof(int), typeof(FormatException))] - [InlineData(typeof(double?), typeof(FormatException))] - [InlineData(typeof(MyEnum?), typeof(ArgumentException))] - public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType) + [InlineData(typeof(int), typeof(InvalidOperationException), typeof(Exception))] + [InlineData(typeof(double?), typeof(InvalidOperationException), typeof(Exception))] + [InlineData(typeof(MyEnum?), typeof(InvalidOperationException), typeof(FormatException))] + public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType, Type innerExceptionType) { // Arrange var vpr = new ValueProviderResult("this-is-not-a-valid-value", null, CultureInfo.InvariantCulture); // Act & Assert - Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType)); + var ex = Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType)); + Assert.IsType(innerExceptionType, ex.InnerException); } [Fact] @@ -354,12 +356,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var frCulture = new CultureInfo("fr-FR"); // Act - var cultureResult = (decimal)vpr.ConvertTo(typeof(decimal), frCulture); - var result = (decimal)vpr.ConvertTo(typeof(decimal)); + var cultureResult = vpr.ConvertTo(typeof(decimal), frCulture); // Assert Assert.Equal(12.5M, cultureResult); - Assert.Equal(125, result); + Assert.Throws(() => vpr.ConvertTo(typeof(decimal))); } [Fact] @@ -387,14 +388,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { get { - yield return new object[] { 42, 42M }; 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, (double)42.0 }; yield return new object[] { 42M, 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[] { (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[] { "00:00:20", TimeSpan.FromSeconds(20) }; yield return new object[]