Home > Enterprise >  How to delete self referencing entity?
How to delete self referencing entity?

Time:10-09

Context

In my current project, there is a hierarchical folder structure saved in a database. The entity is the following:

public class Folder
{
    public string Id { get; set; }
    public string ParentFolderId { get; set; }
    public Folder ParentFolder { get; set; }
    public List<Folder> ChildFolders { get; set; } // Relationship property
    // Other properties
}

Only the root folder has null as ParentFolderId. All other folders have a non-null ParentFolderId. EFCore doesn't allow to set up cascade delete on ParentFolderId.

Question

When a folder is deleted, I would like to delete all subfolders recursively as well. How can this be implemented?

What I tried

In DbContext:

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Entity<Folder>()
        .HasOne(e => e.ParentFolder)
        .WithMany(e => e.ChildFolders)
        .OnDelete(DeleteBehavior.ClientCascade);
}

Delete call:

public void DeleteFolder(string folderId)
{
    var folder = await _db.Folders
        .Include(d => d.ParentFolder)
        .Include(f => f.ChildFolders)
        .FirstOrDefaultAsync(d => d.Id == folderId);

    if (folder == null) return;
    if (folder.ParentFolderId == null) return null;

    _db.Folders.Remove(folder);
    _db.SaveChanges();
}

However, in this situation, I am only loading subfolders one level below in the hierarchy. I can call ThenInclude a bunch of times but cannot guarantee to have reached all subfolders.

CodePudding user response:

Here's a way to do it by including a method to get all the sub-folders. The down side is this requires one select for each level of subfolders in the hierarchy. The alternative would be SQL where you can write a recursive query to get all the ids to delete and then delete them.

public void DeleteFolder(string folderId)
{
    var folder = await _db.Folders
        .FirstOrDefaultAsync(d => d.Id == folderId);

    if (folder == null) return;

    var foldersToDelete = await SubFolders(folder.Id);
    foldersToDelete.Add(folder);

    _db.Folders.RemoveRange(foldersToDelete);
    _db.SaveChanges();
}

public async Task<List<Folder>> SubFolders(string folderId)
{
    var subFolders = await _db.Folders
        .Where(d => d.ParentFolderId == folderId)
        .ToListAsync();

    var allFolders = new List<Folder>();
    foreach(var subFolder in subFolders)
    {
        allFolders.Add(subFolder);
        allFolders.AddRange(await SubFolders(subFolder.Id));
    }

    return allFolders;
}

Note this with run forever if you have any cycles in your folders (like folder A's parent is folder B and folder B's parent is folder A)

CodePudding user response:

I ended up changing @juharr's answer a little bit. Instead of creating a bunch of lists, I create one list and pass its reference among the recursive function calls.

Here is my final code:

public void DeleteFolder(string userId, string folderId)
{
    var folder = await _db.Folders
        .Include(d => d.ParentFolder)
        .Include(f => f.ChildFolders)
        .FirstOrDefaultAsync(d => d.Id == folderId);

    if (folder == null) return;
    if (folder.ParentFolderId == null) return;

    var foldersToBeDeleted = new List<Folder>();

    folder.ChildFolders.ForEach(childFolder => PopulateSubFolder(childFolder.Id, foldersToBeDeleted));
    foldersToBeDeleted.Add(folder);

    _db.Folders.RemoveRange(foldersToBeDeleted);
    _db.SaveChanges();
}

private void PopulateSubFolder(string folderId, ICollection<Folder> foldersToDelete)
{
    var folder = _db.Folders
        .Include(f => f.ChildFolders)
        .FirstOrDefault(f => f.Id == folderId);

    if (folder == null) return;

    // Add child folders
    folder.ChildFolders.ForEach(subFolder => PopulateSubFolder(subFolder.Id, foldersToDelete));

    // Add current folder
    foldersToDelete.Add(folder);
}
  • Related