using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
public class EnumMemberModelBinderProvider : RenamableEnumModelBinderProvider
public EnumMemberModelBinderProvider(MvcOptions options) : base(options) { }
public EnumMemberModelBinderProvider(MvcOptions options, JsonNamingPolicy? namingPolicy, bool allowIntegerValues) : base(options, namingPolicy, allowIntegerValues) { }
protected override bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName)
if (EnumExtensions.TryGetEnumAttribute<System.Runtime.Serialization.EnumMemberAttribute>(enumType, name, out var attr) && attr.Value != null)
overrideName = attr.Value.AsMemory();
return base.TryOverrideName(enumType, name, out overrideName);
public class RenamableEnumModelBinderProvider : IModelBinderProvider
readonly JsonNamingPolicy? namingPolicy;
readonly bool allowIntegerValues;
public RenamableEnumModelBinderProvider(MvcOptions options) : this(options, null, true) { }
public RenamableEnumModelBinderProvider(MvcOptions options, JsonNamingPolicy? namingPolicy, bool allowIntegerValues) =>
(this.namingPolicy, this.allowIntegerValues) = (namingPolicy, allowIntegerValues);
public IModelBinder? GetBinder(ModelBinderProviderContext context)
throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsEnum)
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var binderType = typeof(RenamableEnumModelBinder<>).MakeGenericType(context.Metadata.UnderlyingOrModelType);
TryOverrideName tryOverrideName = (Type t, string n, out ReadOnlyMemory<char> o) => TryOverrideName(t, n, out o);
return (IModelBinder)Activator.CreateInstance(binderType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
args: new object[] { loggerFactory, namingPolicy!, allowIntegerValues, tryOverrideName },
protected virtual bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName)
public class RenamableEnumModelBinder<TEnum> : SimpleModelBinderBase where TEnum : struct, Enum
bool AllowNumbers { get; }
EnumTypeData<TEnum> EnumTypeData { get; }
public RenamableEnumModelBinder(ILoggerFactory loggerFactory, JsonNamingPolicy? namingPolicy, bool allowNumbers, TryOverrideName? tryOverrideName) : base(loggerFactory)
this.AllowNumbers = allowNumbers;
this.EnumTypeData = EnumTypeData<TEnum>.Create(namingPolicy, tryOverrideName);
protected override object? ConvertModelFromString(ModelBindingContext bindingContext, string? value)
if (string.IsNullOrWhiteSpace(value))
if (EnumTypeData.TryLookup(value.AsMemory(), out result))
if (AllowNumbers && EnumExtensions.TryParseEnumFromNumber<TEnum>(value, out result))
throw new FormatException(string.Format("Unknown value {0} for enum {1}", value, nameof(TEnum)));
public abstract class SimpleModelBinderBase : IModelBinder
readonly ILogger _logger;
public SimpleModelBinderBase(ILoggerFactory loggerFactory) => _logger = (loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory))).CreateLogger<SimpleModelBinderBase>();
public Task BindModelAsync(ModelBindingContext bindingContext)
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
return Task.CompletedTask;
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
object? model = ConvertModelFromString(bindingContext, value);
CheckAndSaveModel(bindingContext, valueProviderResult, model);
return Task.CompletedTask;
catch (Exception exception)
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, exception, bindingContext.ModelMetadata);
return Task.CompletedTask;
protected abstract object? ConvertModelFromString(ModelBindingContext bindingContext, string? value);
protected virtual void CheckAndSaveModel(ModelBindingContext bindingContext, ValueProviderResult valueProviderResult, object? model)
if (model == null && !bindingContext.ModelMetadata.IsReferenceOrNullableType)
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
bindingContext.Result = ModelBindingResult.Success(model);
public static partial class EnumExtensions
public static bool TryParseEnumFromNumber<TEnum>(string s, out TEnum value) where TEnum : struct, Enum =>
TryParseEnumFromNumber(s, Type.GetTypeCode(typeof(TEnum)), out value);
internal static bool TryParseEnumFromNumber<TEnum>(string s, TypeCode enumTypeCode, out TEnum value) where TEnum : struct, Enum
Debug.Assert(enumTypeCode == Type.GetTypeCode(typeof(TEnum)));
switch (Type.GetTypeCode(typeof(TEnum)))
if (SByte.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i))
value = Unsafe.As<SByte, TEnum>(ref i);
if (Int16.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i))
value = Unsafe.As<Int16, TEnum>(ref i);
if (Int32.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i))
value = Unsafe.As<Int32, TEnum>(ref i);
if (Int64.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i))
value = Unsafe.As<Int64, TEnum>(ref i);
if (Byte.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i))
value = Unsafe.As<Byte, TEnum>(ref i);
if (UInt16.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i))
value = Unsafe.As<UInt16, TEnum>(ref i);
if (UInt32.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i))
value = Unsafe.As<UInt32, TEnum>(ref i);
if (UInt64.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i))
value = Unsafe.As<UInt64, TEnum>(ref i);
throw new JsonException();
public delegate bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName);
public class EnumTypeData<TEnum> where TEnum : struct, Enum
readonly record struct EnumData(ReadOnlyMemory<char> name, TEnum value, UInt64 UInt64Value);
internal static TypeCode EnumTypeCode { get; } = Type.GetTypeCode(typeof(TEnum));
static Func<EnumData, UInt64, int> EntryComparer { get; } = (item, key) => item.UInt64Value.CompareTo(key);
static bool Flagged { get; } = typeof(TEnum).IsDefined(typeof(FlagsAttribute), false);
private EnumData [] EnumDataArray { get; }
private ILookup<ReadOnlyMemory<char>, int> NameLookup { get; }
private EnumTypeData(EnumData [] enumDataArray, ILookup<ReadOnlyMemory<char>, int> nameLookup) =>
(this.EnumDataArray, this.NameLookup) = (enumDataArray ?? throw new ArgumentNullException(nameof(enumDataArray)), nameLookup ?? throw new ArgumentNullException(nameof(nameLookup)));
public static EnumTypeData<TEnum> Create(JsonNamingPolicy? namingPolicy, TryOverrideName? tryOverrideName)
var enumData = GetEnumData(namingPolicy, tryOverrideName);
return new EnumTypeData<TEnum>(enumData, GetLookupTable(enumData));
static EnumData [] GetEnumData(JsonNamingPolicy? namingPolicy, TryOverrideName? tryOverrideName)
var names = Enum.GetNames<TEnum>();
var values = Enum.GetValues<TEnum>();
return names.Zip(values, (n, v) =>
if (tryOverrideName == null || !tryOverrideName(typeof(TEnum), n, out var jsonName))
jsonName = (namingPolicy == null ? n.AsMemory() : namingPolicy.ConvertName(n).AsMemory());
return new EnumData(jsonName, v, v.ToUInt64(EnumTypeCode));
static int FindEnumDataIndex(EnumData [] enumData, TEnum value, out UInt64 UInt64value)
UInt64value = value.ToUInt64(EnumTypeCode);
return enumData.BinarySearchFirst(UInt64value, EntryComparer);
static ILookup<ReadOnlyMemory<char>, int> GetLookupTable(EnumData [] namesAndValues) =>
Enumerable.Range(0, namesAndValues.Length).ToLookup(i => namesAndValues[i].name, CharMemoryComparer.OrdinalIgnoreCase);
public bool TryFormatAsString(TEnum value, out ReadOnlyMemory<char> name)
return TryFormatSimpleAsString(value, out name);
return TryFormatFlaggedAsString(value, out name);
bool TryFormatSimpleAsString(TEnum value, out ReadOnlyMemory<char> name)
var index = FindEnumDataIndex(EnumDataArray, value, out var _);
name = EnumDataArray[index].name;
bool TryFormatFlaggedAsString(TEnum value, out ReadOnlyMemory<char> name)
var index = FindEnumDataIndex(EnumDataArray, value, out var UInt64Value);
name = EnumDataArray[index].name;
StringBuilder? sb = null;
for (int i = (~index) - 1; i >= 0; i--)
if ((UInt64Value & EnumDataArray[i].UInt64Value) == EnumDataArray[i].UInt64Value && EnumDataArray[i].UInt64Value != 0)
sb = new StringBuilder();
sb.Append(EnumDataArray[i].name.Span);
sb.Insert(0, EnumExtensions.FlagSeparatorString);
sb.Insert(0, EnumDataArray[i].name.Span);
UInt64Value -= EnumDataArray[i].UInt64Value;
if (UInt64Value == 0 && sb != null)
name = sb.ToString().AsMemory();
public bool TryLookup(ReadOnlyMemory<char> name, out TEnum value)
return TryLookupSimple(name, out value);
return TryLookupFlagged(name, out value);
bool TryLookupSimple(ReadOnlyMemory<char> name, out TEnum value)
foreach (var index in NameLookup[name])
if (i == 1 && MemoryExtensions.Equals(EnumDataArray[firstMatch].name.Span, name.Span, StringComparison.Ordinal))
value = EnumDataArray[firstMatch].value;
if (MemoryExtensions.Equals(EnumDataArray[index].name.Span, name.Span, StringComparison.Ordinal))
value = EnumDataArray[index].value;
value = (firstMatch == -1 ? default : EnumDataArray[firstMatch].value);
bool TryLookupFlagged(ReadOnlyMemory<char> name, out TEnum value)
foreach (var slice in name.Split(EnumExtensions.FlagSeparatorChar, StringSplitOptions.TrimEntries))
if (TryLookupSimple(slice, out var thisValue))
UInt64Value |= thisValue.ToUInt64();
value = UInt64Value.ToEnum<TEnum>(EnumTypeCode);
public static partial class EnumExtensions
public const char FlagSeparatorChar = ',';
public const string FlagSeparatorString = ", ";
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 UInt64 ToUInt64<TEnum>(this TEnum value) where TEnum : struct, Enum => value.ToUInt64(EnumTypeData<TEnum>.EnumTypeCode);
internal static UInt64 ToUInt64<TEnum>(this TEnum value, TypeCode enumTypeCode) where TEnum : struct, Enum
Debug.Assert(enumTypeCode == Type.GetTypeCode(typeof(TEnum)));
return enumTypeCode switch
TypeCode.SByte => unchecked((ulong)Unsafe.As<TEnum, SByte>(ref value)),
TypeCode.Int16 => unchecked((ulong)Unsafe.As<TEnum, Int16>(ref value)),
TypeCode.Int32 => unchecked((ulong)Unsafe.As<TEnum, Int32>(ref value)),
TypeCode.Int64 => unchecked((ulong)Unsafe.As<TEnum, Int64>(ref value)),
TypeCode.Byte => Unsafe.As<TEnum, Byte>(ref value),
TypeCode.UInt16 => Unsafe.As<TEnum, UInt16>(ref value),
TypeCode.UInt32 => Unsafe.As<TEnum, UInt32>(ref value),
TypeCode.UInt64 => Unsafe.As<TEnum, UInt64>(ref value),
_ => throw new ArgumentException(enumTypeCode.ToString()),
public static TEnum ToEnum<TEnum>(this UInt64 value) where TEnum : struct, Enum => value.ToEnum<TEnum>(EnumTypeData<TEnum>.EnumTypeCode);
internal static TEnum ToEnum<TEnum>(this UInt64 value, TypeCode enumTypeCode) where TEnum : struct, Enum
Debug.Assert(enumTypeCode == Type.GetTypeCode(typeof(TEnum)));
var i = unchecked((SByte)value);
return Unsafe.As<SByte, TEnum>(ref i);
var i = unchecked((Int16)value);
return Unsafe.As<Int16, TEnum>(ref i);
var i = unchecked((Int32)value);
return Unsafe.As<Int32, TEnum>(ref i);
var i = unchecked((Int64)value);
return Unsafe.As<Int64, TEnum>(ref i);
var i = unchecked((Byte)value);
return Unsafe.As<Byte, TEnum>(ref i);
var i = unchecked((UInt16)value);
return Unsafe.As<UInt16, TEnum>(ref i);
var i = unchecked((UInt32)value);
return Unsafe.As<UInt32, TEnum>(ref i);
var i = unchecked((UInt64)value);
return Unsafe.As<UInt64, TEnum>(ref i);
throw new ArgumentException(enumTypeCode.ToString());
public static class StringExtensions
public static IEnumerable<ReadOnlyMemory<char>> Split(this ReadOnlyMemory<char> chars, char separator, StringSplitOptions options = StringSplitOptions.None)
while ((index = chars.Span.IndexOf(separator)) >= 0)
var slice = chars.Slice(0, index);
if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || slice.Length > 0)
chars = chars.Slice(index + 1);
if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || chars.Length > 0)
public static class ListExtensions
public static int BinarySearch<TValue, TKey>(this TValue [] list, TKey key, Func<TValue, TKey, int> comparer)
if (list == null || comparer == null)
throw new ArgumentNullException();
int high = list.Length - 1;
var mid = low + ((high - low) >> 1);
var order = comparer(list[mid], key);
public static int BinarySearchFirst<TValue, TKey>(this TValue [] list, TKey key, Func<TValue, TKey, int> comparer)
int index = list.BinarySearch(key, comparer);
for (; index > 0 && comparer(list[index-1], key) == 0; index--)
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 enum EnumMemberExample {
[EnumMember(Value = "Trick-Or-Treat")]
PFourteen = ((long)1 << 14),
PThirtyOne = ((long)1 << 31),
PThirtyTwo = ((long)1 << 32),
PSixtyTwo = ((long)1 << 62),
PSixtyThree = ((long)1 << 63),
public enum FlaggedWithCompositeShort : short
All = One | Two | Four | Eight,
public void ConfigureServices(IServiceCollection services)
services.AddControllers(options =>
options.ModelBinderProviders.Insert(0, new EnumMemberModelBinderProvider(options));
public static void Test()
static void TestEnumTypeConverter()
TestEnumTypeConverter<FlagLongEnum>("1,4");
TestEnumTypeConverter<FlaggedWithCompositeShort>("1,31000");
TestEnumTypeConverter<FlaggedWithCompositeShort>("1,-31000");
TestEnumTypeConverter<FlaggedWithCompositeShort>("Zero");
TestEnumTypeConverter<FlaggedWithCompositeShort>("One,Two,Four,8");
static void TestEnumTypeConverter<TEnum>(string value) where TEnum: struct, Enum
var typeConverter = TypeDescriptor.GetConverter(typeof(TEnum));
var model = (TEnum)typeConverter.ConvertFrom(value: value,
culture: CultureInfo.InvariantCulture)!;
Console.WriteLine("Input: {0}, Result: {1}, IsDefined: {2}", value, model, IsDefinedInEnum(model, typeof(TEnum)));
private static bool IsDefinedInEnum(object model, Type modelType)
if (Attribute.IsDefined(modelType, typeof(FlagsAttribute)))
var underlying = Convert.ChangeType(
Enum.GetUnderlyingType(modelType),
CultureInfo.InvariantCulture).ToString();
var converted = model.ToString();
return !string.Equals(underlying, converted, StringComparison.OrdinalIgnoreCase);
return Enum.IsDefined(modelType, model);
public static void Main()
Console.WriteLine("Environment version: {0} ({1}), {2}, 64bit = {3}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion, Environment.Is64BitOperatingSystem);
Console.WriteLine("Failed with unhandled exception: ");