简体   繁体   中英

Include, select nested objects not returning

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?

Explanation

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.


Solution

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
                                        });
  • Notice how you don't need the 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.
  • Instead of an anynomous type, you could use a tuple or a class. The return type is up to you to decide, just make sure that you explicitly reference all the data you want (scalar properties will be loaded automatically for all referenced entities).

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM