Reputation: 325
Hi Im new to LINQ and EF and im trying to understand why the below code doesn't return the nested entities even though I explicitly load them using the include
var x = await _context.AuthorBooks.Where(ub => ub.AuthorId == authorId)
.Include(ub => ub.Book)
.ThenInclude (b=> b.Chapters)
.Select(ub => ub.Book).ToListAsync();
AuthorBooks is the linking object and where I can apply the filter to select only books from the specific author.
I am trying to select the list of all Books with chapters for the given author but the above code returns the list of books but without the nested Chapters.
Any help?
Upvotes: 0
Views: 1172
Reputation: 13823
You're touching on a behavior that does exist in EF.
The problem lies in how EF handles the loading of data. By default, it loads all scalar properties of an object, but not the navigational properties.
Include
influences this behavior, by telling EF to also include a specified navigational property (with all of its scalar properties)
But then we get to Select
. When you use this, you are essentially giving a fixed list of columns that you want to retrieve. EF will limit itself to the fields that you mention.
var x1 = _context.Books.Select(b => b.Name).ToList();
This will result in a query that only retrieves a single column.
var x2 = _context.AuthorBooks.Select(ab => ab.Book).ToList();
Since you're referring to a navigational property (without specifying any particular scalar property inside), EF uses its default behavior of loading all of the navigational property's scalar properties. The query will fetch X columns (where X is the amount of scalar properties in Book
).
var x3 = _context.AuthorBooks.Select(ab => ab.Book.Name).ToList();
This again will result in a query that only retrieves a single column, since you reference a specific scalar property.
1. Invert the query so you don't need Select
.
This works for your current case, but won't work for everything.
var x = await _context.Books
.Include(b=> b.Chapters)
.Where(b => b.AuthorBooks.Any(ab => ab.AuthorId == authorId))
.ToListAsync();
2. Perform the Select
after retrieving the data.
For your case, this will cause you to load the AuthorBook
entities, which is not ideal. It works, but you're fetching more data than you need. However, this approach is better in cases where 1. is not a viable approach
var x = await _context.AuthorBooks
.Include(ub => ub.Book)
.ThenInclude(b=> b.Chapters)
.Where(ub => ub.AuthorId == authorId)
//Fetch the data
.ToList()
//Now transform the result
.Select(ub => ub.Book)
.ToListAsync()
3. Explicitly add the data you want to the Select
var x = await _context.AuthorBooks.Where(ub => ub.AuthorId == authorId)
.Select(ub => new {
Book = ub.Book,
Chapters = ub.Book.Chapters
});
Include
statements. Since you're explicitly telling EF what it should retrieve, it doesn't need to rely on implicit instructions about what navigational properties it should load.4. Add an Include
after the Select
.
Credits go to NetMage for mentioning it first in the comments.
var x = await _context.AuthorBooks.Where(ub => ub.AuthorId == authorId)
.Select(ub => ub.Book)
.Include(b => b.Chapters)
.ToListAsync();
Note that the earlier includes are not necessary since the subsequent Select
overrides them anyway.
Option 4 is the cleanest solution, in my opinion.
However, option 3 is better if you're only interested in a subset of the navigational properties. For example, if you only want to get chapters with a minimum word count:
var x = await _context.AuthorBooks.Where(ub => ub.AuthorId == authorId)
.Select(ub => new {
Book = ub.Book,
Chapters = ub.Book.Chapters.Where(c => c.WordCount > 1000)
});
Include
loads all related properties. An explicit Select
give you the option of loading a subset of related properties, thus lowering the amount of data to transfer.
Upvotes: 1