EF Core如何实现实体字段唯一性校验(用户注册场景)
ASP.NET Core Identity 用户字段唯一性校验完整方案
1. Identity默认Email唯一性的实现逻辑
默认邮箱唯一性是两层防护逻辑:
- 业务层前置拦截:调用
UserManager.CreateAsync时,内部会先通过FindByEmailAsync查询归一化后的邮箱(NormalizedEmail字段,全大写格式)是否已存在,存在直接返回DuplicateEmail类型的Identity错误,不会执行插入操作 - 数据库层兜底:EF Core上下文初始化时,会自动给
NormalizedEmail字段创建唯一索引,就算并发请求绕过了前置校验,数据库层面也会直接拒绝重复数据插入,从根本上避免脏数据
2. PhoneNumber + EmergencyNo 联合唯一校验落地
先确认OnModelCreating中的联合索引配置加了IsUnique()标记,正确写法如下:
protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // 手机号+紧急联系人号码联合唯一索引 builder.Entity<ApplicationUser>() .HasIndex(u => new { u.PhoneNumber, u.EmergencyNo }) .IsUnique() .HasDatabaseName("IX_Users_Phone_Emergency"); }
校验逻辑分两步写在Register.cshtml.cs的注册处理方法中:
第一步:插入前前置查重
在调用_userManager.CreateAsync之前加查重逻辑,减少不必要的数据库异常:
// 联合唯一键前置查重 var isPhoneAndEmergencyExist = await _userManager.Users .AnyAsync(u => u.PhoneNumber == Input.PhoneNumber && u.EmergencyNo == Input.EmergencyNo); if (isPhoneAndEmergencyExist) { ModelState.AddModelError(string.Empty, "当前手机号与紧急联系人号码的组合已被注册,请更换后重试"); return Page(); }
第二步:唯一键冲突异常兜底
只写前置查重防不住并发场景(比如两个请求同时到达,同时通过查重判断,最终同时执行插入),必须加异常捕获:
先写个辅助方法判断是否为数据库唯一键冲突(SqlServer环境,其他数据库对应修改错误码即可):
private bool IsUniqueIndexConflict(Exception ex) { var sqlEx = ex.InnerException as Microsoft.Data.SqlClient.SqlException; // 2601、2627是SqlServer唯一键/唯一索引冲突的固定错误码 return sqlEx != null && (sqlEx.Number == 2601 || sqlEx.Number == 2627); }
把创建用户的逻辑包在try-catch里:
IdentityResult createResult; try { createResult = await _userManager.CreateAsync(user, Input.Password); } catch (Exception ex) when (IsUniqueIndexConflict(ex)) { ModelState.AddModelError(string.Empty, "提交信息重复,请检查手机号、员工号等字段后重试"); return Page(); }
3. EmployeCode、AdharNo等业务字段唯一约束配置
同样走「数据库索引+前置校验+异常兜底」的逻辑,可直接复用上面的兜底逻辑。
第一步:OnModelCreating中配置唯一索引
注意可空字段要加过滤条件,否则多个NULL值会被判定为重复,导致未填写该字段的用户无法注册:
// 员工号唯一索引 builder.Entity<ApplicationUser>() .HasIndex(u => u.EmployeCode) .IsUnique() // 过滤索引:仅对非空值做唯一约束 .HasFilter("[EmployeCode] IS NOT NULL") .HasDatabaseName("IX_Users_EmployeeCode"); // Aadhaar号码唯一索引 builder.Entity<ApplicationUser>() .HasIndex(u => u.AdharNo) .IsUnique() .HasFilter("[AdharNo] IS NOT NULL") .HasDatabaseName("IX_Users_AdharNo");
第二步:注册逻辑加前置查重
// 员工号查重 if (!string.IsNullOrWhiteSpace(Input.EmployeCode)) { var empCodeExist = await _userManager.Users.AnyAsync(u => u.EmployeCode == Input.EmployeCode); if (empCodeExist) { ModelState.AddModelError(nameof(Input.EmployeCode), "该员工号已绑定其他账号"); return Page(); } } // Aadhaar号码查重 if (!string.IsNullOrWhiteSpace(Input.AdharNo)) { var adharExist = await _userManager.Users.AnyAsync(u => u.AdharNo == Input.AdharNo); if (adharExist) { ModelState.AddModelError(nameof(Input.AdharNo), "该Aadhaar号码已被注册"); return Page(); } }
这两个字段的唯一冲突会被之前写的IsUniqueIndexConflict捕获,不需要额外写异常处理逻辑。
4. 落地注意事项
- 加完所有索引配置后,记得执行包管理器控制台命令生成迁移并更新数据库:
Add-Migration AddUserBusinessFieldsUniqueIndexes Update-Database - 如果业务要求字段值不区分大小写,查重时要和数据库排序规则对齐,统一转大写/小写后再比较,避免大小写不同导致的重复漏判
- 不要为了省事只加业务层查重不建数据库索引,并发场景下大概率会出现重复数据
- 如果后续要调整唯一规则,必须先删除旧索引再添加新索引,否则数据库迁移会执行失败
内容的提问来源于stack exchange,提问作者Arnab
相关产品推荐
相关产品推荐

