请求参数验证
解决什么问题
API 接口收到请求参数后,需要在进入业务逻辑之前校验入参合法性(不能为空、长度限制、格式校验等)。Aegis 集成了 FluentValidation,提供了自动验证机制——写好验证器,框架会在请求进入 Controller 之前自动执行校验,不通过直接返回错误响应,不需要手动调用 Validate。
如何引入
在 Component.deps.json 的 Services 中添加 RequestValidation:
{
"Components": {
"Services": [
"RequestValidation"
]
}
}
不需要安装单独的 NuGet 包。FluentValidation 已包含在 Aegis.Transfer 中,RequestValidation 组件会自动注册。
工作原理
启用 RequestValidation 后,框架做了三件事:
- 禁用 DataAnnotations 验证:不再使用
[Required]、[StringLength]等特性标注 - 自动扫描验证器:从所有
*.Dto.dll程序集中扫描AbstractValidator<T>的实现类并注册到 DI 容器 - 统一错误响应:验证失败时自动返回
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(如GetUsersRequest→GetUsersRequestValidator) - Validator 必须放在
*.Dto项目中(框架自动扫描的是*.Dto.dll) - 不需要在 Controller 中手动调用验证,框架自动处理
验证失败的响应格式
验证不通过时,框架自动返回以下格式的响应,不会进入 Controller 方法:
{
"Code": 105,
"MessageType": 1,
"Message": "姓名不能为空"
}
Code固定为105(ResponseCode.ParametersWrong)MessageType为Notice,前端据此决定提示方式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