Skip to content

Instantly share code, notes, and snippets.

@mburbea
Last active September 4, 2017 18:07
Show Gist options
  • Save mburbea/31d5ab04e16d80cd3b6e to your computer and use it in GitHub Desktop.
Save mburbea/31d5ab04e16d80cd3b6e to your computer and use it in GitHub Desktop.
FlagEnumConverter
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