Added a model binder for handling Enum types. [Fixes #6329] Why don't we check for Enum.IsDefined on action parameters of enum type

This commit is contained in:
Kiran Challa 2017-09-08 10:35:00 -07:00
parent db2d9ee56f
commit 35601f95b3
8 changed files with 545 additions and 16 deletions

View File

@ -46,6 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options.AllowBindingUndefinedValueToEnumType));
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());

View File

@ -0,0 +1,84 @@
// 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>
/// <see cref="IModelBinder"/> implementation to bind models for types deriving from <see cref="Enum"/>.
/// </summary>
public class EnumTypeModelBinder : SimpleTypeModelBinder
{
private readonly bool _allowBindingUndefinedValueToEnumType;
public EnumTypeModelBinder(bool allowBindingUndefinedValueToEnumType, Type modelType)
: base(modelType)
{
if (modelType == null)
{
throw new ArgumentNullException(nameof(modelType));
}
_allowBindingUndefinedValueToEnumType = allowBindingUndefinedValueToEnumType;
}
protected override void CheckModel(
ModelBindingContext bindingContext,
ValueProviderResult valueProviderResult,
object model)
{
if (model == null || _allowBindingUndefinedValueToEnumType)
{
base.CheckModel(bindingContext, valueProviderResult, model);
}
else
{
if (IsDefinedInEnum(model, bindingContext))
{
bindingContext.Result = ModelBindingResult.Success(model);
}
else
{
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueIsInvalidAccessor(
valueProviderResult.ToString()));
}
}
}
private bool IsDefinedInEnum(object model, ModelBindingContext bindingContext)
{
var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType;
// Check if the converted value is indeed defined on the enum as EnumTypeConverter
// converts value to the backing type (ex: integer) and does not check if the value is defined on the enum.
if (bindingContext.ModelMetadata.IsFlagsEnum)
{
// Enum.IsDefined does not work with combined flag enum values.
// From EnumDataTypeAttribute.cs in CoreFX.
// Exmaples:
//
// [Flags]
// enum FlagsEnum { Value1 = 1, Value2 = 2, Value4 = 4 }
//
// Valid Scenarios:
// 1. valueproviderresult="Value2,Value4", model=Value2 | Value4, underlying=6, converted=Value2, Value4
// 2. valueproviderresult="2,4", model=Value2 | Value4, underlying=6, converted=Value2, Value4
//
// Invalid Scenarios:
// 1. valueproviderresult="2,10", model=12, underlying=12, converted=12
//
var underlying = Convert.ChangeType(
model,
Enum.GetUnderlyingType(modelType),
CultureInfo.InvariantCulture).ToString();
var converted = model.ToString();
return !string.Equals(underlying, converted, StringComparison.OrdinalIgnoreCase);
}
return Enum.IsDefined(modelType, model);
}
}
}

View File

@ -0,0 +1,38 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// A <see cref="IModelBinderProvider"/> for types deriving from <see cref="Enum"/>.
/// </summary>
public class EnumTypeModelBinderProvider : IModelBinderProvider
{
private readonly bool _allowBindingUndefinedValueToEnumType;
public EnumTypeModelBinderProvider(bool allowBindingUndefinedValueToEnumType)
{
_allowBindingUndefinedValueToEnumType = allowBindingUndefinedValueToEnumType;
}
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsEnum)
{
return new EnumTypeModelBinder(
_allowBindingUndefinedValueToEnumType,
context.Metadata.UnderlyingOrModelType);
}
return null;
}
}
}

View File

@ -72,23 +72,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
value: value);
}
// When converting newModel a null value 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 && !bindingContext.ModelMetadata.IsReferenceOrNullableType)
{
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
CheckModel(bindingContext, valueProviderResult, model);
return Task.CompletedTask;
}
else
{
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
return Task.CompletedTask;
}
catch (Exception exception)
{
@ -109,5 +95,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
return Task.CompletedTask;
}
}
protected virtual void CheckModel(
ModelBindingContext bindingContext,
ValueProviderResult valueProviderResult,
object model)
{
// When converting newModel a null value 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 && !bindingContext.ModelMetadata.IsReferenceOrNullableType)
{
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
}
else
{
bindingContext.Result = ModelBindingResult.Success(model);
}
}
}
}

View File

@ -162,5 +162,11 @@ namespace Microsoft.AspNetCore.Mvc
/// Gets or sets the default value for the Permanent property of <see cref="RequireHttpsAttribute"/>.
/// </summary>
public bool RequireHttpsPermanent { get; set; }
/// <summary>
/// Gets or sets an indication whether the model binding system will bind undefined values to enumeration types.
/// <see langword="false"/> by default.
/// </summary>
public bool AllowBindingUndefinedValueToEnumType { get; set; }
}
}

View File

@ -0,0 +1,76 @@
// 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 Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class EnumTypeModelBinderProviderTest
{
[Theory]
[InlineData(typeof(CarType))]
[InlineData(typeof(CarType?))]
public void ReturnsBinder_ForEnumType(Type modelType)
{
// Arrange
var provider = new EnumTypeModelBinderProvider(allowBindingUndefinedValueToEnumType: true);
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<EnumTypeModelBinder>(result);
}
[Theory]
[InlineData(typeof(CarOptions))]
[InlineData(typeof(CarOptions?))]
public void ReturnsBinder_ForFlagsEnumType(Type modelType)
{
// Arrange
var provider = new EnumTypeModelBinderProvider(allowBindingUndefinedValueToEnumType: true);
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<EnumTypeModelBinder>(result);
}
[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(int))]
[InlineData(typeof(int?))]
public void DoesNotReturnBinder_ForNonEnumTypes(Type modelType)
{
// Arrange
var provider = new EnumTypeModelBinderProvider(allowBindingUndefinedValueToEnumType: false);
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
enum CarType
{
Sedan,
Coupe
}
[Flags]
public enum CarOptions
{
SunRoof = 0x01,
Spoiler = 0x02,
FogLights = 0x04,
TintedWindows = 0x08,
}
}
}

View File

@ -0,0 +1,316 @@
// 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.ComponentModel;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class EnumTypeModelBinderTest
{
[Theory]
[InlineData(true, typeof(IntEnum?))]
[InlineData(true, typeof(FlagsEnum?))]
[InlineData(false, typeof(IntEnum?))]
[InlineData(false, typeof(FlagsEnum?))]
public async Task BindModel_SetsModel_ForEmptyValue_AndNullableEnumTypes(
bool allowBindingUndefinedValueToEnumType,
Type modelType)
{
// Arrange
var binderInfo = GetBinderAndContext(
modelType,
allowBindingUndefinedValueToEnumType,
valueProviderValue: "");
var bindingContext = binderInfo.Item1;
var binder = binderInfo.Item2;
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.Null(bindingContext.Result.Model);
}
[Theory]
[InlineData(true, typeof(IntEnum))]
[InlineData(true, typeof(FlagsEnum))]
[InlineData(false, typeof(IntEnum))]
[InlineData(false, typeof(FlagsEnum))]
public async Task BindModel_AddsErrorToModelState_ForEmptyValue_AndNonNullableEnumTypes(
bool allowBindingUndefinedValueToEnumType,
Type modelType)
{
// Arrange
var message = "The value '' is invalid.";
var binderInfo = GetBinderAndContext(
modelType,
allowBindingUndefinedValueToEnumType,
valueProviderValue: "");
var bindingContext = binderInfo.Item1;
var binder = binderInfo.Item2;
// 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]
[InlineData(true, "Value1")]
[InlineData(true, "1")]
[InlineData(false, "Value1")]
[InlineData(false, "1")]
public async Task BindModel_BindsEnumModels_ForValuesInArray(
bool allowBindingUndefinedValueToEnumType,
string enumValue)
{
// Arrange
var modelType = typeof(IntEnum);
var binderInfo = GetBinderAndContext(
modelType,
allowBindingUndefinedValueToEnumType,
valueProviderValue: new object[] { enumValue });
var bindingContext = binderInfo.Item1;
var binder = binderInfo.Item2;
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
var boundModel = Assert.IsType<IntEnum>(bindingContext.Result.Model);
Assert.Equal(IntEnum.Value1, boundModel);
}
[Theory]
[InlineData("1", true)]
[InlineData("8, 1", true)]
[InlineData("Value2, Value8", true)]
[InlineData("value8,value4,value2,value1", true)]
[InlineData("1", false)]
[InlineData("8, 1", false)]
[InlineData("Value2, Value8", false)]
[InlineData("value8,value4,value2,value1", false)]
public async Task BindModel_BindsTo_NonNullableFlagsEnumType(string flagsEnumValue, bool allowBindingUndefinedValueToEnumType)
{
// Arrange
var modelType = typeof(FlagsEnum);
var enumConverter = TypeDescriptor.GetConverter(modelType);
var expected = enumConverter.ConvertFrom(flagsEnumValue).ToString();
var binderInfo = GetBinderAndContext(
modelType,
allowBindingUndefinedValueToEnumType,
valueProviderValue: new object[] { flagsEnumValue });
var bindingContext = binderInfo.Item1;
var binder = binderInfo.Item2;
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
var boundModel = Assert.IsType<FlagsEnum>(bindingContext.Result.Model);
Assert.Equal(expected, boundModel.ToString());
}
[Theory]
[InlineData("1", true)]
[InlineData("8, 1", true)]
[InlineData("Value2, Value8", true)]
[InlineData("value8,value4,value2,value1", true)]
[InlineData("1", false)]
[InlineData("8, 1", false)]
[InlineData("Value2, Value8", false)]
[InlineData("value8,value4,value2,value1", false)]
public async Task BindModel_BindsTo_NullableFlagsEnumType(string flagsEnumValue, bool allowBindingUndefinedValueToEnumType)
{
// Arrange
var modelType = typeof(FlagsEnum?);
var enumConverter = TypeDescriptor.GetConverter(GetUnderlyingType(modelType));
var expected = enumConverter.ConvertFrom(flagsEnumValue).ToString();
var binderInfo = GetBinderAndContext(
modelType,
allowBindingUndefinedValueToEnumType,
valueProviderValue: new object[] { flagsEnumValue });
var bindingContext = binderInfo.Item1;
var binder = binderInfo.Item2;
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
var boundModel = Assert.IsType<FlagsEnum>(bindingContext.Result.Model);
Assert.Equal(expected, boundModel.ToString());
}
[Theory]
[InlineData(typeof(FlagsEnum), "Value10")]
[InlineData(typeof(FlagsEnum), "Value1, Value10")]
[InlineData(typeof(FlagsEnum), "value10, value1")]
[InlineData(typeof(FlagsEnum?), "Value10")]
[InlineData(typeof(FlagsEnum?), "Value1, Value10")]
[InlineData(typeof(FlagsEnum?), "value10, value1")]
public async Task BindModel_AddsErrorToModelState_ForInvalidEnumValues_IsNotValidMessage(Type modelType, string suppliedValue)
{
// Arrange
var message = $"The value '{suppliedValue}' is not valid.";
var binderInfo = GetBinderAndContext(
modelType,
allowBindingUndefinedValueToEnumType: true,
valueProviderValue: new object[] { suppliedValue });
var bindingContext = binderInfo.Item1;
var binder = binderInfo.Item2;
// 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]
[InlineData(typeof(IntEnum), "")]
[InlineData(typeof(IntEnum), "3")]
[InlineData(typeof(FlagsEnum), "19")]
[InlineData(typeof(FlagsEnum), "0")]
[InlineData(typeof(FlagsEnum), "1, 16")]
// These two values look like big integers but are treated as two separate enum values that are
// or'd together.
[InlineData(typeof(FlagsEnum), "32,015")]
[InlineData(typeof(FlagsEnum), "32,128")]
[InlineData(typeof(IntEnum?), "3")]
[InlineData(typeof(FlagsEnum?), "19")]
[InlineData(typeof(FlagsEnum?), "0")]
[InlineData(typeof(FlagsEnum?), "1, 16")]
// These two values look like big integers but are treated as two separate enum values that are
// or'd together.
[InlineData(typeof(FlagsEnum?), "32,015")]
[InlineData(typeof(FlagsEnum?), "32,128")]
public async Task BindModel_AddsErrorToModelState_ForInvalidEnumValues(Type modelType, string suppliedValue)
{
// Arrange
var message = $"The value '{suppliedValue}' is invalid.";
var binderInfo = GetBinderAndContext(
modelType,
allowBindingUndefinedValueToEnumType: false,
valueProviderValue: new object[] { suppliedValue });
var bindingContext = binderInfo.Item1;
var binder = binderInfo.Item2;
// 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]
[InlineData(typeof(IntEnum), "3", 3)]
[InlineData(typeof(FlagsEnum), "19", 19)]
[InlineData(typeof(FlagsEnum), "0", 0)]
[InlineData(typeof(FlagsEnum), "1, 16", 17)]
// These two values look like big integers but are treated as two separate enum values that are
// or'd together.
[InlineData(typeof(FlagsEnum), "32,015", 47)]
[InlineData(typeof(FlagsEnum), "32,128", 160)]
[InlineData(typeof(IntEnum?), "3", 3)]
[InlineData(typeof(FlagsEnum?), "19", 19)]
[InlineData(typeof(FlagsEnum?), "0", 0)]
[InlineData(typeof(FlagsEnum?), "1, 16", 17)]
// These two values look like big integers but are treated as two separate enum values that are
// or'd together.
[InlineData(typeof(FlagsEnum?), "32,015", 47)]
[InlineData(typeof(FlagsEnum?), "32,128", 160)]
public async Task BindModel_AllowsBindingUndefinedValues_ToEnumTypes(
Type modelType,
string suppliedValue,
long expected)
{
// Arrange
var binderProviderContext = new TestModelBinderProviderContext(modelType);
var binderInfo = GetBinderAndContext(
modelType,
allowBindingUndefinedValueToEnumType: true,
valueProviderValue: new object[] { suppliedValue });
var bindingContext = binderInfo.Item1;
var binder = binderInfo.Item2;
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.IsType(GetUnderlyingType(modelType), bindingContext.Result.Model);
Assert.Equal(expected, Convert.ToInt64(bindingContext.Result.Model));
}
private static (DefaultModelBindingContext, IModelBinder) GetBinderAndContext(
Type modelType,
bool allowBindingUndefinedValueToEnumType,
object valueProviderValue)
{
var binderProviderContext = new TestModelBinderProviderContext(modelType);
var modelName = "theModelName";
var bindingContext = new DefaultModelBindingContext
{
ModelMetadata = binderProviderContext.Metadata,
ModelName = modelName,
ModelState = new ModelStateDictionary(),
ValueProvider = new SimpleValueProvider()
{
{ modelName, valueProviderValue }
}
};
var binderProvider = new EnumTypeModelBinderProvider(allowBindingUndefinedValueToEnumType);
var binder = binderProvider.GetBinder(binderProviderContext);
return (bindingContext, binder);
}
private static Type GetUnderlyingType(Type modelType)
{
var underlyingType = Nullable.GetUnderlyingType(modelType);
if (underlyingType != null)
{
return underlyingType;
}
return modelType;
}
[Flags]
private enum FlagsEnum
{
Value1 = 1,
Value2 = 2,
Value4 = 4,
Value8 = 8,
}
private enum IntEnum
{
Value0 = 0,
Value1 = 1,
Value2 = 2,
MaxValue = int.MaxValue
}
}
}

View File

@ -59,6 +59,7 @@ namespace Microsoft.AspNetCore.Mvc
binder => Assert.IsType<BodyModelBinderProvider>(binder),
binder => Assert.IsType<HeaderModelBinderProvider>(binder),
binder => Assert.IsType<FloatingPointTypeModelBinderProvider>(binder),
binder => Assert.IsType<EnumTypeModelBinderProvider>(binder),
binder => Assert.IsType<SimpleTypeModelBinderProvider>(binder),
binder => Assert.IsType<CancellationTokenModelBinderProvider>(binder),
binder => Assert.IsType<ByteArrayModelBinderProvider>(binder),