private static void SetDependencies(DbContext dbContext, GraphDependency graphDependency, object graphEntity, INavigation navigation, object navigationValue, IDictionary <object, GraphDependency> result) { // Get the nested dependency for the navigationValue so we can add the inverse navigation dependency // incase the navigationValue entity does not have an explicitly defined navigation property back to its principal / dependent // i.e WorkOrderSpare.Spare but the Spare entity does not have a Spare.WorkOrderSpares navigation property var nestedDependency = GetFlatGraph(dbContext, navigationValue, result); if (nestedDependency is null) { return; } if (navigation.IsOnDependent // A navigation for an OwnedType will be dependent on its owner the in efcore dependency hierarchy, // but technically the Owner depends on the OwnedType if the OwnedType is part of its Owner's schema. || OwnedTypeUtil.IsOwnedInSameTableAsOwner(navigation)) { graphDependency.DependsOn.Add((navigationValue, navigation)); nestedDependency.Dependents.Add((graphEntity, navigation.Inverse ?? navigation)); } else { graphDependency.Dependents.Add((navigationValue, navigation)); nestedDependency.DependsOn.Add((graphEntity, navigation.Inverse ?? navigation)); } }
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.InsertOrUpdateOrDelete && 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(cancellationToken) : 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(cancellationToken); } else { transaction.Commit(); } } } finally { if (hasExistingTransaction == false) { if (isAsync) { await transaction.DisposeAsync(); } else { transaction.Dispose(); } } } }