public async Task <ContentTree> CloneContentTree(string treeId, string newVersionCode) { // Use No Tracking since we'll be inserting a new content tree, not updating the source var sourceTree = await _connectDb.ContentTrees .AsNoTracking() .Include(x => x.ContentNodes) .FirstOrDefaultAsync(x => x.Id == treeId); var sourceNodes = sourceTree.ContentNodes.ToList(); var nodeKeyMap = new Dictionary <string, string>(); if (sourceTree == null) { throw new NullReferenceException($"Could not locate a record for version id: {treeId}"); } // Initialize the cloned tree var clonedTree = new ContentTree { Id = Guid.NewGuid().ToString("N"), ContentType = sourceTree.ContentType, ContentId = sourceTree.ContentId, VersionCode = newVersionCode, ContentNodes = new List <ContentNode>() }; // Clone the content node structure foreach (var node in sourceNodes) { // Generate a new node key and track old id var newNodeId = Guid.NewGuid().ToString("N"); nodeKeyMap.Add(node.Id, newNodeId); node.Id = newNodeId; // Instruct the underlying provider to clone a new content instance var clonedWidget = _widgetProvider.CloneSettings(node.WidgetType, node.WidgetId); // Some content types have static or null models, thus return null clones. // This is expected behavior and should result in a null mapping value node.WidgetId = clonedWidget?.Id; } // 2nd pass (efficient) to update parent Ids using the node map foreach (var node in sourceNodes) { if (node.ParentId != null && nodeKeyMap.ContainsKey(node.ParentId)) { node.ParentId = nodeKeyMap[node.ParentId]; } } clonedTree.ContentNodes.AddRange(sourceTree.ContentNodes); //TODO: Wrap in transaction to ensure data integrity _connectDb.ContentTrees.Add(clonedTree); await _connectDb.SaveChangesAsync(); return(clonedTree); }