diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs
index f547985c18f553c7c766a3d6eb976ccbd379acd5..deaf94c0d9068c6af0f9f818f90157f8dbe67627 100644
--- a/Dapper/SqlMapper.cs
+++ b/Dapper/SqlMapper.cs
@@ -37,6 +37,67 @@ public interface IDynamicParameters
/// Information about the query
void AddParameters(IDbCommand command, Identity identity);
}
+
+ ///
+ /// Implement this interface to change default mapping of reader columns to type memebers
+ ///
+ public interface ITypeMap
+ {
+ ///
+ /// Finds best constructor
+ ///
+ /// DataReader column names
+ /// DataReader column types
+ /// Matching constructor or default one
+ ConstructorInfo FindConstructor(string[] names, Type[] types);
+
+ ///
+ /// Gets mapping for constructor parameter
+ ///
+ /// Constructor to resolve
+ /// DataReader column name
+ /// Mapping implementation
+ IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName);
+
+ ///
+ /// Gets member mapping for column
+ ///
+ /// DataReader column name
+ /// Mapping implementation
+ IMemberMap GetMember(string columnName);
+ }
+
+ ///
+ /// Implements this interface to provide custom member mapping
+ ///
+ public interface IMemberMap
+ {
+ ///
+ /// Source DataReader column name
+ ///
+ string ColumnName { get; }
+
+ ///
+ /// Target member type
+ ///
+ Type MemberType { get; }
+
+ ///
+ /// Target property
+ ///
+ PropertyInfo Property { get; }
+
+ ///
+ /// Target field
+ ///
+ FieldInfo Field { get; }
+
+ ///
+ /// Target constructor parameter
+ ///
+ ParameterInfo Parameter { get; }
+ }
+
static Link> bindByNameCache;
static Action GetBindByName(Type commandType)
{
@@ -172,6 +233,15 @@ private static bool TryGetQueryCache(Identity key, out CacheInfo value)
{
lock (_queryCache) { return _queryCache.TryGetValue(key, out value); }
}
+ private static void PurgeQueryCacheByType(Type type)
+ {
+ lock (_queryCache)
+ {
+ var toRemove = _queryCache.Keys.Where(id => id.type == type).ToArray();
+ foreach (var key in toRemove)
+ _queryCache.Remove(key);
+ }
+ }
public static void PurgeQueryCache()
{
lock (_queryCache)
@@ -233,6 +303,16 @@ public static void PurgeQueryCache()
OnQueryCachePurged();
}
+ private static void PurgeQueryCacheByType(Type type)
+ {
+ foreach (var entry in _queryCache)
+ {
+ CacheInfo cache;
+ if (entry.Key.type == type)
+ _queryCache.TryRemove(entry.Key, out cache);
+ }
+ }
+
///
/// Return a count of all the cached queries by dapper
///
@@ -324,7 +404,7 @@ static SqlMapper()
typeMap[typeof(Object)] = DbType.Object;
}
- private const string LinqBinary = "System.Data.Linq.Binary";
+ internal const string LinqBinary = "System.Data.Linq.Binary";
private static DbType LookupDbType(Type type, string name)
{
DbType dbType;
@@ -428,7 +508,10 @@ public override bool Equals(object obj)
///
///
public readonly int hashCode, gridIndex;
- private readonly Type type;
+ ///
+ ///
+ ///
+ public readonly Type type;
///
///
///
@@ -957,8 +1040,8 @@ class DontMap { }
// if our current type has the split, skip the first time you see it.
if (type != typeof(Object))
{
- var props = GetSettableProps(type);
- var fields = GetSettableFields(type);
+ var props = DefaultTypeMap.GetSettableProps(type);
+ var fields = DefaultTypeMap.GetSettableFields(type);
foreach (var name in props.Select(p => p.Name).Concat(fields.Select(f => f.Name)))
{
@@ -1578,32 +1661,43 @@ static readonly MethodInfo
.Where(p => p.GetIndexParameters().Any() && p.GetIndexParameters()[0].ParameterType == typeof(int))
.Select(p => p.GetGetMethod()).First();
- class PropInfo
+ ///
+ /// Gets type map
+ ///
+ ///
+ /// Type map implementation, DefaultTypeMap instance in no override present
+ public static ITypeMap GetTypeMap(Type type)
{
- public string Name { get; set; }
- public MethodInfo Setter { get; set; }
- public Type Type { get; set; }
- }
+ ITypeMap typeMap;
+ lock (_typeMaps)
+ {
+ _typeMaps.TryGetValue(type, out typeMap);
+ }
- static List GetSettableProps(Type t)
- {
- return t
- .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
- .Select(p => new PropInfo
- {
- Name = p.Name,
- Setter = p.DeclaringType == t ?
- p.GetSetMethod(true) :
- p.DeclaringType.GetProperty(p.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetSetMethod(true),
- Type = p.PropertyType
- })
- .Where(info => info.Setter != null)
- .ToList();
+ return typeMap ?? new DefaultTypeMap(type);
}
- static List GetSettableFields(Type t)
+
+ private static readonly Dictionary _typeMaps = new Dictionary();
+
+ ///
+ /// Set custom mapping for type deserializers
+ ///
+ /// Entity type to override
+ /// Mapping rules impementation, null to remove custom map
+ public static void SetTypeMap(Type type, ITypeMap map)
{
- return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList();
+ if (type == null)
+ throw new ArgumentNullException("type");
+
+ if (map == null || map is DefaultTypeMap)
+ lock (_typeMaps)
+ _typeMaps.Remove(type);
+ else
+ lock (_typeMaps)
+ _typeMaps[type] = map;
+
+ PurgeQueryCacheByType(type);
}
///
@@ -1630,8 +1724,7 @@ static List GetSettableFields(Type t)
il.DeclareLocal(type);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stloc_0);
- var properties = GetSettableProps(type);
- var fields = GetSettableFields(type);
+
if (length == -1)
{
length = reader.FieldCount - startBound;
@@ -1642,17 +1735,14 @@ static List GetSettableFields(Type t)
throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn");
}
- var names = new List();
+ var names = Enumerable.Range(startBound, length).Select(i => reader.GetName(i)).ToArray();
- for (int i = startBound; i < startBound + length; i++)
- {
- names.Add(reader.GetName(i));
- }
+ ITypeMap typeMap = GetTypeMap(type);
int index = startBound;
ConstructorInfo specializedConstructor = null;
- ParameterInfo[] specializedParameters = null;
+
if (type.IsValueType)
{
il.Emit(OpCodes.Ldloca_S, (byte)1);
@@ -1665,47 +1755,31 @@ static List GetSettableFields(Type t)
{
types[i - startBound] = reader.GetFieldType(i);
}
- var constructors = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
- bool hasDefaultConstructor = false;
- foreach (ConstructorInfo ctor in constructors.OrderBy(c => c.IsPublic ? 0 : (c.IsPrivate ? 2 : 1)).ThenBy(c => c.GetParameters().Length))
+
+ if (type.IsValueType)
{
- ParameterInfo[] ctorParameters = ctor.GetParameters();
- if (ctorParameters.Length == 0)
+ il.Emit(OpCodes.Ldloca_S, (byte)1);
+ il.Emit(OpCodes.Initobj, type);
+ }
+ else
+ {
+ var ctor = typeMap.FindConstructor(names, types);
+ if (ctor == null)
{
- il.Emit(OpCodes.Newobj, ctor);
- il.Emit(OpCodes.Stloc_1);
- hasDefaultConstructor = true;
- break;
+ string proposedTypes = "(" + String.Join(", ", types.Select((t, i) => t.FullName + " " + names[i]).ToArray()) + ")";
+ throw new InvalidOperationException(String.Format("A parameterless default constructor or one matching signature {0} is required for {1} materialization", proposedTypes, type.FullName));
}
- if (ctorParameters.Length != types.Length)
- continue;
- int i = 0;
- for (; i < ctorParameters.Length; i++)
+ if (ctor.GetParameters().Length == 0)
{
- if (!String.Equals(ctorParameters[i].Name, names[i], StringComparison.OrdinalIgnoreCase))
- break;
- if (types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == LinqBinary)
- continue;
- var unboxedType = Nullable.GetUnderlyingType(ctorParameters[i].ParameterType) ?? ctorParameters[i].ParameterType;
- if (unboxedType != types[i]
- && !(unboxedType.IsEnum && Enum.GetUnderlyingType(unboxedType) == types[i])
- && !(unboxedType == typeof(char) && types[i] == typeof(string)))
- break;
+ il.Emit(OpCodes.Newobj, ctor);
+ il.Emit(OpCodes.Stloc_1);
}
- if (i == ctorParameters.Length)
- {
+ else
specializedConstructor = ctor;
- specializedParameters = ctorParameters;
- break;
- }
- }
- if (!hasDefaultConstructor && specializedConstructor == null)
- {
- string proposedTypes = "(" + String.Join(", ", types.Select((t, i) => t.FullName + " " + names[i]).ToArray()) + ")";
- throw new InvalidOperationException(String.Format("A parameterless default constructor or one matching signature {0} is required for {1} materialization", proposedTypes, type.FullName));
}
}
+
il.BeginExceptionBlock();
if(type.IsValueType)
{
@@ -1716,27 +1790,20 @@ static List GetSettableFields(Type t)
il.Emit(OpCodes.Ldloc_1);// [target]
}
- var setters = specializedConstructor != null ?
- names.Select((n, i) => new { Name = n, Property = new PropInfo() { Type = specializedParameters[i].ParameterType }, Field = (FieldInfo)null }).ToList()
- :
- (from n in names
- let prop = properties.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.Ordinal)) // property case sensitive first
- ?? properties.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.OrdinalIgnoreCase)) // property case insensitive second
- let field = prop != null ? null : (fields.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.Ordinal)) // field case sensitive third
- ?? fields.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.OrdinalIgnoreCase))) // field case insensitive fourth
- select new { Name = n, Property = prop, Field = field }
- ).ToList();
+ var members = (specializedConstructor != null
+ ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n))
+ : names.Select(n => typeMap.GetMember(n))).ToList();
// stack is now [target]
bool first = true;
var allDone = il.DefineLabel();
int enumDeclareLocal = -1;
- foreach (var item in setters)
+ foreach (var item in members)
{
- if (item.Property != null || item.Field != null)
+ if (item != null)
{
- if(specializedConstructor == null)
+ if (specializedConstructor == null)
il.Emit(OpCodes.Dup); // stack is now [target][target]
Label isDbNullLabel = il.DefineLabel();
Label finishLabel = il.DefineLabel();
@@ -1747,7 +1814,7 @@ static List GetSettableFields(Type t)
il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index]
il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object]
- Type memberType = item.Property != null ? item.Property.Type : item.Field.FieldType;
+ Type memberType = item.MemberType;
if (memberType == typeof(char) || memberType == typeof(char?))
{
@@ -1761,7 +1828,7 @@ static List GetSettableFields(Type t)
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;
@@ -1813,11 +1880,11 @@ static List GetSettableFields(Type t)
{
if (type.IsValueType)
{
- il.Emit(OpCodes.Call, item.Property.Setter); // stack is now [target]
+ il.Emit(OpCodes.Call, DefaultTypeMap.GetPropertySetter(item.Property, type)); // stack is now [target]
}
else
{
- il.Emit(OpCodes.Callvirt, item.Property.Setter); // stack is now [target]
+ il.Emit(OpCodes.Callvirt, DefaultTypeMap.GetPropertySetter(item.Property, type)); // stack is now [target]
}
}
else
@@ -1825,18 +1892,18 @@ static List GetSettableFields(Type t)
il.Emit(OpCodes.Stfld, item.Field); // stack is now [target]
}
}
-
+
il.Emit(OpCodes.Br_S, finishLabel); // stack is now [target]
il.MarkLabel(isDbNullLabel); // incoming stack: [target][target][value]
if (specializedConstructor != null)
{
il.Emit(OpCodes.Pop);
- if (item.Property.Type.IsValueType)
+ if (item.MemberType.IsValueType)
{
- int localIndex = il.DeclareLocal(item.Property.Type).LocalIndex;
+ int localIndex = il.DeclareLocal(item.MemberType).LocalIndex;
LoadLocalAddress(il, localIndex);
- il.Emit(OpCodes.Initobj, item.Property.Type);
+ il.Emit(OpCodes.Initobj, item.MemberType);
LoadLocal(il, localIndex);
}
else
@@ -2519,4 +2586,296 @@ public static FeatureSupport Get(IDbConnection connection)
public bool Arrays { get; set; }
}
-}
+ ///
+ /// Represents simple memeber map for one of target parameter or property or field to source DataReader column
+ ///
+ public sealed class SimpleMemberMap : SqlMapper.IMemberMap
+ {
+ private readonly string _columnName;
+ private readonly PropertyInfo _property;
+ private readonly FieldInfo _field;
+ private readonly ParameterInfo _parameter;
+
+ ///
+ /// Creates instance for simple property mapping
+ ///
+ /// DataReader column name
+ /// Target property
+ public SimpleMemberMap(string columnName, PropertyInfo property)
+ {
+ if (columnName == null)
+ throw new ArgumentNullException("columnName");
+
+ if (property == null)
+ throw new ArgumentNullException("property");
+
+ _columnName = columnName;
+ _property = property;
+ }
+
+ ///
+ /// Creates instance for simple field mapping
+ ///
+ /// DataReader column name
+ /// Target property
+ public SimpleMemberMap(string columnName, FieldInfo field)
+ {
+ if (columnName == null)
+ throw new ArgumentNullException("columnName");
+
+ if (field == null)
+ throw new ArgumentNullException("field");
+
+ _columnName = columnName;
+ _field = field;
+ }
+
+ ///
+ /// Creates instance for simple constructor parameter mapping
+ ///
+ /// DataReader column name
+ /// Target constructor parameter
+ public SimpleMemberMap(string columnName, ParameterInfo parameter)
+ {
+ if (columnName == null)
+ throw new ArgumentNullException("columnName");
+
+ if (parameter == null)
+ throw new ArgumentNullException("parameter");
+
+ _columnName = columnName;
+ _parameter = parameter;
+ }
+
+ ///
+ /// DataReader column name
+ ///
+ public string ColumnName
+ {
+ get { return _columnName; }
+ }
+
+ ///
+ /// Target member type
+ ///
+ public Type MemberType
+ {
+ get
+ {
+ if (_field != null)
+ return _field.FieldType;
+
+ if (_property != null)
+ return _property.PropertyType;
+
+ if (_parameter != null)
+ return _parameter.ParameterType;
+
+ return null;
+ }
+ }
+
+ ///
+ /// Target property
+ ///
+ public PropertyInfo Property
+ {
+ get { return _property; }
+ }
+
+ ///
+ /// Target field
+ ///
+ public FieldInfo Field
+ {
+ get { return _field; }
+ }
+
+ ///
+ /// Target constructor parameter
+ ///
+ public ParameterInfo Parameter
+ {
+ get { return _parameter; }
+ }
+ }
+
+ ///
+ /// Represents default type mapping strategy used by Dapper
+ ///
+ public sealed class DefaultTypeMap : SqlMapper.ITypeMap
+ {
+ private readonly List _fields;
+ private readonly List _properties;
+ private readonly Type _type;
+
+ ///
+ /// Creates default type map
+ ///
+ /// Entity type
+ public DefaultTypeMap(Type type)
+ {
+ if (type == null)
+ throw new ArgumentNullException("type");
+
+ _fields = GetSettableFields(type);
+ _properties = GetSettableProps(type);
+ _type = type;
+ }
+
+ internal static MethodInfo GetPropertySetter(PropertyInfo propertyInfo, Type type)
+ {
+ return propertyInfo.DeclaringType == type ?
+ propertyInfo.GetSetMethod(true) :
+ propertyInfo.DeclaringType.GetProperty(propertyInfo.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetSetMethod(true);
+ }
+
+ internal static List GetSettableProps(Type t)
+ {
+ return t
+ .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
+ .Where(p => GetPropertySetter(p, t) != null)
+ .ToList();
+ }
+
+ internal static List GetSettableFields(Type t)
+ {
+ return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList();
+ }
+
+ ///
+ /// Finds best constructor
+ ///
+ /// DataReader column names
+ /// DataReader column types
+ /// Matching constructor or default one
+ public ConstructorInfo FindConstructor(string[] names, Type[] types)
+ {
+ var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ foreach (ConstructorInfo ctor in constructors.OrderBy(c => c.IsPublic ? 0 : (c.IsPrivate ? 2 : 1)).ThenBy(c => c.GetParameters().Length))
+ {
+ ParameterInfo[] ctorParameters = ctor.GetParameters();
+ if (ctorParameters.Length == 0)
+ return ctor;
+
+ if (ctorParameters.Length != types.Length)
+ continue;
+
+ int i = 0;
+ for (; i < ctorParameters.Length; i++)
+ {
+ if (!String.Equals(ctorParameters[i].Name, names[i], StringComparison.OrdinalIgnoreCase))
+ break;
+ if (types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == SqlMapper.LinqBinary)
+ continue;
+ var unboxedType = Nullable.GetUnderlyingType(ctorParameters[i].ParameterType) ?? ctorParameters[i].ParameterType;
+ if (unboxedType != types[i]
+ && !(unboxedType.IsEnum && Enum.GetUnderlyingType(unboxedType) == types[i])
+ && !(unboxedType == typeof(char) && types[i] == typeof(string)))
+ break;
+ }
+
+ if (i == ctorParameters.Length)
+ return ctor;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets mapping for constructor parameter
+ ///
+ /// Constructor to resolve
+ /// DataReader column name
+ /// Mapping implementation
+ public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
+ {
+ var parameters = constructor.GetParameters();
+
+ return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ ///
+ /// Gets member mapping for column
+ ///
+ /// DataReader column name
+ /// Mapping implementation
+ public SqlMapper.IMemberMap GetMember(string columnName)
+ {
+ var property = _properties.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.Ordinal))
+ ?? _properties.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase));
+
+ if (property != null)
+ return new SimpleMemberMap(columnName, property);
+
+ var field = _fields.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.Ordinal))
+ ?? _fields.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase));
+
+ if (field != null)
+ return new SimpleMemberMap(columnName, field);
+
+ return null;
+ }
+ }
+
+ ///
+ /// Implements custom property mapping by user provided criteria (usually presence of some custom attribute with column to member mapping)
+ ///
+ public sealed class CustomPropertyTypeMap : SqlMapper.ITypeMap
+ {
+ private readonly Type _type;
+ private readonly Func _propertySelector;
+
+ ///
+ /// Creates custom property mapping
+ ///
+ /// Target entity type
+ /// Property selector based on target type and DataReader column name
+ public CustomPropertyTypeMap(Type type, Func propertySelector)
+ {
+ if (type == null)
+ throw new ArgumentNullException("type");
+
+ if (propertySelector == null)
+ throw new ArgumentNullException("propertySelector");
+
+ _type = type;
+ _propertySelector = propertySelector;
+ }
+
+ ///
+ /// Always returns default constructor
+ ///
+ /// DataReader column names
+ /// DataReader column types
+ /// Default constructor
+ public ConstructorInfo FindConstructor(string[] names, Type[] types)
+ {
+ return _type.GetConstructor(new Type[0]);
+ }
+
+ ///
+ /// Not impelmeneted as far as default constructor used for all cases
+ ///
+ ///
+ ///
+ ///
+ public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Returns property based on selector strategy
+ ///
+ /// DataReader column name
+ /// Poperty member map
+ public SqlMapper.IMemberMap GetMember(string columnName)
+ {
+ var prop = _propertySelector(_type, columnName);
+ return prop != null ? new SimpleMemberMap(columnName, prop) : null;
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/Tests/Tests.cs b/Tests/Tests.cs
index eaf1c4e97b4c0c74d5e4c844ff5e34ce2747d5d8..8194c2870f85dadaf7c7f9b0b6a2383ed1551ec3 100644
--- a/Tests/Tests.cs
+++ b/Tests/Tests.cs
@@ -10,6 +10,7 @@
using System.Collections;
using System.Reflection;
using System.Dynamic;
+using System.ComponentModel;
#if POSTGRESQL
using Npgsql;
#endif
@@ -1863,6 +1864,38 @@ class ResultsChangeType
public int Z { get; set; }
}
+ public void TestCustomTypeMap()
+ {
+ // default mapping
+ var item = connection.Query("Select 'AVal' as A, 'BVal' as B").Single();
+ item.A.IsEqualTo("AVal");
+ item.B.IsEqualTo("BVal");
+
+ // custom mapping
+ var map = new CustomPropertyTypeMap(typeof(TypeWithMapping),
+ (type, columnName) => type.GetProperties().Where(prop => prop.GetCustomAttributes(false).OfType().Any(attr => attr.Description == columnName)).FirstOrDefault());
+ Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), map);
+
+ item = connection.Query("Select 'AVal' as A, 'BVal' as B").Single();
+ item.A.IsEqualTo("BVal");
+ item.B.IsEqualTo("AVal");
+
+ // reset to default
+ Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), null);
+ item = connection.Query("Select 'AVal' as A, 'BVal' as B").Single();
+ item.A.IsEqualTo("AVal");
+ item.B.IsEqualTo("BVal");
+ }
+
+ public class TypeWithMapping
+ {
+ [Description("B")]
+ public string A { get; set; }
+
+ [Description("A")]
+ public string B { get; set; }
+ }
+
class TransactedConnection : IDbConnection
{
IDbConnection _conn;