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

