diff --git a/Dapper.sln b/Dapper.sln index e993c7a4..4aa75f10 100644 --- a/Dapper.sln +++ b/Dapper.sln @@ -16,7 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution version.json = version.json EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Contrib", "Dapper.Contrib\Dapper.Contrib.csproj", "{4E409F8F-CFBB-4332-8B0A-FD5A283051FD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Contrib", "src\Dapper.Contrib\Dapper.Contrib.csproj", "{4E409F8F-CFBB-4332-8B0A-FD5A283051FD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Contrib", "tests\Dapper.Tests.Contrib\Dapper.Tests.Contrib.csproj", "{DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}" EndProject diff --git a/src/Dapper.Contrib/SqlMapperExtensions.Async.cs b/src/Dapper.Contrib/SqlMapperExtensions.Async.cs index c93e39a4..336987eb 100644 --- a/src/Dapper.Contrib/SqlMapperExtensions.Async.cs +++ b/src/Dapper.Contrib/SqlMapperExtensions.Async.cs @@ -22,7 +22,8 @@ public static partial class SqlMapperExtensions /// The transaction to run under, null (the default) if none /// Number of seconds before command execution timeout /// Entity of T - public static async Task GetAsync(this IDbConnection connection, dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + public static async Task GetAsync(this IDbConnection connection, dynamic id, + IDbTransaction transaction = null, int? commandTimeout = null) where T : class { var type = typeof(T); if (!GetQueries.TryGetValue(type.TypeHandle, out string sql)) @@ -38,9 +39,11 @@ public static async Task GetAsync(this IDbConnection connection, dynamic i dynParams.Add("@id", id); if (!type.IsInterface) - return (await connection.QueryAsync(sql, dynParams, transaction, commandTimeout).ConfigureAwait(false)).FirstOrDefault(); + return (await connection.QueryAsync(sql, dynParams, transaction, commandTimeout) + .ConfigureAwait(false)).FirstOrDefault(); - if (!((await connection.QueryAsync(sql, dynParams).ConfigureAwait(false)).FirstOrDefault() is IDictionary res)) + if (!((await connection.QueryAsync(sql, dynParams).ConfigureAwait(false)).FirstOrDefault() is + IDictionary res)) { return null; } @@ -51,7 +54,8 @@ public static async Task GetAsync(this IDbConnection connection, dynamic i { var val = res[property.Name]; if (val == null) continue; - if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + if (property.PropertyType.IsGenericType && + property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { var genericType = Nullable.GetUnderlyingType(property.PropertyType); if (genericType != null) property.SetValue(obj, Convert.ChangeType(val, genericType), null); @@ -62,7 +66,7 @@ public static async Task GetAsync(this IDbConnection connection, dynamic i } } - ((IProxy)obj).IsDirty = false; //reset change tracking and return + ((IProxy)obj).IsDirty = false; //reset change tracking and return return obj; } @@ -78,7 +82,8 @@ public static async Task GetAsync(this IDbConnection connection, dynamic i /// The transaction to run under, null (the default) if none /// Number of seconds before command execution timeout /// Entity of T - public static Task> GetAllAsync(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + public static Task> GetAllAsync(this IDbConnection connection, + IDbTransaction transaction = null, int? commandTimeout = null) where T : class { var type = typeof(T); var cacheType = typeof(List); @@ -96,12 +101,15 @@ public static Task> GetAllAsync(this IDbConnection connection, { return connection.QueryAsync(sql, null, transaction, commandTimeout); } + return GetAllAsyncImpl(connection, transaction, commandTimeout, sql, type); } - private static async Task> GetAllAsyncImpl(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string sql, Type type) where T : class + private static async Task> GetAllAsyncImpl(IDbConnection connection, + IDbTransaction transaction, int? commandTimeout, string sql, Type type) where T : class { - var result = await connection.QueryAsync(sql, transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); + var result = await connection.QueryAsync(sql, transaction: transaction, commandTimeout: commandTimeout) + .ConfigureAwait(false); var list = new List(); foreach (IDictionary res in result) { @@ -110,7 +118,8 @@ private static async Task> GetAllAsyncImpl(IDbConnection conne { var val = res[property.Name]; if (val == null) continue; - if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + if (property.PropertyType.IsGenericType && + property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { var genericType = Nullable.GetUnderlyingType(property.PropertyType); if (genericType != null) property.SetValue(obj, Convert.ChangeType(val, genericType), null); @@ -120,9 +129,11 @@ private static async Task> GetAllAsyncImpl(IDbConnection conne property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); } } - ((IProxy)obj).IsDirty = false; //reset change tracking and return + + ((IProxy)obj).IsDirty = false; //reset change tracking and return list.Add(obj); } + return list; } @@ -136,7 +147,8 @@ private static async Task> GetAllAsyncImpl(IDbConnection conne /// Number of seconds before command execution timeout /// The specific ISqlAdapter to use, auto-detected based on connection if null /// Identity of inserted entity - public static Task InsertAsync(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, + public static Task InsertAsync(this IDbConnection connection, T entityToInsert, + IDbTransaction transaction = null, int? commandTimeout = null, ISqlAdapter sqlAdapter = null) where T : class { var type = typeof(T); @@ -152,7 +164,8 @@ public static Task InsertAsync(this IDbConnection connection, T entityTo { var typeInfo = type.GetTypeInfo(); bool implementsGenericIEnumerableOrIsGenericIEnumerable = - typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || + typeInfo.ImplementedInterfaces.Any(ti => + ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); if (implementsGenericIEnumerableOrIsGenericIEnumerable) @@ -167,7 +180,8 @@ public static Task InsertAsync(this IDbConnection connection, T entityTo var allProperties = TypePropertiesCache(type); var keyProperties = KeyPropertiesCache(type).ToList(); var computedProperties = ComputedPropertiesCache(type); - var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); + var allPropertiesExceptKeyAndComputed = + allProperties.Except(keyProperties.Union(computedProperties)).ToList(); for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) { @@ -186,7 +200,7 @@ public static Task InsertAsync(this IDbConnection connection, T entityTo sbParameterList.Append(", "); } - if (!isList) //single entity + if (!isList) //single entity { return sqlAdapter.InsertAsync(connection, transaction, commandTimeout, name, sbColumnList.ToString(), sbParameterList.ToString(), keyProperties, entityToInsert); @@ -206,7 +220,8 @@ public static Task InsertAsync(this IDbConnection connection, T entityTo /// The transaction to run under, null (the default) if none /// Number of seconds before command execution timeout /// true if updated, false if not found or not modified (tracked entities) - public static async Task UpdateAsync(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + public static async Task UpdateAsync(this IDbConnection connection, T entityToUpdate, + IDbTransaction transaction = null, int? commandTimeout = null) where T : class { if ((entityToUpdate is IProxy proxy) && !proxy.IsDirty) { @@ -223,7 +238,8 @@ public static async Task UpdateAsync(this IDbConnection connection, T e { var typeInfo = type.GetTypeInfo(); bool implementsGenericIEnumerableOrIsGenericIEnumerable = - typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || + typeInfo.ImplementedInterfaces.Any(ti => + ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); if (implementsGenericIEnumerableOrIsGenericIEnumerable) @@ -256,6 +272,7 @@ public static async Task UpdateAsync(this IDbConnection connection, T e if (i < nonIdProps.Count - 1) sb.Append(", "); } + sb.Append(" where "); for (var i = 0; i < keyProperties.Count; i++) { @@ -264,7 +281,73 @@ public static async Task UpdateAsync(this IDbConnection connection, T e if (i < keyProperties.Count - 1) sb.Append(" and "); } - var updated = await connection.ExecuteAsync(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction).ConfigureAwait(false); + + var updated = await connection + .ExecuteAsync(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction) + .ConfigureAwait(false); + return updated > 0; + } + + /// + /// Use dynamic parameters to update entity in table "Ts" asynchronously using Task, checks if the entity is modified if the entity is tracked by the Get() extension. + /// + /// Type to be updated + /// Open SqlConnection + /// parameters,must have at least one [Key] or [ExplicitKey] property + /// The transaction to run under, null (the default) if none + /// Number of seconds before command execution timeout + /// true if updated, false if not found or not modified (tracked entities) + public static async Task DynamicParametersUpdateAsync(this IDbConnection connection, object parameters, + IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + var type = typeof(T); + + var keyProperties = KeyPropertiesCache(type).ToList(); + var explicitKeyProperties = ExplicitKeyPropertiesCache(type); + if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) + throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); + keyProperties.AddRange(explicitKeyProperties); + + var props = parameters.GetType().GetProperties(); + + var paramKeyProperties = props.Where(x => + keyProperties.Any(p => p.Name.Equals(x.Name, StringComparison.CurrentCultureIgnoreCase))).ToList(); + + + if (!paramKeyProperties.Any()) + { + throw new ArgumentException("Parameters must have at least one [Key] or [ExplicitKey] property"); + } + + + var name = GetTableName(type); + + var sb = new StringBuilder(); + sb.AppendFormat("update {0} set ", name); + + var computedProperties = ComputedPropertiesCache(type); + var nonIdProps = props.Where(x=>!keyProperties.Union(computedProperties).Any(y=>y.Name.Equals(x.Name, StringComparison.CurrentCultureIgnoreCase))).ToList(); + + var adapter = GetFormatter(connection); + + for (var i = 0; i < nonIdProps.Count; i++) + { + var property = nonIdProps[i]; + adapter.AppendColumnNameEqualsValue(sb, property.Name); + if (i < nonIdProps.Count - 1) + sb.Append(", "); + } + + sb.Append(" where "); + for (var i = 0; i < paramKeyProperties.Count(); i++) + { + var property = paramKeyProperties[i]; + adapter.AppendColumnNameEqualsValue(sb, property.Name); + if (i < paramKeyProperties.Count - 1) + sb.Append(" and "); + } + + var updated = await connection.ExecuteAsync(sb.ToString(), parameters, transaction, commandTimeout); return updated > 0; } @@ -277,7 +360,8 @@ public static async Task UpdateAsync(this IDbConnection connection, T e /// The transaction to run under, null (the default) if none /// Number of seconds before command execution timeout /// true if deleted, false if not found - public static async Task DeleteAsync(this IDbConnection connection, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + public static async Task DeleteAsync(this IDbConnection connection, T entityToDelete, + IDbTransaction transaction = null, int? commandTimeout = null) where T : class { if (entityToDelete == null) throw new ArgumentException("Cannot Delete null Object", nameof(entityToDelete)); @@ -292,7 +376,8 @@ public static async Task DeleteAsync(this IDbConnection connection, T e { var typeInfo = type.GetTypeInfo(); bool implementsGenericIEnumerableOrIsGenericIEnumerable = - typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || + typeInfo.ImplementedInterfaces.Any(ti => + ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); if (implementsGenericIEnumerableOrIsGenericIEnumerable) @@ -321,7 +406,9 @@ public static async Task DeleteAsync(this IDbConnection connection, T e if (i < allKeyProperties.Count - 1) sb.Append(" AND "); } - var deleted = await connection.ExecuteAsync(sb.ToString(), entityToDelete, transaction, commandTimeout).ConfigureAwait(false); + + var deleted = await connection.ExecuteAsync(sb.ToString(), entityToDelete, transaction, commandTimeout) + .ConfigureAwait(false); return deleted > 0; } @@ -333,11 +420,13 @@ public static async Task DeleteAsync(this IDbConnection connection, T e /// The transaction to run under, null (the default) if none /// Number of seconds before command execution timeout /// true if deleted, false if none found - public static async Task DeleteAllAsync(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + public static async Task DeleteAllAsync(this IDbConnection connection, + IDbTransaction transaction = null, int? commandTimeout = null) where T : class { var type = typeof(T); var statement = "DELETE FROM " + GetTableName(type); - var deleted = await connection.ExecuteAsync(statement, null, transaction, commandTimeout).ConfigureAwait(false); + var deleted = await connection.ExecuteAsync(statement, null, transaction, commandTimeout) + .ConfigureAwait(false); return deleted > 0; } } @@ -357,7 +446,8 @@ public partial interface ISqlAdapter /// The key columns in this table. /// The entity to insert. /// The Id of the row created. - Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert); + Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, + string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert); } public partial class SqlServerAdapter @@ -374,10 +464,13 @@ public partial class SqlServerAdapter /// The key columns in this table. /// The entity to insert. /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) + public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, + string tableName, string columnList, string parameterList, IEnumerable keyProperties, + object entityToInsert) { var cmd = $"INSERT INTO {tableName} ({columnList}) values ({parameterList}); SELECT SCOPE_IDENTITY() id"; - var multi = await connection.QueryMultipleAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); + var multi = await connection.QueryMultipleAsync(cmd, entityToInsert, transaction, commandTimeout) + .ConfigureAwait(false); var first = await multi.ReadFirstOrDefaultAsync().ConfigureAwait(false); if (first == null || first.id == null) return 0; @@ -407,11 +500,15 @@ public partial class SqlCeServerAdapter /// The key columns in this table. /// The entity to insert. /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) + public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, + string tableName, string columnList, string parameterList, IEnumerable keyProperties, + object entityToInsert) { var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList})"; await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); - var r = (await connection.QueryAsync("SELECT @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false)).ToList(); + var r = (await connection + .QueryAsync("SELECT @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout) + .ConfigureAwait(false)).ToList(); if (r[0] == null || r[0].id == null) return 0; var id = (int)r[0].id; @@ -440,12 +537,15 @@ public partial class MySqlAdapter /// The key columns in this table. /// The entity to insert. /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, + public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, + string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) { var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList})"; await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); - var r = await connection.QueryAsync("SELECT LAST_INSERT_ID() id", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); + var r = await connection + .QueryAsync("SELECT LAST_INSERT_ID() id", transaction: transaction, commandTimeout: commandTimeout) + .ConfigureAwait(false); var id = r.First().id; if (id == null) return 0; @@ -473,7 +573,9 @@ public partial class PostgresAdapter /// The key columns in this table. /// The entity to insert. /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) + public async Task InsertAsync(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); @@ -497,7 +599,8 @@ public async Task InsertAsync(IDbConnection connection, IDbTransaction tran } } - var results = await connection.QueryAsync(sb.ToString(), entityToInsert, transaction, commandTimeout).ConfigureAwait(false); + var results = await connection.QueryAsync(sb.ToString(), entityToInsert, transaction, commandTimeout) + .ConfigureAwait(false); // Return the key by assigning the corresponding property in the object - by product is that it supports compound primary keys var id = 0; @@ -508,6 +611,7 @@ public async Task InsertAsync(IDbConnection connection, IDbTransaction tran if (id == 0) id = Convert.ToInt32(value); } + return id; } } @@ -526,10 +630,13 @@ public partial class SQLiteAdapter /// The key columns in this table. /// The entity to insert. /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) + public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, + string tableName, string columnList, string parameterList, IEnumerable keyProperties, + object entityToInsert) { var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList}); SELECT last_insert_rowid() id"; - var multi = await connection.QueryMultipleAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); + var multi = await connection.QueryMultipleAsync(cmd, entityToInsert, transaction, commandTimeout) + .ConfigureAwait(false); var id = (int)(await multi.ReadFirstAsync().ConfigureAwait(false)).id; var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); @@ -556,14 +663,17 @@ public partial class FbAdapter /// The key columns in this table. /// The entity to insert. /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) + public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, + string tableName, string columnList, string parameterList, IEnumerable keyProperties, + object entityToInsert) { var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); var keyName = propertyInfos[0].Name; - var r = await connection.QueryAsync($"SELECT FIRST 1 {keyName} ID FROM {tableName} ORDER BY {keyName} DESC", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); + var r = await connection.QueryAsync($"SELECT FIRST 1 {keyName} ID FROM {tableName} ORDER BY {keyName} DESC", + transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); var id = r.First().ID; if (id == null) return 0; diff --git a/tests/Dapper.Tests.Contrib/TestSuite.Async.cs b/tests/Dapper.Tests.Contrib/TestSuite.Async.cs index fcd11802..f4225c74 100644 --- a/tests/Dapper.Tests.Contrib/TestSuite.Async.cs +++ b/tests/Dapper.Tests.Contrib/TestSuite.Async.cs @@ -204,7 +204,34 @@ public async Task InsertGetUpdateAsync() Assert.True(await connection.InsertAsync(new User { Name = "Adam", Age = 10 }).ConfigureAwait(false) > originalCount + 1); } } - + [Fact] + public async Task DynamicParametersUpdateAsync() + { + using (var connection = GetOpenConnection()) + { + Assert.Null(await connection.GetAsync(30).ConfigureAwait(false)); + + var id = await connection.InsertAsync(new User { Name = "Adam", Age = 10 }).ConfigureAwait(false); + + var user = await connection.GetAsync(id).ConfigureAwait(false); + + var parameters1 = new { id=user.Id,name="Bob"}; + Assert.True(await connection.DynamicParametersUpdateAsync(parameters1).ConfigureAwait(false)); + Assert.Equal("Bob", (await connection.GetAsync(id).ConfigureAwait(false)).Name); + Assert.Equal(10, (await connection.GetAsync(id).ConfigureAwait(false)).Age); + + var parameters2 = new { id=user.Id,age=20}; + + Assert.True(await connection.DynamicParametersUpdateAsync(parameters2).ConfigureAwait(false)); + Assert.Equal("Bob", (await connection.GetAsync(id).ConfigureAwait(false)).Name); + Assert.Equal(20, (await connection.GetAsync(id).ConfigureAwait(false)).Age); + + var parameters3 = new { id=-1,age=20}; + + Assert.False(await connection.DynamicParametersUpdateAsync(parameters3).ConfigureAwait(false)); + + } + } [Fact] public async Task InsertCheckKeyAsync() {