This repository has been archived by the owner on Dec 14, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
13 changed files
with
1,049 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DecimalModelBinder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Globalization; | ||
using System.Runtime.ExceptionServices; | ||
using System.Threading.Tasks; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders | ||
{ | ||
/// <summary> | ||
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is | ||
/// <see cref="decimal"/>. | ||
/// </summary> | ||
public class DecimalModelBinder : IModelBinder | ||
{ | ||
private readonly NumberStyles _supportedStyles; | ||
|
||
public DecimalModelBinder(NumberStyles supportedStyles) | ||
{ | ||
_supportedStyles = supportedStyles; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public Task BindModelAsync(ModelBindingContext bindingContext) | ||
{ | ||
if (bindingContext == null) | ||
{ | ||
throw new ArgumentNullException(nameof(bindingContext)); | ||
} | ||
|
||
var modelName = bindingContext.ModelName; | ||
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); | ||
if (valueProviderResult == ValueProviderResult.None) | ||
{ | ||
// no entry | ||
return Task.CompletedTask; | ||
} | ||
|
||
var modelState = bindingContext.ModelState; | ||
modelState.SetModelValue(modelName, valueProviderResult); | ||
|
||
var metadata = bindingContext.ModelMetadata; | ||
var type = metadata.UnderlyingOrModelType; | ||
try | ||
{ | ||
var value = valueProviderResult.FirstValue; | ||
var culture = valueProviderResult.Culture; | ||
|
||
object model; | ||
if (string.IsNullOrWhiteSpace(value)) | ||
{ | ||
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty. | ||
model = null; | ||
} | ||
else if (type == typeof(decimal)) | ||
{ | ||
model = decimal.Parse(value, _supportedStyles, culture); | ||
} | ||
else | ||
{ | ||
// unreachable | ||
throw new NotSupportedException(); | ||
} | ||
|
||
// When converting value, a null model may indicate a failed conversion for an otherwise required | ||
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the | ||
// current bindingContext. If not, an error is logged. | ||
if (model == null && !metadata.IsReferenceOrNullableType) | ||
{ | ||
modelState.TryAddModelError( | ||
modelName, | ||
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor( | ||
valueProviderResult.ToString())); | ||
|
||
return Task.CompletedTask; | ||
} | ||
else | ||
{ | ||
bindingContext.Result = ModelBindingResult.Success(model); | ||
return Task.CompletedTask; | ||
} | ||
} | ||
catch (Exception exception) | ||
{ | ||
var isFormatException = exception is FormatException; | ||
if (!isFormatException && exception.InnerException != null) | ||
{ | ||
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve | ||
// this code in case a cursory review of the CoreFx code missed something. | ||
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException; | ||
} | ||
|
||
modelState.TryAddModelError(modelName, exception, metadata); | ||
|
||
// Conversion failed. | ||
return Task.CompletedTask; | ||
} | ||
} | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DoubleModelBinder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Globalization; | ||
using System.Runtime.ExceptionServices; | ||
using System.Threading.Tasks; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders | ||
{ | ||
/// <summary> | ||
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is | ||
/// <see cref="decimal"/>. | ||
/// </summary> | ||
public class DoubleModelBinder : IModelBinder | ||
{ | ||
private readonly NumberStyles _supportedStyles; | ||
|
||
public DoubleModelBinder(NumberStyles supportedStyles) | ||
{ | ||
_supportedStyles = supportedStyles; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public Task BindModelAsync(ModelBindingContext bindingContext) | ||
{ | ||
if (bindingContext == null) | ||
{ | ||
throw new ArgumentNullException(nameof(bindingContext)); | ||
} | ||
|
||
var modelName = bindingContext.ModelName; | ||
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); | ||
if (valueProviderResult == ValueProviderResult.None) | ||
{ | ||
// no entry | ||
return Task.CompletedTask; | ||
} | ||
|
||
var modelState = bindingContext.ModelState; | ||
modelState.SetModelValue(modelName, valueProviderResult); | ||
|
||
var metadata = bindingContext.ModelMetadata; | ||
var type = metadata.UnderlyingOrModelType; | ||
try | ||
{ | ||
var value = valueProviderResult.FirstValue; | ||
var culture = valueProviderResult.Culture; | ||
|
||
object model; | ||
if (string.IsNullOrWhiteSpace(value)) | ||
{ | ||
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty. | ||
model = null; | ||
} | ||
else if (type == typeof(double)) | ||
{ | ||
model = double.Parse(value, _supportedStyles, culture); | ||
} | ||
else | ||
{ | ||
// unreachable | ||
throw new NotSupportedException(); | ||
} | ||
|
||
// When converting value, a null model may indicate a failed conversion for an otherwise required | ||
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the | ||
// current bindingContext. If not, an error is logged. | ||
if (model == null && !metadata.IsReferenceOrNullableType) | ||
{ | ||
modelState.TryAddModelError( | ||
modelName, | ||
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor( | ||
valueProviderResult.ToString())); | ||
|
||
return Task.CompletedTask; | ||
} | ||
else | ||
{ | ||
bindingContext.Result = ModelBindingResult.Success(model); | ||
return Task.CompletedTask; | ||
} | ||
} | ||
catch (Exception exception) | ||
{ | ||
var isFormatException = exception is FormatException; | ||
if (!isFormatException && exception.InnerException != null) | ||
{ | ||
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve | ||
// this code in case a cursory review of the CoreFx code missed something. | ||
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException; | ||
} | ||
|
||
modelState.TryAddModelError(modelName, exception, metadata); | ||
|
||
// Conversion failed. | ||
return Task.CompletedTask; | ||
} | ||
} | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FloatModelBinder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Globalization; | ||
using System.Runtime.ExceptionServices; | ||
using System.Threading.Tasks; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders | ||
{ | ||
/// <summary> | ||
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is | ||
/// <see cref="decimal"/>. | ||
/// </summary> | ||
public class FloatModelBinder : IModelBinder | ||
{ | ||
private readonly NumberStyles _supportedStyles; | ||
|
||
public FloatModelBinder(NumberStyles supportedStyles) | ||
{ | ||
_supportedStyles = supportedStyles; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public Task BindModelAsync(ModelBindingContext bindingContext) | ||
{ | ||
if (bindingContext == null) | ||
{ | ||
throw new ArgumentNullException(nameof(bindingContext)); | ||
} | ||
|
||
var modelName = bindingContext.ModelName; | ||
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); | ||
if (valueProviderResult == ValueProviderResult.None) | ||
{ | ||
// no entry | ||
return Task.CompletedTask; | ||
} | ||
|
||
var modelState = bindingContext.ModelState; | ||
modelState.SetModelValue(modelName, valueProviderResult); | ||
|
||
var metadata = bindingContext.ModelMetadata; | ||
var type = metadata.UnderlyingOrModelType; | ||
try | ||
{ | ||
var value = valueProviderResult.FirstValue; | ||
var culture = valueProviderResult.Culture; | ||
|
||
object model; | ||
if (string.IsNullOrWhiteSpace(value)) | ||
{ | ||
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty. | ||
model = null; | ||
} | ||
else if (type == typeof(float)) | ||
{ | ||
model = float.Parse(value, _supportedStyles, culture); | ||
} | ||
else | ||
{ | ||
// unreachable | ||
throw new NotSupportedException(); | ||
} | ||
|
||
// When converting value, a null model may indicate a failed conversion for an otherwise required | ||
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the | ||
// current bindingContext. If not, an error is logged. | ||
if (model == null && !metadata.IsReferenceOrNullableType) | ||
{ | ||
modelState.TryAddModelError( | ||
modelName, | ||
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor( | ||
valueProviderResult.ToString())); | ||
|
||
return Task.CompletedTask; | ||
} | ||
else | ||
{ | ||
bindingContext.Result = ModelBindingResult.Success(model); | ||
return Task.CompletedTask; | ||
} | ||
} | ||
catch (Exception exception) | ||
{ | ||
var isFormatException = exception is FormatException; | ||
if (!isFormatException && exception.InnerException != null) | ||
{ | ||
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve | ||
// this code in case a cursory review of the CoreFx code missed something. | ||
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException; | ||
} | ||
|
||
modelState.TryAddModelError(modelName, exception, metadata); | ||
|
||
// Conversion failed. | ||
return Task.CompletedTask; | ||
} | ||
} | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
...icrosoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FloatingPointTypeModelBinderProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Globalization; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders | ||
{ | ||
/// <summary> | ||
/// An <see cref="IModelBinderProvider"/> for binding <see cref="decimal"/>, <see cref="double"/>, | ||
/// <see cref="float"/>, and their <see cref="Nullable{T}"/> wrappers. | ||
/// </summary> | ||
public class FloatingPointTypeModelBinderProvider : IModelBinderProvider | ||
{ | ||
// SimpleTypeModelBinder uses DecimalConverter and similar. Those TypeConverters default to NumberStyles.Float. | ||
// Internal for testing. | ||
internal static readonly NumberStyles SupportedStyles = NumberStyles.Float | NumberStyles.AllowThousands; | ||
|
||
/// <inheritdoc /> | ||
public IModelBinder GetBinder(ModelBinderProviderContext context) | ||
{ | ||
if (context == null) | ||
{ | ||
throw new ArgumentNullException(nameof(context)); | ||
} | ||
|
||
var modelType = context.Metadata.UnderlyingOrModelType; | ||
if (modelType == typeof(decimal)) | ||
{ | ||
return new DecimalModelBinder(SupportedStyles); | ||
} | ||
|
||
if (modelType == typeof(double)) | ||
{ | ||
return new DoubleModelBinder(SupportedStyles); | ||
} | ||
|
||
if (modelType == typeof(float)) | ||
{ | ||
return new FloatModelBinder(SupportedStyles); | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DecimalModelBinderTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Globalization; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders | ||
{ | ||
public class DecimalModelBinderTest : FloatingPointTypeModelBinderTest<decimal> | ||
{ | ||
protected override decimal Twelve => 12M; | ||
|
||
protected override decimal TwelvePointFive => 12.5M; | ||
|
||
protected override decimal ThirtyTwoThousand => 32_000M; | ||
|
||
protected override decimal ThirtyTwoThousandPointOne => 32_000.1M; | ||
|
||
protected override IModelBinder GetBinder(NumberStyles numberStyles) | ||
{ | ||
return new DecimalModelBinder(numberStyles); | ||
} | ||
} | ||
} |
Oops, something went wrong.