private static async Task ExecuteWithGraphAsync(DbContext context, IEnumerable <object> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress, CancellationToken cancellationToken, bool isAsync) { if (operationType != OperationType.Insert && operationType != OperationType.InsertOrUpdate && operationType != OperationType.InsertOrUpdateDelete && operationType != OperationType.Update) { throw new InvalidBulkConfigException($"{nameof(BulkConfig)}.{nameof(BulkConfig.IncludeGraph)} only supports Insert or Update operations."); } // Sqlite bulk merge adapter does not support multiple objects of the same type with a zero value primary key if (SqlAdaptersMapping.GetDatabaseType(context) == DbServer.Sqlite) { throw new NotSupportedException("Sqlite is not currently supported due to its BulkInsert implementation."); } bulkConfig.PreserveInsertOrder = true; // Required for SetOutputIdentity ('true' is default but here explicitly assigned again in case it was changed to 'false' in BulkConfing) bulkConfig.SetOutputIdentity = true; // If this is set to false, won't be able to propogate new primary keys to the relationships // If this is set to false, wont' be able to support some code first model types as EFCore uses shadow properties when a relationship's foreign keys arent explicitly defined bulkConfig.EnableShadowProperties = true; var graphNodes = GraphUtil.GetTopologicallySortedGraph(context, entities); if (graphNodes == null) { return; } // Inserting an entity graph must be done within a transaction otherwise the database could end up in a bad state var hasExistingTransaction = context.Database.CurrentTransaction != null; var transaction = context.Database.CurrentTransaction ?? (isAsync ? await context.Database.BeginTransactionAsync() : context.Database.BeginTransaction()); try { // Group the graph nodes by entity type so we can merge them into the database in batches, in the correct order of dependency (topological order) var graphNodesGroupedByType = graphNodes.GroupBy(y => y.Entity.GetType()); foreach (var graphNodeGroup in graphNodesGroupedByType) { var entityClrType = graphNodeGroup.Key; var entityType = context.Model.FindEntityType(entityClrType); if (OwnedTypeUtil.IsOwnedInSameTableAsOwner(entityType)) { continue; } // It is possible the object graph contains duplicate entities (by primary key) but the entities are different object instances in memory. // This an happen when deserializing a nested JSON tree for example. So filter out the duplicates. var entitiesToAction = GetUniqueEntities(context, graphNodeGroup.Select(y => y.Entity)).ToList(); var tableInfo = TableInfo.CreateInstance(context, entityClrType, entitiesToAction, operationType, bulkConfig); if (isAsync) { await SqlBulkOperation.MergeAsync(context, entityClrType, entitiesToAction, tableInfo, operationType, progress, cancellationToken); } else { SqlBulkOperation.Merge(context, entityClrType, entitiesToAction, tableInfo, operationType, progress); } // Set the foreign keys for dependents so they may be inserted on the next loop var dependentsOfSameType = SetForeignKeysForDependentsAndYieldSameTypeDependents(context, entityClrType, graphNodeGroup).ToList(); // If there are any dependents of the same type (parent child relationship), then save those dependent entities again to commit the fk values if (dependentsOfSameType.Any()) { var dependentTableInfo = TableInfo.CreateInstance(context, entityClrType, dependentsOfSameType, operationType, bulkConfig); if (isAsync) { await SqlBulkOperation.MergeAsync(context, entityClrType, dependentsOfSameType, dependentTableInfo, operationType, progress, cancellationToken); } else { SqlBulkOperation.Merge(context, entityClrType, dependentsOfSameType, dependentTableInfo, operationType, progress); } } } if (hasExistingTransaction == false) { if (isAsync) { await transaction.CommitAsync(); } else { transaction.Commit(); } } } finally { if (hasExistingTransaction == false) { if (isAsync) { await transaction.DisposeAsync(); } else { transaction.Dispose(); } } } }