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

Support for Type mapping. #1802

Open
wants to merge 6 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
5 changes: 4 additions & 1 deletion Dapper/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,7 @@ static Dapper.SqlMapper.SetTypeName(this System.Data.DataTable! table, string! t
static Dapper.SqlMapper.ThrowDataException(System.Exception! ex, int index, System.Data.IDataReader! reader, object? value) -> void
static Dapper.SqlMapper.TypeHandlerCache<T>.Parse(object! value) -> T?
static Dapper.SqlMapper.TypeHandlerCache<T>.SetValue(System.Data.IDbDataParameter! parameter, object! value) -> void
static Dapper.SqlMapper.TypeMapProvider -> System.Func<System.Type!, Dapper.SqlMapper.ITypeMap!>!
static Dapper.SqlMapper.TypeMapProvider -> System.Func<System.Type!, Dapper.SqlMapper.ITypeMap!>!
static Dapper.SqlMapper.CurrentAbstractTypeMap.get -> System.Func<System.Type!, System.Type?>!
static Dapper.SqlMapper.AddAbstractTypeMap(System.Func<System.Func<System.Type!, System.Type?>!, System.Func<System.Type!, System.Type?>!>! combiner) -> void
static Dapper.SqlMapper.SetAbstractTypeMap(System.Func<System.Type!, System.Type?>? map) -> void
10 changes: 9 additions & 1 deletion Dapper/SqlMapper.TypeDeserializerCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ internal static Func<DbDataReader, object> GetReader(Type type, DbDataReader rea
found = (TypeDeserializerCache?)byType[type];
if (found is null)
{
byType[type] = found = new TypeDeserializerCache(type);
var mapped = SqlMapper.abstractTypeMap?.Invoke(type);
if( mapped != null && mapped != type )
{
byType[type] = byType[mapped] = found = new TypeDeserializerCache(mapped);
}
else
{
byType[type] = found = new TypeDeserializerCache(type);
}
}
}
}
Expand Down
51 changes: 51 additions & 0 deletions Dapper/SqlMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ static SqlMapper()
[typeof(SqlMoney?)] = TypeMapEntry.DecimalFieldValue,
};
ResetTypeHandlers(false);
abstractTypeMap = t => null;
}

/// <summary>
Expand Down Expand Up @@ -496,6 +497,56 @@ public static void SetDbType(IDataParameter parameter, object value)
return DbType.Object;
}

private static Func<Type, Type?> abstractTypeMap;

/// <summary>
/// Gets the current abstract to concrete mapper (can be null).
/// Use <see cref="AddAbstractTypeMap(Func{Func{Type, Type}, Func{Type, Type}})"/> to combine
/// it with a any new mapping. This function must simply return null (or the type itself) if
/// the type has no mapping or must not be mapped.
/// </summary>
/// <remarks>
/// Once a type has been mapped, it will keep its original mapping until <see cref="PurgeQueryCache"/> is called.
/// </remarks>
public static Func<Type, Type?> CurrentAbstractTypeMap => abstractTypeMap;

/// <summary>
/// Updates <see cref="CurrentAbstractTypeMap"/> with a new one that should combine the
/// current one with any new mappings in a thread safe manner.
/// </summary>
/// <remarks>
/// The <paramref name="combiner"/> may be called more than once in case of concurrent calls.
/// </remarks>
/// <param name="combiner">A function that must combine its input with any rules and returns a new mapper.</param>
public static void AddAbstractTypeMap(Func<Func<Type, Type?>, Func<Type, Type?>> combiner)
{
var spinWait = new SpinWait();
while( true )
{
var current = abstractTypeMap;
if( Interlocked.CompareExchange(ref abstractTypeMap, combiner(current), current) == current )
{
return;
}
spinWait.SpinOnce();
}
}

/// <summary>
/// Sets the <see cref="CurrentAbstractTypeMap"/> to a new function, regardless of its current
/// value. Use <see cref="AddAbstractTypeMap(Func{Func{Type, Type}, Func{Type, Type}})"/> to safely
/// combine a new mapper with the current one.
/// </summary>
/// <remarks>
/// Once a type has been mapped, it will keep its original mapping until <see cref="PurgeQueryCache"/> is called.
/// </remarks>
/// <param name="map">The new mapping function to set. Null to reset it.</param>
public static void SetAbstractTypeMap(Func<Type, Type?>? map)
{
if (map == null) map = t => null;
abstractTypeMap = map;
}

/// <summary>
/// Obtains the data as a list; if it is *already* a list, the original object is returned without
/// any duplication; otherwise, ToList() is invoked.
Expand Down
10 changes: 10 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command

(note: new PRs will not be merged until they add release note wording here)

### 2.2.0

- adds support for Type mapping (resolves issue #1104) by allowing a
requested type to be mapped to another type. This adds to the the
static `SqlMapper`:
- a new `CurrentAbstractTypeMap` mapper function.
- a `SetAbstractTypeMap` to replace it.
- a `AddAbstractTypeMap` that combines a new mapping to the existing one (thread safe).
Once a type has been mapped, it will keep its original mapping until `PurgeQueryCache` is called.

### 2.1.4

- add untyped `GridReader.ReadUnbufferedAsync` API (#1958 via @mgravell)
Expand Down
107 changes: 107 additions & 0 deletions tests/Dapper.Tests/AbstractTypeMappingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Data;
using System.Linq;
using Xunit;

namespace Dapper.Tests
{
[Collection(NonParallelDefinition.Name)]
public sealed class SystemSqlClientAbstractTypeMappingTests : AbstractTypeMappingTests<SystemSqlClientProvider> { }
#if MSSQLCLIENT
[Collection(NonParallelDefinition.Name)]
public sealed class MicrosoftSqlClientAbstractTypeMappingTests : AbstractTypeMappingTests<MicrosoftSqlClientProvider> { }
#endif

public abstract class AbstractTypeMappingTests<TProvider> : TestBase<TProvider> where TProvider : DatabaseProvider
{
[Fact]
public void TestAbstractTypeMapping()
{
var previousMapping = SqlMapper.CurrentAbstractTypeMap;
SqlMapper.PurgeQueryCache();
try
{
SqlMapper.SetAbstractTypeMap(t => t == typeof(AbstractTypeMapping.IThing) ? typeof(AbstractTypeMapping.Thing) : null);

var thing = connection.Query<AbstractTypeMapping.IThing>("select 'Hello!' Name, 42 Power").First();
Assert.Equal(42, thing.Power);
Assert.Equal("Hello!", thing.Name);

var list = connection.Query<AbstractTypeMapping.IThing>("select 'Hello!' Name, 42 Power union all select 'World!' Name, 3712 Power")
.ToList();
Assert.Equal(42, list[0].Power);
Assert.Equal("Hello!", list[0].Name);
Assert.Equal(3712, list[1].Power);
Assert.Equal("World!", list[1].Name);

var firstThing = connection.QueryFirstOrDefault<AbstractTypeMapping.IThing>("select 'Hello!' Name, 42 Power");
Assert.True(firstThing != null);
Assert.Equal(42, firstThing.Power);
Assert.Equal("Hello!", firstThing.Name);
}
finally
{
SqlMapper.SetAbstractTypeMap( previousMapping );
SqlMapper.PurgeQueryCache();
}
}

[Fact]
public void TestAbstractTypeMappingCombination()
{
var previousMapping = SqlMapper.CurrentAbstractTypeMap;
SqlMapper.PurgeQueryCache();
try
{
// IThing is mapped to Thing.
SqlMapper.SetAbstractTypeMap(t => t == typeof(AbstractTypeMapping.IThing) ? typeof(AbstractTypeMapping.Thing) : null);

// "Override": IThing is mapped to ThingMultiplier.
SqlMapper.AddAbstractTypeMap(current =>
{
return t =>
{
if (t == typeof(AbstractTypeMapping.IThing)) return typeof(AbstractTypeMapping.ThingMultiplier);
return current?.Invoke(t);
};
});

var thing = connection.Query<AbstractTypeMapping.IThing>("select 'Hello!' Name, 42 Power").First();
Assert.Equal(84, thing.Power);
Assert.Equal("Hello!", thing.Name);
}
finally
{
SqlMapper.SetAbstractTypeMap( previousMapping );
SqlMapper.PurgeQueryCache();
}
}

public static class AbstractTypeMapping
{
public interface IThing
{
int Power { get; }

string? Name { get; }
}

public class Thing : IThing
{
public int Power { get; set; }

public string? Name { get; set; }
}

public class ThingMultiplier : IThing
{
int _power;

public int Power { get => _power * 2; set => _power = value; }

public string? Name { get; set; }
}
}

}
}