using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Reflection; using System.Text; using System.Collections.Concurrent; using System.Reflection.Emit; using System.Threading; using Dapper; #pragma warning disable 1573, 1591 // xml comments namespace Dapper.Contrib.Extensions { public static partial class SqlMapperExtensions { // ReSharper disable once MemberCanBePrivate.Global public interface IProxy //must be kept public { bool IsDirty { get; set; } } private static readonly ConcurrentDictionary> KeyProperties = new ConcurrentDictionary>(); private static readonly ConcurrentDictionary> TypeProperties = new ConcurrentDictionary>(); private static readonly ConcurrentDictionary> ComputedProperties = new ConcurrentDictionary>(); private static readonly ConcurrentDictionary GetQueries = new ConcurrentDictionary(); private static readonly ConcurrentDictionary TypeTableName = new ConcurrentDictionary(); private static readonly Dictionary AdapterDictionary = new Dictionary { {"sqlconnection", new SqlServerAdapter()}, {"npgsqlconnection", new PostgresAdapter()}, {"sqliteconnection", new SQLiteAdapter()} }; private static IEnumerable ComputedPropertiesCache(Type type) { IEnumerable pi; if (ComputedProperties.TryGetValue(type.TypeHandle, out pi)) { return pi.ToList(); } var computedProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ComputedAttribute)).ToList(); ComputedProperties[type.TypeHandle] = computedProperties; return computedProperties; } private static List KeyPropertiesCache(Type type) { IEnumerable pi; if (KeyProperties.TryGetValue(type.TypeHandle, out pi)) { return pi.ToList(); } var allProperties = TypePropertiesCache(type); var keyProperties = allProperties.Where(p => p.GetCustomAttributes(true).Any(a => a is KeyAttribute)).ToList(); if (keyProperties.Count == 0) { var idProp = allProperties.FirstOrDefault(p => p.Name.ToLower() == "id"); if (idProp != null) { keyProperties.Add(idProp); } } KeyProperties[type.TypeHandle] = keyProperties; return keyProperties; } private static List TypePropertiesCache(Type type) { IEnumerable pis; if (TypeProperties.TryGetValue(type.TypeHandle, out pis)) { return pis.ToList(); } var properties = type.GetProperties().Where(IsWriteable).ToArray(); TypeProperties[type.TypeHandle] = properties; return properties.ToList(); } private static bool IsWriteable(PropertyInfo pi) { var attributes = pi.GetCustomAttributes(typeof(WriteAttribute), false); if (attributes.Length != 1) return true; var writeAttribute = (WriteAttribute)attributes[0]; return writeAttribute.Write; } /// /// Returns a single entity by a single id from table "Ts". /// Id must be marked with [Key] attribute. /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension /// for optimal performance. /// /// Interface or type to create and populate /// Open SqlConnection /// Id of the entity to get, must be marked with [Key] attribute /// Entity of T public static T Get(this IDbConnection connection, dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class { var type = typeof(T); string sql; if (!GetQueries.TryGetValue(type.TypeHandle, out sql)) { var keys = KeyPropertiesCache(type); if (keys.Count() > 1) throw new DataException("Get only supports an entity with a single [Key] property"); if (!keys.Any()) throw new DataException("Get only supports en entity with a [Key] property"); var onlyKey = keys.First(); var name = GetTableName(type); // TODO: pluralizer // TODO: query information schema and only select fields that are both in information schema and underlying class / interface sql = "select * from " + name + " where " + onlyKey.Name + " = @id"; GetQueries[type.TypeHandle] = sql; } var dynParms = new DynamicParameters(); dynParms.Add("@id", id); T obj; if (type.IsInterface) { var res = connection.Query(sql, dynParms).FirstOrDefault() as IDictionary; if (res == null) return null; obj = ProxyGenerator.GetInterfaceProxy(); foreach (var property in TypePropertiesCache(type)) { var val = res[property.Name]; property.SetValue(obj, val, null); } ((IProxy)obj).IsDirty = false; //reset change tracking and return } else { obj = connection.Query(sql, dynParms, transaction, commandTimeout: commandTimeout).FirstOrDefault(); } return obj; } private static string GetTableName(Type type) { string name; if (!TypeTableName.TryGetValue(type.TypeHandle, out name)) { name = type.Name + "s"; if (type.IsInterface && name.StartsWith("I")) name = name.Substring(1); //NOTE: This as dynamic trick should be able to handle both our own Table-attribute as well as the one in EntityFramework var tableattr = type.GetCustomAttributes(false).SingleOrDefault(attr => attr.GetType().Name == "TableAttribute") as dynamic; if (tableattr != null) name = tableattr.Name; TypeTableName[type.TypeHandle] = name; } return name; } /// /// Inserts an entity into table "Ts" and returns identity id. /// /// Open SqlConnection /// Entity to insert /// Identity of inserted entity public static long Insert(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, int? commandTimeout = null) where T : class { var type = typeof(T); var name = GetTableName(type); var sbColumnList = new StringBuilder(null); var allProperties = TypePropertiesCache(type); var keyProperties = KeyPropertiesCache(type); var computedProperties = ComputedPropertiesCache(type); var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count(); i++) { var property = allPropertiesExceptKeyAndComputed.ElementAt(i); sbColumnList.AppendFormat("[{0}]", property.Name); if (i < allPropertiesExceptKeyAndComputed.Count() - 1) sbColumnList.Append(", "); } var sbParameterList = new StringBuilder(null); for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count(); i++) { var property = allPropertiesExceptKeyAndComputed.ElementAt(i); sbParameterList.AppendFormat("@{0}", property.Name); if (i < allPropertiesExceptKeyAndComputed.Count() - 1) sbParameterList.Append(", "); } var adapter = GetFormatter(connection); return adapter.Insert(connection, transaction, commandTimeout, name, sbColumnList.ToString(), sbParameterList.ToString(), keyProperties, entityToInsert); } /// /// Updates entity in table "Ts", checks if the entity is modified if the entity is tracked by the Get() extension. /// /// Type to be updated /// Open SqlConnection /// Entity to be updated /// true if updated, false if not found or not modified (tracked entities) public static bool Update(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class { var proxy = entityToUpdate as IProxy; if (proxy != null) { if (!proxy.IsDirty) return false; } var type = typeof(T); var keyProperties = KeyPropertiesCache(type); if (!keyProperties.Any()) throw new ArgumentException("Entity must have at least one [Key] property"); var name = GetTableName(type); var sb = new StringBuilder(); sb.AppendFormat("update {0} set ", name); var allProperties = TypePropertiesCache(type); var computedProperties = ComputedPropertiesCache(type); var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); for (var i = 0; i < nonIdProps.Count(); i++) { var property = nonIdProps.ElementAt(i); sb.AppendFormat("{0} = @{1}", property.Name, property.Name); if (i < nonIdProps.Count() - 1) sb.AppendFormat(", "); } sb.Append(" where "); for (var i = 0; i < keyProperties.Count(); i++) { var property = keyProperties.ElementAt(i); sb.AppendFormat("{0} = @{1}", property.Name, property.Name); if (i < keyProperties.Count() - 1) sb.AppendFormat(" and "); } var updated = connection.Execute(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction); return updated > 0; } /// /// Delete entity in table "Ts". /// /// Type of entity /// Open SqlConnection /// Entity to delete /// true if deleted, false if not found public static bool Delete(this IDbConnection connection, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null) where T : class { if (entityToDelete == null) throw new ArgumentException("Cannot Delete null Object", "entityToDelete"); var type = typeof(T); var keyProperties = KeyPropertiesCache(type); if (!keyProperties.Any()) throw new ArgumentException("Entity must have at least one [Key] property"); var name = GetTableName(type); var sb = new StringBuilder(); sb.AppendFormat("delete from {0} where ", name); for (var i = 0; i < keyProperties.Count(); i++) { var property = keyProperties.ElementAt(i); sb.AppendFormat("{0} = @{1}", property.Name, property.Name); if (i < keyProperties.Count() - 1) sb.AppendFormat(" and "); } var deleted = connection.Execute(sb.ToString(), entityToDelete, transaction, commandTimeout); return deleted > 0; } /// /// Delete all entities in the table related to the type T. /// /// Type of entity /// Open SqlConnection /// true if deleted, false if none found public static bool DeleteAll(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class { var type = typeof(T); var name = GetTableName(type); var statement = String.Format("delete from {0}", name); var deleted = connection.Execute(statement, null, transaction, commandTimeout); return deleted > 0; } private static ISqlAdapter GetFormatter(IDbConnection connection) { var name = connection.GetType().Name.ToLower(); return !AdapterDictionary.ContainsKey(name) ? new SqlServerAdapter() : AdapterDictionary[name]; } static class ProxyGenerator { private static readonly Dictionary TypeCache = new Dictionary(); private static AssemblyBuilder GetAsmBuilder(string name) { var assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName { Name = name }, AssemblyBuilderAccess.Run); //NOTE: to save, use RunAndSave return assemblyBuilder; } public static T GetInterfaceProxy() { Type typeOfT = typeof(T); object k; if (TypeCache.TryGetValue(typeOfT, out k)) { return (T)k; } var assemblyBuilder = GetAsmBuilder(typeOfT.Name); var moduleBuilder = assemblyBuilder.DefineDynamicModule("SqlMapperExtensions." + typeOfT.Name); //NOTE: to save, add "asdasd.dll" parameter var interfaceType = typeof(IProxy); var typeBuilder = moduleBuilder.DefineType(typeOfT.Name + "_" + Guid.NewGuid(), TypeAttributes.Public | TypeAttributes.Class); typeBuilder.AddInterfaceImplementation(typeOfT); typeBuilder.AddInterfaceImplementation(interfaceType); //create our _isDirty field, which implements IProxy var setIsDirtyMethod = CreateIsDirtyProperty(typeBuilder); // Generate a field for each property, which implements the T foreach (var property in typeof(T).GetProperties()) { var isId = property.GetCustomAttributes(true).Any(a => a is KeyAttribute); CreateProperty(typeBuilder, property.Name, property.PropertyType, setIsDirtyMethod, isId); } var generatedType = typeBuilder.CreateType(); //assemblyBuilder.Save(name + ".dll"); //NOTE: to save, uncomment var generatedObject = Activator.CreateInstance(generatedType); TypeCache.Add(typeOfT, generatedObject); return (T)generatedObject; } private static MethodInfo CreateIsDirtyProperty(TypeBuilder typeBuilder) { var propType = typeof(bool); var field = typeBuilder.DefineField("_" + "IsDirty", propType, FieldAttributes.Private); var property = typeBuilder.DefineProperty("IsDirty", System.Reflection.PropertyAttributes.None, propType, new[] { propType }); const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig; // Define the "get" and "set" accessor methods var currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + "IsDirty", getSetAttr, propType, Type.EmptyTypes); var currGetIl = currGetPropMthdBldr.GetILGenerator(); currGetIl.Emit(OpCodes.Ldarg_0); currGetIl.Emit(OpCodes.Ldfld, field); currGetIl.Emit(OpCodes.Ret); var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + "IsDirty", getSetAttr, null, new[] { propType }); var currSetIl = currSetPropMthdBldr.GetILGenerator(); currSetIl.Emit(OpCodes.Ldarg_0); currSetIl.Emit(OpCodes.Ldarg_1); currSetIl.Emit(OpCodes.Stfld, field); currSetIl.Emit(OpCodes.Ret); property.SetGetMethod(currGetPropMthdBldr); property.SetSetMethod(currSetPropMthdBldr); var getMethod = typeof(IProxy).GetMethod("get_" + "IsDirty"); var setMethod = typeof(IProxy).GetMethod("set_" + "IsDirty"); typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod); typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod); return currSetPropMthdBldr; } private static void CreateProperty(TypeBuilder typeBuilder, string propertyName, Type propType, MethodInfo setIsDirtyMethod, bool isIdentity) { //Define the field and the property var field = typeBuilder.DefineField("_" + propertyName, propType, FieldAttributes.Private); var property = typeBuilder.DefineProperty(propertyName, System.Reflection.PropertyAttributes.None, propType, new[] { propType }); const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig; // Define the "get" and "set" accessor methods var currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName, getSetAttr, propType, Type.EmptyTypes); var currGetIl = currGetPropMthdBldr.GetILGenerator(); currGetIl.Emit(OpCodes.Ldarg_0); currGetIl.Emit(OpCodes.Ldfld, field); currGetIl.Emit(OpCodes.Ret); var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + propertyName, getSetAttr, null, new[] { propType }); //store value in private field and set the isdirty flag var currSetIl = currSetPropMthdBldr.GetILGenerator(); currSetIl.Emit(OpCodes.Ldarg_0); currSetIl.Emit(OpCodes.Ldarg_1); currSetIl.Emit(OpCodes.Stfld, field); currSetIl.Emit(OpCodes.Ldarg_0); currSetIl.Emit(OpCodes.Ldc_I4_1); currSetIl.Emit(OpCodes.Call, setIsDirtyMethod); currSetIl.Emit(OpCodes.Ret); //TODO: Should copy all attributes defined by the interface? if (isIdentity) { var keyAttribute = typeof(KeyAttribute); var myConstructorInfo = keyAttribute.GetConstructor(new Type[] { }); var attributeBuilder = new CustomAttributeBuilder(myConstructorInfo, new object[] { }); property.SetCustomAttribute(attributeBuilder); } property.SetGetMethod(currGetPropMthdBldr); property.SetSetMethod(currSetPropMthdBldr); var getMethod = typeof(T).GetMethod("get_" + propertyName); var setMethod = typeof(T).GetMethod("set_" + propertyName); typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod); typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod); } } } [AttributeUsage(AttributeTargets.Class)] public class TableAttribute : Attribute { public TableAttribute(string tableName) { Name = tableName; } // ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once UnusedAutoPropertyAccessor.Global public string Name { get; set; } } // do not want to depend on data annotations that is not in client profile [AttributeUsage(AttributeTargets.Property)] public class KeyAttribute : Attribute { } [AttributeUsage(AttributeTargets.Property)] public class WriteAttribute : Attribute { public WriteAttribute(bool write) { Write = write; } public bool Write { get; private set; } } [AttributeUsage(AttributeTargets.Property)] public class ComputedAttribute : Attribute { } } public partial interface ISqlAdapter { int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert); } public partial class SqlServerAdapter : ISqlAdapter { public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) { var cmd = String.Format("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList); connection.Execute(cmd, entityToInsert, transaction, commandTimeout); //NOTE: would prefer to use IDENT_CURRENT('tablename') or IDENT_SCOPE but these are not available on SQLCE var r = connection.Query("select @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout); var id = (int)r.First().id; var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); if (propertyInfos.Any()) propertyInfos.First().SetValue(entityToInsert, id, null); return id; } } public partial class PostgresAdapter : ISqlAdapter { public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) { var sb = new StringBuilder(); sb.AppendFormat("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList); // If no primary key then safe to assume a join table with not too much data to return var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); if (!propertyInfos.Any()) sb.Append(" RETURNING *"); else { sb.Append(" RETURNING "); var first = true; foreach (var property in propertyInfos) { if (!first) sb.Append(", "); first = false; sb.Append(property.Name); } } var results = connection.Query(sb.ToString(), entityToInsert, transaction, commandTimeout: commandTimeout).ToList(); // Return the key by assinging the corresponding property in the object - by product is that it supports compound primary keys var id = 0; foreach (var p in propertyInfos) { var value = ((IDictionary)results.First())[p.Name.ToLower()]; p.SetValue(entityToInsert, value, null); if (id == 0) id = Convert.ToInt32(value); } return id; } } public partial class SQLiteAdapter : ISqlAdapter { public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) { var cmd = String.Format("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList); connection.Execute(cmd, entityToInsert, transaction, commandTimeout); var r = connection.Query("select last_insert_rowid() id", transaction: transaction, commandTimeout: commandTimeout); var id = (int)r.First().id; var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); if (propertyInfos.Any()) propertyInfos.First().SetValue(entityToInsert, id, null); return id; } }