diff --git a/Dapper/PublicAPI.Unshipped.txt b/Dapper/PublicAPI.Unshipped.txt
index 91b0e1a43..cf9343ccf 100644
--- a/Dapper/PublicAPI.Unshipped.txt
+++ b/Dapper/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
-#nullable enable
\ No newline at end of file
+#nullable enable
+static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.get -> bool
+static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void
\ No newline at end of file
diff --git a/Dapper/SqlMapper.Settings.cs b/Dapper/SqlMapper.Settings.cs
index cbbc3c687..c19835c11 100644
--- a/Dapper/SqlMapper.Settings.cs
+++ b/Dapper/SqlMapper.Settings.cs
@@ -65,7 +65,7 @@ static Settings()
public static void SetDefaults()
{
CommandTimeout = null;
- ApplyNullValues = PadListExpansions = UseIncrementalPseudoPositionalParameterNames = false;
+ ApplyNullValues = PadListExpansions = UseIncrementalPseudoPositionalParameterNames = PreferTypeHandlersForEnums = false;
AllowedCommandBehaviors = DefaultAllowedCommandBehaviors;
FetchSize = InListStringSplitCount = -1;
}
@@ -129,6 +129,13 @@ public static long FetchSize
///
public static bool SupportLegacyParameterTokens { get; set; } = true;
+ ///
+ /// 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.
+ ///
+ public static bool PreferTypeHandlersForEnums { get; set; }
+
private static long s_FetchSize = -1;
}
}
diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs
index d23e949a5..6210e307e 100644
--- a/Dapper/SqlMapper.cs
+++ b/Dapper/SqlMapper.cs
@@ -465,6 +465,10 @@ public static void SetDbType(IDataParameter parameter, object value)
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))
@@ -2751,7 +2755,12 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true
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; we want to box as the underlying type; that's just *hard*; for
// simplicity, box as Nullable and call SanitizeParameterValue
@@ -3097,7 +3106,16 @@ private static Func GetSimpleValueDeserializer(Type type,
#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);
@@ -3160,6 +3178,10 @@ private static T Parse(object? value)
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);
@@ -3738,22 +3760,31 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind
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.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)
diff --git a/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs b/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs
new file mode 100644
index 000000000..958f0d546
--- /dev/null
+++ b/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs
@@ -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 { }
+#if MSSQLCLIENT
+ [Collection(NonParallelDefinition.Name)]
+ public sealed class MicrosoftSqlClientPreferTypeHandlersForEnumsTests : PreferTypeHandlersForEnumsTests { }
+#endif
+
+ public abstract class PreferTypeHandlersForEnumsTests : TestBase 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; }
+ }
+
+ ///
+ /// A TypeHandler that stores enum values as their name strings
+ /// and parses them back from strings.
+ ///
+ private class StringEnumHandler : SqlMapper.TypeHandler where TEnum : struct, Enum
+ {
+ public static readonly StringEnumHandler 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();
+ 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(
+ "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();
+ 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(
+ "SELECT @Value AS Value", new { Value = input }).Single();
+
+ Assert.Null(result.Value);
+ }
+ finally
+ {
+ SqlMapper.Settings.PreferTypeHandlersForEnums = oldSetting;
+ SqlMapper.ResetTypeHandlers();
+ SqlMapper.PurgeQueryCache();
+ }
+ }
+ }
+}