Home > Net >  EF core load data from child object using FK
EF core load data from child object using FK

Time:08-17

I have a class of Agent

public class Agent
{
    public string Id { get; set; }
    public string Name { get; set; }
    
    public List<AgentUser> Users { get; set; }
}

public class User
{
    public string Id { get; set; }
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }        
}

public class AgentUser
{
    public string Id { get; set; }        

    public string AgentId { get; set; }
    public Agent Agent { get; set; }

    public string UserId { get; set; }
    public User User { get; set; }
}

the relationship is 1-to-many. on the AgentUsers table, it only contains UserId. I want to know how to load the user details(email, firstname, lastName) when I call the Agents table.

This is my query, but it only contains UserId.

var result= await _context.Agents
            .Include(au => au.Users)
            .Where(x => x.Id.Equals(request.Id))
            .AsNoTracking()
            .FirstOrDefaultAsync();

Yes I can do a JOIN but How can I get the details from User table in the most efficient way using the FK only? Thanks!

CodePudding user response:

If you want to use the joining entity in the relationships like your example then the other option when reading data is to use projection rather than loading the entire entity graph:

var result= await _context.Agents
    .Where(a => a.Id.Equals(request.Id))
    .Select(a => new AgentSummaryViewModel
    {
        AgentId = a.Id,
        // other details from agent...
        Users = a.Users.Select(u => new UserSummaryViewModel
        {
            UserId = u.User.Id,
            Email = u.User.EMail,
            //  ... 
        }).ToList()
    })
    .SingleOrDefaultAsync();

This can be simplified using Automapper and it's ProjectTo() method against an EF IQueryable once a mapper is configured with how to build an Agent and User ViewModel. Projection has the added advantage of just loading as much data from the entity graph as your consumer (view, etc.) needs, building efficient queries. There is no need to eager load related entities or worry about tracking references since the entities themselves are not being loaded.

If you want to load that Agent and their Users for editing purposes then:

var agent = await _context.Agents
    .Include(a => a.Users)
        .ThenInclude(u => u.User)
    .Where(a => a.Id.Equals(request.Id))
    .SingleOrDefaultAsync();

Alternatively, with Many-to-Many relationships, if the joining table can be represented with just the two FKs as a composite PK then EF can map this table by convention without needing to declare the AgentUser as an entity. You only need to define the AgentUser if you want other details recorded and accessible about the relationship. So for example if you want to use a soft-delete system with an IsActive flag or such and removing an association between agent and user resulted in that record being made inactive rather than deleted, you would need an AgentUser entity.

So if you can simplify your AgentUser table to just:

AgentId [PK, FK(Agents)]
UserId [PK, FK(Users)]

... then your Entity model can be simplified to:

public class Agent
{
    public string Id { get; set; }
    public string Name { get; set; }
    
    public List<User> Users { get; set; }
}

The entity mapping would need to be added explicityly such as with OnModelCreating or via an IEntityTypeConfiguration implementation that is registered so that:

 modelBuilder.Entity<Agent>()
     .HasMany(x => x.Users)
     .WithMany(x => Agents);

Unfortunately with EF Core, many-to-many relationships mapped this way require navigation properties on both sides. (Agent has Users, and User has Agents)

EF should be able to work out the linking table by convention, but if you want more control over the table name etc. you can explicitly tell it what to use. This is where it can get a little tricky, but you can either do it by defining a joining entity, or without one, using a "shadow" entity. (Essentially a dictionary)

 modelBuilder.Entity<Agent>()
     .HasMany(x => x.Users)
     .WithMany(x => Agents);
     .UsingEntity<Dictionary<string, object>>(
         "AgentUsers",
         l => l.HasOne<Agent>().WithMany().HasForeignKey("AgentId"),
         r => r.HasOne<User>().WithMany().HasForeignKey("UserId"),
         j =>
         {
            j.HasKey("AgentId", "UserId");
            j.ToTable("AgentUsers"); // Or "UserAgents", or "AgentUserLinks" etc.
         });

Edit: A simple example with Automapper...

var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<Agent, AgentSummaryViewModel>();
    cfg.CreateMap<User, UserSummaryViewModel>();
});

var result= await _context.Agents
    .Where(a => a.Id.Equals(request.Id))
    .ProjectTo<AgentSummaryViewModel>()
    .SingleOrDefaultAsync();

"config" above can be extracted from a global Mapper set up with all of your mapper configurations, created ad-hoc as needed, or what I typically do is derive from mapping configuration expressions built as static methods in the view models themselves:

var config = new MapperConfiguration(AgentSummaryViewModel.BuildMapperConfig());

... then in the AgentSummaryViewModel:

public static MapperConfigurationExpression BuildConfig(MapperConfigurationExpression config = null)
{
    if (config == null)
        config = new MapperConfigurationExpression();

    config = UserSummaryViewModel.BuildMapperConfig(config);

    config.CreateMap<Agent, AgentSummaryViewModel>()
       .ForMember(x => x.Users, opt => opt.MapFrom(src => src.Users.Select(u => u.User)); 
       // append any other custom mappings such as renames or flatting from related entities, etc.

    return config;
}

The UserSummaryViewModel would have a similar BuildMapperConfig() method to set up converting a User to UserSummaryViewModel. The resulting mapper configuration expression can chain as many needed related view models as you need, located under their respective view models.

  • Related