基于eShopOnWeb参考应用的Clean Architecture架构中EF Core高效查询投影属性的实现疑问
你的问题非常典型——在遵循Clean Architecture依赖规则(内层不依赖外层)的前提下,既要利用EF Core的投影优化性能,又不能打破层之间的依赖关系。直接在EfRepository里引用Web层的OrderViewModel或PublicApi的Dto肯定会报错,因为Infrastructure层不能依赖外层的Web/PublicApi项目。下面是几种符合架构规范的可行方案:
方案1:使用专用查询服务(参考eShopOnWeb的BasketQueryService模式)
这是eShopOnWeb推荐的方式,把需要直接投影的查询逻辑从通用Repository中剥离出来,放到专门的查询服务里。核心思路是:
- 在ApplicationCore层定义查询服务的接口和对应的业务Dto(这些Dto是业务查询的结果,属于应用核心范畴)
- 在Infrastructure层实现这个查询服务,直接投影到ApplicationCore的Dto(因为Infrastructure依赖ApplicationCore,符合依赖方向)
- Web层再将ApplicationCore的Dto映射到UI用的ViewModel
举个具体实现:
1. 在ApplicationCore定义接口和业务Dto
// ApplicationCore/Interfaces/IOrderQueryService.cs public interface IOrderQueryService { Task<List<OrderSummaryDto>> GetAllOrderSummariesAsync(CancellationToken cancellationToken = default); } // ApplicationCore/Dtos/OrderSummaryDto.cs public class OrderSummaryDto { public DateTime OrderDate { get; set; } public int OrderNumber { get; set; } public Address ShippingAddress { get; set; } public decimal Total { get; set; } }
2. 在Infrastructure实现查询服务
// Infrastructure/Services/OrderQueryService.cs public class OrderQueryService : IOrderQueryService { private readonly CatalogContext _dbContext; public OrderQueryService(CatalogContext dbContext) { _dbContext = dbContext; } public async Task<List<OrderSummaryDto>> GetAllOrderSummariesAsync(CancellationToken cancellationToken = default) { return await _dbContext.Orders .Select(o => new OrderSummaryDto { OrderDate = o.OrderDate, OrderNumber = o.Id, ShippingAddress = o.ShipToAddress, Total = o.Total() }) .ToListAsync(cancellationToken); } }
3. 在Web层使用并映射到ViewModel
// Web层的Controller或PageModel private readonly IOrderQueryService _orderQueryService; private readonly IMapper _mapper; public OrderController(IOrderQueryService orderQueryService, IMapper mapper) { _orderQueryService = orderQueryService; _mapper = mapper; } public async Task<IActionResult> Index(CancellationToken cancellationToken) { var orderDtos = await _orderQueryService.GetAllOrderSummariesAsync(cancellationToken); var viewModels = _mapper.Map<List<OrderViewModel>>(orderDtos); return View(viewModels); }
这种方式的好处是通用Repository保持单一职责(只处理基础的CRUD),专门的查询服务处理复杂的投影或聚合逻辑,完全符合Clean Architecture的依赖规则。
方案2:给通用Repository添加投影方法
如果不想为每个查询都创建专用服务,可以扩展通用IReadRepository,添加支持投影的通用方法,让调用方(Web层或Application层)传递投影表达式,Repository只负责执行查询,不需要知道具体的Dto/ViewModel类型。
1. 扩展IReadRepository接口
// ApplicationCore/Interfaces/IReadRepository.cs public interface IReadRepository<T> where T : class, IAggregateRoot { // 原有方法... Task<List<TResult>> ProjectToAsync<TResult>(Expression<Func<T, TResult>> selector, CancellationToken cancellationToken = default); }
2. 在EfRepository中实现该方法
// Infrastructure/EfRepository.cs public class EfRepository<T> : RepositoryBase<T>, IReadRepository<T>, IRepository<T> where T : class, IAggregateRoot { private readonly CatalogContext _dbContext; public EfRepository(CatalogContext dbContext) : base(dbContext) { _dbContext = dbContext; } public async Task<List<TResult>> ProjectToAsync<TResult>(Expression<Func<T, TResult>> selector, CancellationToken cancellationToken = default) { return await _dbContext.Set<T>() .Select(selector) .ToListAsync(cancellationToken); } }
3. 在Web层调用投影方法
// Web层代码 var viewModels = await _orderRepository.ProjectToAsync<OrderViewModel>( o => new OrderViewModel { OrderDate = o.OrderDate, OrderNumber = o.Id, ShippingAddress = o.ShipToAddress, Total = o.Total() }, cancellationToken);
这种方式保持了Repository的通用性,同时让外层控制投影逻辑,完全符合依赖方向(外层依赖内层,内层不依赖外层)。
方案3:将查询专用Dto移到ApplicationCore层
如果你的Dto是业务查询的结果(而非单纯的UI展示模型),可以把它们从PublicApi或Web层移到ApplicationCore层。这样Infrastructure层可以直接投影到这些Dto,Web层再根据需要映射到UI ViewModel。
注意:不要把Web层的ViewModel移到ApplicationCore——ViewModel是专门为UI设计的,属于Web层职责;而业务查询用的Dto属于应用核心逻辑,放在ApplicationCore是合理的。
不推荐的方案
- 返回dynamic/匿名对象:正如你所说,这不符合架构规范,会丢失类型安全,难以维护。
- 在Infrastructure层引用Web/PublicApi:这会直接打破Clean Architecture的依赖规则,导致层之间的耦合,后期维护成本极高。
总结
优先推荐方案1(查询服务),因为它更贴合Clean Architecture的单一职责原则,把复杂查询逻辑和通用CRUD分离;如果你的查询场景比较简单,**方案2(通用投影方法)**会更简洁。两种方案都能完美解决EF Core投影的依赖问题,同时遵守架构规范。
内容的提问来源于stack exchange,提问作者Ogglas

