From 49920caf12b69647520dd56ece80ba7f0a70dfba Mon Sep 17 00:00:00 2001 From: borisdj Date: Fri, 4 Oct 2019 18:45:34 +0200 Subject: [PATCH] EFCore 3 Release --- .../EFCoreBatchTest.cs | 18 ++-- .../EFCoreBatchTestAsync.cs | 12 ++- EFCore.BulkExtensions/BatchUtil.cs | 82 ++++++++++++++++--- .../EFCore.BulkExtensions.csproj | 4 +- .../IQueryableBatchExtensions.cs | 39 ++++----- EFCore.BulkExtensions/IQueryableExtensions.cs | 31 ++++++- 6 files changed, 138 insertions(+), 48 deletions(-) diff --git a/EFCore.BulkExtensions.Tests/EFCoreBatchTest.cs b/EFCore.BulkExtensions.Tests/EFCoreBatchTest.cs index ecfdc6d8..4fb5cf22 100644 --- a/EFCore.BulkExtensions.Tests/EFCoreBatchTest.cs +++ b/EFCore.BulkExtensions.Tests/EFCoreBatchTest.cs @@ -21,11 +21,11 @@ public void BatchTest(DbServer databaseType) RunInsert(); RunBatchUpdate(); RunBatchDelete(); - RunContainsBatchDelete(); + //RunContainsBatchDelete(); // currently not supported for EFCore 3.0 using (var context = new TestContext(ContextUtil.GetOptions())) { - var lastItem = context.Items.LastOrDefault(); + var lastItem = context.Items.ToList().LastOrDefault(); Assert.Equal(500, lastItem.ItemId); Assert.Equal("Updated", lastItem.Description); Assert.Equal(1.5m, lastItem.Price); @@ -41,7 +41,7 @@ internal void RunDeleteAll(DbServer databaseType) context.Items.Add(new Item { }); // used for initial add so that after RESEED it starts from 1, not 0 context.SaveChanges(); - //context.Items.BatchDelete(); // TODO: Use after BatchDelete gets implemented for v3.0 + context.Items.BatchDelete(); context.BulkDelete(context.Items.ToList()); if (databaseType == DbServer.SqlServer) @@ -63,11 +63,17 @@ private void RunBatchUpdate() decimal price = 0; var query = context.Items.Where(a => a.ItemId <= 500 && a.Price >= price); - query.BatchUpdate(new Item { Description = "Updated", Price = 1.5m }/*, updateColumns*/); + + var parametersDict = new Dictionary // is used to fix issue of getting Query Parameters in .NetCore 3.0 + { + { nameof(price), price } + }; + + query.BatchUpdate(new Item { Description = "Updated", Price = 1.5m }/*, updateColumns*/, parametersDict: parametersDict); var incrementStep = 100; var suffix = " Concatenated"; - query.BatchUpdate(a => new Item { Name = a.Name + suffix, Quantity = a.Quantity + incrementStep }); // example of BatchUpdate Increment/Decrement value in variable + query.BatchUpdate(a => new Item { Name = a.Name + suffix, Quantity = a.Quantity + incrementStep }, parametersDict); // example of BatchUpdate Increment/Decrement value in variable //query.BatchUpdate(a => new Item { Quantity = a.Quantity + 100 }); // example direct value without variable } } @@ -104,7 +110,7 @@ private void RunBatchDelete() } } - private void RunContainsBatchDelete() + private void RunContainsBatchDelete() // currently not supported for EFCore 3.0 { var descriptionsToDelete = new List { "info" }; using (var context = new TestContext(ContextUtil.GetOptions())) diff --git a/EFCore.BulkExtensions.Tests/EFCoreBatchTestAsync.cs b/EFCore.BulkExtensions.Tests/EFCoreBatchTestAsync.cs index f0609c96..e805ad0c 100644 --- a/EFCore.BulkExtensions.Tests/EFCoreBatchTestAsync.cs +++ b/EFCore.BulkExtensions.Tests/EFCoreBatchTestAsync.cs @@ -25,7 +25,7 @@ public async Task BatchTestAsync(DbServer databaseType) using (var context = new TestContext(ContextUtil.GetOptions())) { - var lastItem = context.Items.LastOrDefaultAsync().Result; + var lastItem = (await context.Items.ToListAsync()).Last(); Assert.Equal(500, lastItem.ItemId); Assert.Equal("Updated", lastItem.Description); Assert.Null(lastItem.Price); @@ -87,9 +87,15 @@ private async Task RunBatchUpdateAsync() decimal price = 0; var query = context.Items.Where(a => a.ItemId <= 500 && a.Price >= price); - await query.BatchUpdateAsync(new Item { Description = "Updated" }/*, updateColumns*/); - await query.BatchUpdateAsync(a => new Item { Name = a.Name + " Concatenated", Quantity = a.Quantity + 100, Price = null }); // example of BatchUpdate value Increment/Decrement + var parametersDict = new Dictionary // is used to fix issue of getting Query Parameters in .NetCore 3.0 + { + { nameof(price), price } + }; + + await query.BatchUpdateAsync(new Item { Description = "Updated" }/*, updateColumns*/, parametersDict: parametersDict); + + await query.BatchUpdateAsync(a => new Item { Name = a.Name + " Concatenated", Quantity = a.Quantity + 100, Price = null }, parametersDict); // example of BatchUpdate value Increment/Decrement } } diff --git a/EFCore.BulkExtensions/BatchUtil.cs b/EFCore.BulkExtensions/BatchUtil.cs index b7a96797..7c64b076 100644 --- a/EFCore.BulkExtensions/BatchUtil.cs +++ b/EFCore.BulkExtensions/BatchUtil.cs @@ -10,10 +10,12 @@ using System.Linq.Expressions; using System.Reflection; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace EFCore.BulkExtensions { - static class BatchUtil + public static class BatchUtil { // In comment are Examples of how SqlQuery is changed for Sql Batch @@ -24,9 +26,12 @@ static class BatchUtil // DELETE [a] // FROM [Table] AS [a] // WHERE [a].[Columns] = FilterValues - public static (string, List) GetSqlDelete(IQueryable query, DbContext context) where T : class + public static (string, List) GetSqlDelete(IQueryable query, DbContext context, Dictionary parametersDict) where T : class { (string sql, string tableAlias, string tableAliasSufixAs, IEnumerable innerParameters) = GetBatchSql(query, context, isUpdate: false); + int paramsIndex = 0; + if (parametersDict != null) + innerParameters = parametersDict.Select(a => new SqlParameter($"@__{a.Key}_{paramsIndex++}", a.Value)); innerParameters = ReloadSqlParameters(context, innerParameters.ToList()); // Sqlite requires SqliteParameters tableAlias = (GetDatabaseType(context) == DbServer.SqlServer) ? $"[{tableAlias}]" : tableAlias; @@ -42,9 +47,12 @@ public static (string, List) GetSqlDelete(IQueryable query, DbCont // UPDATE [a] SET [UpdateColumns] = N'updateValues' // FROM [Table] AS [a] // WHERE [a].[Columns] = FilterValues - public static (string, List) GetSqlUpdate(IQueryable query, DbContext context, T updateValues, List updateColumns) where T : class, new() + public static (string, List) GetSqlUpdate(IQueryable query, DbContext context, T updateValues, List updateColumns, Dictionary parametersDict) where T : class, new() { (string sql, string tableAlias, string tableAliasSufixAs, IEnumerable innerParameters) = GetBatchSql(query, context, isUpdate: true); + int paramsIndex = 0; + if (parametersDict != null) + innerParameters = parametersDict.Select(a => new SqlParameter($"@__{a.Key}_{paramsIndex++}", a.Value)); var sqlParameters = new List(innerParameters); string sqlSET = GetSqlSetSegment(context, updateValues, updateColumns, sqlParameters); @@ -62,13 +70,15 @@ public static (string, List) GetSqlDelete(IQueryable query, DbCont /// /// /// - public static (string, List) GetSqlUpdate(IQueryable query, Expression> expression) where T : class + public static (string, List) GetSqlUpdate(IQueryable query, DbContext context, Expression> expression, Dictionary parametersDict) where T : class { - DbContext context = BatchUtil.GetDbContext(query); (string sql, string tableAlias, string tableAliasSufixAs, IEnumerable innerParameters) = GetBatchSql(query, context, isUpdate: true); var sqlColumns = new StringBuilder(); + int paramsIndex = 0; + if (parametersDict != null) + innerParameters = parametersDict.Select(a => new SqlParameter($"@__{a.Key}_{paramsIndex++}", a.Value)); var sqlParameters = new List(innerParameters); - var columnNameValueDict = TableInfo.CreateInstance(GetDbContext(query), new List(), OperationType.Read, new BulkConfig()).PropertyColumnNamesDict; + var columnNameValueDict = TableInfo.CreateInstance(context, new List(), OperationType.Read, new BulkConfig()).PropertyColumnNamesDict; var dbType = GetDatabaseType(context); CreateUpdateBody(columnNameValueDict, tableAlias, expression.Body, dbType, ref sqlColumns, ref sqlParameters); @@ -85,7 +95,7 @@ public static List ReloadSqlParameters(DbContext context, List s if (databaseType == DbServer.Sqlite) { var sqlParametersReloaded = new List(); - foreach(var parameter in sqlParameters) + foreach (var parameter in sqlParameters) { var sqlParameter = (SqlParameter)parameter; sqlParametersReloaded.Add(new SqliteParameter(sqlParameter.ParameterName, sqlParameter.Value)); @@ -100,13 +110,27 @@ public static List ReloadSqlParameters(DbContext context, List s public static (string, string, string, IEnumerable) GetBatchSql(IQueryable query, DbContext context, bool isUpdate) where T : class { - string sqlQuery = query.ToSql(); - IEnumerable innerParameters = new List(); - if (!sqlQuery.Contains(" IN (")) // ToParametrizedSql does not work correctly with Contains that is translated to sql IN command + string sqlQuery; + try + { + sqlQuery = query.ToSql(); + } + catch (Exception ex) { - (sqlQuery, innerParameters) = query.ToParametrizedSql(); + if (ex.Message.StartsWith("Unable to cast object")) + throw new NotSupportedException($"Query with 'Contains' not currently supported on .NetCore 3. ({ex.Message})"); + else + throw ex; + // Unable to cast object of type 'Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlParameterExpression' + // to type 'Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlConstantExpression'. } + IEnumerable innerParameters = new List(); + /*if (!sqlQuery.Contains(" IN (")) // DEPRECATED (EFCore 2) ToParametrizedSql does not work correctly with Contains that is translated to sql IN command + { + //(sqlQuery, innerParameters) = query.ToParametrizedSql(); + }*/ + DbServer databaseType = GetDatabaseType(context); string tableAlias = ""; string tableAliasSufixAs = ""; @@ -126,7 +150,7 @@ public static (string, string, string, IEnumerable) GetBatchSql(IQuer int indexPrefixFROM = sql.IndexOf(Environment.NewLine, 1); // skip NewLine from start of string tableAlias = sql.Substring(7, indexPrefixFROM - 14); // get name of table: "TableName" sql = sql.Substring(indexPrefixFROM, sql.Length - indexPrefixFROM); // remove segment: FROM "TableName" AS "a" - tableAliasSufixAs = " AS " + sql.Substring(8 , 3) + " "; + tableAliasSufixAs = " AS " + sql.Substring(8, 3) + " "; } return (sql, tableAlias, tableAliasSufixAs, innerParameters); @@ -290,7 +314,7 @@ public static DbContext GetDbContext(IQueryable query) var queryCompiler = typeof(EntityQueryProvider).GetField("_queryCompiler", bindingFlags).GetValue(query.Provider); var queryContextFactory = queryCompiler.GetType().GetField("_queryContextFactory", bindingFlags).GetValue(queryCompiler); - var dependencies = typeof(RelationalQueryContextFactory).GetProperty("Dependencies", bindingFlags).GetValue(queryContextFactory); + var dependencies = typeof(RelationalQueryContextFactory).GetField("_dependencies", bindingFlags).GetValue(queryContextFactory); var queryContextDependencies = typeof(DbContext).Assembly.GetType(typeof(QueryContextDependencies).FullName); var stateManagerProperty = queryContextDependencies.GetProperty("StateManager", bindingFlags | BindingFlags.Public).GetValue(dependencies); var stateManager = (IStateManager)stateManagerProperty; @@ -315,5 +339,37 @@ internal static bool IsStringConcat(BinaryExpression binaryExpression) { return method.DeclaringType == typeof(string) && method.Name == nameof(string.Concat); } + + internal static int ExecuteSql(DbContext dbContext, string sql, List sqlParameters) + { + try + { + return dbContext.Database.ExecuteSqlRaw(sql, sqlParameters); + } + catch (Exception ex) + { + if (ex.Message.Contains("Must declare the scalar variable")) + throw new InvalidOperationException($"{ParametersDictNotFoundMessage} SourceMessage: {ex.Message})"); + else + throw ex; + } + } + + internal static async Task ExecuteSqlAsync(DbContext dbContext, string sql, List sqlParameters, CancellationToken cancellationToken = default) + { + try + { + return await dbContext.Database.ExecuteSqlRawAsync(sql, sqlParameters, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + if (ex.Message.Contains("Must declare the scalar variable")) + throw new InvalidOperationException($"{ParametersDictNotFoundMessage} SourceMessage: {ex.Message})"); + else + throw ex; + } + } + + internal static string ParametersDictNotFoundMessage => "For Query with parameterized variable BatchOperation requires argument 'parametersDict' (Name and Value). Example in library ReadMe."; } } diff --git a/EFCore.BulkExtensions/EFCore.BulkExtensions.csproj b/EFCore.BulkExtensions/EFCore.BulkExtensions.csproj index 9c79470b..59171d58 100644 --- a/EFCore.BulkExtensions/EFCore.BulkExtensions.csproj +++ b/EFCore.BulkExtensions/EFCore.BulkExtensions.csproj @@ -3,7 +3,7 @@ netstandard2.1 EFCore.BulkExtensions - 3.0.0-rc + 3.0.0 borisdj EntityFramework EF Core Bulk Batch Extensions for Insert Update Delete and Read (CRUD) operations on SQL Server and SQLite https://github.com/borisdj/EFCore.BulkExtensions @@ -11,7 +11,7 @@ MIT EntityFrameworkCore Entity Framework Core EFCore EF Core Bulk Batch Extensions Insert Update Delete Read - Target switch to .NetStandard 2.1 using .NetCore 3.0 + .NetCore 3.0 RTM 3.0.0.0 3.0.0.0 diff --git a/EFCore.BulkExtensions/IQueryableBatchExtensions.cs b/EFCore.BulkExtensions/IQueryableBatchExtensions.cs index a88cabda..6c17e1ea 100644 --- a/EFCore.BulkExtensions/IQueryableBatchExtensions.cs +++ b/EFCore.BulkExtensions/IQueryableBatchExtensions.cs @@ -4,57 +4,54 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace EFCore.BulkExtensions { public static class IQueryableBatchExtensions { - public static int BatchDelete(this IQueryable query) where T : class + public static int BatchDelete(this IQueryable query, Dictionary parametersDict = null) where T : class { DbContext context = BatchUtil.GetDbContext(query); - (string sql, var sqlParameters) = BatchUtil.GetSqlDelete(query, context); - return context.Database.ExecuteSqlRaw(sql, sqlParameters); + (string sql, var sqlParameters) = BatchUtil.GetSqlDelete(query, context, parametersDict); + return BatchUtil.ExecuteSql(context, sql, sqlParameters); } - public static int BatchUpdate(this IQueryable query, T updateValues, List updateColumns = null) where T : class, new() + public static int BatchUpdate(this IQueryable query, T updateValues, List updateColumns = null, Dictionary parametersDict = null) where T : class, new() { DbContext context = BatchUtil.GetDbContext(query); - var (sql, sqlParameters) = BatchUtil.GetSqlUpdate(query, context, updateValues, updateColumns); - return context.Database.ExecuteSqlRaw(sql, sqlParameters); + var (sql, sqlParameters) = BatchUtil.GetSqlUpdate(query, context, updateValues, updateColumns, parametersDict); + return BatchUtil.ExecuteSql(context, sql, sqlParameters); } - - public static int BatchUpdate(this IQueryable query, Expression> updateExpression) where T : class + public static int BatchUpdate(this IQueryable query, Expression> updateExpression, Dictionary parametersDict = null) where T : class { var context = BatchUtil.GetDbContext(query); - var (sql, sqlParameters) = BatchUtil.GetSqlUpdate(query, updateExpression); - return context.Database.ExecuteSqlRaw(sql, sqlParameters); + var (sql, sqlParameters) = BatchUtil.GetSqlUpdate(query, context, updateExpression, parametersDict); + return BatchUtil.ExecuteSql(context, sql, sqlParameters); } // Async methods - public static async Task BatchDeleteAsync(this IQueryable query, CancellationToken cancellationToken = default) where T : class + public static async Task BatchDeleteAsync(this IQueryable query, Dictionary parametersDict = null, CancellationToken cancellationToken = default) where T : class { DbContext context = BatchUtil.GetDbContext(query); - DbServer databaseType = context.Database.ProviderName.EndsWith(DbServer.Sqlite.ToString()) ? DbServer.Sqlite : DbServer.SqlServer; - (string sql, var sqlParameters) = BatchUtil.GetSqlDelete(query, context); - return await context.Database.ExecuteSqlRawAsync(sql, sqlParameters, cancellationToken).ConfigureAwait(false); + (string sql, var sqlParameters) = BatchUtil.GetSqlDelete(query, context, parametersDict); + return await BatchUtil.ExecuteSqlAsync(context, sql, sqlParameters, cancellationToken).ConfigureAwait(false); } - public static async Task BatchUpdateAsync(this IQueryable query, T updateValues, List updateColumns = null, CancellationToken cancellationToken = default) where T : class, new() + public static async Task BatchUpdateAsync(this IQueryable query, T updateValues, List updateColumns = null, Dictionary parametersDict = null, CancellationToken cancellationToken = default) where T : class, new() { DbContext context = BatchUtil.GetDbContext(query); - var (sql, sqlParameters) = BatchUtil.GetSqlUpdate(query, context, updateValues, updateColumns); - return await context.Database.ExecuteSqlRawAsync(sql, sqlParameters, cancellationToken).ConfigureAwait(false); + var (sql, sqlParameters) = BatchUtil.GetSqlUpdate(query, context, updateValues, updateColumns, parametersDict); + return await BatchUtil.ExecuteSqlAsync(context, sql, sqlParameters, cancellationToken).ConfigureAwait(false); } - public static async Task BatchUpdateAsync(this IQueryable query, Expression> updateExpression, CancellationToken cancellationToken = default) where T : class + public static async Task BatchUpdateAsync(this IQueryable query, Expression> updateExpression, Dictionary parametersDict = null, CancellationToken cancellationToken = default) where T : class { var context = BatchUtil.GetDbContext(query); - var (sql, sqlParameters) = BatchUtil.GetSqlUpdate(query, updateExpression); - return await context.Database.ExecuteSqlRawAsync(sql, sqlParameters, cancellationToken).ConfigureAwait(false); + var (sql, sqlParameters) = BatchUtil.GetSqlUpdate(query, context, updateExpression, parametersDict); + return await BatchUtil.ExecuteSqlAsync(context, sql, sqlParameters, cancellationToken).ConfigureAwait(false); } } } diff --git a/EFCore.BulkExtensions/IQueryableExtensions.cs b/EFCore.BulkExtensions/IQueryableExtensions.cs index 8ef9afee..28523921 100644 --- a/EFCore.BulkExtensions/IQueryableExtensions.cs +++ b/EFCore.BulkExtensions/IQueryableExtensions.cs @@ -6,9 +6,11 @@ using System.Reflection; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; @@ -28,8 +30,28 @@ public static class IQueryableExtensions internal static string ToSql(this IQueryable query) where TEntity : class { - // ! IMPORTANT TODO: - throw new NotSupportedException("Currently not supported for .NET Core 3.0, because in v3.0 there are no classes 'QueryModelGenerator' and 'RelationalQueryModelVisitor'. Will be supported after finding alternative for implementing .ToSql();"); + var enumerator = query.Provider.Execute>(query.Expression).GetEnumerator(); + var enumeratorType = enumerator.GetType(); + + var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance; + var selectExpressionFieldName = "_selectExpression"; + var querySqlGeneratorFactoryFieldName = "_querySqlGeneratorFactory"; + var selectFieldInfo = enumeratorType.GetField(selectExpressionFieldName, bindingFlags); + var sqlGeneratorFieldInfo = enumeratorType.GetField(querySqlGeneratorFactoryFieldName, bindingFlags); + if (selectFieldInfo == null || sqlGeneratorFieldInfo == null) + throw new InvalidOperationException($"Cannot find field {(selectFieldInfo == null ? selectExpressionFieldName : querySqlGeneratorFactoryFieldName) } on type {enumeratorType.Name}"); + + var selectExpression = selectFieldInfo.GetValue(enumerator) as SelectExpression; + var factory = sqlGeneratorFieldInfo.GetValue(enumerator) as IQuerySqlGeneratorFactory; + if (selectExpression == null || factory == null) + throw new InvalidOperationException($"Could not get {(selectFieldInfo == null ? nameof(SelectExpression) : nameof(IQuerySqlGeneratorFactory)) }"); + + var sqlGenerator = factory.Create(); + var command = sqlGenerator.GetCommand(selectExpression); + var sql = command.CommandText; + return sql; + + //DEPRECATED: Used for .NetCore 2 on NetStandard 2.0 /* var queryCompiler = (QueryCompiler)QueryCompilerField.GetValue(query.Provider); var modelGenerator = (QueryModelGenerator)QueryModelGeneratorField.GetValue(queryCompiler); @@ -49,9 +71,12 @@ internal static string ToSql(this IQueryable query) where TEnt */ } + // currently not used internal static (string, IEnumerable) ToParametrizedSql(this IQueryable query) where TEntity : class { - throw new NotSupportedException("Currently not supported for .NET Core 3.0, because in v3.0 there are no classes 'QueryModelGenerator' and 'RelationalQueryModelVisitor'. Will be supported after finding alternative for implementing .ToSql();"); + throw new NotSupportedException("ToParametrizedSql does not work on EFCore 3."); + + //DEPRECATED: Used for .NetCore 2 on NetStandard 2.0 /* var queryCompiler = (QueryCompiler)QueryCompilerField.GetValue(query.Provider); var modelGenerator = (QueryModelGenerator)QueryModelGeneratorField.GetValue(queryCompiler);