我有一个
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)
实体可以/应该作为您的域,中间人的情况很少。 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
)如果您没有这些要求,那么通过分别使用
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% 的情况下完全没有必要/不合理。