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
相关产品推荐
相关产品推荐

