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

C# 结合CQRS的Clean Architecture分页实现规范咨询

Clean Architecture结合CQRS模式下的分页实现合规性疑问

近几周我一直在学习结合CQRS模式的Clean Architecture,现就代码性能优化的最佳实践方案提出疑问,本次问题聚焦分页功能的正确实现方式。

根据我的理解,分页最优实践是在数据库取数阶段完成分页逻辑,避免拉取全量记录后再到UI层执行分页操作。我已将PaginatedList(包含pageNumber、totalPages、数据集合等属性)、RequestParams类定义在Application层,对应的Handler实现代码如下:

public async Task<PaginatedList<TransactionInListViewModel>> Handle(GetTransactionsByBudgetIdQuery request,
    CancellationToken cancellationToken) 
{
    var userId = _userService.GetUserId;
    var transactions = await _transactionRepository.GetTransactionsByBudgetIdAsync(
        userId,
        request.BudgetId,
        request.PageNumber,
        request.PageSize);

    var transactionDto = _mapper.Map<List<TransactionInListViewModel>>(transactions.Items);

    var result = new PaginatedList<TransactionInListViewModel>(
        transactionDto,
        transactions.TotalCount,
        transactions.PageNumber,
        transactions.PageSize);

    return result;
}

该方法携带分页参数,向仓储请求当前用户自身创建、归属指定BudgetId的交易记录。
最初我在仓储层传入参数查询数据库,直接返回Transaction类型的IEnumerable集合,再由Handler完成实体映射后返回分页结果。但此时出现异常:TotalCount及其关联属性(如HasPreviousePage、HasNextPage)计算错误,TotalCount取值等于单页PageSize大小,除PageNumber、PageSize外其余分页相关属性均返回错误结果。

我重构了Infrastructure层的Repository类,修改后代码如下:

public async Task<PaginatedList<Transaction>> GetTransactionsByBudgetIdAsync(int userId, int budgetId, int pageNumber, int pageSize) 
{
    var baseQuery = await _dbContext
        .Transactions
        .Where(t => t.CreatedById == userId)
        .Where(t => t.BudgetId == budgetId)
        .OrderByDescending(t => t.TransactionDate)
        .ToListAsync();

    var count = baseQuery.Count();

    var transactions = baseQuery
        .Skip((pageNumber - 1) * pageSize)
        .Take(pageSize)
        .ToList();

    return new PaginatedList<Transaction>(transactions, count, pageNumber, pageSize);
}

修改后所有分页结果计算均正确,但我认为该实现不够整洁,怀疑其不符合Clean Architecture规则:如果PaginatedList类定义在Application层,是否可以在Infrastructure层的TransactionRepository中直接返回计数准确的PaginatedResult,再由Handler完成结果映射、二次封装分页结果?


解答

1. 先修正当前仓储代码的性能缺陷

你现在的实现先调用了ToListAsync()把所有符合条件的交易记录全量加载到内存,再执行Skip/Take做内存分页,完全违背了数据库端分页的优化目标,数据量过万后就会出现明显的内存占用过高、接口响应慢的问题。
正确的写法是保留IQueryable的延迟执行特性,让总数统计和分页查询都在数据库端完成,修正后的代码如下:

public async Task<PaginatedList<Transaction>> GetTransactionsByBudgetIdAsync(int userId, int budgetId, int pageNumber, int pageSize)
{
    var baseQuery = _dbContext
        .Transactions
        .Where(t => t.CreatedById == userId)
        .Where(t => t.BudgetId == budgetId)
        .OrderByDescending(t => t.TransactionDate);

    // 数据库执行COUNT聚合查询,不加载实体数据
    var totalCount = await baseQuery.CountAsync();
    // 数据库执行分页查询,仅返回当前页需要的记录
    var pageItems = await baseQuery
        .Skip((pageNumber - 1) * pageSize)
        .Take(pageSize)
        .ToListAsync();

    return new PaginatedList<Transaction>(pageItems, totalCount, pageNumber, pageSize);
}

2. 跨层返回PaginatedList完全符合Clean Architecture规范

Clean Architecture的核心依赖规则是仅允许外层依赖内层,禁止内层依赖外层:

  • PaginatedList定义在Application层(属于内层核心抽象)
  • Infrastructure层作为外层实现,依赖内层的类型是完全合规的,不存在架构混乱或者反向依赖的问题
    不需要为Infrastructure层引用Application层的PaginatedList感到顾虑,这是完全符合规则的用法。

3. Handler层的二次映射封装属于合理职责划分

仓储层的核心职责是处理持久化逻辑,返回核心业务实体的分页结果,不需要感知上层ViewModel/DTO的结构;Handler层作为Application层的查询处理入口,负责将数据库实体映射为接口返回需要的ViewModel,再组装成对应泛型的分页结果,这个职责边界非常清晰,不属于冗余代码,完全符合CQRS的职责分离要求。

可选优化点

  • 可以为PaginatedList<T>添加一个泛型转换方法,比如PaginatedList<TResult> Map<TResult>(Func<T, TResult> mapper),简化每次手动组装DTO分页结果的重复代码
  • 一定要在请求入口做分页参数校验:限制pageNumber最小值为1,pageSize设置合理上限(比如最大不超过100),避免恶意传入超大分页参数拖垮数据库

内容的提问来源于stack exchange,提问作者Mate

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 16:48:29