From 36c143ce2920898928c3073ae3469a515e47d551 Mon Sep 17 00:00:00 2001 From: Simon Carr Date: Tue, 1 Dec 2020 12:18:21 +0000 Subject: [PATCH] Added overloads allowing predicate expressions to limit scope DeleteAllAsync() and GetAllAsync() overloads accept a predicate expression that can be used to limit the scope of the action. I.E filter records returned or deleted. --- .../SqlMapperExtensions.Expression.Async.cs | 83 +++++++++++++ .../TestSuite.Expressions.Async.cs | 111 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 Dapper.Contrib/SqlMapperExtensions.Expression.Async.cs create mode 100644 tests/Dapper.Tests.Contrib/TestSuite.Expressions.Async.cs diff --git a/Dapper.Contrib/SqlMapperExtensions.Expression.Async.cs b/Dapper.Contrib/SqlMapperExtensions.Expression.Async.cs new file mode 100644 index 000000000..2109b6b5e --- /dev/null +++ b/Dapper.Contrib/SqlMapperExtensions.Expression.Async.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace Dapper.Contrib.Extensions +{ + public static partial class SqlMapperExtensions + { + /// + /// Returns a list of entities from table "Ts" based on predicate expression. + /// Id of T must be marked with [Key] attribute. + /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension + /// for optimal performance. + /// Entities can be filtered using predicate expression + /// + /// Interface or type to create and populate + /// Open SqlConnection + /// Search terms + /// 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, Expression> predicate, + IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + var type = typeof(T); + var cacheType = typeof(List); + + if (!GetQueries.TryGetValue(cacheType.TypeHandle, out string sql)) + { + GetSingleKey(nameof(GetAll)); + var name = GetTableName(type); + + var where = CreateWhereClause(predicate); + + sql = $"SELECT * FROM {name} {where}"; + GetQueries[cacheType.TypeHandle] = sql; + } + + return !type.IsInterface + ? connection.QueryAsync(sql, null, transaction, commandTimeout) + : GetAllAsyncImpl(connection, transaction, commandTimeout, sql, type); + } + + /// + /// Delete n matching entities in the table related to the type T asynchronously using Task based. + /// + /// Type of entity + /// Open SqlConnection + /// Filter to apply for deletion + /// 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, Expression> predicate, + IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + var type = typeof(T); + var where = CreateWhereClause(predicate); + + var statement = $"DELETE FROM {GetTableName(type)} {where}"; + var deleted = await connection.ExecuteAsync(statement, null, transaction, commandTimeout).ConfigureAwait(false); + return deleted > 0; + } + + private static string CreateWhereClause(Expression> predicate) + { + if (predicate == null) + return ""; + + var p = new StringBuilder(predicate.Body.ToString()); + var pName = predicate.Parameters.First(); + p.Replace(pName.Name + ".", ""); + p.Replace("==", "="); + p.Replace("AndAlso", "and"); + p.Replace("OrElse", "or"); + p.Replace("\"", "\'"); + return $"WHERE {p}"; + } + } +} diff --git a/tests/Dapper.Tests.Contrib/TestSuite.Expressions.Async.cs b/tests/Dapper.Tests.Contrib/TestSuite.Expressions.Async.cs new file mode 100644 index 000000000..450f179e2 --- /dev/null +++ b/tests/Dapper.Tests.Contrib/TestSuite.Expressions.Async.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Dapper.Contrib.Extensions; +using Xunit; + +namespace Dapper.Tests.Contrib +{ + public abstract partial class TestSuite + { + [Fact] + public async Task GetAllAsyncWithOrExpression() + { + const int numberOfEntities = 10; + + var users = new List(numberOfEntities); + + for (var i = 0; i < numberOfEntities; i++) + users.Add(new User {Name = "User " + i, Age = i}); + + using (var connection = GetOpenConnection()) + { + await connection.DeleteAllAsync().ConfigureAwait(false); + + var total = await connection.InsertAsync(users).ConfigureAwait(false); + Assert.Equal(total, numberOfEntities); + + users = (List) await connection.GetAllAsync(x => x.Age == 5 || x.Age == 6).ConfigureAwait(false); + Assert.Equal(2, users.Count); + Assert.NotNull(users.FirstOrDefault(x => x.Age == 5)); + Assert.NotNull(users.FirstOrDefault(x => x.Age == 6)); + + var iusers = await connection.GetAllAsync(x => x.Age == 5 || x.Age == 6).ConfigureAwait(false); + Assert.Equal(2, iusers.Count()); + Assert.NotNull(iusers.FirstOrDefault(x => x.Age == 5)); + Assert.NotNull(iusers.FirstOrDefault(x => x.Age == 6)); + } + } + + [Fact] + public async Task GetAllAsyncWithAndExpression() + { + const int numberOfEntities = 10; + + var users = new List(numberOfEntities); + + for (var i = 0; i < numberOfEntities; i++) + users.Add(new User {Name = "User " + i, Age = i}); + + using (var connection = GetOpenConnection()) + { + await connection.DeleteAllAsync().ConfigureAwait(false); + + var total = await connection.InsertAsync(users).ConfigureAwait(false); + Assert.Equal(total, numberOfEntities); + + users = (List) await connection.GetAllAsync(x => x.Age == 5 && x.Id == 6).ConfigureAwait(false); + Assert.Single(users); + Assert.NotNull(users.FirstOrDefault(x => x.Age == 5 && x.Id == 6)); + + var iusers = await connection.GetAllAsync(x => x.Age == 5 && x.Id == 6).ConfigureAwait(false); + Assert.Single(iusers); + Assert.NotNull(iusers.FirstOrDefault(x => x.Age == 5 && x.Id == 6)); + } + } + + [Fact] + public async Task GetAllAsyncWithStringExpression() + { + const int numberOfEntities = 10; + + var users = new List(numberOfEntities); + + for (var i = 0; i < numberOfEntities; i++) + users.Add(new User {Id = 100 + i, Name = "User " + i, Age = i}); + + using (var connection = GetOpenConnection()) + { + await connection.DeleteAllAsync().ConfigureAwait(false); + + var total = await connection.InsertAsync(users).ConfigureAwait(false); + Assert.Equal(total, numberOfEntities); + + users = (List) await connection.GetAllAsync(x => x.Name == "User 5").ConfigureAwait(false); + Assert.Single(users); + Assert.NotNull(users.FirstOrDefault(x => x.Name == "User 5")); + + var iusers = await connection.GetAllAsync(x => x.Name == "User 5").ConfigureAwait(false); + Assert.Single(iusers); + Assert.NotNull(iusers.FirstOrDefault(x => x.Name == "User 5")); + } + } + + [Fact] + public async Task DeleteAllAsyncWithExpression() + { + using (var connection = GetOpenConnection()) + { + await connection.DeleteAllAsync().ConfigureAwait(false); + + var id1 = await connection.InsertAsync(new User {Name = "Alice", Age = 32}).ConfigureAwait(false); + var id2 = await connection.InsertAsync(new User {Name = "Bob", Age = 33}).ConfigureAwait(false); + + await connection.DeleteAllAsync(x => x.Name == "Alice").ConfigureAwait(false); + + Assert.Null(await connection.GetAsync(id1).ConfigureAwait(false)); + Assert.NotNull(await connection.GetAsync(id2).ConfigureAwait(false)); + } + } + } +}