Skip to content
bitzorcas
中EN

Reference

列表查询与导出范围契约

说明 W0 列表查询契约、组合分页、范围筛选值对象、QueryShape 标量展开、远程选项、关联快照和导出范围校验。

Last updated

本页说明列表用例从 HTTP 请求到 QueryShape,再到分页结果和导出的类型边界。目标形状是 Fields + Paging + Sort;传输仍使用 QUERY,并由 Platform SDK 在链路不支持时降级到 POST /_query。

1. 标准列表信封

新的或已迁移的 HTTP 列表 Query 使用以下结构:

Application 查询契约
// HTTP 信封只组合可序列化查询值;可信租户和当前用户不进入请求模型。
public sealed record ListResourcesQuery(
ResourceListFields Fields,
PaginationParams Paging,
SortRequest? Sort = null)
: IQuery<Result<PagedResult<ResourceSummaryDto>>>,
IPaginatedRequest;
// 业务筛选集中在 Fields,新增范围时不扩大顶层信封。
public sealed record ResourceListFields(
string? SearchText = null,
DateRange? CreatedAt = null,
AmountRange? Amount = null,
EnumFilterSet<ResourceStatus>? Statuses = null);
成员职责约束
Fields业务筛选条件字段名使用业务语言;可包含多个命名范围,不声明顶层通用 Period
PagingHTTP offset 分页使用 PaginationParams;IPaginatedRequest 只用于门禁识别,不提供数据继承
Sort单项标准排序SortRequest(Field, Descending);字段仍须由 QueryShape 白名单验证
返回值列表结果使用 Result<PagedResult<TSummaryDto>>,不新建模块级 *Page 壳

请求中的租户、当前用户和授权范围不是筛选字段。Handler 从可信上下文取得这些值,再把它们交给 Store 或基础谓词;不得允许 JSON 正文覆盖。

2. 分页规范化

PaginationParams 的页码从 1 开始,默认值为 PageIndex=1、PageSize=20。Normalize() 复用 PagingLimits,行为固定如下:

输入规范化结果
PageIndex < 11
PageSize <= 020
PageSize > 10001000
其他合法值原值
var paging = (request.Paging ?? new PaginationParams()).Normalize();
var pageRequest = paging.ToPageRequest();
var window = paging.ToWindow();

ToWindow() 还使用 PageWindow 的深分页保护。默认最大 offset 为 100_000;超过窗口时 CanRead=false,调用方应返回空页或改用游标/键集分页,不能继续构造可能溢出的数据库 offset。

平台硬边界不是同一个数字

GlobalPlatformConstants 只收纳跨模块、不可按租户调整的稳定上限:

常量当前值使用范围
Pagination.DefaultPageSize20在线列表别名,来源仍是 PagingLimits
Pagination.MaxPageSize1000在线列表别名,来源仍是 PagingLimits
Batch.PageSize2000SAR、归档等 worker 的后台批读,不是 HTTP 页大小
Text.IdentifierMaxLength128跨模块稳定标识
Text.DescriptionMaxLength1000短描述
Text.RemarkMaxLength2000备注与审计原因

远程选项 50、Workflow QueryStore 200 和 SCIM 200 是具名协议或引擎边界,不并入在线分页常量。

3. 筛选值对象

DateRange

DateRange 表示可单侧无界的时间闭区间,JSON 固定为对象:

{
"from": "2026-08-01T00:00:00+08:00",
"to": "2026-08-13T00:00:00+08:00"
}

创建和反序列化都经过 DateRange.Create。下界晚于上界时返回 DateRange.Invalid;JSON 入口把该失败转换为 JsonException,不能绕过领域校验。

上界有两种语义:

输入上界To查询展开
当日零点,例如 2026-08-13T00:00:00+08:00补齐到 8 月 13 日最后一个 tickQueryToExclusive 为 8 月 14 日零点
显式时刻,例如 2026-08-13T15:30:45+08:00保持原值展开点使用 To.AddTicks(1) 取得排他上界
null该侧无界不生成上界谓词

Contains 和展示使用闭区间的 To。数据库谓词统一使用排他上界和 LessThan,不能对日界值使用 LessThanOrEqual。

金额、数值和枚举集合

类型JSON/领域语义失败与相等性
AmountRangedecimal? Min/Max 的金额闭区间,可单侧无界Min > Max 返回 NumberRange.Invalid;包含两个端点
NumberRange<TNumber>数量、评分、百分比等可比较值类型的闭区间Min > Max 返回 NumberRange.Invalid
EnumFilterSet<TEnum>对输入去重、排序并冻结,只暴露 IReadOnlySet<TEnum>相等性忽略调用方顺序和重复项;null 按空集合处理

这些类型属于 HTTP/Application 的 TFields。它们表达请求语义,不直接成为生成式数据库字段。

4. QueryShape 的标量展开边界

QueryShape 生成器仍以标量为声明输入。*ListInput.From 或 MapFrom 是组合值对象的唯一展开点:

Infrastructure 查询输入
public sealed partial class ResourceListInput : IDeclareQueryShape
{
// BQRY005 要求分页标量成对出现,且不能标为 QueryField。
public int PageIndex { get; init; }
public int PageSize { get; init; }
[QueryField(DefaultOperator = FilterOperator.GreaterThanOrEqual)]
public DateTimeOffset? CreatedAtFrom { get; init; }
[QueryField(DefaultOperator = FilterOperator.LessThan)]
public DateTimeOffset? CreatedAtTo { get; init; }
public static ResourceListInput From(
ResourceListFields fields,
PaginationParams paging)
{
var normalized = paging.Normalize();
return new ResourceListInput
{
// 日期范围只在此处展开;上界统一转换成 LessThan 所需的排他值。
CreatedAtFrom = fields.CreatedAt?.From,
CreatedAtTo = fields.CreatedAt?.QueryToExclusive
?? fields.CreatedAt?.To?.AddTicks(1),
PageIndex = normalized.PageIndex,
PageSize = normalized.PageSize,
};
}
}

生成器规则 BQRY005 要求 PageIndex 与 PageSize 成对出现,类型只能是 int 或 int?,且两者都不能带 [QueryField]。DateRange、PaginationParams 以及其他组合筛选值对象也不能直接标为 [QueryField];生成器无法替模块猜测字段、运算符或日期上界语义。

主路径如下:

HTTP QUERY:Fields + Paging + Sort

Handler:授权与可信租户

ListInput.From:范围展开与分页规范化

QueryShape:白名单筛选和排序

ReadModel Store / Provider

Result>

5. 远程选项与关联回填

QueryOptionItem

所有远程选择器返回同一展示形状:

new QueryOptionItem(
Value: "user-42",
Label: "张三",
// Selected 只恢复界面状态,不授予访问权限。
Description: "主办律师",
Selected: true,
// Mapping 只能携带服务端登记并经过字段安全处理的展示值。
Mapping: new Dictionary<string, string?>
{
["department"] = "诉讼部",
});
  • Value 是提交给筛选字段的稳定标识;
  • Selected 只用于恢复或最近使用状态,不是授权结果;
  • Mapping 只能包含选择器目录登记、经过字段安全处理的白名单字段;
  • QueryOptionRequest.MaximumPageSize 固定为 50,批量解析标识也最多保留 50 个。

RelatedEntitySnapshot

详情或编辑回填模型可以携带 RelatedEntitySnapshot(Relation, Id, Label, ResourceType, Mapping),以免客户端为了关联名称再次请求详情。它不是聚合关系,也不能携带对方聚合、授权事实或未脱敏字段。列表行默认不携带该快照;需要显示关联信息时,应由 QueryShape 投影或选项 Mapping 提供最小字段。

6. 导出范围

导出不能靠把在线 PageSize 调大实现。ExportScope 的数值已经进入持久化请求快照,不得重排:

名称数值所需范围参数
CurrentPage0复用当前列表筛选、排序与页大小
Selected1非空、无重复的 CheckedIds;最多 1000 个
All2不携带 CheckedIds、PageFrom/PageTo 或 Take
PageRange3PageFrom > 0、PageTo > 0 且 PageFrom <= PageTo
Quantity4Take > 0;最大值由导出策略治理,不复用 PagingLimits

ExportRange 用同一组字段表达范围;持久化的 ExportRequest 目前把 Scope、CheckedIds、PageFrom、PageTo 和 Take 作为扁平属性保存。非对应范围夹带参数会返回 ApplicationErrors.Export.InvalidRequest。

按页范围导出
// TenantId 与 RequestedBy 必须来自可信执行上下文,而不是未经验证的页面字段。
var request = new ExportRequest(
ExportBuilderKey: "billing-invoices",
Format: ExportFormat.Csv,
TenantId: tenantId,
RequestedBy: userId,
Parameters: serializedListFields)
{
// PageRange 必须同时提供两个正页码;反向范围由请求校验器拒绝。
Scope = ExportScope.PageRange,
PageFrom = 2,
PageTo = 4,
};

IExportBuilder 的旧接口只接收 Parameters,因此只允许 All。支持 CurrentPage、Selected、PageRange 或 Quantity 的构建器必须实现 IScopedExportBuilder,从完整且已验证的 ExportRequest 估算行数并分批读取。否则调度器拒绝请求,不能静默扩大为全量导出。

公开的 ExportRequestDisplaySnapshot 会保留 PageFrom、PageTo 和 Take,但不保留选中行标识、租户、用户或原始参数。业务构建器只能通过服务端白名单字段补充可展示条件。

7. 已登记的稳定例外

表面例外边界
Chat 消息CursorPage<T> 游标分页保持游标,不迁为 PaginationParams
远程选择器最大 50 条选择器协议边界,不复制到普通列表
Workflow QueryStore最大 200 条引擎内部护栏,不是 HTTP 列表上限
SCIM 列表最大 200 条遵循 SCIM 分页协议
有界字典可一次读取小枚举、事件类型或小目录超过 200 条或需要交互筛选时迁为标准分页
Operations 租户列表可跨租户读取仅可信运维授权面;请求不接受环境 TenantId
单租户详情GetTenantQuery(string TenantId)路由资源标识的单资源 GET,不是列表字段

新增例外必须记录 owner、边界和删除条件,不能用一次性迁移进度代替稳定契约。

8. 迁移与验证

维护现有列表时按以下顺序处理:

  1. 先导出当前 OpenAPI,确认线上请求仍是扁平形状还是已经使用 Fields/Paging/Sort。
  2. 把业务筛选收进 {Resource}ListFields;时间范围使用业务字段名,不声明顶层 Period。
  3. 用 PaginationParams 和 IPaginatedRequest 替换 HTTP 层平铺分页。
  4. 在 *ListInput.From/MapFrom 一次性展开值对象并规范化分页。
  5. 保持 QueryShape 输入为标量,日期上界使用 LessThan。
  6. 同步更新 OpenAPI artifact、Platform SDK 类型和页面调用。
  7. 运行 Domain 单元测试、模块 Query 测试、生成器测试和架构测试。

当前直接证据包括:

  • PaginationParamsTests:默认值、1000 上限和 PageWindow 转换;
  • DateRangeTests:无界、反向失败、日界补齐、显式时刻和 JSON 校验;
  • RangeValueObjectTests:金额/数值闭区间与枚举集合相等性;
  • ExportRangeContractTests:范围参数校验、枚举数值和快照往返;
  • ListQueryShapeConvergenceArchitectureTests:W6 启用的存量收敛扫描,目前仍为 Skip。

相关

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%