You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 18:18:39