From 672f23b77a5b3dafbe98a8ff13121d97fa53cc3b Mon Sep 17 00:00:00 2001 From: Rikard Andersson Date: Wed, 8 May 2024 13:46:38 +0200 Subject: [PATCH 1/6] Rewrite ToSqlPredicate to handle enums and skip using reflection --- .../Data/Order.cs | 8 +++ .../Data/TestDbContext.cs | 1 + .../DbContextExtensions/BulkMergeAsync.cs | 17 +++++ .../DbContextExtensionsBase.cs | 3 +- .../LinqExtensions/ToSqlPredicateTests.cs | 66 +++++++++++++++++++ .../Extensions/LinqExtensions.cs | 47 +++++++++---- .../N.EntityFrameworkCore.Extensions.csproj | 4 ++ 7 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 N.EntityFrameworkCore.Extensions.Test/LinqExtensions/ToSqlPredicateTests.cs diff --git a/N.EntityFrameworkCore.Extensions.Test/Data/Order.cs b/N.EntityFrameworkCore.Extensions.Test/Data/Order.cs index d1c9fc0..99b92c4 100644 --- a/N.EntityFrameworkCore.Extensions.Test/Data/Order.cs +++ b/N.EntityFrameworkCore.Extensions.Test/Data/Order.cs @@ -17,10 +17,18 @@ public class Order public DateTime DbModifiedDateTime { get; set; } public bool? Trigger { get; set; } public bool Active { get; set; } + public OrderStatus Status { get; set; } public Order() { AddedDateTime = DateTime.UtcNow; Active = true; } } + + public enum OrderStatus + { + Unknown, + Completed, + Error + } } \ No newline at end of file diff --git a/N.EntityFrameworkCore.Extensions.Test/Data/TestDbContext.cs b/N.EntityFrameworkCore.Extensions.Test/Data/TestDbContext.cs index e85c4a8..9f01f8a 100644 --- a/N.EntityFrameworkCore.Extensions.Test/Data/TestDbContext.cs +++ b/N.EntityFrameworkCore.Extensions.Test/Data/TestDbContext.cs @@ -36,6 +36,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().Property("Key3").HasDefaultValueSql("newsequentialid()"); modelBuilder.Entity().Property("DbAddedDateTime").HasDefaultValueSql("getdate()"); modelBuilder.Entity().Property("DbModifiedDateTime").HasComputedColumnSql("getdate()"); + modelBuilder.Entity().Property(p => p.Status).HasConversion(); modelBuilder.Entity().UseTpcMappingStrategy(); modelBuilder.Entity().ToTable("TpcCustomer"); modelBuilder.Entity().ToTable("TpcVendor"); diff --git a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkMergeAsync.cs b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkMergeAsync.cs index 56887fc..b0280ff 100644 --- a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkMergeAsync.cs +++ b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkMergeAsync.cs @@ -325,5 +325,22 @@ public async Task With_ValueGenerated_Computed() Assert.IsTrue(result.RowsInserted == orders.Count(), "The number of rows inserted must match the count of order list"); Assert.IsTrue(newTotal - oldTotal == result.RowsInserted, "The new count minus the old count should match the number of rows inserted."); } + [TestMethod] + public async Task With_Merge_On_Enum() + { + var dbContext = SetupDbContext(true); + await dbContext.BulkSaveChangesAsync(); + var nowDateTime = DateTime.Now; + var orders = new List(); + for (int i = 0; i < 20; i++) + { + orders.Add(new Order { Id = i, Price = 1.57M, DbModifiedDateTime = nowDateTime, Status = OrderStatus.Completed}); + } + + var result = await dbContext.BulkMergeAsync(orders, options => options.MergeOnCondition = (s, t) => s.Id == t.Id && s.Status == t.Status); + + Assert.AreEqual(1, result.RowsInserted); + Assert.AreEqual(19, result.RowsUpdated); + } } } \ No newline at end of file diff --git a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/DbContextExtensionsBase.cs b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/DbContextExtensionsBase.cs index dc54d03..260659b 100644 --- a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/DbContextExtensionsBase.cs +++ b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/DbContextExtensionsBase.cs @@ -56,7 +56,8 @@ protected TestDbContext SetupDbContext(bool populateData, PopulateDataMode mode ExternalId = string.Format("id-{0}", i), Price = 1.25M, AddedDateTime = addedDateTime, - ModifiedDateTime = addedDateTime.AddHours(3) + ModifiedDateTime = addedDateTime.AddHours(3), + Status = OrderStatus.Completed }); id++; } diff --git a/N.EntityFrameworkCore.Extensions.Test/LinqExtensions/ToSqlPredicateTests.cs b/N.EntityFrameworkCore.Extensions.Test/LinqExtensions/ToSqlPredicateTests.cs new file mode 100644 index 0000000..26b37a0 --- /dev/null +++ b/N.EntityFrameworkCore.Extensions.Test/LinqExtensions/ToSqlPredicateTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq.Expressions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace N.EntityFrameworkCore.Extensions.Test.LinqExtensions; + +[TestClass] +public class ToSqlPredicateTests +{ + [TestMethod] + public void Should_handle_int() + { + Expression> expression = (s, t) => s.Id == t.Id; + + var sqlPredicate = expression.ToSqlPredicate("s", "t"); + + Assert.AreEqual("s.Id = t.Id",sqlPredicate); + } + + [TestMethod] + public void Should_handle_enum() + { + Expression> expression = (s, t) => s.Type == t.Type; + + var sqlPredicate = expression.ToSqlPredicate("s", "t"); + + Assert.AreEqual("s.Type = t.Type",sqlPredicate); + } + + [TestMethod] + public void Should_handle_complex_one() + { + Expression> expression = (s, t) => s.Type == t.Type && + (s.Id == t.Id && + s.ExternalId == t.ExternalId); + + var sqlPredicate = expression.ToSqlPredicate("s", "t"); + + Assert.AreEqual("s.Type = t.Type AND s.Id = t.Id AND s.ExternalId = t.ExternalId", sqlPredicate); + } + + [TestMethod] + public void Should_handle_prop_naming() + { + Expression> expression = (source, target) => source.Id == target.Id && + source.ExternalId == target.ExternalId; + + var sqlPredicate = expression.ToSqlPredicate("s", "t"); + + Assert.AreEqual("s.Id = t.Id AND s.ExternalId = t.ExternalId", sqlPredicate); + } + + record Entity + { + public Guid Id { get; set; } + public EntityType Type { get; set; } + public int ExternalId { get; set; } + } + + enum EntityType + { + One, + Two, + Three + } +} diff --git a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs index f8a095a..4da3a25 100644 --- a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs +++ b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs @@ -130,20 +130,45 @@ public static List GetObjectProperties(this Expression(this Expression expression, params string[] parameters) { - var stringBuilder = new StringBuilder((string)expression.Body.GetPrivateFieldValue("DebugView")); - int i = 0; - foreach (var expressionParam in expression.Parameters) - { - if (parameters.Length <= i) break; - stringBuilder.Replace((string)expressionParam.GetPrivateFieldValue("DebugView"), parameters[i]); - i++; - } - stringBuilder.Replace("&&", "AND"); - stringBuilder.Replace("==", "="); - return stringBuilder.ToString(); + var sql = ToSqlString(expression.Body); + + for (var i = 0; i < parameters.Length; i++) + sql = sql.Replace($"${expression.Parameters[i].Name!}.", $"{parameters[i]}."); + + return sql; + } + + static string ToSqlString(Expression expression, string sql = null) + { + sql ??= ""; + if (expression is not BinaryExpression b) + return sql; + + var internalSql = ""; + if (b.Left is MemberExpression mel) + internalSql += $"${mel} = "; + if (b.Right is MemberExpression mer) + internalSql += $"${mer}"; + + if (b.Left is UnaryExpression ubl) + internalSql += $"${ubl.Operand} = "; + if (b.Right is UnaryExpression ubr) + internalSql += $"${ubr.Operand}"; + + if (!string.IsNullOrWhiteSpace(internalSql)) + return internalSql; + + var left = ToSqlString(b.Left, sql); + if (string.IsNullOrWhiteSpace(left)) + return sql; + + var right = ToSqlString(b.Right, sql); + return left + " AND " + right; } + internal static string ToSqlUpdateSetExpression(this Expression expression, string tableName) { List setValues = new List(); diff --git a/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj b/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj index 6596b97..035c7c3 100644 --- a/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj +++ b/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj @@ -32,4 +32,8 @@ Inheritance models supported: Table-Per-Concrete, Table-Per-Hierarchy, Table-Per + + + + \ No newline at end of file From 33e5408b843b516c6968dc9684429403a8e931ff Mon Sep 17 00:00:00 2001 From: NorthernLight1 <49600465+NorthernLight1@users.noreply.github.com> Date: Fri, 10 May 2024 23:19:03 -0400 Subject: [PATCH 2/6] Update LinqExtensions.cs --- N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs index 6fc9666..aa2af58 100644 --- a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs +++ b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs @@ -134,7 +134,7 @@ public static List GetObjectProperties(this Expression(this Expression expression, params string[] parameters) + internal static string ToSqlPredicate2(this Expression expression, params string[] parameters) { var sql = ToSqlString(expression.Body); From 0d3e0f0cf5f19fba9c3ae6dd6c5cda4b07842a22 Mon Sep 17 00:00:00 2001 From: Rikard Andersson Date: Mon, 13 May 2024 10:55:10 +0200 Subject: [PATCH 3/6] Fix breaking insert tests --- .../DbContextExtensions/BulkInsert.cs | 5 +++-- .../DbContextExtensions/BulkInsertAsync.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkInsert.cs b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkInsert.cs index 14b525c..2b51bcd 100644 --- a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkInsert.cs +++ b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkInsert.cs @@ -228,10 +228,11 @@ public void With_Options_InputColumns() var orders = new List(); for (int i = 0; i < 20000; i++) { - orders.Add(new Order { Id = i, ExternalId = i.ToString(), Price = 1.57M, Active = true }); + orders.Add(new Order { Id = i, ExternalId = i.ToString(), Price = 1.57M, Active = true, Status = OrderStatus.Completed}); } int oldTotal = dbContext.Orders.Where(o => o.Price == 1.57M && o.ExternalId == null && o.Active == true).Count(); - int rowsInserted = dbContext.BulkInsert(orders, options => { options.UsePermanentTable = true; options.InputColumns = o => new { o.Price, o.Active, o.AddedDateTime }; }); + int rowsInserted = dbContext.BulkInsert(orders, options => { options.UsePermanentTable = true; + options.InputColumns = o => new { o.Price, o.Active, o.AddedDateTime, o.Status }; }); int newTotal = dbContext.Orders.Where(o => o.Price == 1.57M && o.ExternalId == null && o.Active == true).Count(); Assert.IsTrue(rowsInserted == orders.Count, "The number of rows inserted must match the count of order list"); diff --git a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkInsertAsync.cs b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkInsertAsync.cs index 4bc4366..608e84f 100644 --- a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkInsertAsync.cs +++ b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkInsertAsync.cs @@ -247,10 +247,11 @@ public async Task With_Options_InputColumns() var orders = new List(); for (int i = 0; i < 20000; i++) { - orders.Add(new Order { Id = i, ExternalId = i.ToString(), Price = 1.57M, Active = true }); + orders.Add(new Order { Id = i, ExternalId = i.ToString(), Price = 1.57M, Active = true, Status = OrderStatus.Completed}); } int oldTotal = dbContext.Orders.Where(o => o.Price == 1.57M && o.ExternalId == null && o.Active == true).Count(); - int rowsInserted = await dbContext.BulkInsertAsync(orders, options => { options.UsePermanentTable = true; options.InputColumns = o => new { o.Price, o.Active, o.AddedDateTime }; }); + int rowsInserted = await dbContext.BulkInsertAsync(orders, options => { options.UsePermanentTable = true; + options.InputColumns = o => new { o.Price, o.Active, o.AddedDateTime, o.Status }; }); int newTotal = dbContext.Orders.Where(o => o.Price == 1.57M && o.ExternalId == null && o.Active == true).Count(); Assert.IsTrue(rowsInserted == orders.Count, "The number of rows inserted must match the count of order list"); From a503c2639d5e1647c5d69ad85c909c107647d03d Mon Sep 17 00:00:00 2001 From: Rikard Andersson Date: Mon, 13 May 2024 11:09:16 +0200 Subject: [PATCH 4/6] Use SqlPredicate without reflection --- .../Extensions/LinqExtensions.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs index aa2af58..4a45b5d 100644 --- a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs +++ b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs @@ -134,7 +134,7 @@ public static List GetObjectProperties(this Expression(this Expression expression, params string[] parameters) + internal static string ToSqlPredicate(this Expression expression, params string[] parameters) { var sql = ToSqlString(expression.Body); @@ -196,21 +196,6 @@ internal static string ToSql(this Expression expression) } return sb.ToString(); } - internal static string ToSqlPredicate(this Expression expression, params string[] parameters) - { - var stringBuilder = new StringBuilder((string)expression.Body.GetPrivateFieldValue("DebugView")); - int i = 0; - foreach (var expressionParam in expression.Parameters) - { - if (parameters.Length <= i) break; - stringBuilder.Replace((string)expressionParam.GetPrivateFieldValue("DebugView"), parameters[i]); - i++; - } - stringBuilder.Replace("&&", "AND"); - stringBuilder.Replace("==", "="); - stringBuilder.Replace("(System.Nullable`1[System.Int32])", ""); - return stringBuilder.ToString(); - } internal static string ToSqlUpdateSetExpression(this Expression expression, string tableName) { List setValues = new List(); From de34da1bc237d55b59d0f6c3ce87ea8d4bc05337 Mon Sep 17 00:00:00 2001 From: Rikard Andersson Date: Tue, 14 May 2024 21:38:58 +0200 Subject: [PATCH 5/6] Revert "Use SqlPredicate without reflection" This reverts commit a503c2639d5e1647c5d69ad85c909c107647d03d. --- .../.idea/.gitignore | 13 +++++++++++++ .../Extensions/LinqExtensions.cs | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .idea/.idea.N.EntityFrameworkCore.Extensions/.idea/.gitignore diff --git a/.idea/.idea.N.EntityFrameworkCore.Extensions/.idea/.gitignore b/.idea/.idea.N.EntityFrameworkCore.Extensions/.idea/.gitignore new file mode 100644 index 0000000..b621b64 --- /dev/null +++ b/.idea/.idea.N.EntityFrameworkCore.Extensions/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/.idea.N.EntityFrameworkCore.Extensions.iml +/contentModel.xml +/modules.xml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs index 4a45b5d..aa2af58 100644 --- a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs +++ b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs @@ -134,7 +134,7 @@ public static List GetObjectProperties(this Expression(this Expression expression, params string[] parameters) + internal static string ToSqlPredicate2(this Expression expression, params string[] parameters) { var sql = ToSqlString(expression.Body); @@ -196,6 +196,21 @@ internal static string ToSql(this Expression expression) } return sb.ToString(); } + internal static string ToSqlPredicate(this Expression expression, params string[] parameters) + { + var stringBuilder = new StringBuilder((string)expression.Body.GetPrivateFieldValue("DebugView")); + int i = 0; + foreach (var expressionParam in expression.Parameters) + { + if (parameters.Length <= i) break; + stringBuilder.Replace((string)expressionParam.GetPrivateFieldValue("DebugView"), parameters[i]); + i++; + } + stringBuilder.Replace("&&", "AND"); + stringBuilder.Replace("==", "="); + stringBuilder.Replace("(System.Nullable`1[System.Int32])", ""); + return stringBuilder.ToString(); + } internal static string ToSqlUpdateSetExpression(this Expression expression, string tableName) { List setValues = new List(); From c001821a92e84478beeae175d7264075f4b6b921 Mon Sep 17 00:00:00 2001 From: Rikard Andersson Date: Tue, 14 May 2024 21:42:43 +0200 Subject: [PATCH 6/6] Added replace of non nullable int32 --- N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs index aa2af58..9b69c93 100644 --- a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs +++ b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs @@ -209,6 +209,7 @@ internal static string ToSqlPredicate(this Expression expression, params s stringBuilder.Replace("&&", "AND"); stringBuilder.Replace("==", "="); stringBuilder.Replace("(System.Nullable`1[System.Int32])", ""); + stringBuilder.Replace("(System.Int32)", ""); return stringBuilder.ToString(); } internal static string ToSqlUpdateSetExpression(this Expression expression, string tableName)