diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs index d1a85bbbd4..445368e1aa 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs @@ -298,6 +298,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return GetString("Validation_ValueNotFound"); } + /// + /// Cannot convert value '{0}' to enum type '{1}'. + /// + internal static string ValueProviderResult_CannotConvertEnum + { + get { return GetString("ValueProviderResult_CannotConvertEnum"); } + } + + /// + /// Cannot convert value '{0}' to enum type '{1}'. + /// + internal static string FormatValueProviderResult_CannotConvertEnum(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ValueProviderResult_CannotConvertEnum"), p0, p1); + } + /// /// The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information. /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx index 5d6251b5b4..70fce8fb43 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx @@ -171,6 +171,9 @@ A value is required but was not present in the request. + + Cannot convert value '{0}' to enum type '{1}'. + The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs index 73d4335216..e1e797d6ed 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs @@ -71,6 +71,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return value; } + // In case of a Nullable object, we try again with its underlying type. + var underlyingType = Nullable.GetUnderlyingType(destinationType); + if (underlyingType != null) + { + destinationType = underlyingType; + } + // if this is a user-input value but the user didn't type anything, return no value var valueAsString = value as string; @@ -79,56 +86,63 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return null; } + if (destinationType == typeof(string)) + { + return Convert.ToString(value, culture); + } + if (destinationType == typeof(int)) { - return Convert.ToInt32(value); + return Convert.ToInt32(value, culture); } - else if (destinationType == typeof(bool)) + + if (destinationType == typeof(long)) { - return Boolean.Parse(value.ToString()); + return Convert.ToInt64(value, culture); } - else if (destinationType == typeof(string)) + + if (destinationType == typeof(float)) { - return Convert.ToString(value); + return Convert.ToSingle(value, culture); } - string message = Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType); + + 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); - - // TODO: Revive once we get TypeConverters - //TypeConverter converter = TypeDescriptor.GetConverter(destinationType); - //bool canConvertFrom = converter.CanConvertFrom(value.GetType()); - //if (!canConvertFrom) - //{ - // converter = TypeDescriptor.GetConverter(value.GetType()); - //} - //if (!(canConvertFrom || converter.CanConvertTo(destinationType))) - //{ - // // EnumConverter cannot convert integer, so we verify manually - // if (destinationType.GetTypeInfo().IsEnum && value is int) - // { - // return Enum.ToObject(destinationType, (int)value); - // } - - // // In case of a Nullable object, we try again with its underlying type. - // Type underlyingType = Nullable.GetUnderlyingType(destinationType); - // if (underlyingType != null) - // { - // return ConvertSimpleType(culture, value, underlyingType); - // } - - // throw Error.InvalidOperation(Resources.ValueProviderResult_NoConverterExists, value.GetType(), destinationType); - //} - - //try - //{ - // return canConvertFrom - // ? converter.ConvertFrom(null, culture, value) - // : converter.ConvertTo(null, culture, value, destinationType); - //} - //catch (Exception ex) - //{ - // throw Error.InvalidOperation(ex, Resources.ValueProviderResult_ConversionThrew, value.GetType(), destinationType); - //} } private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType) diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs index db9f63c5d0..a173ee2467 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs @@ -1,7 +1,9 @@ -using System.Globalization; +using System; +using System.Collections.Generic; +using System.Globalization; using Xunit; -namespace Microsoft.AspNet.Mvc.ModelBinding.Test +namespace Microsoft.AspNet.Mvc.ModelBinding { public class ValueProviderResultTest { @@ -24,5 +26,381 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test Assert.Equal(0, convertedValue); } + + [Fact] + public void ConvertToCanConvertArraysToSingleElements() + { + // Arrange + var vpr = new ValueProviderResult(new int[] { 1, 20, 42 }, "", CultureInfo.InvariantCulture); + + // Act + var converted = (string)vpr.ConvertTo(typeof(string)); + + // Assert + Assert.Equal("1", converted); + } + + [Fact] + public void ConvertToCanConvertSingleElementsToArrays() + { + // Arrange + var vpr = new ValueProviderResult(42, "", CultureInfo.InvariantCulture); + + // Act + var converted = (string[])vpr.ConvertTo(typeof(string[])); + + // Assert + Assert.NotNull(converted); + var result = Assert.Single(converted); + Assert.Equal("42", result); + } + + [Fact] + public void ConvertToCanConvertSingleElementsToSingleElements() + { + // Arrange + var vpr = new ValueProviderResult(42, "", CultureInfo.InvariantCulture); + + // Act + var converted = (string)vpr.ConvertTo(typeof(string)); + + // Assert + Assert.NotNull(converted); + Assert.Equal("42", converted); + } + + [Fact] + public void ConvertingNullStringToNullableIntReturnsNull() + { + // Arrange + object original = null; + var vpr = new ValueProviderResult(original, "", CultureInfo.InvariantCulture); + + // Act + var returned = (int?)vpr.ConvertTo(typeof(int?)); + + // Assert + Assert.Equal(returned, null); + } + + [Fact] + public void ConvertingWhiteSpaceStringToNullableIntReturnsNull() + { + // Arrange + var original = " "; + var vpr = new ValueProviderResult(original, "", CultureInfo.InvariantCulture); + + // Act + var returned = (int?)vpr.ConvertTo(typeof(int?)); + + // Assert + Assert.Equal(returned, null); + } + + [Fact] + public void ConvertToReturnsNullIfArrayElementValueIsNull() + { + // Arrange + var vpr = new ValueProviderResult(new string[] { null }, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(int)); + + // Assert + Assert.Null(outValue); + } + + [Fact] + public void ConvertToReturnsNullIfTryingToConvertEmptyArrayToSingleElement() + { + // Arrange + var vpr = new ValueProviderResult(new int[0], "", CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(int)); + + // Assert + Assert.Null(outValue); + } + + [Theory] + [InlineData("")] + [InlineData(" \t \r\n ")] + public void ConvertToReturnsNullIfTrimmedValueIsEmptyString(object value) + { + // Arrange + var vpr = new ValueProviderResult(value, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(int)); + + // Assert + Assert.Null(outValue); + } + + [Fact] + public void ConvertToReturnsNullIfTrimmedValueIsEmptyString() + { + // Arrange + var vpr = new ValueProviderResult(rawValue: null, attemptedValue: null, culture: CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(int[])); + + // Assert + Assert.Null(outValue); + } + + [Fact] + public void ConvertToReturnsValueIfArrayElementIsIntegerAndDestinationTypeIsEnum() + { + // Arrange + var vpr = new ValueProviderResult(new object[] { 1 }, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(MyEnum)); + + // Assert + Assert.Equal(outValue, MyEnum.Value1); + } + + [Fact] + public void ConvertToReturnsValueIfArrayElementIsStringValueAndDestinationTypeIsEnum() + { + // Arrange + var vpr = new ValueProviderResult(new object[] { "1" }, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(MyEnum)); + + // Assert + Assert.Equal(outValue, MyEnum.Value1); + } + + [Fact] + public void ConvertToReturnsValueIfArrayElementIsStringKeyAndDestinationTypeIsEnum() + { + // Arrange + var vpr = new ValueProviderResult(new object[] { "Value1" }, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(MyEnum)); + + // Assert + Assert.Equal(outValue, MyEnum.Value1); + } + + [Fact] + public void ConvertToReturnsValueIfElementIsStringAndDestinationIsNullableInteger() + { + // Arrange + var vpr = new ValueProviderResult("12", null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(int?)); + + // Assert + Assert.Equal(12, outValue); + } + + [Fact] + public void ConvertToReturnsValueIfElementIsStringAndDestinationIsNullableDouble() + { + // Arrange + var vpr = new ValueProviderResult("12.5", null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(double?)); + + // Assert + Assert.Equal(12.5, outValue); + } + + [Fact] + public void ConvertToReturnsValueIfElementIsDecimalAndDestinationIsNullableInteger() + { + // Arrange + var vpr = new ValueProviderResult(12M, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(int?)); + + // Assert + Assert.Equal(12, outValue); + } + + [Fact] + public void ConvertToReturnsValueIfElementIsDecimalAndDestinationIsNullableDouble() + { + // Arrange + var vpr = new ValueProviderResult(12.5M, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(double?)); + + // Assert + Assert.Equal(12.5, outValue); + } + + [Fact] + public void ConvertToReturnsValueIfElementIsDecimalDoubleAndDestinationIsNullableInteger() + { + // Arrange + var vpr = new ValueProviderResult(12.5M, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(int?)); + + // Assert + Assert.Equal(12, outValue); + } + + [Fact] + public void ConvertToReturnsValueIfElementIsDecimalDoubleAndDestinationIsNullableLong() + { + // Arrange + var vpr = new ValueProviderResult(12.5M, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(long?)); + + // Assert + Assert.Equal(12L, outValue); + } + + [Fact] + public void ConvertToReturnsValueIfArrayElementInstanceOfDestinationType() + { + // Arrange + var vpr = new ValueProviderResult(new object[] { "some string" }, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(string)); + + // Assert + Assert.Equal("some string", outValue); + } + + [Theory] + [InlineData(new object[] { new[] { 1, 0 }})] + [InlineData(new object[] { new[] {"Value1", "Value0" }})] + public void ConvertTo_ConvertsEnumArrays(object value) + { + // Arrange + var vpr = new ValueProviderResult(value, null, CultureInfo.InvariantCulture); + + // Act + var outValue = (MyEnum[])vpr.ConvertTo(typeof(MyEnum[])); + + // Assert + Assert.Equal(2, outValue.Length); + Assert.Equal(MyEnum.Value1, outValue[0]); + Assert.Equal(MyEnum.Value0, outValue[1]); + } + + [Fact] + public void ConvertToReturnsValueIfInstanceOfDestinationType() + { + // Arrange + var original = new[] { "some string" }; + var vpr = new ValueProviderResult(original, null, CultureInfo.InvariantCulture); + + // Act + var outValue = vpr.ConvertTo(typeof(string[])); + + // Assert + Assert.Same(original, outValue); + } + + [Theory] + [InlineData(typeof(int), typeof(FormatException))] + [InlineData(typeof(double?), typeof(FormatException))] + [InlineData(typeof(MyEnum?), typeof(ArgumentException))] + public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType) + { + // Arrange + var vpr = new ValueProviderResult("this-is-not-a-valid-value", null, CultureInfo.InvariantCulture); + + // Act & Assert + Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType)); + } + + [Fact] + public void ConvertToThrowsIfNoConverterExists() + { + // Arrange + var vpr = new ValueProviderResult("x", null, CultureInfo.InvariantCulture); + var destinationType = typeof(MyClassWithoutConverter); + + // Act & Assert + var ex = Assert.Throws(() => vpr.ConvertTo(destinationType)); + Assert.Equal("The parameter conversion from type 'System.String' to type " + + "'Microsoft.AspNet.Mvc.ModelBinding.ValueProviderResultTest+MyClassWithoutConverter' " + + "failed because no type converter can convert between these types.", + ex.Message); + } + + [Fact] + public void ConvertToUsesProvidedCulture() + { + // Arrange + var original = "12,5"; + var vpr = new ValueProviderResult(original, null, new CultureInfo("en-GB")); + var frCulture = new CultureInfo("fr-FR"); + + // Act + var cultureResult = (decimal)vpr.ConvertTo(typeof(decimal), frCulture); + var result = (decimal)vpr.ConvertTo(typeof(decimal)); + + // Assert + Assert.Equal(12.5M, cultureResult); + Assert.Equal(125, result); + } + + [Fact] + public void CulturePropertyDefaultsToInvariantCulture() + { + // Arrange + var result = new ValueProviderResult(null, null, null); + + // Act & assert + Assert.Same(CultureInfo.InvariantCulture, result.Culture); + } + + [Theory] + [MemberData("IntrinsicConversionData")] + public void ConvertToCanConvertIntrinsics(object initialValue, T expectedValue) + { + // Arrange + var result = new ValueProviderResult(initialValue, "", CultureInfo.InvariantCulture); + + // Act & Assert + Assert.Equal(expectedValue, result.ConvertTo(typeof(T))); + } + + public static IEnumerable IntrinsicConversionData + { + get + { + yield return new object[] { 42, 42M }; + yield return new object[] { 42, 42L }; + 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[] { (float)42.0, 42 }; + yield return new object[] { (double)42.0, 42 }; + } + } + + private class MyClassWithoutConverter + { + } + + private enum MyEnum + { + Value0 = 0, + Value1 = 1 + } } } \ No newline at end of file