# 模型验证 (07_Validation)

最后更新: 2024-09-20


# 📚 概述

DarkM 模型验证模块基于 FluentValidation 构建,提供强类型的模型验证功能,支持自定义验证规则、异步验证等。

源代码位置: DarkM/src/Framework/Validation


# 🏗️ 模块架构

# 完整目录结构

Validation/
├── Validation.Abstractions/
│   ├── IValidateResultFormatHandler.cs    # 验证结果格式化接口
│   └── ValidateResultFormatAttribute.cs   # 验证结果格式化特性
│
└── Validation.FluentValidation/
    ├── ServiceCollectionExtensions.cs     # DI 扩展
    ├── FluentValidationExtensions.cs      # FluentValidation 扩展
    ├── ValidateResultFormatHandler.cs     # 验证结果格式化处理器
    └── Validators/
        ├── PhoneValidator.cs              # 手机号验证器
        ├── IPValidator.cs                 # IP 验证器
        └── UrlValidator.cs                # URL 验证器
1
2
3
4
5
6
7
8
9
10
11
12
13

# 💡 配置说明

# appsettings.json 配置

{
  "Validation": {
    // 是否启用验证
    "Enabled": true,
    // 验证失败时返回详细错误
    "DetailedErrors": true
  }
}
1
2
3
4
5
6
7
8

# 配置项详解

配置项 类型 默认值 说明
Enabled bool true 是否启用验证
DetailedErrors bool true 返回详细错误信息

# 🔧 创建验证器

# 1. 定义验证器类

文件位置: User.Web/Validators/UserAddModelValidator.cs

using FluentValidation;
using DarkM.Module.User.Application.UserService.ViewModels;

namespace DarkM.Module.User.Web.Validators
{
    /// <summary>
    /// 用户添加模型验证器
    /// </summary>
    public class UserAddModelValidator : AbstractValidator<UserAddModel>
    {
        public UserAddModelValidator()
        {
            // 用户名验证
            RuleFor(x => x.Username)
                .NotEmpty().WithMessage("用户名不能为空")
                .Length(3, 20).WithMessage("用户名长度 3-20 位")
                .Matches("^[a-zA-Z0-9_]+$").WithMessage("用户名只能包含字母、数字和下划线");
            
            // 密码验证
            RuleFor(x => x.Password)
                .NotEmpty().WithMessage("密码不能为空")
                .MinimumLength(6).WithMessage("密码长度至少 6 位");
            
            // 邮箱验证
            RuleFor(x => x.Email)
                .EmailAddress().WithMessage("邮箱格式不正确")
                .When(x => !string.IsNullOrEmpty(x.Email));
            
            // 手机号验证
            RuleFor(x => x.Phone)
                .Matches("^1[3-9]\\d{9}$").WithMessage("手机号格式不正确")
                .When(x => !string.IsNullOrEmpty(x.Phone));
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 2. 异步验证

public class UserAddModelValidator : AbstractValidator<UserAddModel>
{
    private readonly IUserRepository _userRepository;
    
    public UserAddModelValidator(IUserRepository userRepository)
    {
        _userRepository = userRepository;
        
        RuleFor(x => x.Username)
            .NotEmpty().WithMessage("用户名不能为空")
            .MustAsync(BeUniqueUsername).WithMessage("用户名已存在");
    }
    
    private async Task<bool> BeUniqueUsername(
        string username, 
        CancellationToken cancellationToken)
    {
        return !await _userRepository.ExistsAsync(username);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3. 条件验证

RuleFor(x => x.Email)
    .NotEmpty().WithMessage("邮箱不能为空")
    .EmailAddress().WithMessage("邮箱格式不正确")
    .When(x => x.ContactType == ContactType.Email);

RuleFor(x => x.Phone)
    .NotEmpty().WithMessage("手机号不能为空")
    .Matches("^1[3-9]\\d{9}$").WithMessage("手机号格式不正确")
    .When(x => x.ContactType == ContactType.Phone);
1
2
3
4
5
6
7
8
9

# 4. 自定义验证

RuleFor(x => x.Password)
    .NotEmpty().WithMessage("密码不能为空")
    .Must(IsValidPassword).WithMessage("密码必须包含字母和数字");

private bool IsValidPassword(string password)
{
    return password.Any(char.IsLetter) && password.Any(char.IsDigit);
}
1
2
3
4
5
6
7
8

# 💡 使用示例

# 1. 在 Controller 中手动验证

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using FluentValidation;
using DarkM.Module.User.Web.Validators;
using DarkM.Module.User.Application.UserService.ViewModels;

[Description("用户管理")]
public class UserController : ModuleController
{
    private readonly IUserService _userService;
    private readonly IValidator<UserAddModel> _validator;
    
    public UserController(
        IUserService userService, 
        IValidator<UserAddModel> validator)
    {
        _userService = userService;
        _validator = validator;
    }
    
    [HttpPost]
    [Description("添加用户")]
    public async Task<IResultModel> Add(UserAddModel model)
    {
        // 手动验证
        var result = await _validator.ValidateAsync(model);
        if (!result.IsValid)
        {
            return ResultModel.Failed(result.Errors.First().ErrorMessage);
        }
        
        return await _userService.AddAsync(model);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 2. 自动验证

DarkM 已集成自动验证,Controller 会自动验证模型:

[HttpPost]
[Description("添加用户")]
public async Task<IResultModel> Add(UserAddModel model)
{
    // 模型已自动验证,无需手动调用
    return await _userService.AddAsync(model);
}
1
2
3
4
5
6
7

# 3. 验证结果处理

var result = await _validator.ValidateAsync(model);

if (!result.IsValid)
{
    // 获取所有错误
    var errors = result.Errors.Select(e => e.ErrorMessage).ToList();
    
    // 获取第一个错误
    var firstError = result.Errors.First().ErrorMessage;
    
    // 按字段分组错误
    var errorsByField = result.Errors
        .GroupBy(e => e.PropertyName)
        .ToDictionary(
            g => g.Key, 
            g => g.Select(e => e.ErrorMessage).ToList());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 🔧 内置验证规则

规则 说明 示例
NotEmpty 不能为空 RuleFor(x => x.Name).NotEmpty()
NotNull 不能为 null RuleFor(x => x.Email).NotNull()
Length 长度限制 RuleFor(x => x.Name).Length(3, 20)
MinimumLength 最小长度 RuleFor(x => x.Pwd).MinimumLength(6)
MaximumLength 最大长度 RuleFor(x => x.Desc).MaximumLength(500)
EmailAddress 邮箱格式 RuleFor(x => x.Email).EmailAddress()
Matches 正则匹配 RuleFor(x => x.Phone).Matches("^1\\d{10}$")
InclusiveBetween 范围 RuleFor(x => x.Age).InclusiveBetween(18, 60)
Must 自定义验证 RuleFor(x => x.Name).Must(BeValid)
MustAsync 异步自定义验证 RuleFor(x => x.Name).MustAsync(BeUnique)

# 🔧 验证规则组合

RuleFor(x => x.Password)
    .NotEmpty().WithMessage("密码不能为空")
    .MinimumLength(6).WithMessage("密码至少 6 位")
    .MaximumLength(20).WithMessage("密码最多 20 位")
    .Matches(".*[A-Z].*").WithMessage("密码必须包含大写字母")
    .Matches(".*[a-z].*").WithMessage("密码必须包含小写字母")
    .Matches(".*\\d.*").WithMessage("密码必须包含数字");
1
2
3
4
5
6
7

# 📚 最佳实践

# 1. 验证器命名

验证器类以 Validator 结尾:

public class UserAddModelValidator : AbstractValidator<UserAddModel>
public class OrderQueryModelValidator : AbstractValidator<OrderQueryModel>
1
2

# 2. 错误消息本地化

RuleFor(x => x.Username)
    .NotEmpty().WithMessage(x => Localizer["UsernameRequired"])
    .Length(3, 20).WithMessage(x => Localizer["UsernameLength"]);
1
2
3

# 3. 验证器复用

// 定义通用验证规则
public class EmailValidator : AbstractValidator<string>
{
    public EmailValidator()
    {
        RuleFor(x => x)
            .EmailAddress().WithMessage("邮箱格式不正确");
    }
}

// 在其他验证器中使用
RuleFor(x => x.Email).SetValidator(new EmailValidator());
1
2
3
4
5
6
7
8
9
10
11
12

# 4. 级联验证

RuleFor(x => x.Address)
    .NotNull()
    .SetValidator(new AddressValidator());

public class AddressValidator : AbstractValidator<AddressModel>
{
    public AddressValidator()
    {
        RuleFor(x => x.Province).NotEmpty();
        RuleFor(x => x.City).NotEmpty();
        RuleFor(x => x.Detail).NotEmpty();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 🔍 常见问题

# Q1: 验证器未生效?

检查清单:

  1. ✅ 是否正确实现了 AbstractValidator<T>
  2. ✅ 验证器是否注册到容器
  3. ✅ 模型是否与验证器类型匹配

解决方案:

// 确保验证器在正确的命名空间
namespace DarkM.Module.User.Web.Validators
{
    public class UserAddModelValidator : AbstractValidator<UserAddModel>
    {
        // ...
    }
}
1
2
3
4
5
6
7
8

# Q2: 异步验证不工作?

确保:

  1. ✅ 使用 MustAsync 而不是 Must
  2. ✅ 使用 ValidateAsync 而不是 Validate

示例:

// ✅ 正确
RuleFor(x => x.Username).MustAsync(BeUniqueUsername);
var result = await _validator.ValidateAsync(model);

// ❌ 错误
RuleFor(x => x.Username).Must(BeUniqueUsername);
var result = _validator.Validate(model);
1
2
3
4
5
6
7

# Q3: 条件验证不生效?

检查 When 条件是否正确:

RuleFor(x => x.Email)
    .NotEmpty()
    .When(x => x.ContactType == ContactType.Email);  // 条件
1
2
3

# Q4: 如何验证集合?

解决方案:

RuleForEach(x => x.Items)
    .SetValidator(new ItemValidator());

public class ItemValidator : AbstractValidator<ItemModel>
{
    public ItemValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Quantity).GreaterThan(0);
    }
}
1
2
3
4
5
6
7
8
9
10
11

# Q5: 如何验证嵌套对象?

解决方案:

RuleFor(x => x.Profile)
    .NotNull()
    .SetValidator(new ProfileValidator());

public class ProfileValidator : AbstractValidator<ProfileModel>
{
    public ProfileValidator()
    {
        RuleFor(x => x.Bio).MaximumLength(500);
        RuleFor(x => x.Avatar).EmailAddress();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 📚 相关文档


# 🔗 参考链接


最后更新: 2024-09-20