其他
ASP.NET Core 分层验证落地:用…
在 ASP.NET Core 应用需要分层、可测试且能覆盖属性、跨字段、配置、文件上传和请求管道验证时,用这些模式避免验证遗漏、重复和不一致。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 38/100 ×0.15
适用场景
ASP.NET Core 分层验证落地:特定
解决思路
- 按验证层选择技术:客户端用 jQuery Validation、HTML5;模型绑定用 Model Binders;应用层用 FluentValidation、DataAnnotations;配置启动用 IValidateOptions<T>;数据库用 EF Core Configurations。
- 按复杂度选择并实现验证方式:简单属性级约束用 DataAnnotations;跨属性规则实现 IValidatableObject;可复用属性级规则继承 ValidationAttribute;启动时配置验证实现 IValidateOptions<T>;复杂、可测试、异步或依赖数据库的规则用 FluentValidation。
- 在请求模型属性上使用 System.ComponentModel.DataAnnotations 特性,例如 [Required]、[StringLength(200, MinimumLength = 1)]、[Range(0.01, 1_000_000)]、[RegularExpression(^[A-Z]{2,4}-\d{4,8}$)]、[EmailAddress]、[Url]、[Range(0, int.MaxValue)]。
- 使用 DataAnnotations 特性参考:[Required] 非空;[StringLength] 最小/最大长度;[Range] 数值/日期范围;[RegularExpression] 模式匹配;[EmailAddress] 邮箱格式;[Phone] 电话格式;[Url] URL 格式;[CreditCard] Luhn 校验;[Compare] 属性相等;[MaxLength]/[MinLength] 集合/字符串长度;.NET 8+ 的 [AllowedValues]、[DeniedValues]、[Length]、[Base64String]。
- 创建属性级自定义 ValidationAttribute:继承 ValidationAttribute,重写 IsValid(object? value, ValidationContext validationContext),失败时返回 new ValidationResult(ErrorMessage, [validationContext.MemberName!]),例如 FutureDateAttribute 验证 DateOnly 必须晚于当前 UTC 日期。
- 创建类级自定义 ValidationAttribute:使用 [AttributeUsage(AttributeTargets.Class)],在 IsValid 中通过反射读取 StartProperty 和 EndProperty 属性值,比较 DateOnly 并在失败时返回 ValidationResult 和 EndProperty。
- 在模型实现 IValidatableObject 并实现 Validate(ValidationContext validationContext),用 yield return new ValidationResult 返回跨属性错误,例如 ShipByDate 必须晚于 OrderDate、订单总额不能超过 1,000,000、包含需发货商品时必须提供 ShipByDate。
- 当验证逻辑仅属于一个模型时使用 IValidatableObject;当同一规则跨多个模型复用时使用自定义 ValidationAttribute。
- 创建配置选项类如 DatabaseOptions 时,属性必须使用 { get; set; } 而不是 { get; init; },因为配置绑定器需要修改属性。
- 实现 IValidateOptions<T>:在 Validate(string? name, T options) 中收集 failures,检查例如 ConnectionString 非空、MaxRetryCount 在 0 到 10、MinPoolSize 不大于 MaxPoolSize,最后返回 ValidateOptionsResult.Fail(failures) 或 ValidateOptionsResult.Success。
- 注册配置验证:使用 builder.Services.AddOptions<DatabaseOptions>().BindConfiguration(DatabaseOptions.SectionName).ValidateDataAnnotations().ValidateOnStart(),并 AddSingleton<IValidateOptions<DatabaseOptions>, DatabaseOptionsValidator>()。
- 安装 FluentValidation 包:<PackageReference Include='FluentValidation' Version='11.9.*' /> 和 <PackageReference Include='FluentValidation.DependencyInjectionExtensions' Version='11.9.*' />。
- 配置 FluentValidation:调用 builder.Services.AddFluentValidationAutoValidation()、AddFluentValidationClientsideAdapters()、AddValidatorsFromAssemblyContaining<Program>()。
- 创建 FluentValidation 验证器:继承 AbstractValidator<T>,在构造函数中用 RuleFor 定义规则,例如 FirstName 的 NotEmpty、MinimumLength(3)、MaximumLength(50)、Matches(^[a-zA-Z\s'-]+$)。
- 在 FluentValidation 中验证 Email:NotEmpty、EmailAddress、MustAsync(BeUniqueEmail),并在 BeUniqueEmail 中使用 _dbContext.Users.AnyAsync(u => u.Email == email, cancellationToken) 检查唯一性。
- 在 FluentValidation 中验证密码:NotEmpty、MinimumLength(8)、Matches([A-Z])、Matches([a-z])、Matches([0-9])、Matches([^a-zA-Z0-9])。
- 在 FluentValidation 中验证确认密码和条款:ConfirmPassword 用 Equal(x => x.Password),AcceptsTos 用 Equal(true)。
- 条件验证:对 ShippingAddress 使用 NotEmpty().When(x => x.RequiresShipping);对 PickupLocation 使用 NotEmpty().When(x => !x.RequiresShipping);用 When(x => x.IsExpressShipping, () => { ... }) 包裹规则。
- 集合验证:对 Items 使用 NotEmpty() 和 Must(items => items.Count <= 100);对每个元素使用 RuleForEach(x => x.Items).ChildRules(item => { item.RuleFor(x => x.ProductId).NotEmpty(); item.RuleFor(x => x.Quantity).GreaterThan(0).LessThanOrEqualTo(999); })。
- 创建 MediatR 验证管道行为:ValidationBehavior<TRequest,TResponse> 实现 IPipelineBehavior<TRequest,TResponse>,无验证器时直接调用 next,否则创建 ValidationContext<TRequest>,用 Task.WhenAll 执行所有 IValidator<TRequest>.ValidateAsync,收集 failures,有错误抛出 ValidationException。
- 定义 ValidationException:包含 IReadOnlyList<ValidationFailure> Errors,并提供 ToDictionary() 按 PropertyName 分组返回错误消息数组。
- 注册 MediatR 管道:builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblyContaining<Program>(); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); })。
- 手动执行 DataAnnotations 验证:用 ValidationContext 和 Validator.TryValidateObject(instance, context, results, validateAllProperties: true) 返回是否有效和错误列表;务必传 validateAllProperties: true,否则只检查 [Required]。
- 验证文件上传:在 FluentValidation 中设置 _allowedExtensions 为 .jpg、.jpeg、.png、.pdf 和 MaxFileSize = 10 * 1024 * 1024,对 File 使用 NotNull().Must(BeValidSize).Must(BeValidExtension),对 Description 使用 MaximumLength(500).When(x => !string.IsNullOrEmpty(x.Description))。
- 文件上传辅助方法:BeValidSize 检查 file.Length <= MaxFileSize;BeValidExtension 用 Path.GetExtension(file.FileName).ToLowerInvariant() 并检查是否在 _allowedExtensions 中。
- 避免重复验证:不要在多个地方同时写 ModelState.AddModelError 和 FluentValidation 的 RuleFor;集中到验证器中。
- 避免静默验证失败:捕获 ValidationException 后不要直接 return Page(),而要遍历 ex.Errors 并调用 ModelState.AddModelError(error.PropertyName, error.ErrorMessage)。
- 不要只信任客户端验证:服务端始终检查 ModelState.IsValid,未通过则返回 Page(),通过后再 SaveToDatabase(input)。
- 使用 Validator.TryValidateObject 时始终传 validateAllProperties: true。
- 配置选项类必须使用 { get; set; } 而不是 { get; init; }。
- 注意 IValidatableObject.Validate() 只在所有特性验证通过后运行,不要用它做主要验证。
- 不要通过构造函数向 ValidationAttribute 注入服务;在 IsValid() 内使用 validationContext.GetService<T>()。
- 将 IValidateOptions<T> 注册为 singleton。
- 不要忘记 ValidateOnStart(),否则选项验证只在首次访问时运行。
- 客户端验证可用 jQuery Validation、HTML5 提供即时反馈并减少服务器负载,但服务端仍需验证。
适用边界 · 注意事项
- 非 .NET/ASP.NET Core 项目不适用这些具体 API 和库集成方式。
- 如果项目没有使用 FluentValidation 或 MediatR,不应为简单 CRUD 强行引入这些依赖。
- 纯客户端原型或不需要服务端校验的场景可只做最小验证。