Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 5fa8a91

Browse files
author
sornaks
committed
-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.
1 parent a41b9dc commit 5fa8a91

File tree

8 files changed

+59
-135
lines changed

8 files changed

+59
-135
lines changed

src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata)
4242
private static bool CanBindType(Type modelType)
4343
{
4444
// Simple types cannot use this binder
45-
var isComplexType = !ValueProviderResult.CanConvertFromString(modelType);
45+
var isComplexType = !TypeHelper.HasStringConverter(modelType);
4646
if (!isComplexType)
4747
{
4848
return false;

src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
1313
{
1414
ModelBindingHelper.ValidateBindingContext(bindingContext);
1515

16-
if (!ValueProviderResult.CanConvertFromString(bindingContext.ModelType))
16+
if (!TypeHelper.HasStringConverter(bindingContext.ModelType))
1717
{
1818
// this type cannot be converted
1919
return false;

src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.ComponentModel;
56
using System.Reflection;
67

78
namespace Microsoft.AspNet.Mvc.ModelBinding
@@ -18,5 +19,10 @@ internal static bool IsSimpleType(Type type)
1819
type.Equals(typeof(DateTimeOffset)) ||
1920
type.Equals(typeof(TimeSpan));
2021
}
22+
23+
internal static bool HasStringConverter(Type type)
24+
{
25+
return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
26+
}
2127
}
2228
}

src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public virtual bool ConvertEmptyStringToNull
107107

108108
public virtual bool IsComplexType
109109
{
110-
get { return !ValueProviderResult.CanConvertFromString(ModelType); }
110+
get { return !TypeHelper.HasStringConverter(ModelType); }
111111
}
112112

113113
public bool IsNullableValueType

src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs

Lines changed: 31 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections;
6+
using System.ComponentModel;
67
using System.Globalization;
78
using System.Reflection;
89

@@ -53,7 +54,7 @@ public virtual object ConvertTo([NotNull] Type type, CultureInfo culture)
5354
if (value == null)
5455
{
5556
// treat null route parameters as though they were the default value for the type
56-
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) :
57+
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) :
5758
null;
5859
}
5960

@@ -68,34 +69,8 @@ public virtual object ConvertTo([NotNull] Type type, CultureInfo culture)
6869

6970
public static bool CanConvertFromString(Type destinationType)
7071
{
71-
return GetConverterDelegate(destinationType) != null;
72-
}
73-
74-
private object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
75-
{
76-
if (value == null || value.GetType().IsAssignableFrom(destinationType))
77-
{
78-
return value;
79-
}
80-
81-
// In case of a Nullable object, we try again with its underlying type.
82-
destinationType = UnwrapNullableType(destinationType);
83-
84-
// if this is a user-input value but the user didn't type anything, return no value
85-
var valueAsString = value as string;
86-
if (valueAsString != null && string.IsNullOrWhiteSpace(valueAsString))
87-
{
88-
return null;
89-
}
90-
91-
var converter = GetConverterDelegate(destinationType);
92-
if (converter == null)
93-
{
94-
var message = Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType);
95-
throw new InvalidOperationException(message);
96-
}
97-
98-
return converter(value, culture);
72+
return TypeHelper.IsSimpleType(UnwrapNullableType(destinationType)) ||
73+
TypeHelper.HasStringConverter(destinationType);
9974
}
10075

10176
private object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
@@ -144,121 +119,57 @@ private object UnwrapPossibleArrayType(CultureInfo culture, object value, Type d
144119
return ConvertSimpleType(culture, value, destinationType);
145120
}
146121

147-
private static Func<object, CultureInfo, object> GetConverterDelegate(Type destinationType)
122+
private object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
148123
{
149-
destinationType = UnwrapNullableType(destinationType);
150-
151-
if (destinationType == typeof(string))
152-
{
153-
return (value, culture) => Convert.ToString(value, culture);
154-
}
155-
156-
if (destinationType == typeof(int))
157-
{
158-
return (value, culture) => Convert.ToInt32(value, culture);
159-
}
160-
161-
if (destinationType == typeof(long))
162-
{
163-
return (value, culture) => Convert.ToInt64(value, culture);
164-
}
165-
166-
if (destinationType == typeof(float))
124+
if (value == null || value.GetType().IsAssignableFrom(destinationType))
167125
{
168-
return (value, culture) => Convert.ToSingle(value, culture);
126+
return value;
169127
}
170128

171-
if (destinationType == typeof(double))
172-
{
173-
return (value, culture) => Convert.ToDouble(value, culture);
174-
}
129+
// In case of a Nullable object, we try again with its underlying type.
130+
destinationType = UnwrapNullableType(destinationType);
175131

176-
if (destinationType == typeof(decimal))
132+
// if this is a user-input value but the user didn't type anything, return no value
133+
var valueAsString = value as string;
134+
if (valueAsString != null && string.IsNullOrWhiteSpace(valueAsString))
177135
{
178-
return (value, culture) => Convert.ToDecimal(value, culture);
136+
return null;
179137
}
180138

181-
if (destinationType == typeof(bool))
139+
var converter = TypeDescriptor.GetConverter(destinationType);
140+
var canConvertFrom = converter.CanConvertFrom(value.GetType());
141+
if (!canConvertFrom)
182142
{
183-
return (value, culture) => Convert.ToBoolean(value, culture);
143+
converter = TypeDescriptor.GetConverter(value.GetType());
184144
}
185-
186-
if (destinationType == typeof(DateTime))
145+
if (!(canConvertFrom || converter.CanConvertTo(destinationType)))
187146
{
188-
return (value, culture) =>
147+
// EnumConverter cannot convert integer, so we verify manually
148+
if (destinationType.IsEnum() && (value is int))
189149
{
190-
ThrowIfNotStringType(value, destinationType);
191-
return DateTime.Parse((string)value, culture);
192-
};
193-
}
150+
return Enum.ToObject(destinationType, (int)value);
151+
}
194152

195-
if (destinationType == typeof(DateTimeOffset))
196-
{
197-
return (value, culture) =>
198-
{
199-
ThrowIfNotStringType(value, destinationType);
200-
return DateTimeOffset.Parse((string)value, culture);
201-
};
153+
throw new InvalidOperationException(
154+
Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType));
202155
}
203156

204-
if (destinationType == typeof(TimeSpan))
157+
try
205158
{
206-
return (value, culture) =>
207-
{
208-
ThrowIfNotStringType(value, destinationType);
209-
return TimeSpan.Parse((string)value, culture);
210-
};
159+
return canConvertFrom
160+
? converter.ConvertFrom(null, culture, value)
161+
: converter.ConvertTo(null, culture, value, destinationType);
211162
}
212-
213-
if (destinationType == typeof(Guid))
163+
catch (Exception ex)
214164
{
215-
return (value, culture) =>
216-
{
217-
ThrowIfNotStringType(value, destinationType);
218-
return Guid.Parse((string)value);
219-
};
220-
}
221-
222-
if (destinationType.GetTypeInfo().IsEnum)
223-
{
224-
return (value, culture) =>
225-
{
226-
// EnumConverter cannot convert integer, so we verify manually
227-
if ((value is int))
228-
{
229-
if (Enum.IsDefined(destinationType, value))
230-
{
231-
return Enum.ToObject(destinationType, (int)value);
232-
}
233-
234-
throw new FormatException(
235-
Resources.FormatValueProviderResult_CannotConvertEnum(value,
236-
destinationType));
237-
}
238-
else
239-
{
240-
ThrowIfNotStringType(value, destinationType);
241-
return Enum.Parse(destinationType, (string)value);
242-
}
243-
};
165+
throw new InvalidOperationException(
166+
Resources.FormatValueProviderResult_ConversionThrew(value.GetType(), destinationType), ex);
244167
}
245-
246-
return null;
247168
}
248169

249170
private static Type UnwrapNullableType(Type destinationType)
250171
{
251172
return Nullable.GetUnderlyingType(destinationType) ?? destinationType;
252173
}
253-
254-
private static void ThrowIfNotStringType(object value, Type destinationType)
255-
{
256-
var type = value.GetType();
257-
if (type != typeof(string))
258-
{
259-
var message = Resources.FormatValueProviderResult_NoConverterExists(type, destinationType);
260-
throw new InvalidOperationException(message);
261-
}
262-
}
263174
}
264175
}

src/Microsoft.AspNet.Mvc.ModelBinding/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"System.Collections": "4.0.10-beta-*",
2525
"System.Collections.Concurrent": "4.0.0-beta-*",
2626
"System.ComponentModel": "4.0.0-beta-*",
27+
"System.ComponentModel.TypeConverter": "4.0.0-beta-*",
2728
"System.Diagnostics.Contracts": "4.0.0-beta-*",
2829
"System.Diagnostics.Debug": "4.0.10-beta-*",
2930
"System.Diagnostics.Tools": "4.0.0-beta-*",

test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public async Task BindModel_ReturnsFalse_IfTypeCannotBeConverted(Type destinatio
3434
}
3535

3636
[Theory]
37+
[InlineData(typeof(byte))]
38+
[InlineData(typeof(short))]
3739
[InlineData(typeof(int))]
3840
[InlineData(typeof(long))]
3941
[InlineData(typeof(Guid))]
@@ -62,8 +64,8 @@ public async Task BindModel_ReturnsTrue_IfTypeCanBeConverted(Type destinationTyp
6264
public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
6365
{
6466
// Arrange
65-
var message = TestPlatformHelper.IsMono ? "Input string was not in the correct format" :
66-
"Input string was not in a correct format.";
67+
var message = "The parameter conversion from type 'System.String' to type 'System.Int32' failed." +
68+
" See the inner exception for more information.";
6769
var bindingContext = GetBindingContext(typeof(int));
6870
bindingContext.ValueProvider = new SimpleHttpValueProvider
6971
{
@@ -78,7 +80,7 @@ public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState(
7880
// Assert
7981
Assert.True(retVal);
8082
Assert.Null(bindingContext.Model);
81-
Assert.Equal(false, bindingContext.ModelState.IsValid);
83+
Assert.False(bindingContext.ModelState.IsValid);
8284
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
8385
Assert.Equal(message, error.ErrorMessage);
8486
}

test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ public void ConvertToReturnsValueIfArrayElementInstanceOfDestinationType()
289289
[Theory]
290290
[InlineData(new object[] { new[] { 1, 0 } })]
291291
[InlineData(new object[] { new[] { "Value1", "Value0" } })]
292+
[InlineData(new object[] { new[] { "Value1", "value0" } })]
292293
public void ConvertTo_ConvertsEnumArrays(object value)
293294
{
294295
// Arrange
@@ -318,16 +319,17 @@ public void ConvertToReturnsValueIfInstanceOfDestinationType()
318319
}
319320

320321
[Theory]
321-
[InlineData(typeof(int), typeof(FormatException))]
322-
[InlineData(typeof(double?), typeof(FormatException))]
323-
[InlineData(typeof(MyEnum?), typeof(ArgumentException))]
324-
public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType)
322+
[InlineData(typeof(int), typeof(InvalidOperationException), typeof(Exception))]
323+
[InlineData(typeof(double?), typeof(InvalidOperationException), typeof(Exception))]
324+
[InlineData(typeof(MyEnum?), typeof(InvalidOperationException), typeof(FormatException))]
325+
public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType, Type innerExceptionType)
325326
{
326327
// Arrange
327328
var vpr = new ValueProviderResult("this-is-not-a-valid-value", null, CultureInfo.InvariantCulture);
328329

329330
// Act & Assert
330-
Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType));
331+
var ex = Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType));
332+
Assert.IsType(innerExceptionType, ex.InnerException);
331333
}
332334

333335
[Fact]
@@ -354,12 +356,11 @@ public void ConvertToUsesProvidedCulture()
354356
var frCulture = new CultureInfo("fr-FR");
355357

356358
// Act
357-
var cultureResult = (decimal)vpr.ConvertTo(typeof(decimal), frCulture);
358-
var result = (decimal)vpr.ConvertTo(typeof(decimal));
359+
var cultureResult = vpr.ConvertTo(typeof(decimal), frCulture);
359360

360361
// Assert
361362
Assert.Equal(12.5M, cultureResult);
362-
Assert.Equal(125, result);
363+
Assert.Throws<InvalidOperationException>(() => vpr.ConvertTo(typeof(decimal)));
363364
}
364365

365366
[Fact]
@@ -387,14 +388,17 @@ public static IEnumerable<object[]> IntrinsicConversionData
387388
{
388389
get
389390
{
390-
yield return new object[] { 42, 42M };
391391
yield return new object[] { 42, 42L };
392+
yield return new object[] { 42, (short)42 };
392393
yield return new object[] { 42, (float)42.0 };
393394
yield return new object[] { 42, (double)42.0 };
394395
yield return new object[] { 42M, 42 };
395396
yield return new object[] { 42L, 42 };
397+
yield return new object[] { 42, (byte)42 };
398+
yield return new object[] { (short)42, 42 };
396399
yield return new object[] { (float)42.0, 42 };
397400
yield return new object[] { (double)42.0, 42 };
401+
yield return new object[] { (byte)42, 42 };
398402
yield return new object[] { "2008-01-01", new DateTime(2008, 01, 01) };
399403
yield return new object[] { "00:00:20", TimeSpan.FromSeconds(20) };
400404
yield return new object[]

0 commit comments

Comments
 (0)