Home > Enterprise >  How to rebuild a tree hierarchy from a closure table (Entity Framework Core 5)
How to rebuild a tree hierarchy from a closure table (Entity Framework Core 5)

Time:10-11

I'm having a tough time building my tree hierarchy for a dto. The tree hierarchy is captured in a closure table with depths. The closure table is built with triggers in the database, and is not relationally mapped to the Teams table in EF.

This is my basic Class structure:

public class Team
{
  public Guid Id {get; set;}
  public string Title {get; set;}
  public Guid? ParentTeamId {get; set;}
  public Team ParentTeam {get; set;}
}

public class TeamDto
{
  public Guid Id {get; set;}
  public string Title {get; set;}
  public Guid? ParentTeamId {get; set;}
  public ICollection<TeamDto> SubTeams {get; set;} = new List<TeamDto>();
}

The Closure table is structured like so:

public class Closure
{
  public Guid ParentId {get; set;}
  public Guid ChildId {get; set;}
  public int Depth {get; set;}
}

The EF definitions are as such:

public void Configure(EntityTypeBuilder<Team> builder)
    {
        builder.HasOne(p => p.ParentTeam)
            .WithMany()
            .HasForeignKey(k => k.ParentTeamId)
            .IsRequired(false)
            .OnDelete(DeleteBehavior.Cascade);
    }
public void Configure(EntityTypeBuilder<Closure> builder)
    {
        builder.HasKey(aa => new {aa.ChildId, aa.ParentId});
    }

The best I've been able to do when querying against this is to fill subTeams at the first level. I'm using AutoMapper, but am not wedded to an AutoMapper solution.

My query to get a list of teams related to a user(already captured in the teamMemberships list, and _mapper is the AutoMapper injection and _context is my database context injection) looks like this:

var teamList = await _context.Closures
                .Where(x => teamMemberships.Contains(x.ChildId))
                .Select(x => x.ParentId)
                .ToListAsync();

var query = _context.Teams
  .Where(d => teamList.Contains(d.Id) && x.ParentTeamId == null)
  .Select(x => new TeamDto
  {
    Id = x.Id,
    ParentTeamId = x.ParentTeamId,
    Title = x.Title,
    SubTeams = _mapper.Map<List<TeamDto>>(_context.Teams.Where(y => y.ParentTeamId == x.Id))
   })
   .AsQueryable();

I need to somehow pull SubTeams out recursively so that the full tree is established in the Dto. I'm thinking that there may be a query referencing the closure table that could do this. The dto when sent as a json should look something like this:

[
  {
    "id": "e5500e84-21fb-4b5e-aa9b-a95e51d3dd05",
    "parentId": null,
    "title": "Team 1",
    "subTeams": [
      {
        "id": "05a8d423-feb1-4ea6-9596-d26b18feaa9e",
        "parentId": "e5500e84-21fb-4b5e-aa9b-a95e51d3dd05",
        "title": "Team 1.1",
        "subTeams": [
          {
            "id": "251358d9-3c5a-43b5-91f1-c2e82778c50b",
            "parentId": "05a8d423-feb1-4ea6-9596-d26b18feaa9e",
            "title": "Team 1.1.1",
            "subTeams": []
          },
          {
            "id": "4e27be0b-3a7b-4c8c-8039-2a18e1afcbd0",
            "parentId": "05a8d423-feb1-4ea6-9596-d26b18feaa9e",
            "title": "Team 1.1.2",
            "subTeams": []
          }
        ]
      },
      {
        "id": "2a4d56fc-00f0-4418-80a2-7d870c0ddee5",
        "parentId": "e5500e84-21fb-4b5e-aa9b-a95e51d3dd05",
        "title": "Team 1.2",
        "subTeams": []
      }
    ]
  },
  ...
]

Any insight is greatly appreciated.

CodePudding user response:

Thanks to Gert Arnold and the others that answered.

My solution did involve a recursive function. I pulled all related teams using the team closure table, then built the tree in memory from that pull.

// teamMemberships is a list of teamIds that are relevant to the user
// I pull all related teams to that list (because a user, for example is a member
// of Team 1 implicitly if they are a member of 1.1, but not explicitly) because
// I need all ancestors and descendants of the selected teams
            var allRelatedTeams = await _context.TeamClosures
                .Where(x => teamMemberships.Contains(x.ChildId) || teamMemberships.Contains(x.ParentId))
                .Select(x => x.ParentId)
                .ToListAsync();

            var teams = _context.Teams
                .Where(x => allRelatedTeams.Contains(x.Id))
                .ProjectTo<TeamDto>(_mapper.ConfigurationProvider)
                .AsQueryable();
            
            var query = teams
                .Where(x => x.ParentTeamId == null)
                .AsQueryable();

            foreach (var team in query)
            {
                ParseSubTeams(team, await teams.ToListAsync());
            }

Recursive function:

        private void ParseSubTeams(TeamDto team, List<TeamDto> teamList)
        {
            var childTeams = teamList
                .Where(x => _context.TeamClosures
                    .Where(y => y.ParentId == team.Id)
                    .Select(z => z.ChildId)
                    .ToList().Contains(x.Id))
                .ToList();
            foreach (var subTeam in childTeams)
            {
                if (subTeam.ParentTeamId == team.Id)
                {
                    team.SubTeams.Add(_mapper.Map<TeamDto>(subTeam));
                    ParseSubTeams(subTeam, teamList);
                }
            }
        }

            
  • Related