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.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/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"); diff --git a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkMergeAsync.cs b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkMergeAsync.cs index dcda14c..ebdd81c 100644 --- a/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkMergeAsync.cs +++ b/N.EntityFrameworkCore.Extensions.Test/DbContextExtensions/BulkMergeAsync.cs @@ -351,5 +351,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 736637f..aa72d63 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 c15238f..9b69c93 100644 --- a/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs +++ b/N.EntityFrameworkCore.Extensions/Extensions/LinqExtensions.cs @@ -134,6 +134,43 @@ public static List GetObjectProperties(this Expression(this Expression expression, params string[] parameters) + { + 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 ToSql(this ExpressionType expressionType) { string value = string.Empty; @@ -172,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) diff --git a/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj b/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj index 467ffaa..ce7e7d3 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