mirror of
https://github.com/DapperLib/Dapper.git
synced 2026-10-16 21:03:18 +08:00
Fix/prefer typehandlers for enums (#2200)
* Add Settings.PreferTypeHandlersForEnums opt-in flag When enabled, Dapper checks for a registered TypeHandler for enum types before falling back to the default integer boxing behavior. This allows custom enum serialization (e.g. storing enums as strings via TypeHandlers) to work on both reads and writes. Patched locations: - LookupDbType: return DbType.Object with handler when flag is on - CreateParamInfoGenerator: skip integer boxing when handler exists - GetSimpleValueDeserializer: route to TypeHandler before Enum.ToObject - Parse<T>: route to TypeHandler before Enum.ToObject Default is false — zero behavior change for existing users. * Patch IL-emitted type deserializer and add tests - Fix 6th enum-before-handler location in GetTypeDeserializer IL emit, preserving Nullable<T> wrapping for nullable enum properties - Fix em-dash in comment (non-ASCII) - Add two tests: round-trip write+read via StringEnumHandler, and nullable enum with null value * Update SDK version from 10.0.102 to 8.0.100 * Update global.json we have fixed the CI glitch separately --------- Co-authored-by: Marc Gravell <marc.gravell@gmail.com>
This commit is contained in:
@@ -1 +1,3 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.get -> bool
|
||||
static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void
|
||||
@@ -65,7 +65,7 @@ namespace Dapper
|
||||
public static void SetDefaults()
|
||||
{
|
||||
CommandTimeout = null;
|
||||
ApplyNullValues = PadListExpansions = UseIncrementalPseudoPositionalParameterNames = false;
|
||||
ApplyNullValues = PadListExpansions = UseIncrementalPseudoPositionalParameterNames = PreferTypeHandlersForEnums = false;
|
||||
AllowedCommandBehaviors = DefaultAllowedCommandBehaviors;
|
||||
FetchSize = InListStringSplitCount = -1;
|
||||
}
|
||||
@@ -129,6 +129,13 @@ namespace Dapper
|
||||
/// </summary>
|
||||
public static bool SupportLegacyParameterTokens { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When true, Dapper checks for a registered TypeHandler for enum types before
|
||||
/// falling back to the default behavior of sending enums as their underlying integer type.
|
||||
/// This enables custom enum serialization (e.g. storing enums as strings), while preserving existing behavior.
|
||||
/// </summary>
|
||||
public static bool PreferTypeHandlersForEnums { get; set; }
|
||||
|
||||
private static long s_FetchSize = -1;
|
||||
}
|
||||
}
|
||||
|
||||
+45
-14
@@ -465,6 +465,10 @@ namespace Dapper
|
||||
if (nullUnderlyingType is not null) type = nullUnderlyingType;
|
||||
if (type.IsEnum && !typeMap.ContainsKey(type))
|
||||
{
|
||||
if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out handler))
|
||||
{
|
||||
return DbType.Object;
|
||||
}
|
||||
type = Enum.GetUnderlyingType(type);
|
||||
}
|
||||
if (typeMap.TryGetValue(type, out var mapEntry))
|
||||
@@ -2755,7 +2759,12 @@ namespace Dapper
|
||||
|
||||
if ((nullType ?? propType).IsEnum)
|
||||
{
|
||||
if (nullType is not null)
|
||||
if (handler is not null)
|
||||
{
|
||||
// TypeHandler registered - box as the enum type, handler does conversion
|
||||
checkForNull = nullType is not null;
|
||||
}
|
||||
else if (nullType is not null)
|
||||
{
|
||||
// Nullable<SomeEnum>; we want to box as the underlying type; that's just *hard*; for
|
||||
// simplicity, box as Nullable<SomeEnum> and call SanitizeParameterValue
|
||||
@@ -3101,7 +3110,16 @@ namespace Dapper
|
||||
#pragma warning restore 618
|
||||
|
||||
if (effectiveType.IsEnum)
|
||||
{ // assume the value is returned as the correct type (int/byte/etc), but box back to the typed enum
|
||||
{
|
||||
if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out var enumHandler))
|
||||
{
|
||||
return r =>
|
||||
{
|
||||
var val = r.GetValue(index);
|
||||
return val is DBNull ? null! : enumHandler.Parse(type, val)!;
|
||||
};
|
||||
}
|
||||
// assume the value is returned as the correct type (int/byte/etc), but box back to the typed enum
|
||||
return r =>
|
||||
{
|
||||
var val = r.GetValue(index);
|
||||
@@ -3164,6 +3182,10 @@ namespace Dapper
|
||||
type = Nullable.GetUnderlyingType(type) ?? type;
|
||||
if (type.IsEnum)
|
||||
{
|
||||
if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out ITypeHandler? enumHandler))
|
||||
{
|
||||
return (T)enumHandler.Parse(type, value)!;
|
||||
}
|
||||
if (value is float || value is double || value is decimal)
|
||||
{
|
||||
value = Convert.ChangeType(value, Enum.GetUnderlyingType(type), CultureInfo.InvariantCulture);
|
||||
@@ -3742,22 +3764,31 @@ namespace Dapper
|
||||
|
||||
if (unboxType.IsEnum)
|
||||
{
|
||||
Type numericType = Enum.GetUnderlyingType(unboxType);
|
||||
if (colType == typeof(string))
|
||||
if (Settings.PreferTypeHandlersForEnums && typeHandlers.ContainsKey(unboxType))
|
||||
{
|
||||
stringEnumLocal ??= il.DeclareLocal(typeof(string));
|
||||
il.Emit(OpCodes.Castclass, typeof(string)); // stack is now [...][string]
|
||||
il.Emit(OpCodes.Stloc, stringEnumLocal); // stack is now [...]
|
||||
il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [...][enum-type-token]
|
||||
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null);// stack is now [...][enum-type]
|
||||
il.Emit(OpCodes.Ldloc, stringEnumLocal); // stack is now [...][enum-type][string]
|
||||
il.Emit(OpCodes.Ldc_I4_1); // stack is now [...][enum-type][string][true]
|
||||
il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [...][enum-as-object]
|
||||
il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [...][typed-value]
|
||||
#pragma warning disable 618
|
||||
il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod(nameof(TypeHandlerCache<int>.Parse))!, null); // stack is now [...][typed-value]
|
||||
#pragma warning restore 618
|
||||
}
|
||||
else
|
||||
{
|
||||
FlexibleConvertBoxedFromHeadOfStack(il, colType, unboxType, numericType);
|
||||
Type numericType = Enum.GetUnderlyingType(unboxType);
|
||||
if (colType == typeof(string))
|
||||
{
|
||||
stringEnumLocal ??= il.DeclareLocal(typeof(string));
|
||||
il.Emit(OpCodes.Castclass, typeof(string)); // stack is now [...][string]
|
||||
il.Emit(OpCodes.Stloc, stringEnumLocal); // stack is now [...]
|
||||
il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [...][enum-type-token]
|
||||
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null);// stack is now [...][enum-type]
|
||||
il.Emit(OpCodes.Ldloc, stringEnumLocal); // stack is now [...][enum-type][string]
|
||||
il.Emit(OpCodes.Ldc_I4_1); // stack is now [...][enum-type][string][true]
|
||||
il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [...][enum-as-object]
|
||||
il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [...][typed-value]
|
||||
}
|
||||
else
|
||||
{
|
||||
FlexibleConvertBoxedFromHeadOfStack(il, colType, unboxType, numericType);
|
||||
}
|
||||
}
|
||||
|
||||
if (nullUnderlyingType is not null)
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
"version": "10.0.102",
|
||||
"rollForward": "latestMajor"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapper.Tests
|
||||
{
|
||||
[Collection(NonParallelDefinition.Name)]
|
||||
public sealed class SystemSqlClientPreferTypeHandlersForEnumsTests : PreferTypeHandlersForEnumsTests<SystemSqlClientProvider> { }
|
||||
#if MSSQLCLIENT
|
||||
[Collection(NonParallelDefinition.Name)]
|
||||
public sealed class MicrosoftSqlClientPreferTypeHandlersForEnumsTests : PreferTypeHandlersForEnumsTests<MicrosoftSqlClientProvider> { }
|
||||
#endif
|
||||
|
||||
public abstract class PreferTypeHandlersForEnumsTests<TProvider> : TestBase<TProvider> where TProvider : DatabaseProvider
|
||||
{
|
||||
private enum Color
|
||||
{
|
||||
Red = 1,
|
||||
Green = 2,
|
||||
Blue = 3
|
||||
}
|
||||
|
||||
private class ColorResult
|
||||
{
|
||||
public Color Value { get; set; }
|
||||
}
|
||||
|
||||
private class NullableColorResult
|
||||
{
|
||||
public Color? Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A TypeHandler that stores enum values as their name strings
|
||||
/// and parses them back from strings.
|
||||
/// </summary>
|
||||
private class StringEnumHandler<TEnum> : SqlMapper.TypeHandler<TEnum> where TEnum : struct, Enum
|
||||
{
|
||||
public static readonly StringEnumHandler<TEnum> Instance = new();
|
||||
public int ParseCallCount;
|
||||
public int SetValueCallCount;
|
||||
|
||||
public override TEnum Parse(object? value)
|
||||
{
|
||||
ParseCallCount++;
|
||||
return (TEnum)Enum.Parse(typeof(TEnum), (string)value!);
|
||||
}
|
||||
|
||||
public override void SetValue(IDbDataParameter parameter, TEnum value)
|
||||
{
|
||||
SetValueCallCount++;
|
||||
parameter.DbType = DbType.AnsiString;
|
||||
parameter.Value = value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnumTypeHandler_WriteAndRead_UsesHandlerWhenEnabled()
|
||||
{
|
||||
var handler = new StringEnumHandler<Color>();
|
||||
var oldSetting = SqlMapper.Settings.PreferTypeHandlersForEnums;
|
||||
try
|
||||
{
|
||||
SqlMapper.ResetTypeHandlers();
|
||||
SqlMapper.AddTypeHandler(typeof(Color), handler);
|
||||
SqlMapper.Settings.PreferTypeHandlersForEnums = true;
|
||||
SqlMapper.PurgeQueryCache();
|
||||
|
||||
// Round-trip: write as string, read back via handler
|
||||
var result = connection.Query<ColorResult>(
|
||||
"SELECT @Value AS Value", new { Value = Color.Green }).Single();
|
||||
|
||||
Assert.Equal(Color.Green, result.Value);
|
||||
Assert.True(handler.SetValueCallCount > 0, "SetValue should have been called");
|
||||
Assert.True(handler.ParseCallCount > 0, "Parse should have been called");
|
||||
}
|
||||
finally
|
||||
{
|
||||
SqlMapper.Settings.PreferTypeHandlersForEnums = oldSetting;
|
||||
SqlMapper.ResetTypeHandlers();
|
||||
SqlMapper.PurgeQueryCache();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnumTypeHandler_NullableWithNull_ReturnsNull()
|
||||
{
|
||||
var handler = new StringEnumHandler<Color>();
|
||||
var oldSetting = SqlMapper.Settings.PreferTypeHandlersForEnums;
|
||||
try
|
||||
{
|
||||
SqlMapper.ResetTypeHandlers();
|
||||
SqlMapper.AddTypeHandler(typeof(Color), handler);
|
||||
SqlMapper.Settings.PreferTypeHandlersForEnums = true;
|
||||
SqlMapper.PurgeQueryCache();
|
||||
|
||||
Color? input = null;
|
||||
var result = connection.Query<NullableColorResult>(
|
||||
"SELECT @Value AS Value", new { Value = input }).Single();
|
||||
|
||||
Assert.Null(result.Value);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SqlMapper.Settings.PreferTypeHandlersForEnums = oldSetting;
|
||||
SqlMapper.ResetTypeHandlers();
|
||||
SqlMapper.PurgeQueryCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user