ASP.NET Web API 6 自定义ValidationAttribute校验用户名唯一报错
错误原因
- 构造函数参数不匹配:你定义的
AllValidationAttribute构造函数要求必须传入TalkBackDbContext类型的参数,但在属性上标注特性时仅给ErrorMessage属性赋值,没有传入必填的构造参数,编译器找不到匹配的构造函数,直接触发编译错误。 - 特性依赖注入方式错误:Attribute属于编译期生成的元数据,由CLR直接实例化,不受DI容器管理,无法通过构造函数直接注入DbContext这类运行时服务,原写法本身不符合C#特性的运行规则。
- 校验逻辑位置错误:你把用户名查重逻辑写在了
FormatErrorMessage方法中,这个方法仅负责错误文本的格式化输出,不承担校验逻辑的执行职责,且方法传入的name参数是属性的显示名称,不是用户输入的属性值,就算逻辑能执行也无法得到正确结果,真正的校验逻辑需要重写IsValid方法实现。
正确实现步骤
1. 重写自定义校验特性
移除带DbContext参数的构造函数,重写IsValid方法,通过校验上下文的服务容器获取DbContext实例,同时兼容新增、编辑两种场景的查重逻辑:
public class UniqueUserNameAttribute : ValidationAttribute { protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { // 从校验上下文获取DI容器中的数据库上下文 var dbContext = validationContext.GetService<TalkBackDbContext>(); if (dbContext == null) { throw new InvalidOperationException("数据库上下文未注册,无法执行用户名唯一性校验"); } var inputUserName = value?.ToString()?.Trim(); // 空值校验交给Required特性处理,此处直接跳过 if (string.IsNullOrWhiteSpace(inputUserName)) { return ValidationResult.Success; } // 编辑场景下需要排除当前实体本身,避免把自己判定为重复 if (validationContext.ObjectInstance is User currentUser) { bool userExists = dbContext.Users.Any(u => u.UserName == inputUserName && u.Id != currentUser.Id); return userExists ? new ValidationResult(ErrorMessage ?? "用户名已存在,请更换后重试") : ValidationResult.Success; } // 新增场景直接查重 bool newUserExists = dbContext.Users.Any(u => u.UserName == inputUserName); return newUserExists ? new ValidationResult(ErrorMessage ?? "用户名已存在,请更换后重试") : ValidationResult.Success; } }
2. 修改实体类的特性标注
替换原来的AllValidation特性,新特性提供无参构造函数,可以正常配置ErrorMessage属性:
[Table("User")] public partial class User { [Key] public Guid Id { get; set; } = Guid.NewGuid(); [Column("userName")] [StringLength(200)] [UniqueUserName(ErrorMessage = "用户名已被占用")] [Required(ErrorMessage = "Username is a required field")] public string UserName { get; set; } = null!; [StringLength(50, MinimumLength = 3, ErrorMessage = "The password should be between 3 characters to 50")] [Required(ErrorMessage = "Password is a required field")] public string Password { get; set; } = null!; [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy/MM/dd}")] public DateTime? LastLogin { get; set; } public string? Token { get; set; } }
3. 补充数据库层面的唯一约束
模型层的校验无法覆盖高并发的竞态场景,必须在数据库层面给UserName字段添加唯一索引,从根本上避免重复数据写入:
public class TalkBackDbContext : DbContext { public DbSet<User> Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 给用户名添加唯一索引 modelBuilder.Entity<User>() .HasIndex(u => u.UserName) .IsUnique() .HasDatabaseName("IX_User_UserName"); } }
编写完成后执行EF Core迁移命令更新数据库即可。
注意:ASP.NET Core 及以上版本默认支持在ValidationContext中获取DI服务,无需额外配置即可直接使用
GetService方法拿取DbContext实例。
内容的提问来源于stack exchange,提问作者Tommy Udi
相关产品推荐
相关产品推荐

