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

Commit

Permalink
Add FloatingPointTypeModelBinderProvider and related binders
Browse files Browse the repository at this point in the history
- #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
dougbu committed Jul 3, 2017
1 parent 057a853 commit ccefcaf
Show file tree
Hide file tree
Showing 13 changed files with 1,049 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public void Configure(MvcOptions options)
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
Expand Down
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;
}
}
}
}
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;
}
}
}
}
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;
}
}
}
}
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;
}
}
}
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);
}
}
}
Loading

0 comments on commit ccefcaf

Please sign in to comment.