ASP.NET Core更新用户报预期影响1行实际0行错误修复方案
问题描述
开发ASP.NET Core应用时,调用EditOrCreateInformation()方法修改用户数据抛出如下异常:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: 'Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.'
现有实现代码如下:
控制器层代码
[HttpPost] public IActionResult EditOrCreateInformation(ApplicationUserVm model, ApplicationUser user) { if (ModelState.IsValid) { var olddata = context.Users.Where(a => a.Id == user.Id).AsNoTracking().FirstOrDefault(); string oldfilename = olddata.PhotoUrl; if (model.Photo == null) { model.PhotoUrl = oldfilename; } if (oldfilename != null) { if (model.Photo != null && System.IO.File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "PhotoFiles/PhotoProfile", oldfilename))) { System.IO.File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "PhotoFiles/PhotoProfile", oldfilename)); string PhysicalPath = Path.Combine(Directory.GetCurrentDirectory() + "/wwwroot", "PhotoFiles/PhotoProfile/"); // 2) Get File Name string FileName = Guid.NewGuid() + Path.GetFileName(model.Photo.FileName); // 3) Merge Physical Path + File Name string FinalPath = Path.Combine(PhysicalPath, FileName); // 4) Save The File As Streams "Data Over Time" using(var stream = new FileStream(FinalPath, FileMode.Create)) { model.Photo.CopyTo(stream); } model.PhotoUrl = FileName; } } else { string PhysicalPath = Path.Combine(Directory.GetCurrentDirectory() + "/wwwroot", "PhotoFiles/PhotoProfile/"); // 2) Get File Name string FileName = Guid.NewGuid() + Path.GetFileName(model.Photo.FileName); // 3) Merge Physical Path + File Name string FinalPath = Path.Combine(PhysicalPath, FileName); // 4) Save The File As Streams "Data Over Time" using(var stream = new FileStream(FinalPath, FileMode.Create)) { model.Photo.CopyTo(stream); } model.PhotoUrl = FileName; } var obj = mapper.Map < ApplicationUser > (model); applicationUser.Update(obj); toastNotification.AddSuccessToastMessage("Your Information Updated successfully"); return RedirectToAction("MyProfile", "Profile", new { Area = "Identity" }); } return View(model); }
仓储层代码
public ApplicationUser Update(ApplicationUser obj) { db.Entry(obj).State = EntityState.Modified; db.SaveChanges(); return db.Users.Where(a => a.Id == obj.Id).FirstOrDefault(); }
错误原因
- 触发该异常的核心逻辑是EF Core生成的UPDATE语句最终没有匹配到任何数据库行,WHERE条件命中数为0,结合现有代码具体诱因有3个:
- 控制器方法通过模型绑定接收
ApplicationUser user参数,表单未提交该对象的完整字段时,user.Id为空/默认值,后续映射出的待更新实体主键不正确 - 仓储层更新逻辑存在缺陷:直接将未被DbContext跟踪的外部传入实体标记为
EntityState.Modified,EF Core生成UPDATE语句时会自动把主键、并发令牌字段加入WHERE条件。ASP.NET Core Identity的ApplicationUser默认带ConcurrencyStamp并发令牌字段,AutoMapper从ViewModel映射实体时,该字段会被覆盖为null/默认值,导致WHERE条件匹配失败 - 头像赋值逻辑存在漏洞:仅在
model.Photo == null时才给model.PhotoUrl赋旧头像地址,存在旧头像但用户未上传新头像时,PhotoUrl会被覆盖为null,同时存在大量重复的文件保存逻辑
- 控制器方法通过模型绑定接收
修复方案
1. 修正仓储层更新逻辑
禁止直接给无跟踪的外部实体强行设置Modified状态,改为先查询数据库获取上下文跟踪的实体,再将新值覆盖到跟踪实体上,避免并发令牌、系统字段被异常覆盖:
public ApplicationUser Update(ApplicationUser obj) { // 根据主键查询上下文跟踪的数据库实体 var trackedUser = db.Users.FirstOrDefault(a => a.Id == obj.Id); if (trackedUser == null) { throw new KeyNotFoundException("待更新的用户不存在"); } // 将传入实体的可修改字段值覆盖到跟踪实体,自动跳过主键、并发令牌等未传值字段 db.Entry(trackedUser).CurrentValues.SetValues(obj); db.SaveChanges(); return trackedUser; }
如果使用AutoMapper实现字段映射,需要在映射配置中跳过Identity系统字段,避免覆盖关键值:
// 映射配置示例 CreateMap<ApplicationUserVm, ApplicationUser>() .ForMember(dest => dest.Id, opt => opt.Ignore()) .ForMember(dest => dest.ConcurrencyStamp, opt => opt.Ignore()) .ForMember(dest => dest.LockoutEnd, opt => opt.Ignore()) .ForMember(dest => dest.LockoutEnabled, opt => opt.Ignore()) .ForMember(dest => dest.AccessFailedCount, opt => opt.Ignore());
2. 修正控制器逻辑
- 移除方法参数中多余的
ApplicationUser user,用户Id直接从当前登录用户的身份凭证中获取,防止越权修改 - 重构头像处理逻辑,消除重复代码,默认保留旧头像地址
[HttpPost] public IActionResult EditOrCreateInformation(ApplicationUserVm model) { if (ModelState.IsValid) { // 从当前登录用户身份获取Id,不依赖表单提交的用户参数 var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier); var olddata = context.Users.AsNoTracking().FirstOrDefault(a => a.Id == currentUserId); if (olddata == null) { return NotFound(); } string oldfilename = olddata.PhotoUrl; // 默认保留旧头像地址 model.PhotoUrl = oldfilename; // 用户上传了新头像时才处理文件替换 if (model.Photo != null) { // 删除旧头像文件 if (!string.IsNullOrEmpty(oldfilename)) { string oldFilePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "PhotoFiles/PhotoProfile", oldfilename); if (System.IO.File.Exists(oldFilePath)) { System.IO.File.Delete(oldFilePath); } } // 保存新头像文件 string physicalPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "PhotoFiles/PhotoProfile/"); string fileName = Guid.NewGuid() + Path.GetFileName(model.Photo.FileName); string finalPath = Path.Combine(physicalPath, fileName); using(var stream = new FileStream(finalPath, FileMode.Create)) { model.Photo.CopyTo(stream); } model.PhotoUrl = fileName; } var obj = mapper.Map<ApplicationUser>(model); // 强制指定Id为当前用户Id,避免越权 obj.Id = currentUserId; applicationUser.Update(obj); toastNotification.AddSuccessToastMessage("Your Information Updated successfully"); return RedirectToAction("MyProfile", "Profile", new { Area = "Identity" }); } return View(model); }
补充说明
如果需要正式启用并发校验功能,可以在ApplicationUserVm中增加ConcurrencyStamp字段,编辑页面将该值存入隐藏域随表单提交,更新时EF Core会自动对比该值与数据库中的值,若不一致则抛出并发异常提示用户数据已被其他操作修改。
内容的提问来源于stack exchange,提问作者Abdalrahman Adel
相关产品推荐
相关产品推荐

