XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

数据校验 ​

这一章讲 Validate 阶段怎样校验操作的请求:先按编译期生成的 JSON Schema 校验载荷,再对实现 IValidatableObject 的请求执行自校验,失败时以字段级错误返回。读完你能给请求加约束、写一条 Schema 表达不了的业务规则,并看懂客户端收到的 400 响应。前置知识是操作契约与结果与错误处理。

为什么需要 ​

进入业务代码的请求必须是合法的:必填字段都在、数字在范围内、字符串不超长。如果每个操作自己写一遍 if 检查,规则会散落各处,HTTP、gRPC、MCP、命令行四种入口还得各自保证一致;如果在运行期用反射读注解,Native AOT 下又跑不起来。

框架把校验放进内核管道的 Validate 阶段,对所有协议入口统一执行:请求类型上的 DataAnnotations 由源生成器在编译期折进 JSON Schema,运行期按 Schema 校验载荷,零反射;Schema 表达不了的规则写在请求类型的 IValidatableObject.Validate 里。两步任一失败,调用以 VALIDATION 种类的结果结束,不进入 Handle 阶段,HTTP 上是 400 且带字段级 errors。框架不在运行期反射读取 DataAnnotations,也不集成第三方校验库。

两步校验 ​

内核的内置中间件 ValidationMiddleware 登记在 Validate 阶段开头,只处理带操作描述的调用,按顺序执行:

步骤做什么失败码
1. 取载荷HTTP、gRPC、MCP、命令行进来的调用用协议适配器绑定好的 JSON 载荷(空载荷视为 {});类型化调用(IOperationInvoker.InvokeAsync<TRequest, TResponse>)按序列化元数据把请求对象编码一次载荷不是 JSON:KERNEL_VALIDATION_001
2. Schema 校验操作带 Schema 时按请求 Schema 校验载荷KERNEL_VALIDATION_003
3. 解码与自校验解码成请求对象并记录在上下文;请求实现了 IValidatableObject 时调用 Validate,每条 ValidationResult 按成员名列为违规KERNEL_VALIDATION_004

任一步失败,调用不进入 Idempotency、Transaction 与 Handle 阶段。管道阶段的全貌见执行管道。

最小示例 ​

我们给下单请求加两类约束:名称 1 到 64 个字符、金额 1 到 1000000(注解,进 Schema),名称不能只有空白(自校验)。先写请求类型:

csharp
// PlaceOrderRequest.cs
using System.ComponentModel.DataAnnotations;
using XiHan.Framework.Primitives;

namespace Shop.Orders;

public sealed record PlaceOrderRequest(
    [Required, StringLength(64, MinimumLength = 1)] string Name,
    [Range(1, 1_000_000)] int Amount) : IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // Schema 只能约束长度,表达不了「全是空白」;文案走消息键,按调用方语言取
        if (string.IsNullOrWhiteSpace(Name))
        {
            yield return new ValidationResult(ShopMessages.OrderNameBlank.L(), [nameof(Name)]);
        }
    }
}

消息键与语言文件(Localization/en-US.json、Localization/zh-CN.json,其余区域各一份,以 LogicalName="Localization/%(Filename)%(Extension)" 嵌入,见全球化和本地化):

csharp
// ShopMessages.cs
namespace Shop;

public static class ShopMessages
{
    public const string OrderNameBlank = "shop.order.name_blank";
}
json
{ "shop.order.name_blank": "The order name must not be blank." }
json
{ "shop.order.name_blank": "订单名称不能只有空白。" }

操作本身不写任何检查,拿到的请求一定合法:

csharp
// CreateOrder.cs
using XiHan.Framework.Kernel.Operations;
using XiHan.Framework.Primitives;

namespace Shop.Orders;

[Operation(Id, Expose = Expose.Http)]
public sealed class CreateOrder : IOperation<PlaceOrderRequest, OrderResponse>
{
    public const string Id = "orders.create";

    public ValueTask<Result<OrderResponse>> HandleAsync(PlaceOrderRequest request, InvocationContext context)
        => ValueTask.FromResult<Result<OrderResponse>>(new OrderResponse(Guid.NewGuid(), request.Name, request.Amount));
}

public sealed record OrderResponse(Guid Id, string Name, int Amount);

请求与响应登记到 JsonSerializerContext,特性里调用生成的 AddGeneratedOperations(),Program.cs 装配 HttpProtocolFeature、注册消息目录并 MapXiHanOperations(),写法与结果与错误处理的最小示例相同。运行后发一个坏请求:

bash
curl -i -X POST localhost:5000/orders -H "Content-Type: application/json" -H "Accept-Language: zh-CN" -d '{"amount":0}'

得到 400:

json
{
  "type": "urn:xihan:error:KERNEL_VALIDATION_003",
  "title": "VALIDATION",
  "status": 400,
  "detail": "操作 'orders.create' 的请求不符合契约:$.name: required property 'name' is missing",
  "instance": "/orders",
  "code": "KERNEL_VALIDATION_003",
  "traceId": "5978653a746fa20f0dce9f4a2f954bac",
  "errors": { "name": ["缺少必填属性 'name'"], "amount": ["值必须 >= 1"] },
  "details": { "id": "orders.create", "reason": "$.name: required property 'name' is missing" }
}

再发 {"name":" ","amount":3}:Schema 通过(长度是 3),自校验拒绝,code 变成 KERNEL_VALIDATION_004,errors 是 { "name": ["订单名称不能只有空白。"] }。两种拒绝的 errors 形状相同,前端表单据此定位字段。

用注解声明约束 ​

请求类型上的 DataAnnotations 由 Kernel.Generators 在编译期折进请求 Schema,位置记录的主构造参数上的注解同样生效:

注解Schema
[Required]进 required 并去掉可空;字符串另加 minLength: 1(AllowEmptyStrings = true 除外)
[Range]minimum / maximum,开区间用 exclusiveMinimum / exclusiveMaximum
[StringLength]、[MinLength]、[MaxLength]、[Length]字符串的 minLength / maxLength,集合的 minItems / maxItems
[RegularExpression]pattern

不可空、没有默认值的属性本身就进 required,不需要 [Required];[Required] 的额外作用是让字符串至少有一个字符。[EmailAddress]、[Url] 等注解不折进 Schema,写成自校验。

Schema 校验支持的关键字:type(含可空)、properties、required、additionalProperties、items、enum、const、anyOf / oneOf / allOf、$ref、minimum / maximum 与开区间、minLength / maxLength、pattern、minItems / maxItems。format 等其他关键字不断言。一次最多报告 32 条违规,嵌套深度上限 64。

自校验 ​

Schema 表达不了的规则(字段之间的关系、业务上的黑名单、需要查服务的检查)写在请求类型的 IValidatableObject.Validate 里:

  • ValidationContext 带请求类型名与本次调用的服务提供器,validationContext.GetService(typeof(T)) 能取到作用域服务。
  • ErrorMessage 是应用自己的文案,用 key.L() 取,得到的是调用方的语言;对应的键要写进应用的语言文件。
  • MemberNames 里的成员名按 camelCase 转成序列化名,成为字段级错误的键。
  • 自校验在 Schema 通过之后才执行,所以方法体里可以假定注解约束已经满足。

失败结果 ​

校验失败的 Error 带这些细节:

细节内容
id操作 ID
reason第一条违规的文本
violations全部违规的文本,每条形如 $.path: reason
errors违规对象(SchemaViolation):字段键、原因、消息键与参数

宿主把 errors 翻译成调用方语言并按字段分组。字段键是去掉根 $ 的点分路径(lines[0].sku 这类嵌套字段同样适用);缺少必填属性的违规挂在缺失属性自己的路径上。

错误码情形
KERNEL_VALIDATION_001载荷不是 JSON、为 null 或与请求类型不匹配
KERNEL_VALIDATION_002类型化调用的请求或响应类型与登记不符
KERNEL_VALIDATION_003载荷不满足请求 Schema
KERNEL_VALIDATION_004IValidatableObject 自校验失败

gRPC 上这些失败是 InvalidArgument,命令行上退出码 2,MCP 上是 isError 结果,映射表见结果与错误处理。

不经过校验的路径 ​

  • IOperationInvoker.ExecuteAsync 用于调用方已经在管道内的场合,直接执行操作,不校验;要校验就用 InvokeAsync。
  • 宿主端点在进入管道之前绑定载荷(HTTP 的路由值、请求体与查询串);绑定失败直接写 Problem Details,不进入管道。
  • XiHan:Kernel:UseBuiltInMiddleware=false 时 ValidationMiddleware 不登记,应用需要自己在 Validate 阶段提供校验。

需要额外校验(如按租户配置的业务规则)时,在 Validate 阶段末尾登记应用自己的中间件,用 context.GetOperationRequest() 取解码后的请求对象(Maybe<object>),失败用 context.SetOutcome(Error.Validation(...)) 结束调用:

csharp
builder.AddXiHan(xihan => xihan
    .UseStandardProfile()
    .ConfigurePipeline(pipeline => pipeline.UseAt<TenantRulesMiddleware>(PipelineStage.Validate)));

中间件的写法见执行管道。

常见问题 ​

为什么不用 FluentValidation 这类库 ​

它们在运行期用反射或表达式树构建规则,Native AOT 下不可用,而且只在一种入口生效。框架的两步校验对 HTTP、gRPC、MCP、命令行统一执行,注解进 Schema 还能同时生成 OpenAPI 与 MCP 的 inputSchema。复杂规则写在 IValidatableObject.Validate 里。

校验错误的文案能自定义吗 ​

Schema 违规的文案是内核的消息键(kernel.validation.required、kernel.validation.minimum 等),按调用方语言翻译,不按字段自定义。需要业务化的提示就把规则放进自校验,用自己的消息键。

嵌套对象与集合里的字段怎么报 ​

字段键是点分路径,如 lines[0].sku。嵌套对象进 $defs,它们属性上的注解同样折进对应的定义;自校验要自己递归调用子对象的 Validate。

一次请求有多处错误,会全部报出来吗 ​

Schema 校验一次最多报告 32 条违规;自校验的每条 ValidationResult 都报。两步不混报:Schema 不通过时不执行自校验。

响应也会校验吗 ​

不会。响应 Schema 用于 OpenAPI、gRPC 的 protobuf 映射与 MCP 的 outputSchema,运行期不断言。类型化调用的响应类型与登记不符时报 KERNEL_VALIDATION_002,那是编程错误。

相关包与下一步 ​

  • Kernel:ValidationMiddleware、SchemaViolation、JsonSchemaValidator
  • Kernel.Generators:注解折进 Schema 的生成器
  • Primitives:Error.Validation 与 MessageKeyExtensions.L
  • 规范:RFC-0011《操作契约与 XGI》的「校验」一节
  • 下一章:领域模型

Released under The MIT License