Last active
September 4, 2017 18:07
-
-
Save mburbea/31d5ab04e16d80cd3b6e to your computer and use it in GitHub Desktop.
FlagEnumConverter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Newtonsoft.Json; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Collections.Concurrent; | |
using System.Globalization; | |
using System.Runtime.Serialization; | |
namespace Extensions | |
{ | |
public class FlagEnumConverter : JsonConverter | |
{ | |
/// <summary> | |
/// Simple struct to hold enum members. | |
/// </summary> | |
struct EnumMember | |
{ | |
public readonly ulong Value; | |
public readonly string FieldName; | |
public readonly string DisplayName; | |
public bool HasAttributeName { get { return FieldName != DisplayName; } } | |
public EnumMember(ulong value, string fieldName, string attributeName) | |
{ | |
Value = value; | |
FieldName = fieldName; | |
DisplayName = attributeName; | |
} | |
} | |
static readonly ConcurrentDictionary<Type, List<EnumMember>> EnumValueCache = new ConcurrentDictionary<Type, List<EnumMember>>(); | |
/// <summary> | |
/// Reinterpret the binary representation of the value as a ulong. E.g. (sbyte)-128 is 0x80. However, if you convert to long | |
/// and then ulong you are stuck with a large negative value. | |
/// </summary> | |
/// <param name="value"></param> | |
/// <param name="code"></param> | |
/// <returns></returns> | |
private static ulong ReinterpretValue(object value, TypeCode code) | |
{ | |
switch (code) | |
{ | |
case TypeCode.SByte: | |
// ReSharper disable RedundantCast | |
return (UInt64) (Byte) (SByte) value; | |
case TypeCode.Int16: | |
return (UInt64)(UInt16)(Int16)value; | |
case TypeCode.Int32: | |
return (UInt64) (UInt32) (Int32) value; | |
case TypeCode.Int64: | |
return (UInt64) (long) value; | |
case TypeCode.Byte: | |
case TypeCode.UInt16: | |
case TypeCode.UInt32: | |
case TypeCode.UInt64: | |
return Convert.ToUInt64(value, null); | |
// ReSharper restore RedundantCast | |
} | |
throw new InvalidOperationException(); | |
} | |
/// <summary> | |
/// Determines whether this instance can convert the specified object type. | |
/// </summary> | |
/// <param name="objectType">Type of the object.</param> | |
/// <returns> | |
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>. | |
/// </returns> | |
public override bool CanConvert(Type objectType) | |
{ | |
if (!objectType.IsValueType) return false; | |
var suspect = Nullable.GetUnderlyingType(objectType) ?? objectType; | |
return suspect.IsEnum; | |
} | |
static List<EnumMember> GetEnumMembers(Type type) | |
{ | |
return EnumValueCache.GetOrAdd(type, CreateEnumCache); | |
} | |
public bool CamelCaseText { get; set; } | |
private static List<EnumMember> CreateEnumCache(Type enumType) | |
{ | |
var typeCode = Type.GetTypeCode(enumType); | |
var enumCache = new List<EnumMember>(); | |
foreach (var field in enumType.GetFields()) | |
{ | |
if (!field.IsLiteral) continue; | |
ulong value = ReinterpretValue(field.GetValue(null),typeCode); | |
// we only care about desaturated flag values. | |
// combo values are less interesting to the client. | |
if (value != 0 && (value & (value - 1)) == 0) | |
{ | |
var attributeName = field | |
.GetCustomAttributes(typeof(EnumMemberAttribute), true) | |
.Cast<EnumMemberAttribute>() | |
.Select(a => a.Value) | |
.SingleOrDefault() ?? field.Name; | |
enumCache.Add(new EnumMember(value, field.Name, attributeName)); | |
} | |
} | |
return enumCache; | |
} | |
/// <summary> | |
/// Reads the JSON representation of the object. | |
/// </summary> | |
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param><param name="objectType">Type of the object.</param><param name="existingValue">The existing value of object being read.</param><param name="serializer">The calling serializer.</param> | |
/// <returns> | |
/// The object value. | |
/// </returns> | |
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |
{ | |
var enumType = Nullable.GetUnderlyingType(objectType); | |
var isNullable = enumType != null; | |
if (!isNullable) enumType = objectType; | |
var token = reader.TokenType; | |
if (token == JsonToken.Null) | |
{ | |
if (!isNullable) | |
{ | |
throw new JsonSerializationException(string.Format(CultureInfo.InvariantCulture, "Cannot convert null value to {0}", objectType)); | |
} | |
return null; | |
} | |
if (token == JsonToken.Integer) | |
{ | |
return Enum.ToObject(enumType, reader.Value); | |
} | |
var enumMembers = GetEnumMembers(enumType); | |
if (token == JsonToken.String) | |
{ | |
var fieldText = reader.Value.ToString().Trim(); | |
if (fieldText == string.Empty && isNullable) | |
{ | |
return null; | |
} | |
var result = enumMembers | |
.Where(f => f.DisplayName.Equals(fieldText, StringComparison.OrdinalIgnoreCase)) | |
.Select(t => t.FieldName) | |
.FirstOrDefault(); | |
if (result != null) return Enum.Parse(enumType, result, true); | |
} | |
else if (token == JsonToken.StartArray) | |
{ | |
var flags = serializer.Deserialize<string[]>(reader); | |
if (flags.Length == 0) return Enum.ToObject(enumType, 0); | |
var result = flags | |
.Join(enumMembers, f => f, p => p.DisplayName, (p, r) => r.Value, StringComparer.OrdinalIgnoreCase) | |
.Aggregate(0ul, (sum, v) => sum + v); | |
return Type.GetTypeCode(enumType) == TypeCode.UInt64 ? Enum.ToObject(enumType, result) : Enum.ToObject(enumType, unchecked((long)result)); | |
} | |
throw new JsonSerializationException(string.Format(CultureInfo.InvariantCulture, "Error converting value {0} to type '{1}'", reader.Value, objectType)); | |
} | |
static string ToCamelCase(string s) | |
{ | |
if (string.IsNullOrEmpty(s)) | |
return s; | |
if (!char.IsUpper(s[0])) | |
return s; | |
char[] chars = s.ToCharArray(); | |
for (int i = 0; i < chars.Length; i++) | |
{ | |
bool hasNext = (i + 1 < chars.Length); | |
if (i > 0 && hasNext && !char.IsUpper(chars[i + 1])) | |
break; | |
chars[i] = char.ToLower(chars[i], CultureInfo.InvariantCulture); | |
} | |
return new string(chars); | |
} | |
private string ConvertToDisplayName(EnumMember member) | |
{ | |
// always honor the attribute display name. | |
if (member.HasAttributeName) | |
{ | |
return member.DisplayName; | |
} | |
return CamelCaseText ? ToCamelCase(member.DisplayName) : member.DisplayName; | |
} | |
/// <summary> | |
/// Writes the JSON representation of the object. | |
/// </summary> | |
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param> | |
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |
{ | |
if (value == null) | |
{ | |
writer.WriteNull(); | |
return; | |
} | |
var e = (Enum)value; | |
var enumName = e.ToString("G"); | |
if (char.IsNumber(enumName[0]) || enumName[0] == '-') | |
{ | |
writer.WriteValue(value); | |
return; | |
} | |
var enumType = value.GetType(); | |
if (enumType.IsGenericType) | |
{ | |
enumType = Nullable.GetUnderlyingType(enumType); | |
} | |
var flags = GetEnumMembers(enumType); | |
var uv = ReinterpretValue(value, Type.GetTypeCode(enumType)); | |
var retVal = (uv == 0ul) ? | |
Enumerable.Empty<string>() : | |
flags | |
.Where(f => (uv & f.Value) == f.Value) | |
.Select(ConvertToDisplayName); | |
serializer.Serialize(writer, retVal); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment