Prevent properties being loaded unless specifically requested
回答1
The problem source
This isn't an EF behavior, it's an Automapper behavior.
public IQueryable<BookDTO> Get()
{
var books = dbContext.Book;
var dto = books.ProjectTo<BookDTO>(_mapper.ConfigurationProvider);
return dto;
}
ProjectTo<>
intentionally selects all properties that it can map to. If you tell it to project to a BookDTO
, it will do its best to fill all of the properties defined in BookDTO
, which includes the author.
Further explanation
Entity Framework has certain behaviors in regards to loading navigational properties, generally described as lazy and eager loading. Initially, you'd think that this was the source of the problem.
However, when you use a Select
, you effectively override EF's loading behaviors and tell it explicitly what it should load for you. This is by intention, to be used in cases where EF's simple behaviors don't provide the precise control that you're looking for.
You're not using a Select
, but you are using ProjectTo<>
which internally uses a Select
(which it generates based on the Automapper configuration), which means that as far as EF is concerned, you are overriding the loading behavior and "you" (i.e. Automapper) are explicitly telling EF to load the author.
The solution(s)
You can tell Automapper to ignore a property using the correct attribute:
public partial class Book
{
public int BookId { get; set; }
public int AuthorId { get; set; }
public string Title { get; set; }
[NotMapped]
public Author Author { get; set; }
}
This will lead to Automapper not fetching the related author from the database.
However, part of the strength of ProjectTo<>
is that you no longer have to manage what you do/don't want to load and instead let Automapper figure it out based on the provided DTO. It's not too bad to put one attribute on a DTO, but if you start applying this on a large scale, it's going to increase development and maintenance complexity.
Instead, I would suggest you create two separate DTO classes, one with Author information and one without. That way, you don't have to manually control the mapping behavior (any more than you should), and it'll also save you on a bunch of null checks that you don't need to perform when handling this DTO without its author being loaded as well.
回答2
The answer by Flater and the comment from Lucian Bargaoanu have led me to the correct implementation (Explicit expansion). In the Automapper mapping profile I can specify that I don't want to automatically expand each property e.g.
CreateMap<Book, BookDTO>()
.ForMember(x => x.Author, options => options.ExplicitExpansion())
.ReverseMap();
If I then change my overloaded Get method to pass the includes into the ProjectTo method:
public IQueryable<BookDTO> Get(params Expression<Func<BookDTO, object>>[] includes)
{
var books = dbContext.Book
.Select(x => x);
var dto = books.ProjectTo<BookDTO>(_mapper.ConfigurationProvider, null, includes);
return dto;
}
This then means that by default calling BookService.Get().ToList() will result in:
{
"bookId":1,
"authorId":1,
"title":"Book A",
"author": null
}
But calling BookService.Get(x => x.Author).ToList() will return:
{
"bookId":1,
"authorId":1,
"title":"Book A",
"author":{
"authorId":1,
"name":"Some Author"
}
}
This means that I can continue using AutoMapper without all the properties being automatically populated by EF Core.
回答3
You can use Lazy Loading for this.
There are two options: lazy loading via proxies or via the ILazyLoader
service.
I personally have always gone with the proxies approach.
[Package Manager Console]
install-package Microsoft.EntityFrameworkCore.Proxies
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLazyLoadingProxies();
}
Now mark all navigation properties you don't want to be eagerly loaded as 'virtual' and they will be lazy loaded.
More information and documentation for this can be found here: https://www.learnentityframeworkcore.com/lazy-loading