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 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());
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
[InlineData(typeof(byte))]
|
||||
[InlineData(typeof(short))]
|
||||
[InlineData(typeof(int))]
|
||||
[InlineData(typeof(long))]
|
||||
[InlineData(typeof(Guid))]
|
||||
[InlineData(typeof(double))]
|
||||
[InlineData(typeof(DayOfWeek))]
|
||||
[MemberData(nameof(ConvertableTypeData))]
|
||||
public async Task BindModel_CreatesError_WhenTypeConversionIsNull(Type destinationType)
|
||||
{
|
||||
// Arrange
|
||||
|
|
@ -262,6 +256,55 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
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]
|
||||
public async Task BindModel_ValidValueProviderResultWithProvidedCulture_ReturnsModel()
|
||||
{
|
||||
|
|
@ -349,11 +392,49 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
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]
|
||||
[InlineData("0", 0)]
|
||||
[InlineData("1", 1)]
|
||||
[InlineData("13", 13)]
|
||||
[InlineData("Value1", 1)]
|
||||
[MemberData(nameof(EnumValues))]
|
||||
public async Task BindModel_BindsIntEnumModels(string flagsEnumValue, int expected)
|
||||
{
|
||||
// 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)]
|
||||
public async Task BindModel_BindsFlagsEnumModels(string flagsEnumValue, int expected)
|
||||
{
|
||||
|
|
@ -396,13 +477,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
Value1 = 1,
|
||||
Value2 = 2,
|
||||
Value4 = 4,
|
||||
Value8 = 8
|
||||
Value8 = 8,
|
||||
}
|
||||
|
||||
private enum IntEnum
|
||||
{
|
||||
Value0 = 0,
|
||||
Value1 = 1,
|
||||
Value2 = 2,
|
||||
MaxValue = int.MaxValue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -152,6 +153,50 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
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]
|
||||
public async Task BindParameter_WithMultipleValues_GetsBoundToFirstValue()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
binder => Assert.IsType<ServicesModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<BodyModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<HeaderModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<FloatingPointTypeModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<SimpleTypeModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<CancellationTokenModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<ByteArrayModelBinderProvider>(binder),
|
||||
|
|
|
|||
Loading…
Reference in New Issue