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

Blazor中EF Core重复更新对象触发跟踪错误的原因及解决

问题:Blazor + EF Core 二次编辑文章时触发跟踪错误无法保存

我用Blazor结合Entity Framework Core开发了一个与SQL Server交互的新闻板应用,支持文章存储和视频附加,实现基础CRUD功能。首次进入编辑页面修改文章并保存时操作正常,但再次进入同一文章编辑保存时,会触发跟踪错误,修改无法生效。

已确认以下几点:

  • 服务使用Scoped而非Singleton模式
  • 所有数据库查询均添加了.AsNoTracking()
  • 每个输入字段的ID唯一

相关代码

Pages/ArticleEdit.razor

@page "/article/edit/{ArticleId:int}"

@using EntitySQL.Data
@using EntitySQL.Models
@using EntitySQL.Services

@inject NewsServices Service
@inject NavigationManager Navigation

<PageTitle>Edit Article</PageTitle>

@if (@ArticleId == -1) {
    <p>没有找到对应ID的文章!</p>
} else {
    <h1>编辑 "@name"</h1>
    
    <div class="grid-container-2">
        <label for="name">标题:</label>
        <input type="text" id="name" @bind-value="@name">
        <label for="story">内容:</label>
        <textarea id="story" rows="12" @bind="@story"/>
    </div>
    
    <div class="gap"/>
    
    <div class="grid-container-3">
        
        @for(int i = 0; i < videoList.Count; i++){
            int index = i;
            <section>
                <p>视频 @(index + 1):</p>
                <label for="@("vname" + index)">名称:</label>
                <input type="text" id="@("vname" + index)" @bind-value="@videoList[@index].Name">
                <label for="@("vlink" + index)">链接:</label>
                <input type="text" id="@("vlink" + index)" @bind-value="@videoList[@index].Link">
            </section>
        }
    </div>
    
    <div class="gap"/>
    
    <button type="button" onclick="@(() => Submit())">保存修改!</button>
}

@code {
    [Parameter]
    public int ArticleId { get; set; }
    string name;
    string story;
    List<Video> videoList;
    
    protected override void OnInitialized() 
    {
        Article article = Service.GetArticle(ArticleId);
        name = article.Name;
        story = string.Join(Constants.PARAGRAPH_SEPARATOR, article.Paragraphs);
        
        // 以下代码用于添加额外空视频项(暂注释)
        /*
        List<Video> videos = article.Videos?.ToList();
        videoList = Enumerable.Repeat(new Video {Name = "", Link = ""}, Constants.MAX_VIDEOS).ToList();
        for (int i = 0; i < videos.Count; i++)
        {
            Video video = videos[i];
            videoList[i] = video;
        }
        */

        videoList = article.Videos?.ToList();
    }
    
    public void Submit()
    {
        ICollection<String> paragraphs = story.Split(Constants.PARAGRAPH_SEPARATOR, StringSplitOptions.RemoveEmptyEntries).ToList();
        Service.UpdateArticle(ArticleId, name, paragraphs, videoList);
        Navigation.NavigateTo($"/article/read/{ArticleId}");
    }
}

Pages/ArticleRead.razor

@page "/article/read/{ArticleId:int}"

@using EntitySQL.Models
@using EntitySQL.Services

@inject NewsServices Service

<PageTitle>查看文章</PageTitle>

@if(@ArticleId == -1)
{
    <p>没有找到对应ID的文章!</p>
} 
else
{
    <h1>@article.Name</h1>
    <a href="/article/edit/@ArticleId">编辑这篇文章</a>
    
    @foreach (string paragraph in @article.Paragraphs)
    {
            <p>@paragraph</p>
    }
    
    <section class="grid-container">
        @foreach (Video video in @article.Videos)
        {
            <figure>
                    <h2>@video.Name</h2>
                    <a href="@video.Link">@video.Link</a>
                </figure>
        }
    </section>
}

@code
{
    [Parameter]
    public int ArticleId { get; set; }
    Article article;
    
    protected override void OnInitialized()
    {
        article = Service.GetArticle(ArticleId);
    }
}

NewsServices.cs

using EntitySQL.Data;
using EntitySQL.Models;

using Microsoft.EntityFrameworkCore;

namespace EntitySQL.Services;

public class NewsServices
{
    private readonly NewsContext context;
    
    public NewsServices(NewsContext con)
    {
        context = con;
    }
    
    public ICollection<Article> GetAllArticles()
    {
        return context.Articles
            .Include(a => a.Videos)
            .AsNoTracking()
            .ToList();
    }
    
    public int AddArticle(Article article)
    {
        if (article.Name.Length == 0 || article.Paragraphs.Count == 0)
        {
            return Constants.INVALID_ARGUMENT;
        }

        context.Articles.Add(article);
        context.SaveChanges();

        return 0;
    }
    
    public Article GetArticle(int Id)
    {
        return context.Articles
            .Include(a => a.Videos)
            .AsNoTracking()
            .SingleOrDefault(a => a.Id == Id);
    }
    
    public int UpdateArticle(int Id, string Name, ICollection<String> Paragraphs, ICollection<Video>? Videos)
    {
        Article article = context.Articles.Find(Id);

        if (article == null)
        {
            return Constants.INVALID_ARGUMENT;
        } 
        
        article.Name = Name;
        article.Paragraphs = Paragraphs;
        article.Videos = Videos;

        context.SaveChanges();

        return 0;
    }
    
    public int DeleteArticle(int Id)
    {
        Article article = context.Articles.Find(Id);

        if (article == null) 
        {
            return Constants.INVALID_ARGUMENT;
        }
        
        context.Articles.Remove(article);
        context.SaveChanges();

        return 0;
    }
    
    public int AddVideo(string Name, string Link)
    {
        if (Name.Length + Link.Length == 0)
        {
            return Constants.INVALID_ARGUMENT;
        }

        context.Videos.Add(new Video
                               {
                                   Name = Name,
                                   Link = Link
                               });
        context.SaveChanges();

        return 0;
    }
    
    public Video GetVideo(int Id)
    {
        return context.Videos
            .AsNoTracking()
            .SingleOrDefault(a => a.Id == Id);
    }
    
    public int DeleteVideo(int Id)
    {
        Video video = context.Videos.Find(Id);

        if (video == null)
        {
            return Constants.INVALID_ARGUMENT;
        }
        
        context.Videos.Remove(video);
        context.SaveChanges();

        return 0;
    }
}

问题原因与解决方法

核心原因

第二次编辑时,videoList中的Video实体是通过AsNoTracking()查询返回的无跟踪对象。在UpdateArticle方法中,直接将这个集合赋值给了由context.Articles.Find(Id)加载的被跟踪文章实体的Videos属性。当执行SaveChanges()时,EF Core会尝试跟踪这些原本无状态的Video实体,若同一上下文已存在相同ID的跟踪实体,就会触发跟踪冲突错误。此外,直接替换关联集合的方式也不符合EF Core的实体状态管理规范。

解决步骤

1. 重构UpdateArticle方法,正确处理关联视频状态

不要直接替换Videos集合,而是逐个处理现有视频的更新、新增和删除:

public int UpdateArticle(int Id, string Name, ICollection<string> Paragraphs, ICollection<Video>? Videos)
{
    var article = context.Articles
        .Include(a => a.Videos) // 加载文章关联的现有视频
        .SingleOrDefault(a => a.Id == Id);

    if (article == null)
    {
        return Constants.INVALID_ARGUMENT;
    } 
    
    // 更新文章基本信息
    article.Name = Name;
    article.Paragraphs = Paragraphs;

    if (Videos == null)
    {
        // 清空所有视频
        article.Videos.Clear();
    }
    else
    {
        // 处理现有视频:更新或删除
        var existingVideos = article.Videos.ToList();
        foreach (var existingVideo in existingVideos)
        {
            var matchingVideo = Videos.FirstOrDefault(v => v.Id == existingVideo.Id);
            if (matchingVideo != null)
            {
                // 更新视频属性
                existingVideo.Name = matchingVideo.Name;
                existingVideo.Link = matchingVideo.Link;
            }
            else
            {
                // 删除不在新列表中的视频
                article.Videos.Remove(existingVideo);
                context.Videos.Remove(existingVideo);
            }
        }

        // 添加新视频(默认ID为0的视为新增)
        foreach (var video in Videos)
        {
            if (video.Id == 0)
            {
                article.Videos.Add(video);
            }
        }
    }

    context.SaveChanges();
    return 0;
}

2. 优化编辑页面的视频列表初始化逻辑

如果需要支持新增视频,确保新增的Video对象ID为默认值0:

protected override void OnInitialized() 
{
    Article article = Service.GetArticle(ArticleId);
    name = article.Name;
    story = string.Join(Constants.PARAGRAPH_SEPARATOR, article.Paragraphs);

    // 初始化视频列表,若无视频则创建空列表
    videoList = article.Videos?.ToList() ?? new List<Video>();
    // 若需要允许用户添加更多视频,可添加空项
    // while(videoList.Count < Constants.MAX_VIDEOS)
    // {
    //     videoList.Add(new Video { Name = "", Link = "" });
    // }
}

关键说明

  • EF Core中,AsNoTracking()返回的实体不会被上下文跟踪,但当关联到被跟踪的实体时,EF Core会尝试启动跟踪,若存在重复ID的跟踪实体则会报错。
  • 处理关联实体更新时,最佳实践是加载现有关联数据,逐个处理更新、新增和删除,而非直接替换整个集合。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 01:19:52