VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 数据验证——用C#开发表单输入合法性检查

第一部分:C#基础入门
8.数据验证——表单输入合法性检查
实例介绍
数据验证是后端开发的“第一道防线”——用户通过表单输入的信息(如注册、登录、提交订单)往往存在格式错误、逻辑矛盾甚至恶意注入,若直接传入业务逻辑,会导致数据污染、系统崩溃或安全漏洞。
.NET生态提供了多种验证方案,其中DataAnnotations(数据注解)是最基础且易用的方式:通过在模型类的属性上标记特性(如[Required]、[EmailAddress]),即可快速定义验证规则;配合System.ComponentModel.DataAnnotations命名空间下的验证工具,能轻松实现规则校验并返回友好错误信息。
本实例以用户注册表单为场景,覆盖常见验证规则(必填、长度、格式、比较、自定义逻辑),带你vb.net教程C#教程python教程SQL教程access 2010教程掌握DataAnnotations的核心用法,同时拓展自定义验证和FluentValidation等进阶方案,让你明白“为什么验证”和“如何优雅验证”。
需求分析

  1. 场景选择
    模拟用户注册场景,表单包含以下字段:
    用户名:必填,长度2-20字符;
    密码:必填,长度8-16字符,需包含大小写字母、数字、特殊字符(如!@#$%^&*);
    确认密码:需与密码一致;
    邮箱:必填,格式合法;
    手机号:可选,但填写需符合中国手机号格式(11位数字,以13/14/15/17/18/19开头);
    年龄:必填,范围18-100岁。
  2. 功能需求
    规则定义:用DataAnnotations标记模型属性,明确每个字段的验证规则;
    验证逻辑:封装通用验证方法,接收模型对象,返回详细错误信息(字段+错误描述);
    错误反馈:模拟前端表单提交,输出验证结果(成功/失败+错误列表);
    自定义验证:实现“密码不能包含用户名”的自定义规则(DataAnnotations原生不支持);
    异常处理:捕获验证过程中的异常(如模型为空、特性参数错误)。
  3. 测试场景
    输入完全合法的注册信息,验证通过;
    输入缺少必填字段(如用户名空),验证失败并返回对应错误;
    输入格式错误(如邮箱无@、手机号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}");
	}
	}
	}
	}

逐行讲解

  1. 自定义验证特性(PasswordNotContainUsernameAttribute)
    继承ValidationAttribute:所有自定义验证特性必须继承此类,重写IsValid方法;
    ValidationContext:包含验证上下文信息(如当前模型对象、服务提供者),通过validationContext.ObjectInstance获取模型;
    逻辑实现:判断密码是否包含用户名(不区分大小写),返回ValidationResult.Success表示通过,否则返回错误信息;
    ErrorMessage:支持自定义错误信息(通过特性构造函数传入,或直接在重写方法中指定)。
  2. 用户注册模型(UserRegisterModel)
    [Required]:标记字段为必填,ErrorMessage自定义错误提示;
    [StringLength]:限制字符串长度(MinimumLength和MaximumLength);
    [RegularExpression]:用正则表达式验证格式(如密码复杂度、手机号);
    [Compare]:比较两个字段的值(如确认密码与密码一致);
    [EmailAddress]:内置邮箱格式验证(无需自己写正则);
    [Range]:限制数值类型的范围(如年龄18-100);
    可选字段:PhoneNumber用?标记为可空,未填写时不触发验证。
  3. 验证帮助类(ValidationHelper)
    泛型方法:支持任意模型类型的验证;
    ValidationContext:初始化时传入模型对象,后续可用于依赖注入(如验证用户名是否已存在时调用数据库);
    Validator.TryValidateObject:核心验证方法,validateAllProperties: true表示验证所有属性(即使某个属性验证失败,仍继续验证其他属性);
    错误映射:将ValidationResult中的MemberNames(字段名)与ErrorMessage映射为字典,方便前端展示。
  4. 测试程序
    合法场景:所有字段符合规则,验证通过;
    非法场景1:覆盖长度不足、格式错误、比较失败、年龄超限等多种错误;
    非法场景2:触发自定义验证规则(密码包含用户名);
    PrintValidationResult:统一打印验证结果,直观展示错误分布。
    基础知识拓展
  5. DataAnnotations常用特性分类
分类 特性示例 作用
必填验证 [Required] 标记字段为必填
长度验证 [StringLength]、[MaxLength] 限制字符串长度
格式验证 [RegularExpression]、[EmailAddress]、[Url] 验证字符串格式(内置或自定义正则)
范围验证 [Range] 限制数值/日期类型的范围
比较验证 [Compare] 比较两个字段的值(如密码与确认密码)
自定义验证 继承ValidationAttribute 实现复杂业务逻辑验证(如密码不能包含用户名、用户名不能重复)
  1. 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


相关教程