Il mio problema è che EF Core sta caricando di default tutte le mie proprietà definite di una classe, mentre voglio che non le carichi a meno che non le chieda specificamente.
Ad esempio, prendi questo semplice esempio di un libro con un autore (tutti i modelli sono uguali in questo esempio ma solo per mostrare il modello utilizzato):
Entità del database:
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; }
}
}
Notate che non c'è alcun uso di proprietà "virtuali" su nessuna di queste classi poiché leggo che questo è ciò che ha detto a EF di popolarle automaticamente
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);
}
}
Servizio:
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);
}
}
}
Se chiamo il metodo BookService.Get (). ToList () nel controller, popola automaticamente l'autore nei risultati json, ad es.
{
"bookId":1,
"authorId":1,
"title":"Book A",
"author":{
"authorId":1,
"name":"Some Author"
}
}
Mentre voglio solo che sia:
{
"bookId":1,
"authorId":1,
"title":"Book A",
"author": null
}
Come se volessi popolare l'oggetto Author, chiamerei il mio metodo sovraccarico usando BookService.Get (x => x.Author) .ToList ()
Presumo che ciò sia legato alla funzionalità di caricamento desideroso o pigro, ma non sono sicuro di come. EDIT: La documentazione li descrive come " Caricamento lento significa che i dati relativi vengono caricati in modo trasparente dal database quando si accede alla proprietà di navigazione ". Dicono anche che " Caricamento desideroso significa che i dati correlati vengono caricati dal database come parte della query iniziale " che è il comportamento che desidero, ma solo per le proprietà che specifico.
Esiste un modo in EF Core che posso ottenerlo per popolare le proprietà solo se le sto specificatamente richiedendo di essere incluse?
La risposta di Flater e il commento di Lucian Bargaoanu mi hanno portato alla corretta implementazione (Espansione esplicita). Nel profilo di mappatura di Automapper posso specificare che non desidero espandere automaticamente ciascuna proprietà, ad es
CreateMap<Book, BookDTO>()
.ForMember(x => x.Author, options => options.ExplicitExpansion())
.ReverseMap();
Se poi cambio il mio metodo di overload per passare le inclusioni nel metodo ProjectTo:
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;
}
Ciò significa che, per impostazione predefinita, chiamando BookService.Get (). ToList () si tradurrà in:
{
"bookId":1,
"authorId":1,
"title":"Book A",
"author": null
}
Ma chiamando BookService.Get (x => x.Author) .ToList () restituirà:
{
"bookId":1,
"authorId":1,
"title":"Book A",
"author":{
"authorId":1,
"name":"Some Author"
}
}
Ciò significa che posso continuare a utilizzare AutoMapper senza che tutte le proprietà vengano popolate automaticamente da EF Core.
Questo non è un comportamento EF, è un comportamento di Automapper.
public IQueryable<BookDTO> Get()
{
var books = dbContext.Book;
var dto = books.ProjectTo<BookDTO>(_mapper.ConfigurationProvider);
return dto;
}
ProjectTo<>
seleziona intenzionalmente tutte le proprietà su cui può essere mappato. Se gli dici di proiettare su un BookDTO
, farà del suo meglio per riempire tutte le proprietà definite in BookDTO
, che include l'autore.
Entity Framework ha determinati comportamenti riguardo al caricamento delle proprietà di navigazione, generalmente descritti come caricamento lento e desideroso. Inizialmente, penseresti che questa fosse la fonte del problema.
Tuttavia, quando si utilizza un Select
, si sovrascrive efficacemente i comportamenti di caricamento di EF e si dice esplicitamente cosa dovrebbe caricare per te. Questo è intenzionale, da utilizzare nei casi in cui i comportamenti semplici di EF non forniscono il controllo preciso che stai cercando.
Non stai utilizzando un Select
, ma si sta utilizzando ProjectTo<>
, che usa internamente un Select
(che si genera in base alla configurazione Automapper), il che significa che, per quanto EF è interessato, si sta sostituzione del comportamento di carico e "voi "(ovvero Automapper) stanno esplicitamente dicendo a EF di caricare l'autore.
Puoi dire ad Automapper di ignorare una proprietà usando l'attributo corretto:
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; }
}
Questo porterà ad Automapper a non recuperare l'autore correlato dal database.
Tuttavia, parte del punto di forza di ProjectTo<>
è che non devi più gestire ciò che fai / non vuoi caricare e invece lascia che Automapper lo capisca in base al DTO fornito. Non è un male mettere un attributo su un DTO, ma se inizi ad applicarlo su larga scala, aumenterà la complessità di sviluppo e manutenzione.
Invece, suggerirei di creare due classi DTO separate, una con informazioni sull'autore e una senza. In questo modo, non devi controllare manualmente il comportamento della mappatura (non più di quanto dovresti) e ti salverà anche su un sacco di controlli null che non devi eseguire quando gestisci questo DTO senza il suo autore anche caricato.