跳到主要内容
版本:3.0.0

请求参数验证

解决什么问题

API 接口收到请求参数后,需要在进入业务逻辑之前校验入参合法性(不能为空、长度限制、格式校验等)。Aegis 集成了 FluentValidation,提供了自动验证机制——写好验证器,框架会在请求进入 Controller 之前自动执行校验,不通过直接返回错误响应,不需要手动调用 Validate

如何引入

Component.deps.jsonServices 中添加 RequestValidation

{
"Components": {
"Services": [
"RequestValidation"
]
}
}

不需要安装单独的 NuGet 包。FluentValidation 已包含在 Aegis.Transfer 中,RequestValidation 组件会自动注册。

工作原理

启用 RequestValidation 后,框架做了三件事:

  1. 禁用 DataAnnotations 验证:不再使用 [Required][StringLength] 等特性标注
  2. 自动扫描验证器:从所有 *.Dto.dll 程序集中扫描 AbstractValidator<T> 的实现类并注册到 DI 容器
  3. 统一错误响应:验证失败时自动返回 ApiResponse,第一个错误信息作为 Message

这意味着你只需要写验证器类,不需要在 Controller 里手动调用验证逻辑。

标准写法

Request 和 Validator 放在一起

验证器与对应的 Request 写在同一个文件中,命名为 <RequestName>Validator

using FluentValidation;

namespace Aegis.Webapi.Dto.Requests;

public record GetUsersRequest
{
public string Name { get; init; }
public DateTime Birthday { get; init; }
public int Age { get; init; }
}

public class GetUsersRequestValidator : AbstractValidator<GetUsersRequest>
{
public GetUsersRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("姓名不能为空");

RuleFor(x => x.Age)
.Must(x => x >= 0 && x < 200)
.WithMessage("请输入正常年龄");
}
}

关键约定

  • Request 使用 record 类型,属性用 init
  • Validator 命名必须是 <RequestName>Validator(如 GetUsersRequestGetUsersRequestValidator
  • Validator 必须放在 *.Dto 项目中(框架自动扫描的是 *.Dto.dll
  • 不需要在 Controller 中手动调用验证,框架自动处理

验证失败的响应格式

验证不通过时,框架自动返回以下格式的响应,不会进入 Controller 方法:

{
"Code": 105,
"MessageType": 1,
"Message": "姓名不能为空"
}
  • Code 固定为 105ResponseCode.ParametersWrong
  • MessageTypeNotice,前端据此决定提示方式
  • Message 取第一条验证错误信息

常用验证规则

必填与空值检查

RuleFor(x => x.Name).NotEmpty().WithMessage("姓名不能为空");
RuleFor(x => x.Phone).NotNull().WithMessage("手机号不能为空");

NotEmpty 同时检查 null、空字符串和空白字符串。NotNull 只检查 null

长度限制

RuleFor(x => x.Name).Length(2, 20).WithMessage("姓名长度必须在 2-20 之间");
RuleFor(x => x.Code).MaximumLength(50).WithMessage("编码最长 50 个字符");
RuleFor(x => x.Remark).MinimumLength(10).WithMessage("备注至少 10 个字符");

范围与比较

RuleFor(x => x.Age).GreaterThan(0).WithMessage("年龄必须大于 0");
RuleFor(x => x.Age).LessThan(150).WithMessage("年龄必须小于 150");
RuleFor(x => x.StartDate).LessThan(x => x.EndDate).WithMessage("开始日期必须早于结束日期");

正则匹配

RuleFor(x => x.Phone).Matches(@"^1[3-9]\d{9}$").WithMessage("手机号格式不正确");
RuleFor(x => x.Email).EmailAddress().WithMessage("邮箱格式不正确");

自定义逻辑

使用 Must 编写任意校验逻辑:

RuleFor(x => x.Age)
.Must(x => x >= 0 && x < 200)
.WithMessage("请输入正常年龄");

RuleFor(x => x.IdCard)
.Must(idCard => IsValidIdCard(idCard))
.WithMessage("身份证号不合法");

枚举值检查

RuleFor(x => x.Status).IsInEnum().WithMessage("状态值不合法");

常见组合写法

同一属性链式规则

RuleFor(x => x.Name)
.NotEmpty().WithMessage("姓名不能为空")
.Length(2, 20).WithMessage("姓名长度必须在 2-20 之间");

集合验证

// 验证集合中每个元素
RuleForEach(x => x.Tags).NotEmpty().WithMessage("标签不能为空");

// 验证集合不为空
RuleFor(x => x.Items).NotEmpty().WithMessage("至少需要一个条目");

嵌套对象验证

public record CreateOrderRequest
{
public string OrderNo { get; init; }
public AddressDto ShippingAddress { get; init; }
}

public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderRequestValidator()
{
RuleFor(x => x.OrderNo).NotEmpty().WithMessage("订单号不能为空");
RuleFor(x => x.ShippingAddress).SetValidator(new AddressValidator());
}
}

public class AddressValidator : AbstractValidator<AddressDto>
{
public AddressValidator()
{
RuleFor(x => x.City).NotEmpty().WithMessage("城市不能为空");
RuleFor(x => x.Detail).NotEmpty().WithMessage("详细地址不能为空");
}
}

错误消息中的占位符

WithMessage 支持内置占位符:

// {PropertyName} 替换为属性名
RuleFor(x => x.Name).NotEmpty().WithMessage("{PropertyName}不能为空");
// 输出:Name不能为空

// 用 WithName 给属性起别名
RuleFor(x => x.Name).NotEmpty().WithName("姓名").WithMessage("{PropertyName}不能为空");
// 输出:姓名不能为空

常用占位符:

占位符说明
{PropertyName}属性名(或 WithName 指定的别名)
{PropertyValue}当前属性值
{ComparisonValue}比较目标值
{MinLength}最小长度
{MaxLength}最大长度
{TotalLength}实际长度

条件验证

When / Unless

只在满足条件时执行验证:

// 当 Type 为 1 时才验证 Amount
RuleFor(x => x.Amount).GreaterThan(0).When(x => x.Type == 1);

// 除非 IsDraft 为 true,否则验证 Title
RuleFor(x => x.Title).NotEmpty().Unless(x => x.IsDraft);

Otherwise

When(x => x.IsVip, () =>
{
RuleFor(x => x.Discount).InclusiveBetween(0.8m, 1.0m);
}).Otherwise(() =>
{
RuleFor(x => x.Discount).Equal(1.0m);
});

边界与限制

  • 验证器必须放在 *.Dto 项目中。框架在启动时只扫描 *.Dto.dll,放在其他项目中不会被发现
  • Validator 的泛型参数必须和对应的 Request 类型完全匹配
  • 同一个 Request 只能有一个 Validator,多个会导致注册冲突
  • 框架已禁用 DataAnnotations 验证([Required] 等特性不生效),统一使用 FluentValidation