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

Add custom mapping of properties to columns #1349

Closed
wants to merge 1 commit into from
Closed
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
244 changes: 237 additions & 7 deletions Dapper.Contrib/SqlMapperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,169 @@
using System.Threading;

using Dapper;
using System.Linq.Expressions;

namespace Dapper.Contrib.Extensions
{
/// <summary>
/// Class to build custom mappings
/// </summary>
/// <typeparam name="T">Type for which the custom mappings are built</typeparam>
public class MappingConfigurationFor<T>
{
/// <summary>
/// Mapped properties
/// </summary>
public IEnumerable<PropertyInfo> TypeProperties => typeProperties.AsEnumerable();
/// <summary>
/// Mapped auto-generated key properties
/// </summary>
public IEnumerable<PropertyInfo> KeyProperties => keyProperties.AsEnumerable();
/// <summary>
/// Mapped explicit key properties
/// </summary>
public IEnumerable<PropertyInfo> ExplicitKeyProperties => explicitKeyProperties.AsEnumerable();
/// <summary>
/// Mapped computed properties
/// </summary>
public IEnumerable<PropertyInfo> ComputedProperties => computedProperties.AsEnumerable();

List<PropertyInfo> typeProperties = new List<PropertyInfo>();
List<PropertyInfo> keyProperties = new List<PropertyInfo>();
List<PropertyInfo> explicitKeyProperties = new List<PropertyInfo>();
List<PropertyInfo> computedProperties = new List<PropertyInfo>();
Dictionary<string, PropertyInfo> columnMappings = new Dictionary<string, PropertyInfo>();

private static PropertyInfo GetPropertyInfo<TProperty>(Expression<Func<T, TProperty>> propertySelector)
{
var propertyName = (propertySelector.Body as MemberExpression)?.Member.Name
?? throw new InvalidOperationException("Expression does not point to a property");
return typeof(T).GetProperty(propertyName)
?? throw new InvalidOperationException("Expression does not point to a property");
}

/// <summary>
/// Map property
/// </summary>
/// <typeparam name="TProperty">Property type</typeparam>
/// <param name="propertySelector">Property selector like this: <code>p => p.PropertyName</code></param>
/// <returns>This object</returns>
public MappingConfigurationFor<T> Map<TProperty>(Expression<Func<T, TProperty>> propertySelector)
{
typeProperties.Add(GetPropertyInfo(propertySelector));
return this;
}

/// <summary>
/// Map autogenerated key property
/// </summary>
/// <typeparam name="TProperty">Property type</typeparam>
/// <param name="propertySelector">Property selector like this: <code>p => p.PropertyName</code></param>
/// <returns>This object</returns>
public MappingConfigurationFor<T> MapKey<TProperty>(Expression<Func<T, TProperty>> propertySelector)
{
var propertyInfo = GetPropertyInfo(propertySelector);
typeProperties.Add(propertyInfo);
keyProperties.Add(propertyInfo);
return this;
}

/// <summary>
/// Map explicit key property
/// </summary>
/// <typeparam name="TProperty">Property type</typeparam>
/// <param name="propertySelector">Property selector like this: <code>p => p.PropertyName</code></param>
/// <returns>This object</returns>
public MappingConfigurationFor<T> MapExplicitKey<TProperty>(Expression<Func<T, TProperty>> propertySelector)
{
var propertyInfo = GetPropertyInfo(propertySelector);
typeProperties.Add(propertyInfo);
explicitKeyProperties.Add(propertyInfo);
return this;
}

/// <summary>
/// Map computed property
/// </summary>
/// <typeparam name="TProperty">Property type</typeparam>
/// <param name="propertySelector">Property selector like this: <code>p => p.PropertyName</code></param>
/// <returns>This object</returns>
public MappingConfigurationFor<T> MapComputed<TProperty>(Expression<Func<T, TProperty>> propertySelector)
{
var propertyInfo = GetPropertyInfo(propertySelector);
typeProperties.Add(propertyInfo);
computedProperties.Add(propertyInfo);
return this;
}

/// <summary>
/// Map property
/// </summary>
/// <typeparam name="TProperty">Property type</typeparam>
/// <param name="propertySelector">Property selector like this: <code>p => p.PropertyName</code></param>
/// <param name="columnName">Column name to which map the property</param>
/// <returns>This object</returns>
public MappingConfigurationFor<T> Map<TProperty>(Expression<Func<T, TProperty>> propertySelector, string columnName)
{
Map(propertySelector);
columnMappings.Add(columnName, GetPropertyInfo(propertySelector));
return this;
}

/// <summary>
/// Map auto-generated key property
/// </summary>
/// <typeparam name="TProperty">Property type</typeparam>
/// <param name="propertySelector">Property selector like this: <code>p => p.PropertyName</code></param>
/// <param name="columnName">Column name to which map the property</param>
/// <returns>This object</returns>
public MappingConfigurationFor<T> MapKey<TProperty>(Expression<Func<T, TProperty>> propertySelector, string columnName)
{
MapKey(propertySelector);
columnMappings.Add(columnName, GetPropertyInfo(propertySelector));
return this;
}

/// <summary>
/// Map explicit key property
/// </summary>
/// <typeparam name="TProperty">Property type</typeparam>
/// <param name="propertySelector">Property selector like this: <code>p => p.PropertyName</code></param>
/// <param name="columnName">Column name to which map the property</param>
/// <returns>This object</returns>
public MappingConfigurationFor<T> MapExplicitKey<TProperty>(Expression<Func<T, TProperty>> propertySelector, string columnName)
{
MapExplicitKey(propertySelector);
columnMappings.Add(columnName, GetPropertyInfo(propertySelector));
return this;
}

/// <summary>
/// Map computed property
/// </summary>
/// <typeparam name="TProperty">Property type</typeparam>
/// <param name="propertySelector">Property selector like this: <code>p => p.PropertyName</code></param>
/// <param name="columnName">Column name to which map the property</param>
/// <returns>This object</returns>
public MappingConfigurationFor<T> MapComputed<TProperty>(Expression<Func<T, TProperty>> propertySelector, string columnName)
{
MapComputed(propertySelector);
columnMappings.Add(columnName, GetPropertyInfo(propertySelector));
return this;
}

/// <summary>
/// Call this method when all mappings have been defined to apply them.
/// </summary>
public MappingConfigurationFor<T> LoadConfiguration()
{
SqlMapperExtensions.SetTypeMap<T>(typeProperties, keyProperties, explicitKeyProperties, computedProperties);
foreach (var keyValuePair in columnMappings)
SqlMapperExtensions.SetColumnMap<T>(keyValuePair.Value, keyValuePair.Key);
return this;
}
}

/// <summary>
/// The Dapper.Contrib extensions for Dapper
/// </summary>
Expand Down Expand Up @@ -52,10 +212,38 @@ public interface ITableNameMapper
/// <param name="type">The <see cref="Type"/> to get a table name for.</param>
public delegate string TableNameMapperDelegate(Type type);

private class PropertyId
{
public readonly RuntimeTypeHandle typeHandle;
public readonly string propertyName;

public PropertyId(RuntimeTypeHandle typeHandle, string propertyName)
{
this.typeHandle = typeHandle;
this.propertyName = propertyName;
}

public override bool Equals(object obj)
{
return obj is PropertyId id
&& typeHandle.Equals(id.typeHandle)
&& propertyName == id.propertyName;
}

public override int GetHashCode()
{
var hashCode = -1937055282;
hashCode = hashCode * -1521134295 + typeHandle.GetHashCode();
hashCode = hashCode * -1521134295 + propertyName.GetHashCode();
return hashCode;
}
}

private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> KeyProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> ExplicitKeyProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> TypeProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> ComputedProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
private static ConcurrentDictionary<PropertyId, string> CustomColumnNameMap = null;
private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> GetQueries = new ConcurrentDictionary<RuntimeTypeHandle, string>();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> TypeTableName = new ConcurrentDictionary<RuntimeTypeHandle, string>();

Expand All @@ -71,6 +259,48 @@ private static readonly Dictionary<string, ISqlAdapter> AdapterDictionary
["fbconnection"] = new FbAdapter()
};

/// <summary>
/// Allows the creation of a custom type map.
/// </summary>
/// <typeparam name="T">The type for which to create the custom map</typeparam>
/// <param name="typeProperties">PropertyInfo objects pointing to all writable properties of the object, including primary keys if applicable.</param>
/// <param name="keyProperties">PropertyInfo objects pointing to the implicitly set primary key property of the object.</param>
/// <param name="explicitKeyProperties">PropertyInfo objects pointing to explicitly set primary key properties of the object.</param>
/// <param name="computedProperties">PropertyInfo objects pointing to the computed properties of the object.</param>
public static void SetTypeMap<T>(
IEnumerable<PropertyInfo> typeProperties,
IEnumerable<PropertyInfo> keyProperties,
IEnumerable<PropertyInfo> explicitKeyProperties,
IEnumerable<PropertyInfo> computedProperties)
{
var typeHandle = typeof(T).TypeHandle;
TypeProperties[typeHandle] = typeProperties.ToList();
KeyProperties[typeHandle] = keyProperties.ToList();
ExplicitKeyProperties[typeHandle] = explicitKeyProperties.ToList();
ComputedProperties[typeHandle] = computedProperties.ToList();
}

/// <summary>
/// Allows mapping of propertis to columns with different names
/// </summary>
/// <typeparam name="T">Type that contains the property to map</typeparam>
/// <param name="propertyInfo">PropertyInfo object pointing to the property to map</param>
/// <param name="columnName">Name of the column that the property should be mapped to</param>
public static void SetColumnMap<T>(PropertyInfo propertyInfo, string columnName)
{
if (CustomColumnNameMap is null)
CustomColumnNameMap = new ConcurrentDictionary<PropertyId, string>();
CustomColumnNameMap[new PropertyId(typeof(T).TypeHandle, propertyInfo.Name)] = columnName;
}

private static string GetColumnNameFor(Type type, PropertyInfo propertyInfo)
{
if (CustomColumnNameMap != null && CustomColumnNameMap.TryGetValue(new PropertyId(type.TypeHandle, propertyInfo.Name), out string columnName))
return columnName;
else
return propertyInfo.Name;
}

private static List<PropertyInfo> ComputedPropertiesCache(Type type)
{
if (ComputedProperties.TryGetValue(type.TypeHandle, out IEnumerable<PropertyInfo> pi))
Expand Down Expand Up @@ -176,7 +406,7 @@ public static T Get<T>(this IDbConnection connection, dynamic id, IDbTransaction
var key = GetSingleKey<T>(nameof(Get));
var name = GetTableName(type);

sql = $"select * from {name} where {key.Name} = @id";
sql = $"select * from {name} where {GetColumnNameFor(type, key)} = @id";
GetQueries[type.TypeHandle] = sql;
}

Expand All @@ -196,7 +426,7 @@ public static T Get<T>(this IDbConnection connection, dynamic id, IDbTransaction

foreach (var property in TypePropertiesCache(type))
{
var val = res[property.Name];
var val = res[GetColumnNameFor(type, property)];
if (val == null) continue;
if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Expand Down Expand Up @@ -252,7 +482,7 @@ public static IEnumerable<T> GetAll<T>(this IDbConnection connection, IDbTransac
var obj = ProxyGenerator.GetInterfaceProxy<T>();
foreach (var property in TypePropertiesCache(type))
{
var val = res[property.Name];
var val = res[GetColumnNameFor(type, property)];
if (val == null) continue;
if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Expand Down Expand Up @@ -352,7 +582,7 @@ public static long Insert<T>(this IDbConnection connection, T entityToInsert, ID
for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++)
{
var property = allPropertiesExceptKeyAndComputed[i];
adapter.AppendColumnName(sbColumnList, property.Name); //fix for issue #336
adapter.AppendColumnName(sbColumnList, GetColumnNameFor(type, property)); //fix for issue #336
if (i < allPropertiesExceptKeyAndComputed.Count - 1)
sbColumnList.Append(", ");
}
Expand Down Expand Up @@ -440,15 +670,15 @@ public static bool Update<T>(this IDbConnection connection, T entityToUpdate, ID
for (var i = 0; i < nonIdProps.Count; i++)
{
var property = nonIdProps[i];
adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336
adapter.AppendColumnNameEqualsValue(sb, GetColumnNameFor(type, property)); //fix for issue #336
if (i < nonIdProps.Count - 1)
sb.Append(", ");
}
sb.Append(" where ");
for (var i = 0; i < keyProperties.Count; i++)
{
var property = keyProperties[i];
adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336
adapter.AppendColumnNameEqualsValue(sb, GetColumnNameFor(type, property)); //fix for issue #336
if (i < keyProperties.Count - 1)
sb.Append(" and ");
}
Expand Down Expand Up @@ -505,7 +735,7 @@ public static bool Delete<T>(this IDbConnection connection, T entityToDelete, ID
for (var i = 0; i < keyProperties.Count; i++)
{
var property = keyProperties[i];
adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336
adapter.AppendColumnNameEqualsValue(sb, GetColumnNameFor(type, property)); //fix for issue #336
if (i < keyProperties.Count - 1)
sb.Append(" and ");
}
Expand Down