Reputation: 1210
My issue is that EF Core is by default loading all my defined properties of a class, whereas I want it to not load them unless I specifically ask for them.
For example, take this simple example of a book with an author (all models are the same in this example but just to show the pattern used):
Database entity:
using System;
namespace Test.Models.DBModels
{
public partial class Book
{
public int BookId { get; set; }
public int AuthorId { get; set; }
public string Title { get; set; }
public Author Author { get; set; }
}
}
DTO:
using System;
using System.Collections.Generic;
namespace Test.Models.DTOModels
{
public partial class BookDTO
{
public int BookId { get; set; }
public int AuthorId { get; set; }
public string Title { get; set; }
public AuthorDTO Author { get; set; }
}
}
ViewModel:
using System;
using System.Collections.Generic;
namespace Test.Models.ViewModels
{
public partial class BookVM
{
public int BookId { get; set; }
public int AuthorId { get; set; }
public string Title { get; set; }
public AuthorVM Author { get; set; }
}
}
Notice there is no use of "virtual" properties on any of these classes as I'd read that this is what told EF to automatically populate them
DBContext:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Test.Models.DBModels;
namespace Test.DAL
{
public partial class TestContext : DbContext
{
public TestContext()
{
}
public TestContext(DbContextOptions<TestContext> options)
: base(options)
{
}
public virtual DbSet<Author> Author { get; set; }
public virtual DbSet<Book> Book { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
//optionsBuilder.UseSqlServer("connectionstring");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
Service:
using AutoMapper;
using System.Collections.Generic;
using System.Linq;
using Test.BLL.Interfaces;
using Test.DAL;
using Test.Models.DomainModels;
using Test.Models.DTOModels;
using Microsoft.EntityFrameworkCore;
using AutoMapper.QueryableExtensions;
using System.Linq.Expressions;
using System;
namespace Test.BLL.Implementations
{
public class BookService : IBookService
{
private readonly TestContext dbContext;
private readonly IMapper _mapper;
public BookService(TestContext dbContext, IMapper mapper)
{
this.dbContext = dbContext;
this._mapper = mapper;
}
public IQueryable<BookDTO> Get()
{
var books = dbContext.Book;
var dto = books.ProjectTo<BookDTO>(_mapper.ConfigurationProvider);
return dto;
}
public IQueryable<BookDTO> Get(params Expression<Func<Book, object>>[] includes)
{
var books = dbContext.Book
.Select(x => x);
foreach (var include in includes)
books = books.Include(include);
var dto = books.ProjectTo<BookDTO>(_mapper.ConfigurationProvider);
return dto;
}
}
}
Controller:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Test.BLL.Implementations;
using Test.Models.DTOModels;
using Test.Models.ViewModels;
namespace Test.WebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
private readonly BookService BookService;
private readonly IMapper _mapper;
public BookController(BookService BookService, IMapper mapper)
{
this.BookService = BookService;
this._mapper = mapper;
}
public IActionResult Index()
{
var books = _mapper.Map<IEnumerable<BookDTO>, IEnumerable<BookVM>>(BookService.Get().ToList());
return Ok(books);
}
}
}
If I call the BookService.Get().ToList() method in the controller then it is automatically populating the Author in the json results e.g.
{
"bookId":1,
"authorId":1,
"title":"Book A",
"author":{
"authorId":1,
"name":"Some Author"
}
}
Whereas I only want it to be:
{
"bookId":1,
"authorId":1,
"title":"Book A",
"author": null
}
As if I wanted the Author object populated I'd call my overloaded method using BookService.Get(x => x.Author).ToList()
I assume this is related to eager or lazy loading functionality, but I'm not sure how. EDIT: The documentation describes them as "Lazy loading means that the related data is transparently loaded from the database when the navigation property is accessed". They also say that "Eager loading means that the related data is loaded from the database as part of the initial query" which is the behavior I want, but only for the properties I specify.
Is there a way in EF Core that I can get it to only populate the properties if I am specifically requesting them to be included?
Upvotes: 1
Views: 2305
Reputation: 1210
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.
Upvotes: 4
Reputation: 13773
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.
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.
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.
Upvotes: 4
Reputation: 720
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
Upvotes: 1