Запретить загрузку свойств, если не указано иное - PullRequest
1 голос
/ 04 марта 2020

Моя проблема в том, что EF Core по умолчанию загружает все мои определенные свойства класса, в то время как я хочу, чтобы он не загружал их, если я специально не запрашиваю их.

Например, возьмем этот простой пример книга с автором (все модели в этом примере одинаковы, но только для того, чтобы показать используемый шаблон):

Сущность базы данных:

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; }
    }
}

Обратите внимание, что «виртуальные» свойства не используются ни в одном из этих классов, так как я прочитал, что это то, что сказал EF для их автоматического заполнения

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);
    }
}

Служба:

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;
        }

    }
}

Контроллер:

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);
        }
    }
}

Если я вызываю BookService.Get () .ToList () метод в контроллере, тогда он автоматически заполняет автора в результатах json, например,

{
   "bookId":1,
   "authorId":1,
   "title":"Book A",
   "author":{
      "authorId":1,
      "name":"Some Author"
   }
}

В то время как я хочу, чтобы это было:

{
   "bookId":1,
   "authorId":1,
   "title":"Book A",
   "author": null
}

Как если бы я хотел заполнить объект Author, я бы вызвал мой перегруженный метод, используя BookService.Get (x => x.Autho r) .ToList ()

Я предполагаю, что это связано с функциональностью активной или отложенной загрузки, но я не уверен, как это сделать. РЕДАКТИРОВАТЬ: Документация описывает их как " Ленивая загрузка означает, что связанные данные прозрачно загружаются из базы данных при обращении к свойству навигации ". Они также говорят, что « Стремительная загрузка означает, что связанные данные загружаются из базы данных как часть начального запроса », что является желаемым поведением, но только для указанных мною свойств.

Есть ли способ в EF Core, чтобы я мог заполнить свойства, только если я специально запрашиваю их включение?

Ответы [ 3 ]

1 голос
/ 04 марта 2020

Источник проблемы

Это не поведение EF, а поведение Automapper.

public IQueryable<BookDTO> Get()
{
    var books = dbContext.Book;
    var dto = books.ProjectTo<BookDTO>(_mapper.ConfigurationProvider);
    return dto;
}

ProjectTo<> преднамеренно выбирает все свойства, на которые оно может отображаться. Если вы скажете ему проецировать на BookDTO, он сделает все возможное, чтобы заполнить все свойства, определенные в BookDTO, включая автора.


Дальнейшее объяснение

Entity Framework имеет определенные особенности в отношении загрузки навигационных свойств, обычно описываемых как отложенная и активная загрузка. Первоначально вы могли бы подумать, что это было источником проблемы.

Однако, когда вы используете Select, вы эффективно переопределяете поведение загрузки EF и прямо указываете, что он должен загружать для вас. Это намеренно, для использования в случаях, когда простое поведение EF не обеспечивает точный контроль, который вы ищете.

Вы не используете Select, но используете ProjectTo<> который внутренне использует Select (который генерируется на основе конфигурации Automapper), что означает, что в отношении EF вы переопределяете поведение загрузки, и «вы» (т.е. Automapper) явное указание EF загружать автора.


Решение (я)

Вы можете указать Automapper игнорировать свойство, используя правильный атрибут:

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; }
}

Это приведет к тому, что Automapper не получит связанного автора из базы данных.

Однако, часть силы ProjectTo<> в том, что вам больше не нужно управлять тем, что вы делаете / не хотите загружать, и вместо этого пусть Automapper выяснит это на основе предоставленного DTO. Не так уж плохо поместить один атрибут в DTO, но если вы начнете применять его в больших масштабах, это увеличит сложность разработки и сопровождения.

Вместо этого я бы предложил вам создать два отдельных класса DTO, один с информацией об авторе и один без. Таким образом, вам не нужно вручную управлять поведением отображения (больше, чем нужно), и это также сэкономит вам кучу пустых проверок, которые вам не нужно выполнять при обработке этого DTO без его автора загружается также.

0 голосов
/ 04 марта 2020

Ответ Флэтера и комментарий Люсьена Баргаоану привели меня к правильной реализации (явное расширение). В профиле сопоставления Automapper я могу указать, что не хочу автоматически расширять каждое свойство, например

CreateMap<Book, BookDTO>()
  .ForMember(x => x.Author, options => options.ExplicitExpansion())
  .ReverseMap();

Если я затем изменю свой перегруженный метод Get, чтобы передать включения в метод 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;
}        

Это означает, что по умолчанию вызов BookService.Get (). ToList () приведет к:

{
   "bookId":1,
   "authorId":1,
   "title":"Book A",
   "author": null
}

Но вызову BookService.Get (x => x.Author) .ToList ( ) вернет:

{
   "bookId":1,
   "authorId":1,
   "title":"Book A",
   "author":{
      "authorId":1,
      "name":"Some Author"
   }
}

Это означает, что я могу продолжать использовать AutoMapper без автоматического заполнения всех свойств EF Core.

0 голосов
/ 04 марта 2020

Вы можете использовать Lazy Loading для этого.

Есть два варианта: отложенная загрузка через прокси или через сервис ILazyLoader.

лично у меня всегда ушли с подходом прокси.

[Package Manager Console]
install-package Microsoft.EntityFrameworkCore.Proxies
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseLazyLoadingProxies();
}

Теперь отметьте все свойства навигации, которые вы не хотите загружать, как «виртуальные», и они будут загружены с отложенной загрузкой.

Подробнее Информацию и документацию для этого можно найти здесь: https://www.learnentityframeworkcore.com/lazy-loading

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...