DapperLib / Dapper

Dapper - a simple object mapper for .Net
https://www.learndapper.com/
Other
17.51k stars 3.68k forks source link

Custom type handlers for enums are ignored #259

Open chilversc opened 9 years ago

chilversc commented 9 years ago

I have an enum that is serialized to the DB using a single letter code. I tried adding a custom type handler for the enum to handle the conversion but dapper seems to ignore the custom type handler.

Given the example code I expected to see the values; ID = 10, Type = B ID = 1, Type = Foo

What I actually got was: ID = 1, Type = 2 DataException, Error parsing column 1 (Type=F - String)


void Main()
{
    SqlMapper.AddTypeHandler(new ItemTypeHandler());
    using (var connection = new SqlConnection("Server=(local); Database=tempdb; Integrated Security=SSPI")) {
        connection.Open();
        connection.Query ("SELECT @ID AS ID, @Type AS Type", new Item {ID = 10, Type = ItemType.Bar}).Dump();
        connection.Query<Item> ("SELECT 1 AS ID, 'F' AS Type").Dump();
    }
}

class ItemTypeHandler : SqlMapper.TypeHandler<ItemType>
{
    public override ItemType Parse(object value)
    {
        var c = ((string) value) [0];
        switch (c) {
            case 'F': return ItemType.Foo;
            case 'B': return ItemType.Bar;
            default: throw new ArgumentOutOfRangeException();
        }
    }

    public override void SetValue(IDbDataParameter p, ItemType value)
    {
        p.DbType = DbType.AnsiStringFixedLength;
        p.Size = 1;

        if (value == ItemType.Foo)
            p.Value = "F";
        else if (value == ItemType.Bar)
            p.Value = "B";
        else
            throw new ArgumentOutOfRangeException();
    }
}

class Item
{
    public long ID { get; set; }
    public ItemType Type { get; set; }
}

enum ItemType
{
    Foo = 1,
    Bar = 2,
}
agentGTAM commented 9 months ago

@mgravell Dude, it's 2024, how many more years will this take? I get that you hesitate, that you are wary, cautious and all of that good stuff but at some point it becomes something else.

bkothe commented 9 months ago

I, too, have just spent hours trying to make this work and come to find it's been an issue for near 9 years. Any movement on this?

thefex commented 6 months ago

I use ugly but generic workaround here as follows, just make sure you bind DB model into "MessageType" (property):

Data model class:

public ChatMessageType? ChatMessageType { get; set; }
// HACK, WORKAROUND -> Dapper Enum Limitations ... https://github.com/DapperLib/Dapper/issues/259 
[JsonIgnore] 
public string MessageType
{
    get => ChatMessageType.GetDescription();
    set => ChatMessageType = EnumExtensions<ChatMessageType>.BuildUsingDescription(value);
} 
 internal static class EnumExtensions
  {
      private static readonly ConcurrentDictionary<Enum, string> CachedDescriptionMap = new ConcurrentDictionary<Enum, string>();
      public static string GetDescription(this Enum value)
      {
          // Reflection will be called only N * Enum Values Count times - important as this might be called pretty often via Dapper/SQL mapping...
          if (CachedDescriptionMap.ContainsKey(value))
              return CachedDescriptionMap[value];

          Type type = value.GetType();
          string name = Enum.GetName(type, value);
          if (name != null)
          {
              FieldInfo field = type.GetField(name);
              if (field != null)
              {
                  if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attr)
                  {
                      CachedDescriptionMap.TryAdd(value, attr.Description);
                      return attr.Description;
                  }
              }
          }

          throw new InvalidOperationException("There is no description for " + value.ToString() + "," + type); 
      }

public static class EnumExtensions<TEnum> where TEnum : struct, Enum
{
    private static readonly ConcurrentDictionary<string, TEnum?> CachedDescriptionToEnumMap = new ConcurrentDictionary<string, TEnum?>();
    public static TEnum BuildUsingDescription(string enumDescription)
    {
        // Reflection will be called only N * Enum Values Count times - important as this might be called pretty often via Dapper/SQL mapping...
        if (CachedDescriptionToEnumMap.ContainsKey(enumDescription))
        {
            var cachedEnum = CachedDescriptionToEnumMap[enumDescription];

            // even if throws exception... make sure it's cached
            if (!cachedEnum.HasValue)
                throw new InvalidOperationException(enumDescription + " - " + typeof(TEnum) +
                                                    " - this enum does not have following value: " + enumDescription);
            else
                return cachedEnum.Value;
        }

        Type enumType = typeof(TEnum);

        var enumFields = enumType.GetFields(BindingFlags.Static | BindingFlags.Public);
        var enumDescriptionValueMap = enumFields.ToDictionary(x => x.GetCustomAttribute<DescriptionAttribute>()?.Description,
            x => x.Name);

        if (!enumDescriptionValueMap.Keys.Contains(enumDescription))
        {
            CachedDescriptionToEnumMap.TryAdd(enumDescription, null);
            throw new InvalidOperationException(enumDescription + " - " + typeof(TEnum) + " - this enum does not have following value: " + enumDescription);
        }

        var enumValueName = enumDescriptionValueMap[enumDescription];

        try
        {
            var parsedEnum = (TEnum)Enum.Parse(typeof(TEnum), enumValueName);
            CachedDescriptionToEnumMap.TryAdd(enumDescription, parsedEnum);
            return parsedEnum;
        }
        catch (Exception)
        {
            CachedDescriptionToEnumMap.TryAdd(enumDescription, null);
            throw;
        }
    } 
}

sample enum:

public enum ChatMessageType
{
    [Description("my_message")]
    MyMessage,
    [Description("other_message")]
    Other,
    [Description("system_message")]
    System
}
phillip-haydon commented 3 months ago

Yearly update:

Project still appears dead. Issue is now 9 years old with no resolution in sight.

djimmo commented 1 month ago

Maybe this helps. I combined what I found online into something that works

https://gist.github.com/djimmo/b54a1643f15bd4e54303992ddce4a968