Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom type handlers for enums are ignored #458

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Dapper.Tests/Tests.Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ public void TestEnumParamsWithoutNullable()
obj.B.IsEqualTo(EnumParam.B);
obj.C.IsEqualTo((EnumParam)0);
}

[Fact]
public void TestEnumParameterUsesTypeMapper()
{
SqlMapper.AddTypeHandler(ItemTypeHandler.Default);
var result = connection.QuerySingle<string>("SELECT @Foo", new {Item.Foo});
"F".IsEqualTo(result);
}

[Fact]
public void TestEnumResultUsesTypeMapper()
{
SqlMapper.AddTypeHandler(ItemTypeHandler.Default);
var result = connection.QuerySingle<Item>("SELECT 'F'");
Item.Foo.IsEqualTo(result);
}

enum EnumParam : short
{
None, A, B
Expand Down Expand Up @@ -88,5 +105,45 @@ class TestEnumClassNoNull
{
public TestEnum EnumEnum { get; set; }
}

enum Item
{
None,
Foo,
Bar
}

class ItemTypeHandler : SqlMapper.TypeHandler<Item>
{
public static readonly ItemTypeHandler Default = new ItemTypeHandler();

public override Item Parse(object value)
{
var c = ((string) value)[0];
switch (c)
{
case 'F': return Item.Foo;
case 'B': return Item.Bar;
default: throw new ArgumentOutOfRangeException();
}
}

public override void SetValue(IDbDataParameter parameter, Item value)
{
parameter.DbType = DbType.AnsiStringFixedLength;
parameter.Size = 1;
parameter.Value = Format(value);
}

private string Format(Item value)
{
switch (value)
{
case Item.Foo: return "F";
case Item.Bar: return "B";
default: throw new ArgumentOutOfRangeException();
}
}
}
}
}
82 changes: 41 additions & 41 deletions Dapper/SqlMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,10 @@ public static DbType LookupDbType(Type type, string name, bool demand, out IType
handler = null;
var nullUnderlyingType = Nullable.GetUnderlyingType(type);
if (nullUnderlyingType != null) type = nullUnderlyingType;
if (typeHandlers.TryGetValue(type, out handler))
{
return DbType.Object;
}
if (type.IsEnum() && !typeMap.ContainsKey(type))
{
type = Enum.GetUnderlyingType(type);
Expand All @@ -372,10 +376,6 @@ public static DbType LookupDbType(Type type, string name, bool demand, out IType
{
return DbType.Binary;
}
if (typeHandlers.TryGetValue(type, out handler))
{
return DbType.Object;
}
if (typeof(IEnumerable).IsAssignableFrom(type))
{
return DynamicParameters.EnumerableMultiParameter;
Expand Down Expand Up @@ -1613,15 +1613,15 @@ private static Func<IDataReader, object> GetDeserializer(Type type, IDataReader
{
return GetDapperRowDeserializer(reader, startBound, length, returnNullIfFirstMissing);
}
ITypeHandler handler;
if (typeHandlers.TryGetValue(type, out handler))
{
return GetHandlerDeserializer(handler, type, startBound);
}
Type underlyingType = null;
if (!(typeMap.ContainsKey(type) || type.IsEnum() || type.FullName == LinqBinary ||
(type.IsValueType() && (underlyingType = Nullable.GetUnderlyingType(type)) != null && underlyingType.IsEnum())))
{
ITypeHandler handler;
if (typeHandlers.TryGetValue(type, out handler))
{
return GetHandlerDeserializer(handler, type, startBound);
}
return GetTypeDeserializer(type, reader, startBound, length, returnNullIfFirstMissing);
}
return GetStructDeserializer(type, underlyingType ?? type, startBound);
Expand Down Expand Up @@ -2508,6 +2508,15 @@ private static Action<IDbCommand, object> GetParameterReader(IDbConnection cnn,
private static Func<IDataReader, object> GetStructDeserializer(Type type, Type effectiveType, int index)
{
// no point using special per-type handling here; it boils down to the same, plus not all are supported anyway (see: SqlDataReader.GetChar - not supported!)
ITypeHandler handler;
if (typeHandlers.TryGetValue(type, out handler))
{
return r =>
{
var val = r.GetValue(index);
return val is DBNull ? null : handler.Parse(type, val);
};
}
#pragma warning disable 618
if (type == typeof(char))
{ // this *does* need special handling, though
Expand Down Expand Up @@ -2535,15 +2544,6 @@ private static Func<IDataReader, object> GetStructDeserializer(Type type, Type e
return val is DBNull ? null : Enum.ToObject(effectiveType, val);
};
}
ITypeHandler handler;
if(typeHandlers.TryGetValue(type, out handler))
{
return r =>
{
var val = r.GetValue(index);
return val is DBNull ? null : handler.Parse(type, val);
};
}
return r =>
{
var val = r.GetValue(index);
Expand All @@ -2557,6 +2557,11 @@ private static T Parse<T>(object value)
if (value is T) return (T)value;
var type = typeof(T);
type = Nullable.GetUnderlyingType(type) ?? type;
ITypeHandler handler;
if (typeHandlers.TryGetValue(type, out handler))
{
return (T)handler.Parse(type, value);
}
if (type.IsEnum())
{
if (value is float || value is double || value is decimal)
Expand All @@ -2565,11 +2570,6 @@ private static T Parse<T>(object value)
}
return (T)Enum.ToObject(type, value);
}
ITypeHandler handler;
if (typeHandlers.TryGetValue(type, out handler))
{
return (T)handler.Parse(type, value);
}
return (T)Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
}

Expand Down Expand Up @@ -2817,7 +2817,22 @@ private static Func<IDataReader, object> GetTypeDeserializerImpl(
Type colType = reader.GetFieldType(index);
Type memberType = item.MemberType;

if (memberType == typeof(char) || memberType == typeof(char?))
// unbox nullable enums as the primitive, i.e. byte etc
var nullUnderlyingType = Nullable.GetUnderlyingType(memberType);
var unboxType = nullUnderlyingType != null && nullUnderlyingType.IsEnum() ? nullUnderlyingType : memberType;

bool hasTypeHandler = typeHandlers.ContainsKey(unboxType);

if (hasTypeHandler)
{
il.Emit(OpCodes.Dup); // stack is now [target][target][value][value]
il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [target][target][value-as-object][DBNull or null]
il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [target][target][value-as-object]
#pragma warning disable 618
il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod(nameof(TypeHandlerCache<int>.Parse)), null); // stack is now [target][target][typed-value]
#pragma warning restore 618
}
else if (memberType == typeof(char) || memberType == typeof(char?))
{
il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(
memberType == typeof(char) ? nameof(SqlMapper.ReadChar) : nameof(SqlMapper.ReadNullableChar), BindingFlags.Static | BindingFlags.Public), null); // stack is now [target][target][typed-value]
Expand All @@ -2828,11 +2843,6 @@ private static Func<IDataReader, object> GetTypeDeserializerImpl(
il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [target][target][value-as-object][DBNull or null]
il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [target][target][value-as-object]

// unbox nullable enums as the primitive, i.e. byte etc

var nullUnderlyingType = Nullable.GetUnderlyingType(memberType);
var unboxType = nullUnderlyingType != null && nullUnderlyingType.IsEnum() ? nullUnderlyingType : memberType;

if (unboxType.IsEnum())
{
Type numericType = Enum.GetUnderlyingType(unboxType);
Expand Down Expand Up @@ -2869,19 +2879,9 @@ private static Func<IDataReader, object> GetTypeDeserializerImpl(
else
{
TypeCode dataTypeCode = TypeExtensions.GetTypeCode(colType), unboxTypeCode = TypeExtensions.GetTypeCode(unboxType);
bool hasTypeHandler;
if ((hasTypeHandler = typeHandlers.ContainsKey(unboxType)) || colType == unboxType || dataTypeCode == unboxTypeCode || dataTypeCode == TypeExtensions.GetTypeCode(nullUnderlyingType))
if (colType == unboxType || dataTypeCode == unboxTypeCode || dataTypeCode == TypeExtensions.GetTypeCode(nullUnderlyingType))
{
if (hasTypeHandler)
{
#pragma warning disable 618
il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod(nameof(TypeHandlerCache<int>.Parse)), null); // stack is now [target][target][typed-value]
#pragma warning restore 618
}
else
{
il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value]
}
il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value]
}
else
{
Expand Down