在 ASP.NET Core Web API 中分别使用实体和业务模型从相关表中获取数据的正确方法是什么?

问题描述 投票:0回答:1

我有一个

BookEntity
和我的域
Book
模型:

public class BookEntity
{
     [Key]
     public Guid BookId { get; set; }
     public string Title { get; set; } = string.Empty;
     public string Description { get; set; } = string.Empty;
     public decimal Price { get; set; }

     public BookDetailEntity BookDetail { get; set; } = null!;

     [ForeignKey("Genre")]
     public Guid GenreId { get; set; }
     public GenreEntity Genre { get; set; } = null!;

     public ICollection<BookAuthorMap> BookAuthor = new List<BookAuthorMap>();
}
namespace BookStore.Core.Models
{
    public class Book
    {
        public const int MAX_TITLE_LENGTH = 250;

        private Book(Guid id, string title, string description, decimal price)
        {
            Id = id;
            Title = title;
            Description = description;
            Price = price;
        }

        public Guid Id { get; }
        public string Title { get; } = string.Empty;
        public string Description { get; } = string.Empty;
        public decimal Price { get; }

        public static (Book Book, string Error) Create (
            Guid id,  string title, 
            string description, decimal price)
        {
            var error = string.Empty;

            if (string.IsNullOrEmpty(title) || title.Length > MAX_TITLE_LENGTH)
            {
                error = "Title cannot be empty or longer than 250";
            }

            if (price < 0) 
                error = "Price cannot be negative";

            var book = new Book (id, title, description, price);

            return (book, error);
        }
    }
}

我还有其他彼此相关的实体,例如这个:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BookStore.DataAccess.Entities
{
    public class BookDetailEntity
    {
        [Key]
        public Guid BookDetailId { get; set; }
        public string ISBN { get; set; } = string.Empty;
        public int Pages { get; set; }
        public DateTime PublicationDate { get; set; }
        public string Language { get; set; } = string.Empty;

        [ForeignKey("Book")]
        public Guid BookId { get; set; }
        public BookEntity Book { get; set; } = null!;
    }
}

我从

Book
获取我的实体
DbSet
,然后解析到域模型,然后解析到前端的 DTO:

public async Task<List<Book>> GetAllBooks()
{
    var bookEntities = await _context.Books
        .AsNoTracking()
        .ToListAsync();

    var books = bookEntities
        .Select(b => Book.Create(b.BookId, b.Title, b.Description, b.Price).Book)
        .ToList();
        
    return books;
}
public class BooksController : BaseController
{
    private readonly IBooksService _booksService;

    public BooksController(IBooksService booksService)
    {
        _booksService = booksService;
    }

    [HttpGet]
    public async Task<ActionResult<List<BooksResponse>>> GetBooks()
    {
        var books = await _booksService.GetAllBooks();

        var response = books.Select(b => new BooksResponse(b.Id, b.Title, b.Description, b.Price));

        return Ok(response);
    }
}

我应该采取什么方法来获取所有相关数据?

扩展业务模型或以一种方法或其他方法使用存储库方法?

项目正在使用清洁架构(存储库 - 服务 - API)

entity-framework asp.net-core asp.net-core-webapi domain-driven-design relationship
1个回答
0
投票

实体可以/应该作为您的域,中间人的情况很少。 DTO/ViewModel 可以进行投影,理想情况下,您需要 EF 或与 EF

IQueryable
配合使用的映射库来管理投影,以便 EF 可以构建高效的查询来仅拉回投影所需的数据。

您最好避免使用如下代码:

var bookEntities = await _context.Books
    .AsNoTracking()
    .ToListAsync();

var books = bookEntities
    .Select(b => Book.Create(b.BookId, b.Title, b.Description, b.Price).Book)
    .ToList();

您的“BookService”实际上充当 EF 上的存储库模式包装器。 EF 已经通过

DbSet
提供了此功能。这种方法的问题是您需要使用第一个语句将整个表具体化到内存中。正如您所看到的,这引起了人们对如何处理您可能需要更多关系的情况的疑问。同样,如果您的控制器方法之一需要更多信息,或者只是一个计数,该怎么办?如果您想分页并只显示前(或后)50 条记录怎么办?事情很快就会变得复杂且低效。

基于 EF 的存储库实际上只需要考虑以下几个原因:

  • 稍后作为单元测试的抽象。 (这样的话,它们应该很薄,露出来
    IQueryable
  • 封装常见的低级规则,如授权检查、软删除等
  • EF 对数据库的访问需要与从其他来源(例如第 3 方 API、NoSQL 存储等)检索的数据互换的情况
  • 类似地,域的多个使用者受益于域的相同、标准操作集的情况。消费者!=应用程序中的多个控制器,请考虑通过 CQRS 对 Web 应用程序 + 桌面应用程序 + API 进行标准域操作。

如果您没有这些要求,那么通过分别使用

DbContext
DbSet
作为您的工作单元和存储库,您可以避免很多麻烦、性能限制和/或复杂性。

相反,将“Book”实体视为域对象和 BookDTO,您的控制器代码看起来更像是:

[HttpGet]
public async Task<ActionResult<ICollection<BooksResponse>>> GetBooks()
{
    var books = await _context.Books
        .Select(b => new BooksResponse
        {
            Id = b.Id,
            Title = b.Title,
            // ...
            Pages = b.BookDetail.Pages
        }).ToListAsync();

    return Ok(books);
}

或者,您可以为您想要返回的相关数据声明子 DTO。

为了整理这个问题,您可以使用 Automapper、Mapperly、Mapster 等支持 EF 的 IQueryable 的映射器来简化该表达式。许多工作都遵循映射 Entity => DTO 的约定,或者您可以为任何特定的内容显式配置它们。

// Automapper:

    var books = await _context.Books
        .ProjectTo<BooksResponse>(config)
        .ToListAsync();

其中“config”是设置用于处理 Book -> DTO 的映射配置。

如果您想要更容易进行单元测试的东西,或者具有集中式通用规则,那么您可以注入一个公开的简单存储库

IQueryable

    var books = await _repository.GetAll()
        .ProjectTo<BooksResponse>(config)
        .ToListAsync();

    return Ok(books);

...其中存储库是一个简单的薄包装器:


public class BookRepository : IBookRepository
{
    IQueryable<Book> IBookRepository.GetAll()
    {
        IQueryable<Book> query = _context.Books;
        return query;
    }   
}

作为常见业务逻辑(例如软删除)的示例:

    IQueryable<Book> IBookRepository.GetAll(bool includeInactive = false)
    {
        IQueryable<Book> query = _context.Books;

        if (!includeInactive)
            query = query.Where(x => x.IsActive);

        return query;
    }   

现在我们对 EF 有了一个抽象,可以更轻松地用 Mock 替换它以进行单元测试。它还可以确保执行通用规则或优化。通过采用

IQueryable
,我们的消费者仍然可以完全控制数据的消费方式。例如如何排序、如何细化过滤、分页、投影、急切加载相关数据等。

保持简单,这将大有帮助,保持性能并减少麻烦。尝试将代码从 EF 中抽象出来的兔子洞比大多数人预期的要更深、更混乱、更令人困惑,而且 99.5% 的情况下完全没有必要/不合理。

© www.soinside.com 2019 - 2024. All rights reserved.