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

Commit ccefcaf

Browse files
committed
Add FloatingPointTypeModelBinderProvider and related binders
- #5502 - support thousands separators for `decimal`, `double` and `float` - add tests demonstrating `SimpleTypeModelBinder` does not support thousands separators for numeric types - add tests demonstrating use of commas (not thousands separators) with `enum` values
1 parent 057a853 commit ccefcaf

File tree

13 files changed

+1049
-12
lines changed

13 files changed

+1049
-12
lines changed

src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public void Configure(MvcOptions options)
4545
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
4646
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
4747
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
48+
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
4849
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
4950
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
5051
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Globalization;
6+
using System.Runtime.ExceptionServices;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
10+
{
11+
/// <summary>
12+
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is
13+
/// <see cref="decimal"/>.
14+
/// </summary>
15+
public class DecimalModelBinder : IModelBinder
16+
{
17+
private readonly NumberStyles _supportedStyles;
18+
19+
public DecimalModelBinder(NumberStyles supportedStyles)
20+
{
21+
_supportedStyles = supportedStyles;
22+
}
23+
24+
/// <inheritdoc />
25+
public Task BindModelAsync(ModelBindingContext bindingContext)
26+
{
27+
if (bindingContext == null)
28+
{
29+
throw new ArgumentNullException(nameof(bindingContext));
30+
}
31+
32+
var modelName = bindingContext.ModelName;
33+
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
34+
if (valueProviderResult == ValueProviderResult.None)
35+
{
36+
// no entry
37+
return Task.CompletedTask;
38+
}
39+
40+
var modelState = bindingContext.ModelState;
41+
modelState.SetModelValue(modelName, valueProviderResult);
42+
43+
var metadata = bindingContext.ModelMetadata;
44+
var type = metadata.UnderlyingOrModelType;
45+
try
46+
{
47+
var value = valueProviderResult.FirstValue;
48+
var culture = valueProviderResult.Culture;
49+
50+
object model;
51+
if (string.IsNullOrWhiteSpace(value))
52+
{
53+
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
54+
model = null;
55+
}
56+
else if (type == typeof(decimal))
57+
{
58+
model = decimal.Parse(value, _supportedStyles, culture);
59+
}
60+
else
61+
{
62+
// unreachable
63+
throw new NotSupportedException();
64+
}
65+
66+
// When converting value, a null model may indicate a failed conversion for an otherwise required
67+
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
68+
// current bindingContext. If not, an error is logged.
69+
if (model == null && !metadata.IsReferenceOrNullableType)
70+
{
71+
modelState.TryAddModelError(
72+
modelName,
73+
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
74+
valueProviderResult.ToString()));
75+
76+
return Task.CompletedTask;
77+
}
78+
else
79+
{
80+
bindingContext.Result = ModelBindingResult.Success(model);
81+
return Task.CompletedTask;
82+
}
83+
}
84+
catch (Exception exception)
85+
{
86+
var isFormatException = exception is FormatException;
87+
if (!isFormatException && exception.InnerException != null)
88+
{
89+
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
90+
// this code in case a cursory review of the CoreFx code missed something.
91+
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
92+
}
93+
94+
modelState.TryAddModelError(modelName, exception, metadata);
95+
96+
// Conversion failed.
97+
return Task.CompletedTask;
98+
}
99+
}
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Globalization;
6+
using System.Runtime.ExceptionServices;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
10+
{
11+
/// <summary>
12+
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is
13+
/// <see cref="decimal"/>.
14+
/// </summary>
15+
public class DoubleModelBinder : IModelBinder
16+
{
17+
private readonly NumberStyles _supportedStyles;
18+
19+
public DoubleModelBinder(NumberStyles supportedStyles)
20+
{
21+
_supportedStyles = supportedStyles;
22+
}
23+
24+
/// <inheritdoc />
25+
public Task BindModelAsync(ModelBindingContext bindingContext)
26+
{
27+
if (bindingContext == null)
28+
{
29+
throw new ArgumentNullException(nameof(bindingContext));
30+
}
31+
32+
var modelName = bindingContext.ModelName;
33+
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
34+
if (valueProviderResult == ValueProviderResult.None)
35+
{
36+
// no entry
37+
return Task.CompletedTask;
38+
}
39+
40+
var modelState = bindingContext.ModelState;
41+
modelState.SetModelValue(modelName, valueProviderResult);
42+
43+
var metadata = bindingContext.ModelMetadata;
44+
var type = metadata.UnderlyingOrModelType;
45+
try
46+
{
47+
var value = valueProviderResult.FirstValue;
48+
var culture = valueProviderResult.Culture;
49+
50+
object model;
51+
if (string.IsNullOrWhiteSpace(value))
52+
{
53+
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
54+
model = null;
55+
}
56+
else if (type == typeof(double))
57+
{
58+
model = double.Parse(value, _supportedStyles, culture);
59+
}
60+
else
61+
{
62+
// unreachable
63+
throw new NotSupportedException();
64+
}
65+
66+
// When converting value, a null model may indicate a failed conversion for an otherwise required
67+
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
68+
// current bindingContext. If not, an error is logged.
69+
if (model == null && !metadata.IsReferenceOrNullableType)
70+
{
71+
modelState.TryAddModelError(
72+
modelName,
73+
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
74+
valueProviderResult.ToString()));
75+
76+
return Task.CompletedTask;
77+
}
78+
else
79+
{
80+
bindingContext.Result = ModelBindingResult.Success(model);
81+
return Task.CompletedTask;
82+
}
83+
}
84+
catch (Exception exception)
85+
{
86+
var isFormatException = exception is FormatException;
87+
if (!isFormatException && exception.InnerException != null)
88+
{
89+
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
90+
// this code in case a cursory review of the CoreFx code missed something.
91+
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
92+
}
93+
94+
modelState.TryAddModelError(modelName, exception, metadata);
95+
96+
// Conversion failed.
97+
return Task.CompletedTask;
98+
}
99+
}
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Globalization;
6+
using System.Runtime.ExceptionServices;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
10+
{
11+
/// <summary>
12+
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is
13+
/// <see cref="decimal"/>.
14+
/// </summary>
15+
public class FloatModelBinder : IModelBinder
16+
{
17+
private readonly NumberStyles _supportedStyles;
18+
19+
public FloatModelBinder(NumberStyles supportedStyles)
20+
{
21+
_supportedStyles = supportedStyles;
22+
}
23+
24+
/// <inheritdoc />
25+
public Task BindModelAsync(ModelBindingContext bindingContext)
26+
{
27+
if (bindingContext == null)
28+
{
29+
throw new ArgumentNullException(nameof(bindingContext));
30+
}
31+
32+
var modelName = bindingContext.ModelName;
33+
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
34+
if (valueProviderResult == ValueProviderResult.None)
35+
{
36+
// no entry
37+
return Task.CompletedTask;
38+
}
39+
40+
var modelState = bindingContext.ModelState;
41+
modelState.SetModelValue(modelName, valueProviderResult);
42+
43+
var metadata = bindingContext.ModelMetadata;
44+
var type = metadata.UnderlyingOrModelType;
45+
try
46+
{
47+
var value = valueProviderResult.FirstValue;
48+
var culture = valueProviderResult.Culture;
49+
50+
object model;
51+
if (string.IsNullOrWhiteSpace(value))
52+
{
53+
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
54+
model = null;
55+
}
56+
else if (type == typeof(float))
57+
{
58+
model = float.Parse(value, _supportedStyles, culture);
59+
}
60+
else
61+
{
62+
// unreachable
63+
throw new NotSupportedException();
64+
}
65+
66+
// When converting value, a null model may indicate a failed conversion for an otherwise required
67+
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
68+
// current bindingContext. If not, an error is logged.
69+
if (model == null && !metadata.IsReferenceOrNullableType)
70+
{
71+
modelState.TryAddModelError(
72+
modelName,
73+
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
74+
valueProviderResult.ToString()));
75+
76+
return Task.CompletedTask;
77+
}
78+
else
79+
{
80+
bindingContext.Result = ModelBindingResult.Success(model);
81+
return Task.CompletedTask;
82+
}
83+
}
84+
catch (Exception exception)
85+
{
86+
var isFormatException = exception is FormatException;
87+
if (!isFormatException && exception.InnerException != null)
88+
{
89+
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
90+
// this code in case a cursory review of the CoreFx code missed something.
91+
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
92+
}
93+
94+
modelState.TryAddModelError(modelName, exception, metadata);
95+
96+
// Conversion failed.
97+
return Task.CompletedTask;
98+
}
99+
}
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Globalization;
6+
7+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
8+
{
9+
/// <summary>
10+
/// An <see cref="IModelBinderProvider"/> for binding <see cref="decimal"/>, <see cref="double"/>,
11+
/// <see cref="float"/>, and their <see cref="Nullable{T}"/> wrappers.
12+
/// </summary>
13+
public class FloatingPointTypeModelBinderProvider : IModelBinderProvider
14+
{
15+
// SimpleTypeModelBinder uses DecimalConverter and similar. Those TypeConverters default to NumberStyles.Float.
16+
// Internal for testing.
17+
internal static readonly NumberStyles SupportedStyles = NumberStyles.Float | NumberStyles.AllowThousands;
18+
19+
/// <inheritdoc />
20+
public IModelBinder GetBinder(ModelBinderProviderContext context)
21+
{
22+
if (context == null)
23+
{
24+
throw new ArgumentNullException(nameof(context));
25+
}
26+
27+
var modelType = context.Metadata.UnderlyingOrModelType;
28+
if (modelType == typeof(decimal))
29+
{
30+
return new DecimalModelBinder(SupportedStyles);
31+
}
32+
33+
if (modelType == typeof(double))
34+
{
35+
return new DoubleModelBinder(SupportedStyles);
36+
}
37+
38+
if (modelType == typeof(float))
39+
{
40+
return new FloatModelBinder(SupportedStyles);
41+
}
42+
43+
return null;
44+
}
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Globalization;
5+
6+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
7+
{
8+
public class DecimalModelBinderTest : FloatingPointTypeModelBinderTest<decimal>
9+
{
10+
protected override decimal Twelve => 12M;
11+
12+
protected override decimal TwelvePointFive => 12.5M;
13+
14+
protected override decimal ThirtyTwoThousand => 32_000M;
15+
16+
protected override decimal ThirtyTwoThousandPointOne => 32_000.1M;
17+
18+
protected override IModelBinder GetBinder(NumberStyles numberStyles)
19+
{
20+
return new DecimalModelBinder(numberStyles);
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)