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

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个:
    1. 控制器方法通过模型绑定接收ApplicationUser user参数,表单未提交该对象的完整字段时,user.Id为空/默认值,后续映射出的待更新实体主键不正确
    2. 仓储层更新逻辑存在缺陷:直接将未被DbContext跟踪的外部传入实体标记为EntityState.Modified,EF Core生成UPDATE语句时会自动把主键、并发令牌字段加入WHERE条件。ASP.NET Core Identity的ApplicationUser默认带ConcurrencyStamp并发令牌字段,AutoMapper从ViewModel映射实体时,该字段会被覆盖为null/默认值,导致WHERE条件匹配失败
    3. 头像赋值逻辑存在漏洞:仅在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 10:48:17