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

ASP.NET Core中如何实现邮箱唯一性的服务端验证?

解决EF Core邮箱唯一性验证抛出SqlException的问题

你配置的唯一索引会在数据库层面阻止重复邮箱插入,但直接抛出SqlException不够友好,以下是几种实现服务端友好验证提示的方案:

1. 保存前手动检查邮箱唯一性(简单直接)

在新增或编辑Manager的逻辑中,先查询数据库确认邮箱是否已存在,区分新增和编辑场景(编辑时需排除当前记录):

新增场景示例

[HttpPost]
public async Task<IActionResult> Create(Manager manager)
{
    if (ModelState.IsValid)
    {
        // 检查邮箱是否已被注册
        var existingManager = await _context.Managers.FirstOrDefaultAsync(m => m.Email == manager.Email);
        if (existingManager != null)
        {
            ModelState.AddModelError("Email", "该邮箱已被注册");
            return View(manager);
        }
        _context.Add(manager);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(manager);
}

编辑场景示例

[HttpPost]
public async Task<IActionResult> Edit(int id, Manager manager)
{
    if (id != manager.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        // 编辑时排除当前Manager的记录,避免自己和自己冲突
        var existingManager = await _context.Managers.FirstOrDefaultAsync(m => m.Email == manager.Email && m.Id != manager.Id);
        if (existingManager != null)
        {
            ModelState.AddModelError("Email", "该邮箱已被注册");
            return View(manager);
        }
        try
        {
            _context.Update(manager);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!ManagerExists(manager.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(manager);
}

2. 自定义验证属性(复用性高)

创建可复用的验证特性,直接标注在Email属性上,减少重复代码:

自定义验证特性代码

public class UniqueEmailAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var dbContext = (ApplicationDbContext)validationContext.GetService(typeof(ApplicationDbContext));
        var email = value as string;
        var currentManager = validationContext.ObjectInstance as Manager;

        if (!string.IsNullOrEmpty(email))
        {
            // 区分新增(Id为0)和编辑场景
            var duplicate = dbContext.Managers.FirstOrDefault(m => 
                m.Email == email && m.Id != (currentManager?.Id ?? 0));
            
            if (duplicate != null)
            {
                return new ValidationResult("该邮箱已被注册");
            }
        }
        return ValidationResult.Success;
    }
}

标注到模型属性

修改Manager类的Email属性,添加自定义特性:

[Required]
[StringLength(450)]
[UniqueEmail] // 新增自定义验证特性
public string Email { get; set; }

3. 捕获SqlException转为验证错误(兜底并发场景)

前面两种方案可能存在并发漏洞:两个请求同时检查邮箱都不存在,然后同时插入,依然会触发数据库唯一索引异常。需要在保存时捕获异常并转换为友好提示:

重写DbContext的SaveChangesAsync

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    try
    {
        return await base.SaveChangesAsync(cancellationToken);
    }
    catch (DbUpdateException ex)
    {
        if (ex.InnerException is SqlException sqlEx)
        {
            // SQL Server唯一键冲突错误码:2601、2627
            if (sqlEx.Number is 2601 or 2627 && sqlEx.Message.Contains("IX_Managers_Email"))
            {
                throw new InvalidOperationException("该邮箱已被注册");
            }
        }
        throw;
    }
}

在Controller中捕获异常并添加到ModelState

[HttpPost]
public async Task<IActionResult> Create(Manager manager)
{
    if (ModelState.IsValid)
    {
        try
        {
            _context.Add(manager);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (InvalidOperationException ex)
        {
            ModelState.AddModelError("Email", ex.Message);
            return View(manager);
        }
    }
    return View(manager);
}

额外说明

你的代码中同时用了数据注解[Index(nameof(Email), IsUnique=true)]和Fluent API配置唯一索引,两者选其一即可,避免重复配置。

内容的提问来源于stack exchange,提问作者Diggi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 17:01:22