使用Aspnet [FromQuery]模型绑定中的Enummember值的枚举

发布于 2025-02-13 21:39:18 字数 1901 浏览 0 评论 0 原文

通过使用标准[FromQuery],将查询字符串供应到.NET对象中

[Route("[controller]")]
public class SamplesController
    : ControllerBase
{
    [HttpGet]
    public IActionResult Get([FromQuery]QueryModel queryModel)
    {
        if (!queryModel.Status.HasValue)
        {
            return BadRequest("Problem in deserialization");
        }
        
        return Ok(queryModel.Status.Value.GetEnumDisplayName());
    }
}

该项目

public class QueryModel
{
    /// <summary>
    /// The foo parameter
    /// </summary>
    /// <example>bar</example>
    public string? Foo { get; init; } = null;
    
    /// <summary>
    /// The status
    /// </summary>
    /// <example>on-hold</example>
    public Status? Status { get; set; } = null;
}

我在.net 6 microsoft.net.sdk.sdk.web 项目中有一个端点, > enummember 属性我想用来从中挑选的值。

public enum Status
{
    [EnumMember(Value = "open")]
    Open,
    
    [EnumMember(Value = "on-hold")]
    OnHold
}

默认情况下,.NET 6在必要时不考虑 enummember

目标是能够

http://localhost:5000/Samples?Foo=bar&Status=on-hold 

通过使用其 status.onhold 值来发送诸如控制器的操作 QueryModel QueryModel 。 >

我尝试过没有运气的扩展库,其中包含一个转换器,但是使用 [FromQuery] 时,转换器不会触发。请参阅 https://github.com/macross-software/core/core/issues/30

我添加了一个重现问题的项目,并作为提供解决方案的沙箱** https://gitlab.com/sunnyatticsoftware/issues/string-to-to-enum-mvc/-/tree/feature/1-original-problem

注意:我需要一个解决方案枚举和不需要任何外部依赖性(仅.NET SDK)。

I have an endpoint in .NET 6 Microsoft.NET.Sdk.Web project that deserialize query strings into a .NET object by using the standard [FromQuery]

[Route("[controller]")]
public class SamplesController
    : ControllerBase
{
    [HttpGet]
    public IActionResult Get([FromQuery]QueryModel queryModel)
    {
        if (!queryModel.Status.HasValue)
        {
            return BadRequest("Problem in deserialization");
        }
        
        return Ok(queryModel.Status.Value.GetEnumDisplayName());
    }
}

The model contains an enum

public class QueryModel
{
    /// <summary>
    /// The foo parameter
    /// </summary>
    /// <example>bar</example>
    public string? Foo { get; init; } = null;
    
    /// <summary>
    /// The status
    /// </summary>
    /// <example>on-hold</example>
    public Status? Status { get; set; } = null;
}

And the enum has EnumMember attributes which value I want to use to deserialize from.

public enum Status
{
    [EnumMember(Value = "open")]
    Open,
    
    [EnumMember(Value = "on-hold")]
    OnHold
}

By default, .NET 6 does not take into consideration the EnumMember when deserializing.

The goal is to be able to send requests such as

http://localhost:5000/Samples?Foo=bar&Status=on-hold 

and have the controller's action deserialize the QueryModel with the proper Status.OnHold value by using its EnumMember

I have tried without luck an extensions library that contains a converter, but the converter is not getting triggered when using [FromQuery]. See https://github.com/Macross-Software/core/issues/30

I have added a project to reproduce problem and as a sandbox to provide a solution**
https://gitlab.com/sunnyatticsoftware/issues/string-to-enum-mvc/-/tree/feature/1-original-problem

NOTE: I would need a solution where the Enum and the does not require any external dependency (just .NET sdk).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

笔芯 2025-02-20 21:39:18

自定义枚举转换器可能是您的选择。通过利用现有的 enumConverter 类,我们需要的是具有自定义的 convertfrom 方法:

public class CustomEnumConverter : EnumConverter
{
    public CustomEnumConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] Type type) : base(type)
    {
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (value is string strValue)
        {
            try
            {
                foreach (var name in Enum.GetNames(EnumType))
                {
                    var field = EnumType.GetField(name);
                    if (field != null)
                    {
                        var enumMember = (EnumMemberAttribute)(field.GetCustomAttributes(typeof(EnumMemberAttribute), true).Single());
                        if (strValue.Equals(enumMember.Value, StringComparison.OrdinalIgnoreCase))
                        {
                            return Enum.Parse(EnumType, name, true);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                throw new FormatException((string)value, e);
            }
        }

        return base.ConvertFrom(context, culture, value);
    }
}

然后将转换器装饰到您的模型类:

[TypeConverter(typeof(CustomEnumConverter))]
public enum Status
{
    [EnumMember(Value = "open")]
    Open,
    
    [EnumMember(Value = "on-hold")]
    OnHold
}

然后我们可以获得“ on-hold” “解析。您可能还需要覆盖用于打印 enummember 值的 contergo()。有点骇客,但是如果您想要纯的.NET解决方案,这应该是最小的可行解决方案之一。

A custom Enum converter might be your choice. By leveraging the existing EnumConverter class what we need is to have a customized ConvertFrom method:

public class CustomEnumConverter : EnumConverter
{
    public CustomEnumConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] Type type) : base(type)
    {
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (value is string strValue)
        {
            try
            {
                foreach (var name in Enum.GetNames(EnumType))
                {
                    var field = EnumType.GetField(name);
                    if (field != null)
                    {
                        var enumMember = (EnumMemberAttribute)(field.GetCustomAttributes(typeof(EnumMemberAttribute), true).Single());
                        if (strValue.Equals(enumMember.Value, StringComparison.OrdinalIgnoreCase))
                        {
                            return Enum.Parse(EnumType, name, true);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                throw new FormatException((string)value, e);
            }
        }

        return base.ConvertFrom(context, culture, value);
    }
}

And then decorate the converter to your Model class:

[TypeConverter(typeof(CustomEnumConverter))]
public enum Status
{
    [EnumMember(Value = "open")]
    Open,
    
    [EnumMember(Value = "on-hold")]
    OnHold
}

then we can get the "on-hold" parsed. You might also want to override the ConverTo() for printing the EnumMember value to swagger. It is a bit hacky, but if you want a pure .NET solution this should be one of the minimal viable solutions.

enter image description here

沉溺在你眼里的海 2025-02-20 21:39:18

之后的文档指南,您可以创建自己的Microsoft类的版本 (和基类 )取代传入枚举通过 enummemberattribute 用原始枚举名称重命名的值名称:然后在绑定之前:

// Begin code for enum model binding
public class EnumMemberEnumTypeModelBinderProvider : IModelBinderProvider 
{
    public EnumMemberEnumTypeModelBinderProvider(MvcOptions options) { }

    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));
        
        if (context.Metadata.IsEnum)
        {
            var enumType = context.Metadata.UnderlyingOrModelType;
            Debug.Assert(enumType.IsEnum);
            var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
            if (EnumExtensions.TryGetEnumMemberOverridesToOriginals(enumType, out var overridesToOriginals))
                return new EnumMemberEnumTypeModelBinder(suppressBindingUndefinedValueToEnumType: true, enumType, loggerFactory, overridesToOriginals);
        }
        
        return null;
    }
}

public class EnumMemberEnumTypeModelBinder : ExtensibleSimpleTypeModelBinder
{
    // Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/EnumTypeModelBinder.cs#L58
    readonly Type enumType;
    readonly bool isFlagged;
    readonly Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals;
    readonly TypeConverter typeConverter;

    public EnumMemberEnumTypeModelBinder(bool suppressBindingUndefinedValueToEnumType, Type modelType, ILoggerFactory loggerFactory, Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals) : base(modelType, loggerFactory)
    {
        this.enumType = Nullable.GetUnderlyingType(modelType) ?? modelType;
        if (!this.enumType.IsEnum)
            throw new ArgumentException();
        this.isFlagged = Attribute.IsDefined(enumType, typeof(FlagsAttribute));
        this.overridesToOriginals = overridesToOriginals ?? throw new ArgumentNullException(nameof(overridesToOriginals));
        this.typeConverter = TypeDescriptor.GetConverter(this.enumType);
    }
    
    protected override string? GetValueFromBindingContext(ValueProviderResult valueProviderResult) => 
        EnumExtensions.ReplaceRenamedEnumValuesToOriginals(base.GetValueFromBindingContext(valueProviderResult), isFlagged, overridesToOriginals);

    protected override void CheckModel(ModelBindingContext bindingContext, ValueProviderResult valueProviderResult, object? model)
    {
        if (model == null)
        {
            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)
    {
        // Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/EnumTypeModelBinder.cs#L58
        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)
        {
            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);
    }
}

public class ExtensibleSimpleTypeModelBinder : IModelBinder
{
    // Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/SimpleTypeModelBinder.cs
    private readonly TypeConverter _typeConverter;
    private readonly ILogger _logger;

    public ExtensibleSimpleTypeModelBinder(Type type, ILoggerFactory loggerFactory) : this(type, loggerFactory, null) { }
    
    public ExtensibleSimpleTypeModelBinder(Type type, ILoggerFactory loggerFactory, TypeConverter? typeConverter)
    {
        if (type == null)
            throw new ArgumentNullException(nameof(type));
        if (loggerFactory == null)
            throw new ArgumentNullException(nameof(loggerFactory));
        _typeConverter = typeConverter ?? TypeDescriptor.GetConverter(type);
        _logger = loggerFactory.CreateLogger<ExtensibleSimpleTypeModelBinder>();
    }

    protected virtual string? GetValueFromBindingContext(ValueProviderResult valueProviderResult) => valueProviderResult.FirstValue;

    /// <inheritdoc />
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
            throw new ArgumentNullException(nameof(bindingContext));

        //_logger.AttemptingToBindModel(bindingContext);

        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult == ValueProviderResult.None)
        {
            //_logger.FoundNoValueInRequest(bindingContext);
            // no entry
            //_logger.DoneAttemptingToBindModel(bindingContext);
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

        try
        {
            var value = GetValueFromBindingContext(valueProviderResult);

            object? model;
            if (bindingContext.ModelType == typeof(string))
            {
                // Already have a string. No further conversion required but handle ConvertEmptyStringToNull.
                if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && string.IsNullOrWhiteSpace(value))
                    model = null;
                else
                    model = value;
            }
            else if (string.IsNullOrWhiteSpace(value))
            {
                // Other than the StringConverter, converters Trim() the value then throw if the result is empty.
                model = null;
            }
            else
            {
                model = _typeConverter.ConvertFrom(context: null,culture: valueProviderResult.Culture, value: value);
            }

            CheckModel(bindingContext, valueProviderResult, model);

            //_logger.DoneAttemptingToBindModel(bindingContext);
            return Task.CompletedTask;
        }
        catch (Exception exception)
        {
            var isFormatException = exception is FormatException;
            if (!isFormatException && exception.InnerException != null)
            {
                // TypeConverter throws System.Exception wrapping the FormatException,
                // so we capture the inner exception.
                exception = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
            }

            bindingContext.ModelState.TryAddModelError(bindingContext.ModelName,exception, bindingContext.ModelMetadata);

            // Were able to find a converter for the type but conversion failed.
            return Task.CompletedTask;
        }
    }

    /// <inheritdoc/>
    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);
        }
    }
}

// End code for enum model binding

/********************************************************/
// Begin general enum parsing code

public class CharMemoryComparer : IEqualityComparer<ReadOnlyMemory<char>>
{
    public static CharMemoryComparer OrdinalIgnoreCase { get; } = new CharMemoryComparer(StringComparison.OrdinalIgnoreCase);
    public static CharMemoryComparer Ordinal { get; }  = new CharMemoryComparer(StringComparison.Ordinal);

    readonly StringComparison comparison;
    CharMemoryComparer(StringComparison comparison) => this.comparison = comparison;
    public bool Equals(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y) => MemoryExtensions.Equals(x.Span, y.Span, comparison);
    public int GetHashCode(ReadOnlyMemory<char> obj) => String.GetHashCode(obj.Span, comparison);
}

public static partial class EnumExtensions
{
    public const char FlagSeparatorChar = ',';
    public const string FlagSeparatorString = ", ";
    
    public static bool TryGetEnumMemberOverridesToOriginals(Type enumType, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out Dictionary<ReadOnlyMemory<char>, string>? overridesToOriginals)
    {
        if (enumType == null)
            throw new ArgumentNullException(nameof(enumType));
        if (!enumType.IsEnum)
            throw new ArgumentException(nameof(enumType));
        overridesToOriginals = null;
        foreach (var name in Enum.GetNames(enumType))
        {
            if (TryGetEnumAttribute<EnumMemberAttribute>(enumType, name, out var attr) && !string.IsNullOrWhiteSpace(attr.Value))
            {
                overridesToOriginals = overridesToOriginals ?? new(CharMemoryComparer.OrdinalIgnoreCase);
                overridesToOriginals.Add(attr.Value.AsMemory(), name);
            }
        }
        return overridesToOriginals != null;
    }
    
    public static bool TryGetEnumAttribute<TAttribute>(Type type, string name, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out TAttribute? attribute) where TAttribute : System.Attribute
    {
        var member = type.GetMember(name).SingleOrDefault();
        attribute = member?.GetCustomAttribute<TAttribute>(false);
        return attribute != null;
    }
    
    public static string? ReplaceRenamedEnumValuesToOriginals(string? value, bool isFlagged, Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals)
    {
        if (string.IsNullOrWhiteSpace(value))
            return value;
        var trimmed = value.AsMemory().Trim();
        if (overridesToOriginals.TryGetValue(trimmed, out var @override))
            value = @override;
        else if (isFlagged && trimmed.Length > 0)
        {
            var sb = new StringBuilder();
            bool replaced = false;
            foreach (var n in trimmed.Split(EnumExtensions.FlagSeparatorChar, StringSplitOptions.TrimEntries))
            {
                ReadOnlySpan<char> toAppend;
                if (overridesToOriginals.TryGetValue(n, out var @thisOverride))
                {
                    toAppend = thisOverride.AsSpan();
                    replaced = true;
                }
                else
                    toAppend = n.Span;
                sb.Append(sb.Length == 0 ? null : EnumExtensions.FlagSeparatorString).Append(toAppend);
            }
            if (replaced)
                value = sb.ToString();
        }
        return value;
    }
}

public static class StringExtensions
{
    public static IEnumerable<ReadOnlyMemory<char>> Split(this ReadOnlyMemory<char> chars, char separator, StringSplitOptions options = StringSplitOptions.None)
    {
        int index;
        while ((index = chars.Span.IndexOf(separator)) >= 0)
        {
            var slice = chars.Slice(0, index);
            if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
                slice = slice.Trim();
            if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || slice.Length > 0)
                yield return slice;
            chars = chars.Slice(index + 1);
        }
        if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
            chars = chars.Trim();
        if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || chars.Length > 0)
            yield return chars;
    }
}

然后在 configureservices()中添加粘合剂, so:so:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
                            {
                                options.ModelBinderProviders.Insert(0, new EnumMemberEnumTypeModelBinderProvider(options));
                            });
}       

notes:

  • enumtypemodelbinder 和基类 SimpleTypemodelbinder 没有提供有用的扩展点来自定义输入值字符串的解析,因此有必要复制一些逻辑。


  • 精确模拟 simperepepemodelbinder 的逻辑有些困难,因为它支持数字和文本枚举值 - 包括两个枚举的混合物。上面的活页夹保留了该功能,但还允许成功绑定原始枚举名称。因此,值 on-hold onhold 将绑定到 status.onhold

  • 相反,如果您不想支持枚举数字值的绑定,则可以调整 jsonenummberstringenumconverter 来自此答案 to system.text.json:如何为枚举值指定自定义名称? 。演示小提琴在这里。这种方法还避免了与原始的,未悔的枚举名称结合。

  • 将覆盖名称与原始枚举名称匹配是对情况不敏感的,因此不支持仅支持的覆盖名称。

Following the documentation guide Custom Model Binding in ASP.NET Core, you can create your own versions of Microsoft's classes EnumTypeModelBinderProvider, EnumTypeModelBinder (and base class SimpleTypeModelBinder) that replace incoming enum value names that have been renamed via EnumMemberAttribute with the original enum names before binding:

// Begin code for enum model binding
public class EnumMemberEnumTypeModelBinderProvider : IModelBinderProvider 
{
    public EnumMemberEnumTypeModelBinderProvider(MvcOptions options) { }

    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));
        
        if (context.Metadata.IsEnum)
        {
            var enumType = context.Metadata.UnderlyingOrModelType;
            Debug.Assert(enumType.IsEnum);
            var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
            if (EnumExtensions.TryGetEnumMemberOverridesToOriginals(enumType, out var overridesToOriginals))
                return new EnumMemberEnumTypeModelBinder(suppressBindingUndefinedValueToEnumType: true, enumType, loggerFactory, overridesToOriginals);
        }
        
        return null;
    }
}

public class EnumMemberEnumTypeModelBinder : ExtensibleSimpleTypeModelBinder
{
    // Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/EnumTypeModelBinder.cs#L58
    readonly Type enumType;
    readonly bool isFlagged;
    readonly Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals;
    readonly TypeConverter typeConverter;

    public EnumMemberEnumTypeModelBinder(bool suppressBindingUndefinedValueToEnumType, Type modelType, ILoggerFactory loggerFactory, Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals) : base(modelType, loggerFactory)
    {
        this.enumType = Nullable.GetUnderlyingType(modelType) ?? modelType;
        if (!this.enumType.IsEnum)
            throw new ArgumentException();
        this.isFlagged = Attribute.IsDefined(enumType, typeof(FlagsAttribute));
        this.overridesToOriginals = overridesToOriginals ?? throw new ArgumentNullException(nameof(overridesToOriginals));
        this.typeConverter = TypeDescriptor.GetConverter(this.enumType);
    }
    
    protected override string? GetValueFromBindingContext(ValueProviderResult valueProviderResult) => 
        EnumExtensions.ReplaceRenamedEnumValuesToOriginals(base.GetValueFromBindingContext(valueProviderResult), isFlagged, overridesToOriginals);

    protected override void CheckModel(ModelBindingContext bindingContext, ValueProviderResult valueProviderResult, object? model)
    {
        if (model == null)
        {
            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)
    {
        // Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/EnumTypeModelBinder.cs#L58
        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)
        {
            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);
    }
}

public class ExtensibleSimpleTypeModelBinder : IModelBinder
{
    // Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/SimpleTypeModelBinder.cs
    private readonly TypeConverter _typeConverter;
    private readonly ILogger _logger;

    public ExtensibleSimpleTypeModelBinder(Type type, ILoggerFactory loggerFactory) : this(type, loggerFactory, null) { }
    
    public ExtensibleSimpleTypeModelBinder(Type type, ILoggerFactory loggerFactory, TypeConverter? typeConverter)
    {
        if (type == null)
            throw new ArgumentNullException(nameof(type));
        if (loggerFactory == null)
            throw new ArgumentNullException(nameof(loggerFactory));
        _typeConverter = typeConverter ?? TypeDescriptor.GetConverter(type);
        _logger = loggerFactory.CreateLogger<ExtensibleSimpleTypeModelBinder>();
    }

    protected virtual string? GetValueFromBindingContext(ValueProviderResult valueProviderResult) => valueProviderResult.FirstValue;

    /// <inheritdoc />
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
            throw new ArgumentNullException(nameof(bindingContext));

        //_logger.AttemptingToBindModel(bindingContext);

        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult == ValueProviderResult.None)
        {
            //_logger.FoundNoValueInRequest(bindingContext);
            // no entry
            //_logger.DoneAttemptingToBindModel(bindingContext);
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

        try
        {
            var value = GetValueFromBindingContext(valueProviderResult);

            object? model;
            if (bindingContext.ModelType == typeof(string))
            {
                // Already have a string. No further conversion required but handle ConvertEmptyStringToNull.
                if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && string.IsNullOrWhiteSpace(value))
                    model = null;
                else
                    model = value;
            }
            else if (string.IsNullOrWhiteSpace(value))
            {
                // Other than the StringConverter, converters Trim() the value then throw if the result is empty.
                model = null;
            }
            else
            {
                model = _typeConverter.ConvertFrom(context: null,culture: valueProviderResult.Culture, value: value);
            }

            CheckModel(bindingContext, valueProviderResult, model);

            //_logger.DoneAttemptingToBindModel(bindingContext);
            return Task.CompletedTask;
        }
        catch (Exception exception)
        {
            var isFormatException = exception is FormatException;
            if (!isFormatException && exception.InnerException != null)
            {
                // TypeConverter throws System.Exception wrapping the FormatException,
                // so we capture the inner exception.
                exception = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
            }

            bindingContext.ModelState.TryAddModelError(bindingContext.ModelName,exception, bindingContext.ModelMetadata);

            // Were able to find a converter for the type but conversion failed.
            return Task.CompletedTask;
        }
    }

    /// <inheritdoc/>
    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);
        }
    }
}

// End code for enum model binding

/********************************************************/
// Begin general enum parsing code

public class CharMemoryComparer : IEqualityComparer<ReadOnlyMemory<char>>
{
    public static CharMemoryComparer OrdinalIgnoreCase { get; } = new CharMemoryComparer(StringComparison.OrdinalIgnoreCase);
    public static CharMemoryComparer Ordinal { get; }  = new CharMemoryComparer(StringComparison.Ordinal);

    readonly StringComparison comparison;
    CharMemoryComparer(StringComparison comparison) => this.comparison = comparison;
    public bool Equals(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y) => MemoryExtensions.Equals(x.Span, y.Span, comparison);
    public int GetHashCode(ReadOnlyMemory<char> obj) => String.GetHashCode(obj.Span, comparison);
}

public static partial class EnumExtensions
{
    public const char FlagSeparatorChar = ',';
    public const string FlagSeparatorString = ", ";
    
    public static bool TryGetEnumMemberOverridesToOriginals(Type enumType, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out Dictionary<ReadOnlyMemory<char>, string>? overridesToOriginals)
    {
        if (enumType == null)
            throw new ArgumentNullException(nameof(enumType));
        if (!enumType.IsEnum)
            throw new ArgumentException(nameof(enumType));
        overridesToOriginals = null;
        foreach (var name in Enum.GetNames(enumType))
        {
            if (TryGetEnumAttribute<EnumMemberAttribute>(enumType, name, out var attr) && !string.IsNullOrWhiteSpace(attr.Value))
            {
                overridesToOriginals = overridesToOriginals ?? new(CharMemoryComparer.OrdinalIgnoreCase);
                overridesToOriginals.Add(attr.Value.AsMemory(), name);
            }
        }
        return overridesToOriginals != null;
    }
    
    public static bool TryGetEnumAttribute<TAttribute>(Type type, string name, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out TAttribute? attribute) where TAttribute : System.Attribute
    {
        var member = type.GetMember(name).SingleOrDefault();
        attribute = member?.GetCustomAttribute<TAttribute>(false);
        return attribute != null;
    }
    
    public static string? ReplaceRenamedEnumValuesToOriginals(string? value, bool isFlagged, Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals)
    {
        if (string.IsNullOrWhiteSpace(value))
            return value;
        var trimmed = value.AsMemory().Trim();
        if (overridesToOriginals.TryGetValue(trimmed, out var @override))
            value = @override;
        else if (isFlagged && trimmed.Length > 0)
        {
            var sb = new StringBuilder();
            bool replaced = false;
            foreach (var n in trimmed.Split(EnumExtensions.FlagSeparatorChar, StringSplitOptions.TrimEntries))
            {
                ReadOnlySpan<char> toAppend;
                if (overridesToOriginals.TryGetValue(n, out var @thisOverride))
                {
                    toAppend = thisOverride.AsSpan();
                    replaced = true;
                }
                else
                    toAppend = n.Span;
                sb.Append(sb.Length == 0 ? null : EnumExtensions.FlagSeparatorString).Append(toAppend);
            }
            if (replaced)
                value = sb.ToString();
        }
        return value;
    }
}

public static class StringExtensions
{
    public static IEnumerable<ReadOnlyMemory<char>> Split(this ReadOnlyMemory<char> chars, char separator, StringSplitOptions options = StringSplitOptions.None)
    {
        int index;
        while ((index = chars.Span.IndexOf(separator)) >= 0)
        {
            var slice = chars.Slice(0, index);
            if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
                slice = slice.Trim();
            if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || slice.Length > 0)
                yield return slice;
            chars = chars.Slice(index + 1);
        }
        if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
            chars = chars.Trim();
        if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || chars.Length > 0)
            yield return chars;
    }
}

Then add the binder in ConfigureServices() like so:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
                            {
                                options.ModelBinderProviders.Insert(0, new EnumMemberEnumTypeModelBinderProvider(options));
                            });
}       

Notes:

  • EnumTypeModelBinder and base class SimpleTypeModelBinder provide no useful extension points to customize the parsing of the incoming value string, thus it was necessary to copy some of their logic.

  • Precisely emulating the logic of SimpleTypeModelBinder is somewhat difficult because it supports both numeric and textual enum values -- including mixtures of both for flags enums. The binder above retains that capability, but at a cost of also allowing original enum names to be bound successfully. Thus the values on-hold and onhold will be bound to Status.OnHold.

  • Conversely, if you do not want to support binding of numeric values for enums, you could adapt the code of JsonEnumMemberStringEnumConverter from this answer to System.Text.Json: How do I specify a custom name for an enum value?. Demo fiddle here. This approach also avoids binding to the original, unrenamed enum names.

  • Matching of override names with original enum names is case-insensitive, so override names that differ only in case are not supported.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文