diff --git a/EFCore.BulkExtensions.Tests/IncludeGraph/Issue1337.cs b/EFCore.BulkExtensions.Tests/IncludeGraph/Issue1337.cs new file mode 100644 index 00000000..daaad4c7 --- /dev/null +++ b/EFCore.BulkExtensions.Tests/IncludeGraph/Issue1337.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EFCore.BulkExtensions.Tests.IncludeGraph; + +public class Issue1337 +{ + [Fact] + public async Task Issue1337Test() + { + using var context = new MyDbContext(ContextUtil.GetOptions(databaseName: $"{nameof(EFCoreBulkTest)}_Issue1337")); + + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + var bulkConfig = new BulkConfig { IncludeGraph = true, CalculateStats = true }; + + Cust[] entities = [ + new() + { + CustId = "test1", + ContactMethods = new() + { + HomePhone = "homephone1", + EmailAdresses = [ + new Email() { Address = "email1", Type = "emailtype1" }, + new Email() { Address = "email2", Type = "emailtype2" }, + ] + }, + } + ]; + + // Without any changes, this crashes with System.InvalidOperationException : Column 'ContactMethodsCustomerId' does not allow DBNull.Value. + + // With the changes in this PR until now, the merge of the emails works, but now I get an error when trying to read the output of that operation: + // System.InvalidOperationException : An exception was thrown while attempting to evaluate a LINQ query parameter expression. See the inner exception for more information. + // ----System.InvalidOperationException : Cannot create a DbSet for 'Email' because it is configured as an owned entity type and must be accessed through its owning entity type 'ContactMethods'.See https://aka.ms/efcore-docs-owned for more information. + + await context.BulkInsertOrUpdateAsync(entities, bulkConfig); + } +} + +public class Cust +{ + public string CustId { get; set; } = default!; + public ContactMethods ContactMethods { get; set; } = default!; +} + +public class ContactMethods +{ + public string HomePhone { get; set; } = default!; + public ICollection EmailAdresses { get; set; } = default!; +} + +public class Email +{ + public string Address { get; set; } = default!; + public string Type { get; set; } = default!; +} + +public class MyDbContext(DbContextOptions opts) : DbContext(opts) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(c => + c.OwnsOne(c => c.ContactMethods, cm => cm.OwnsMany(y => y.EmailAdresses))); + } +} diff --git a/EFCore.BulkExtensions/DbContextBulkTransactionGraphUtil.cs b/EFCore.BulkExtensions/DbContextBulkTransactionGraphUtil.cs index 7bace99f..73c48f9a 100644 --- a/EFCore.BulkExtensions/DbContextBulkTransactionGraphUtil.cs +++ b/EFCore.BulkExtensions/DbContextBulkTransactionGraphUtil.cs @@ -100,6 +100,28 @@ private static async Task ExecuteWithGraphAsync(DbContext context, IEnumerable + context.Model.FindEntityType(ng.Key) is { } entityType + && OwnedTypeUtil.IsOwnedInSameTableAsOwner(entityType))) + { + foreach(var dependentOfSameType in SetForeignKeysForDependentsAndYieldSameTypeDependents(context, ng.Key, ng)) + { + var dependentTableInfo = TableInfo.CreateInstance(context, entityClrType, dependentsOfSameType, operationType, bulkConfig); + + if (isAsync) + { + await SqlBulkOperation.MergeAsync(context, entityClrType, dependentsOfSameType, dependentTableInfo, operationType, progress, cancellationToken).ConfigureAwait(false); + } + else + { + SqlBulkOperation.Merge(context, entityClrType, dependentsOfSameType, dependentTableInfo, operationType, progress); + } + + UpdateStats(stats, bulkConfig); + } + } } if (bulkConfig.CalculateStats) diff --git a/EFCore.BulkExtensions/Util/GraphUtil.cs b/EFCore.BulkExtensions/Util/GraphUtil.cs index 7cf31e12..f7e879be 100644 --- a/EFCore.BulkExtensions/Util/GraphUtil.cs +++ b/EFCore.BulkExtensions/Util/GraphUtil.cs @@ -110,7 +110,11 @@ private static void SetDependencies(DbContext dbContext, GraphDependency graphDe graphDependency.DependsOn.Add((navigationValue, navigation)); nestedDependency.Dependents.Add((graphEntity, navigation.Inverse ?? navigation)); } - else + + // Not sure this is the right thing to do here, but this way the "owned types in same table as owner" + // also get added to dependents, which is necessary to be able to set foreign keys of potential dependents of this owned type + // (See the change in DbContextBulkTransactionGraphUtil ExecuteWithGraphAsync) + if (!navigation.IsOnDependent) { graphDependency.Dependents.Add((navigationValue, navigation)); nestedDependency.DependsOn.Add((graphEntity, navigation.Inverse ?? navigation));