I got 3 models: Human
, Skill
and HumanSkill
. There is a many to many relationship between Human
and Skill
, the HumanSkill
is the intermediary table between them.
My query to the database loads the collection of the intermediary table HumanSkill
correctly, but does not load the reference navigation property Skill
through which I want to load the Skill
name (Human -> HumanSkill -> Skill -> Skill.name
) using a query projection with select.
public IActionResult Preview(int humanId)
{
var currentHuman = this.db.Humans
.Where(x => x.Id == humanId)
.Select(r => new HumanPreviewViewModel
{
PrimaryData = r.PrimaryData,
// How should I write this line?
Skills = r.Skills.ToList(),
}).SingleOrDefault();
return View(currentResume);
}
Human model:
public class Human
{
public Human()
{
this.Skills = new HashSet<HumanSkill>();
}
public int Id { get; set; }
public virtual PrimaryData PrimaryData { get; set; }
public virtual ICollection<HumanSkill> Skills { get; set; }
}
HumanSkill
model:
public class HumanSkill
{
public int Id { get; set; }
public int HumanId { get; set; }
public Human Human { get; set; }
public int SkillId { get; set; }
public Skill Skill { get; set; }
}
Skill model:
public class Skill
{
public Skill()
{
this.Humans = new HashSet<HumanSkill>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<HumanSkill> Humans { get; set; }
}
HumanPreviewViewModel:
public class HumanPreviewViewModel
{
public HumanPreviewViewModel()
{
}
public PrimaryData PrimaryData { get; set; }
public List<HumanSkill> Skills { get; set; }
}
}
CodePudding user response:
How can I achieve this without using include?
If you use some data from Skills table in the Select, EF will perform the necessary joins to retrieve the data
var currentHuman = this.db.Humans
.Where(x => x.Id == humanId)
.Select(r => new HumanPreviewViewModel
{
PrimaryData = r.PrimaryData,
SkillNames = r.Skills.Select(hs => hs.Skill.Name).ToList(),
}).SingleOrDefault();
CodePudding user response:
When projecting from entity to a view model, avoid mixing them. For example, do not have a view model contain a reference or set of entities. While it might not seem necessary, if you want a list of the skills with their ID and name in the HumanPreviewViewModel then create a serialize-able view model for the skill as well as the PrimaryData if that is another related entity. Where PrimaryData might be a one-to-one or a many-to-one the desired properties from this relation can be "flattened" into the view model.
[Serializable]
public class HumanPreviewViewModel
{
public Id { get; set; }
public string DataPoint1 { get; set; }
public string DataPoint2 { get; set; }
public List<SkillViewModel> Skills { get; set; }
}
[Serializable]
public class SkillViewModel
{
public int Id { get; set; }
public string Name { get; set; }
}
Then when you go to extract your Humans:
var currentHuman = this.db.Humans
.Where(x => x.Id == humanId)
.Select(r => new HumanPreviewViewModel
{
Id = r.Id,
DataPoint1 = r.PrimaryData.DataPoint1,
DataPoint2 = r.PrimaryData.DataPoint2,
Skills = r.Skills.Select(s => new SkillViewModel
{
Id = s.Skill.Id,
Name = s.Skill.Name
}).ToList()
}).SingleOrDefault();
The reason you don't mix view models and entities even if they share all of the desired fields is that your entities will typically contain references to more entities. When something like your view model gets sent to a Serializer such as to send to a client from an API or due to a page calling something an innocent looking as:
var model = @Html.Raw(Json.Encode(Model));
then the serializer can, and will touch navigation properties in your referenced entities which will trigger numerous lazy load calls.