-
数据验证——用C#开发表单输入合法性检查
第一部分:C#基础入门
8.数据验证——表单输入合法性检查
实例介绍
数据验证是后端开发的“第一道防线”——用户通过表单输入的信息(如注册、登录、提交订单)往往存在格式错误、逻辑矛盾甚至恶意注入,若直接传入业务逻辑,会导致数据污染、系统崩溃或安全漏洞。
.NET生态提供了多种验证方案,其中DataAnnotations(数据注解)是最基础且易用的方式:通过在模型类的属性上标记特性(如[Required]、[EmailAddress]),即可快速定义验证规则;配合System.ComponentModel.DataAnnotations命名空间下的验证工具,能轻松实现规则校验并返回友好错误信息。
本实例以用户注册表单为场景,覆盖常见验证规则(必填、长度、格式、比较、自定义逻辑),带你vb.net教程C#教程python教程SQL教程access 2010教程掌握DataAnnotations的核心用法,同时拓展自定义验证和FluentValidation等进阶方案,让你明白“为什么验证”和“如何优雅验证”。
需求分析
-
场景选择
模拟用户注册场景,表单包含以下字段:
用户名:必填,长度2-20字符;
密码:必填,长度8-16字符,需包含大小写字母、数字、特殊字符(如!@#$%^&*);
确认密码:需与密码一致;
邮箱:必填,格式合法;
手机号:可选,但填写需符合中国手机号格式(11位数字,以13/14/15/17/18/19开头);
年龄:必填,范围18-100岁。 -
功能需求
规则定义:用DataAnnotations标记模型属性,明确每个字段的验证规则;
验证逻辑:封装通用验证方法,接收模型对象,返回详细错误信息(字段+错误描述);
错误反馈:模拟前端表单提交,输出验证结果(成功/失败+错误列表);
自定义验证:实现“密码不能包含用户名”的自定义规则(DataAnnotations原生不支持);
异常处理:捕获验证过程中的异常(如模型为空、特性参数错误)。 -
测试场景
输入完全合法的注册信息,验证通过;
输入缺少必填字段(如用户名空),验证失败并返回对应错误;
输入格式错误(如邮箱无@、手机号10位),返回格式错误;
输入逻辑矛盾(如密码与确认密码不一致、密码包含用户名),返回逻辑错误;
输入边界值(如年龄18/100,用户名长度2/20),验证通过。
代码实现
csharp
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace DataValidationDemo
{
// ==================== 1. 自定义验证特性(密码不能包含用户名) ====================
public class PasswordNotContainUsernameAttribute : ValidationAttribute
{
// 重写验证逻辑
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// 获取当前验证的模型对象(UserRegisterModel)
var model = validationContext.ObjectInstance as UserRegisterModel;
if (model == null)
{
return new ValidationResult("验证模型为空");
}
// 获取密码和用户名
string password = value?.ToString() ?? string.Empty;
string username = model.Username ?? string.Empty;
// 验证逻辑:密码包含用户名则返回错误
if (password.Contains(username, StringComparison.OrdinalIgnoreCase))
{
return new ValidationResult(ErrorMessage ?? "密码不能包含用户名");
}
// 验证通过
return ValidationResult.Success!;
}
}
// ==================== 2. 用户注册模型(带验证规则) ====================
public class UserRegisterModel
{
[Required(ErrorMessage = "用户名不能为空")]
[StringLength(20, MinimumLength = 2, ErrorMessage = "用户名长度应在2-20字符之间")]
public string Username { get; set; } = string.Empty;
[Required(ErrorMessage = "密码不能为空")]
[StringLength(16, MinimumLength = 8, ErrorMessage = "密码长度应在8-16字符之间")]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[^da-zA-Z]).{8,16}$",
ErrorMessage = "密码需包含大小写字母、数字和特殊字符(如!@#$%^&*)")]
[PasswordNotContainUsername(ErrorMessage = "密码不能包含用户名")]
public string Password { get; set; } = string.Empty;
[Required(ErrorMessage = "确认密码不能为空")]
[Compare("Password", ErrorMessage = "两次输入的密码不一致")]
public string ConfirmPassword { get; set; } = string.Empty;
[Required(ErrorMessage = "邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
public string Email { get; set; } = string.Empty;
[RegularExpression(@"^1[3-9]d{9}$", ErrorMessage = "手机号格式不正确(需为11位有效数字)")]
public string? PhoneNumber { get; set; } // 可选字段
[Required(ErrorMessage = "年龄不能为空")]
[Range(18, 100, ErrorMessage = "年龄应在18-100岁之间")]
public int Age { get; set; }
}
// ==================== 3. 验证帮助类(封装通用验证逻辑) ====================
public static class ValidationHelper
{
/// <summary>
/// 验证模型对象,返回错误信息列表(键:字段名,值:错误描述)
/// </summary>
public static Dictionary<string, string> Validate<T>(T model) where T : class
{
var errors = new Dictionary<string, string>();
if (model == null)
{
errors.Add("Global", "验证对象不能为空");
return errors;
}
// 创建验证上下文(包含模型对象和可选服务)
var validationContext = new ValidationContext(model);
// 存储验证结果
var validationResults = new List<ValidationResult>();
// 执行验证:true表示验证所有规则(即使前面失败)
bool isValid = Validator.TryValidateObject(model, validationContext, validationResults, validateAllProperties: true);
if (!isValid)
{
// 遍历验证结果,映射到字段-错误字典
foreach (var result in validationResults)
{
// 若字段为null(如全局错误),用"Global"作为键
string fieldName = result.MemberNames.FirstOrDefault() ?? "Global";
if (!errors.ContainsKey(fieldName))
{
errors.Add(fieldName, result.ErrorMessage!);
}
}
}
return errors;
}
}
// ==================== 4. 测试程序 ====================
class Program
{
static void Main(string[] args)
{
Console.WriteLine("=== 测试1:完全合法的注册信息 ===");
var validUser = new UserRegisterModel
{
Username = "ZhangSan",
Password = "Zhang@1234",
ConfirmPassword = "Zhang@1234",
Email = "zhangsan@example.com",
PhoneNumber = "13800138000",
Age = 25
};
PrintValidationResult(validUser);
Console.WriteLine("
=== 测试2:缺少必填字段+格式错误 ===");
var invalidUser1 = new UserRegisterModel
{
Username = "Z", // 长度不足
Password = "123456", // 无大小写和特殊字符
ConfirmPassword = "1234567", // 与密码不一致
Email = "zhangsan.example.com", // 无@
Age = 17 // 年龄不足
};
PrintValidationResult(invalidUser1);
Console.WriteLine("
=== 测试3:密码包含用户名 ===");
var invalidUser2 = new UserRegisterModel
{
Username = "LiSi",
Password = "LiSi1234", // 包含用户名
ConfirmPassword = "LiSi1234",
Email = "lisi@example.com",
Age = 30
};
PrintValidationResult(invalidUser2);
}
/// <summary>
/// 打印验证结果
/// </summary>
static void PrintValidationResult<T>(T model) where T : class
{
var errors = ValidationHelper.Validate(model);
if (errors.Count == 0)
{
Console.WriteLine("✅ 验证通过!");
return;
}
Console.WriteLine("❌ 验证失败,错误信息:");
foreach (var (field, message) in errors)
{
Console.WriteLine($" {field}:{message}");
}
}
}
}
逐行讲解
-
自定义验证特性(PasswordNotContainUsernameAttribute)
继承ValidationAttribute:所有自定义验证特性必须继承此类,重写IsValid方法;
ValidationContext:包含验证上下文信息(如当前模型对象、服务提供者),通过validationContext.ObjectInstance获取模型;
逻辑实现:判断密码是否包含用户名(不区分大小写),返回ValidationResult.Success表示通过,否则返回错误信息;
ErrorMessage:支持自定义错误信息(通过特性构造函数传入,或直接在重写方法中指定)。 -
用户注册模型(UserRegisterModel)
[Required]:标记字段为必填,ErrorMessage自定义错误提示;
[StringLength]:限制字符串长度(MinimumLength和MaximumLength);
[RegularExpression]:用正则表达式验证格式(如密码复杂度、手机号);
[Compare]:比较两个字段的值(如确认密码与密码一致);
[EmailAddress]:内置邮箱格式验证(无需自己写正则);
[Range]:限制数值类型的范围(如年龄18-100);
可选字段:PhoneNumber用?标记为可空,未填写时不触发验证。 -
验证帮助类(ValidationHelper)
泛型方法:支持任意模型类型的验证;
ValidationContext:初始化时传入模型对象,后续可用于依赖注入(如验证用户名是否已存在时调用数据库);
Validator.TryValidateObject:核心验证方法,validateAllProperties: true表示验证所有属性(即使某个属性验证失败,仍继续验证其他属性);
错误映射:将ValidationResult中的MemberNames(字段名)与ErrorMessage映射为字典,方便前端展示。 -
测试程序
合法场景:所有字段符合规则,验证通过;
非法场景1:覆盖长度不足、格式错误、比较失败、年龄超限等多种错误;
非法场景2:触发自定义验证规则(密码包含用户名);
PrintValidationResult:统一打印验证结果,直观展示错误分布。
基础知识拓展 - DataAnnotations常用特性分类
| 分类 | 特性示例 | 作用 |
|---|---|---|
| 必填验证 | [Required] | 标记字段为必填 |
| 长度验证 | [StringLength]、[MaxLength] | 限制字符串长度 |
| 格式验证 | [RegularExpression]、[EmailAddress]、[Url] | 验证字符串格式(内置或自定义正则) |
| 范围验证 | [Range] | 限制数值/日期类型的范围 |
| 比较验证 | [Compare] | 比较两个字段的值(如密码与确认密码) |
| 自定义验证 | 继承ValidationAttribute | 实现复杂业务逻辑验证(如密码不能包含用户名、用户名不能重复) |
-
DataAnnotations vs FluentValidation
DataAnnotations简单易用,但复杂场景下灵活性不足;FluentValidation是第三方库,通过代码链式调用定义规则,更适合复杂验证逻辑:
csharp
// FluentValidation示例(需安装FluentValidation包)
using FluentValidation;
public class UserRegisterValidator : AbstractValidator<UserRegisterModel>
{
public UserRegisterValidator()
{
RuleFor(x => x.Username)
.NotEmpty().WithMessage("用户名不能为空")
.Length(2,20).WithMessage("用户名长度应在2-20之间");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("密码不能为空")
.Length(8,16).WithMessage("密码长度应在8-16之间")
.Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[^da-zA-Z]).{8,16}$").WithMessage("密码需包含大小写字母、数字和特殊字符")
.Must((model, password) => !password.Contains(model.Username)).WithMessage("密码不能包含用户名");
RuleFor(x => x.ConfirmPassword).Equal(x => x.Password).WithMessage("两次输入的密码不一致");
}
}
优势:支持链式调用、条件验证(When)、异步验证(MustAsync)、依赖注入,更易维护复杂规则。
3. 客户端验证 vs 服务端验证
客户端验证:通过JavaScript实现(如ASP.NET Core中的jquery-validation),提升用户体验(即时反馈),但可被绕过(如禁用JS);
服务端验证:必须实现的最后一道防线,确保数据合法性(即使客户端验证被绕过,服务端仍能拦截错误数据);
最佳实践:客户端验证+服务端验证双重保障(前端用DataAnnotations生成data-val-*属性,后端用ValidationHelper验证)。
4. 自定义验证的进阶场景
异步验证:验证用户名是否已存在时,需调用数据库(异步操作),可通过[AsyncValidator](ASP.NET Core)或FluentValidation的MustAsync实现;
依赖注入:在ValidationContext中传入服务(如IUserRepository),实现数据库查询;
本地化:将错误信息存入资源文件(如ErrorMessageResourceName和ErrorMessageResourceType),支持多语言。
扩展思考
1.如何验证用户名是否已存在?
o用FluentValidation的MustAsync方法,注入IUserRepository查询数据库:
csharp
RuleFor(x => x.Username)
.MustAsync(async (name, cancellation) => !await _userRepository.IsUsernameExistsAsync(name))
.WithMessage("用户名已存在");
2.如何在ASP.NET Core中集成验证?
在Controller中用[ApiController]特性,自动触发模型验证,错误信息会被封装为ProblemDetails返回;
手动验证:var errors = ValidationHelper.Validate(model); if (errors.Any()) return BadRequest(errors);
3.如何处理嵌套模型验证?
对嵌套模型(如UserRegisterModel包含AddressModel),在AddressModel上标记[ValidateNested](FluentValidation)或手动递归验证;
4.如何优化验证性能?
避免在验证规则中执行 heavy 操作(如复杂计算),将其移至业务逻辑层;
缓存常用验证结果(如正则表达式编译为静态变量)。
总结
数据验证是后端开发的“基本功”,DataAnnotations是入门级方案,适合简单场景;FluentValidation是进阶方案,适合复杂业务逻辑。本实例通过用户注册场景,覆盖了常见验证规则和自定义逻辑,同时拓展了验证的最佳实践(双重验证、异步验证、本地化)。
记住:验证不仅是技术问题,更是用户体验问题——友好的错误提示能减少用户挫败感,而严谨的服务端验证能保障系统安全。在实际开发中,需根据业务复杂度选择合适的验证方案,平衡易用性和灵活性。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49420.html










