F. Korf
F. Korf

Reputation: 95

How to use entity's objects which are not loaded because of laziness (lazy-loading)?

I am trying to set up property for an entity which uses entity's objects (other entities loaded from database based on foreign key). My code looks like this:

[Table("Notes")]
public class Note : FullAuditedEntity
{
    public virtual int EntityAId { get; set; }
    public EntityA EntityA { get; set; }

    public virtual int EntityBId { get; set; }
    public EntityB EntityB { get; set; }    

    public List<NoteXHashtag> AdditionalHashtags { get; set; } = new List<CardXHashtag>();

    [NotMapped]
    public List<Hashtag> AllHashtags
    {
        get
        {
            List<Hashtag> allHashtags = new List<Hashtag>();
            allHashtags.AddRange(AdditionalHashtags.Select(x => x.Hashtag));
            allHashtags.Add(EntityA.Hashtag);
            allHashtags.Add(EntityB.Hashtag);
            return allHashtags;
        }
    }
}

When I try to use property AllHashtags from outside, EntityA and EntityB are null. How to tell Entity Framework to load them from database when I need to work with them?

Note: When I am calling AllHashtags from outside, I have both EntityA and EntityB Included:

noteRepository
.GetAll()
.Include(x => x.AdditionalHashtags).ThenInclude(x => x.Hashtag)
.Include(x => x.EntityA).ThenInclude(x => x.Hashtag)
.Include(x => x.EntityB).ThenInclude(x => x.Hashtag)
.Select(x => new NoteDetailDto()
{
    Id = x.Id,
    AllHashtags = x.AllHashtags
});

Upvotes: 2

Views: 304

Answers (1)

Steve Py
Steve Py

Reputation: 34908

If you are using projection (.Select()) then you do not need to use .Include() to have access to related entities.

I would start by looking at what your repository .GetAll() method is returning. .Include() only works against IQueryable so if the repository method is effectively doing something like this:

return _context.Notes.AsQueryable();

or this:

return _context.Notes.Where(x => x.SomeCondition);

Then you can leverage Include() outside of the repository method. However, if you were returning something like this:

return _context.Notes.Where(x => x.SomeCondition).ToList().AsQueryable();

Then the type would grant access the Include() method, but Include would not actually include the related tables. You can observe this by using a profiler on the database to inspect the queries. With Include, the query would join the EntityA and EntityB tables into the SELECT statement.

With regards to your example statement, it's obviously a simplified example you've supplied, but from what I can see it should not execute if the GetAll() method was returning an EF IQueryable. If it did return an EF IQueryable then accessing the AllHashtags property in a Select would result in an error because AllHashtags is not a mapped property of Note. This means that if your real code looks something like that, then GetAll() is not returning an EF IQueryable, or you've possibly created extension methods for Include/ThenInclude for IEnumerable that do an AsQueryable() to satisfy the EF include operations. (These will not work)

To get the results you want, I would recommend avoiding the use of an unmapped property on the entity, but rather take a 2-step approach with an anonymous type and keep the business logic transformation in the business layer:

Step 1. Ensure that the repository method is just returning an EF IQueryable, so context.[DbSet<TEntity>].AsQueryable() or context.[DbSet<TEntity>].Where([condition]) is fine:

return _context.Notes.AsQueryable();

Step 2. Define a DTO for your Hashtag. (avoid send/receive entities)

[Serializable]
public class HashtagDto
{
   public int Id { get; set;}
   public string Text { get; set; }
   // etc.
}

Step 3. Select the appropriate fields you care about into an anonymous type and materialize it:

var noteData = repository.GetAll()
    .Select( x= > new {
        x.Id,
        AdditionalHashtags = x.AdditionalHashtags.Select(h => new HashtagDto { Id = h.Id, Text = h.Text }).ToList(),
        EntityAHashtag = new HashTagDto { Id = x.EntityA.Hashtag.Id, Text = x.EntityA.Hashtag.Text },
        EntityBHashtag = new HashTagDto { Id = x.EntityB.Hashtag.Id, Text = x.EntityB.Hashtag.Text },
    ).ToList();

Step 4. Compose your view model / DTO:

var noteDetails = noteData.Select(x => new NoteDetailDto
{
    Id = x.Id,
    AllHashTags = condenseHashtags(x.AdditionalHashtags, EntityAHashtag, EntityBHashtag);
}).ToList();

Where the condenseHashtags is just a simple utility method:

private static ICollection<HashTagDto> condenseHashtags(IEnumerable<HashtagDto> source1, HashtagDto source2, HashtagDto source3)
{
   var condensedHashtags = new List<HashtagDto>(source1);
   if (source2 != null)  
       condensedHashtags.Add(source2);
   if (source3 != null)  
       condensedHashtags.Add(source3);
   return condensedHashtags;
}

The above example is synchronous, it can be transformed into async code without too much trouble if load performance is a concern for server responsiveness.

Steps 3 & 4 can be combined in a single statement, but there needs to be a .ToList() between them as Step 4 needs to run against Linq2Object to condense the hashtags. Step 3 ensures that the Linq2EF expression composes an efficient query to just return the information about the Note, and the associated hashtags that we care about, nothing more. Step 4 condenses those individual details into a DTO structure we intend to return.

Step 2 is important as you should avoid sending Entities back to the client/consumers. Sending entities can lead to performance issues and possible errors if lazy loading is enabled, or leads to incomplete renditions of data being passed around if lazy loading isn't enabled and you neglect to eager-load related info. (Is a related detail really #null, or did you forget to include it?) It can also reveal more about your data structure than consumers should necessarily know, and reflects larger data transfer packets than are needed. Accepting entities back from a client is highly inadvisable since it opens the door for unexpected data tampering, stale data overwrites, and your typical gambit of bugs around dealing with reattaching detached entities that a context may know about. If your code simply attaches entities to a DbContext, sets the modified state, and saves changes. Even if you don't do that today, it opens the door for later modifications to start doing it given an entity is already present in the call. Receive DTOs, load the entity, validate a row version, validate the snot out of the DTO against the entity, and only update the fields that are expected to change.

Upvotes: 1

Related Questions