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
This commit is contained in:
parent
c50f55d1de
commit
c351712419
|
|
@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
|
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
|
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
|
||||||
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
|
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
|
||||||
|
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
|
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
|
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
|
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 DoubleModelBinderTest : FloatingPointTypeModelBinderTest<double>
|
||||||
|
{
|
||||||
|
protected override double Twelve => 12.0;
|
||||||
|
|
||||||
|
protected override double TwelvePointFive => 12.5;
|
||||||
|
|
||||||
|
protected override double ThirtyTwoThousand => 32_000.0;
|
||||||
|
|
||||||
|
protected override double ThirtyTwoThousandPointOne => 32_000.1;
|
||||||
|
|
||||||
|
protected override IModelBinder GetBinder(NumberStyles numberStyles)
|
||||||
|
{
|
||||||
|
return new DoubleModelBinder(numberStyles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 FloatModelBinderTest : FloatingPointTypeModelBinderTest<float>
|
||||||
|
{
|
||||||
|
protected override float Twelve => 12.0F;
|
||||||
|
|
||||||
|
protected override float TwelvePointFive => 12.5F;
|
||||||
|
|
||||||
|
protected override float ThirtyTwoThousand => 32_000.0F;
|
||||||
|
|
||||||
|
protected override float ThirtyTwoThousandPointOne => 32_000.1F;
|
||||||
|
|
||||||
|
protected override IModelBinder GetBinder(NumberStyles numberStyles)
|
||||||
|
{
|
||||||
|
return new FloatModelBinder(numberStyles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
{
|
||||||
|
public class FloatingPointTypeModelBinderProviderTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(object))]
|
||||||
|
[InlineData(typeof(Calendar))]
|
||||||
|
[InlineData(typeof(TestClass))]
|
||||||
|
[InlineData(typeof(List<int>))]
|
||||||
|
public void Create_ForCollectionOrComplexTypes_ReturnsNull(Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new FloatingPointTypeModelBinderProvider();
|
||||||
|
var context = new TestModelBinderProviderContext(modelType);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(int))]
|
||||||
|
[InlineData(typeof(int?))]
|
||||||
|
[InlineData(typeof(char))]
|
||||||
|
[InlineData(typeof(string))]
|
||||||
|
[InlineData(typeof(DateTime))]
|
||||||
|
[InlineData(typeof(DateTime?))]
|
||||||
|
public void Create_ForUnsupportedSimpleTypes_ReturnsNull(Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new FloatingPointTypeModelBinderProvider();
|
||||||
|
var context = new TestModelBinderProviderContext(modelType);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(decimal))]
|
||||||
|
[InlineData(typeof(decimal?))]
|
||||||
|
public void Create_ForDecimalTypes_ReturnsBinder(Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new FloatingPointTypeModelBinderProvider();
|
||||||
|
var context = new TestModelBinderProviderContext(modelType);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<DecimalModelBinder>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(double))]
|
||||||
|
[InlineData(typeof(double?))]
|
||||||
|
public void Create_ForDoubleTypes_ReturnsBinder(Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new FloatingPointTypeModelBinderProvider();
|
||||||
|
var context = new TestModelBinderProviderContext(modelType);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<DoubleModelBinder>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(float))]
|
||||||
|
[InlineData(typeof(float?))]
|
||||||
|
public void Create_ForFloatTypes_ReturnsBinder(Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new FloatingPointTypeModelBinderProvider();
|
||||||
|
var context = new TestModelBinderProviderContext(modelType);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<FloatModelBinder>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestClass
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
// 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.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
{
|
||||||
|
public abstract class FloatingPointTypeModelBinderTest<TFloatingPoint> where TFloatingPoint: struct
|
||||||
|
{
|
||||||
|
public static TheoryData<Type> ConvertableTypeData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<Type>
|
||||||
|
{
|
||||||
|
typeof(TFloatingPoint),
|
||||||
|
typeof(TFloatingPoint?),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract TFloatingPoint Twelve { get; }
|
||||||
|
|
||||||
|
protected abstract TFloatingPoint TwelvePointFive { get; }
|
||||||
|
|
||||||
|
protected abstract TFloatingPoint ThirtyTwoThousand { get; }
|
||||||
|
|
||||||
|
protected abstract TFloatingPoint ThirtyTwoThousandPointOne { get; }
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_ReturnsFailure_IfAttemptedValueCannotBeParsed(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "some-value" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_CreatesError_IfAttemptedValueCannotBeParsed(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var message = "The value 'not a number' is not valid.";
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "not a number" },
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
Assert.False(bindingContext.ModelState.IsValid);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal(message, error.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_CreatesError_IfAttemptedValueCannotBeCompletelyParsed(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "12_5" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal("The value '12_5' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
|
||||||
|
Assert.Null(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedWhitespace(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
|
||||||
|
{
|
||||||
|
{ "theModelName", " 12" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder(NumberStyles.Float & ~NumberStyles.AllowLeadingWhite);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal("The value ' 12' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
|
||||||
|
Assert.Null(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedDecimal(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "12.5" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder(NumberStyles.Float & ~NumberStyles.AllowDecimalPoint);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal("The value '12.5' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
|
||||||
|
Assert.Null(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedThousandsSeparator(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "32,000" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder(NumberStyles.Float);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal("The value '32,000' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
|
||||||
|
Assert.Null(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_ReturnsFailed_IfValueProviderEmpty(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result);
|
||||||
|
Assert.Empty(bindingContext.ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" \t \r\n ")]
|
||||||
|
public async Task BindModel_CreatesError_IfTrimmedAttemptedValueIsEmpty_NonNullableDestination(string value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var message = $"The value '{value}' is invalid.";
|
||||||
|
var bindingContext = GetBindingContext(typeof(TFloatingPoint));
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", value },
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal(message, error.ErrorMessage, StringComparer.Ordinal);
|
||||||
|
Assert.Null(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" \t \r\n ")]
|
||||||
|
public async Task BindModel_ReturnsNull_IfTrimmedAttemptedValueIsEmpty_NullableDestination(string value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(typeof(TFloatingPoint?));
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", value }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
var entry = Assert.Single(bindingContext.ModelState);
|
||||||
|
Assert.Equal("theModelName", entry.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_Twelve(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "12" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal(Twelve, bindingContext.Result.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
[ReplaceCulture("en-GB", "en-GB")]
|
||||||
|
public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_TwelvePointFive(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "12.5" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal(TwelvePointFive, bindingContext.Result.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_FrenchTwelvePointFive(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "12,5" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal(TwelvePointFive, bindingContext.Result.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_ThirtyTwoThousand(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "32,000" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal(ThirtyTwoThousand, bindingContext.Result.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_ThirtyTwoThousandPointOne(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "32,000.1" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal(ThirtyTwoThousandPointOne, bindingContext.Result.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
|
public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_FrenchThirtyTwoThousandPointOne(Type destinationType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(destinationType);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "32 000,1" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Equal(ThirtyTwoThousandPointOne, bindingContext.Result.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract IModelBinder GetBinder(NumberStyles numberStyles);
|
||||||
|
|
||||||
|
private IModelBinder GetBinder()
|
||||||
|
{
|
||||||
|
return GetBinder(FloatingPointTypeModelBinderProvider.SupportedStyles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultModelBindingContext GetBindingContext(Type modelType)
|
||||||
|
{
|
||||||
|
return new DefaultModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(modelType),
|
||||||
|
ModelName = "theModelName",
|
||||||
|
ModelState = new ModelStateDictionary(),
|
||||||
|
ValueProvider = new SimpleValueProvider() // empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestClass
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -109,13 +109,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(byte))]
|
[MemberData(nameof(ConvertableTypeData))]
|
||||||
[InlineData(typeof(short))]
|
|
||||||
[InlineData(typeof(int))]
|
|
||||||
[InlineData(typeof(long))]
|
|
||||||
[InlineData(typeof(Guid))]
|
|
||||||
[InlineData(typeof(double))]
|
|
||||||
[InlineData(typeof(DayOfWeek))]
|
|
||||||
public async Task BindModel_CreatesError_WhenTypeConversionIsNull(Type destinationType)
|
public async Task BindModel_CreatesError_WhenTypeConversionIsNull(Type destinationType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -262,6 +256,55 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static TheoryData<Type> BiggerNumericTypes
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Data set does not include bool, byte, sbyte, or char because they do not need thousands separators.
|
||||||
|
return new TheoryData<Type>
|
||||||
|
{
|
||||||
|
typeof(decimal),
|
||||||
|
typeof(double),
|
||||||
|
typeof(float),
|
||||||
|
typeof(int),
|
||||||
|
typeof(long),
|
||||||
|
typeof(short),
|
||||||
|
typeof(uint),
|
||||||
|
typeof(ulong),
|
||||||
|
typeof(ushort),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(BiggerNumericTypes))]
|
||||||
|
public async Task BindModel_ThousandsSeparators_LeadToErrors(Type type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(type);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "32,000" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var binder = new SimpleTypeModelBinder(type);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
|
||||||
|
var entry = Assert.Single(bindingContext.ModelState);
|
||||||
|
Assert.Equal("theModelName", entry.Key);
|
||||||
|
Assert.Equal("32,000", entry.Value.AttemptedValue);
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
|
||||||
|
|
||||||
|
var error = Assert.Single(entry.Value.Errors);
|
||||||
|
Assert.Equal("The value '32,000' is not valid.", error.ErrorMessage);
|
||||||
|
Assert.Null(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BindModel_ValidValueProviderResultWithProvidedCulture_ReturnsModel()
|
public async Task BindModel_ValidValueProviderResultWithProvidedCulture_ReturnsModel()
|
||||||
{
|
{
|
||||||
|
|
@ -349,11 +392,49 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.Equal(IntEnum.Value1, boundModel);
|
Assert.Equal(IntEnum.Value1, boundModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static TheoryData<string, int> EnumValues
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string, int>
|
||||||
|
{
|
||||||
|
{ "0", 0 },
|
||||||
|
{ "1", 1 },
|
||||||
|
{ "13", 13 },
|
||||||
|
{ "Value1", 1 },
|
||||||
|
{ "Value1, Value2", 3 },
|
||||||
|
// These two values look like big integers but are treated as two separate enum values that are
|
||||||
|
// or'd together.
|
||||||
|
{ "32,015", 47 },
|
||||||
|
{ "32,128", 160 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0", 0)]
|
[MemberData(nameof(EnumValues))]
|
||||||
[InlineData("1", 1)]
|
public async Task BindModel_BindsIntEnumModels(string flagsEnumValue, int expected)
|
||||||
[InlineData("13", 13)]
|
{
|
||||||
[InlineData("Value1", 1)]
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(typeof(IntEnum));
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", flagsEnumValue }
|
||||||
|
};
|
||||||
|
|
||||||
|
var binder = new SimpleTypeModelBinder(typeof(IntEnum));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
var boundModel = Assert.IsType<IntEnum>(bindingContext.Result.Model);
|
||||||
|
Assert.Equal((IntEnum)expected, boundModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(EnumValues))]
|
||||||
[InlineData("Value8, Value4", 12)]
|
[InlineData("Value8, Value4", 12)]
|
||||||
public async Task BindModel_BindsFlagsEnumModels(string flagsEnumValue, int expected)
|
public async Task BindModel_BindsFlagsEnumModels(string flagsEnumValue, int expected)
|
||||||
{
|
{
|
||||||
|
|
@ -396,13 +477,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Value1 = 1,
|
Value1 = 1,
|
||||||
Value2 = 2,
|
Value2 = 2,
|
||||||
Value4 = 4,
|
Value4 = 4,
|
||||||
Value8 = 8
|
Value8 = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum IntEnum
|
private enum IntEnum
|
||||||
{
|
{
|
||||||
Value0 = 0,
|
Value0 = 0,
|
||||||
Value1 = 1,
|
Value1 = 1,
|
||||||
|
Value2 = 2,
|
||||||
MaxValue = int.MaxValue
|
MaxValue = int.MaxValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -152,6 +153,50 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture("en-GB", "en-GB")]
|
||||||
|
public async Task BindDecimalParameter_WithData_GetsBound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
var parameter = new ParameterDescriptor
|
||||||
|
{
|
||||||
|
Name = "Parameter1",
|
||||||
|
BindingInfo = new BindingInfo(),
|
||||||
|
ParameterType = typeof(decimal),
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
|
{
|
||||||
|
request.QueryString = QueryString.Create("Parameter1", "32,000.99");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
// ModelBindingResult
|
||||||
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
|
// Model
|
||||||
|
var model = Assert.IsType<decimal>(modelBindingResult.Model);
|
||||||
|
Assert.Equal(32000.99M, model);
|
||||||
|
|
||||||
|
// ModelState
|
||||||
|
Assert.True(modelState.IsValid);
|
||||||
|
|
||||||
|
Assert.Equal(1, modelState.Keys.Count());
|
||||||
|
var key = Assert.Single(modelState.Keys);
|
||||||
|
Assert.Equal("Parameter1", key);
|
||||||
|
Assert.Equal("32,000.99", modelState[key].AttemptedValue);
|
||||||
|
Assert.Equal("32,000.99", modelState[key].RawValue);
|
||||||
|
Assert.Empty(modelState[key].Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BindParameter_WithMultipleValues_GetsBoundToFirstValue()
|
public async Task BindParameter_WithMultipleValues_GetsBoundToFirstValue()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
binder => Assert.IsType<ServicesModelBinderProvider>(binder),
|
binder => Assert.IsType<ServicesModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<BodyModelBinderProvider>(binder),
|
binder => Assert.IsType<BodyModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<HeaderModelBinderProvider>(binder),
|
binder => Assert.IsType<HeaderModelBinderProvider>(binder),
|
||||||
|
binder => Assert.IsType<FloatingPointTypeModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<SimpleTypeModelBinderProvider>(binder),
|
binder => Assert.IsType<SimpleTypeModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<CancellationTokenModelBinderProvider>(binder),
|
binder => Assert.IsType<CancellationTokenModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<ByteArrayModelBinderProvider>(binder),
|
binder => Assert.IsType<ByteArrayModelBinderProvider>(binder),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue