You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于eShopOnWeb参考应用的Clean Architecture架构中EF Core高效查询投影属性的实现疑问

解决Clean Architecture下EF Core投影到Dto/ViewModel的依赖问题

你的问题非常典型——在遵循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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 16:37:34