Skip to content
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
4 changes: 3 additions & 1 deletion Dapper/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
#nullable enable
static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.get -> bool
static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void
9 changes: 8 additions & 1 deletion Dapper/SqlMapper.Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -129,6 +129,13 @@ public static long FetchSize
/// </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;
}
}
Expand Down
59 changes: 45 additions & 14 deletions Dapper/SqlMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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<SomeEnum>; we want to box as the underlying type; that's just *hard*; for
// simplicity, box as Nullable<SomeEnum> and call SanitizeParameterValue
Expand Down Expand Up @@ -3097,7 +3106,16 @@ private static Func<DbDataReader, object> 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);
Expand Down Expand Up @@ -3160,6 +3178,10 @@ private static T Parse<T>(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);
Expand Down Expand Up @@ -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<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)
Expand Down
112 changes: 112 additions & 0 deletions tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}