Blazor .NET8+EF Core:含子实体的Activity增改页面实现问询
问题描述
在Blazor Web App .NET 8项目中使用Entity Framework Core,现有Activity、VolunteerNecessity等关联实体类。尝试创建带EditForm的Razor页面,实现包含所有子元素的Activity创建/更新,但出现跟踪错误、不允许插入标识等问题。需要解决:
- 编写Razor页面及.cs脚本,实现Activity及其所有子实体的增改
- 正确收集可添加到Activity的现有项
- 正确的Activity增改方式
尝试的代码
Context = _dbContext.CreateDbContext(); // 收集现有项,也试过加.AsNoTracking() List<SalesItem> _salesItems = await Context.SalesItems.Where(x => x.Deleted == false).ToListAsync(); List<Volunteer> _volunteers= await Context.Volunteers.Where(x => x.Deleted == false).ToListAsync(); // 添加新Activity Context.Add(_activity);
实体类定义
Activity.cs
public class Activity { public string Name { get; set; } public List<VolunteerNecessity> VolunteerNecessities { get; set; } public List<SalesItemNecessity> SalesItemNecessities { get; set; } }
VolunteerNecessity.cs
public class VolunteerNecessity { public Volunteer Volunteer { get; set; } public int VolunteerId { get; set; } public string AdditionalInformation { get; set; } public VolunteerActivity VolunteerActivity { get; set; } public int VolunteerActivityId { get; set; } }
SalesItemNecessity.cs
public class SalesItemNecessity { public SalesItem SalesItem { get; set; } public int SalesItemId { get; set; } public string AdditionalInformation { get; set; } public int Amount { get; set; } }
Volunteer.cs
public class Volunteer { public string Name { get; set; } }
VolunteerActivity.cs
public class VolunteerActivity { public string Name { get; set; } }
SalesItem.cs
public class SalesItem { public string Name { get; set; } }
解决方案
1. 补全实体类主键与关系配置
EF Core要求每个实体必须有主键,否则会触发跟踪和标识插入错误。先给所有实体添加主键,明确关联关系:
补全后的实体类
// Activity.cs public class Activity { public int Id { get; set; } // 主键 public string Name { get; set; } public List<VolunteerNecessity> VolunteerNecessities { get; set; } = new(); // 初始化空列表避免空引用 public List<SalesItemNecessity> SalesItemNecessities { get; set; } = new(); } // VolunteerNecessity.cs public class VolunteerNecessity { public int Id { get; set; } // 主键 public int VolunteerId { get; set; } public Volunteer Volunteer { get; set; } public string AdditionalInformation { get; set; } public int VolunteerActivityId { get; set; } public VolunteerActivity VolunteerActivity { get; set; } public int ActivityId { get; set; } // 外键:关联Activity public Activity Activity { get; set; } // 导航属性 } // SalesItemNecessity.cs public class SalesItemNecessity { public int Id { get; set; } // 主键 public int SalesItemId { get; set; } public SalesItem SalesItem { get; set; } public string AdditionalInformation { get; set; } public int Amount { get; set; } public int ActivityId { get; set; } // 外键:关联Activity public Activity Activity { get; set; } // 导航属性 } // Volunteer.cs public class Volunteer { public int Id { get; set; } // 主键 public string Name { get; set; } } // VolunteerActivity.cs public class VolunteerActivity { public int Id { get; set; } // 主键 public string Name { get; set; } } // SalesItem.cs public class SalesItem { public int Id { get; set; } // 主键 public string Name { get; set; } }
DbContext配置
在DbContext中用Fluent API明确实体关系:
public class AppDbContext : DbContext { public DbSet<Activity> Activities { get; set; } public DbSet<VolunteerNecessity> VolunteerNecessities { get; set; } public DbSet<SalesItemNecessity> SalesItemNecessities { get; set; } public DbSet<Volunteer> Volunteers { get; set; } public DbSet<VolunteerActivity> VolunteerActivities { get; set; } public DbSet<SalesItem> SalesItems { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Activity与VolunteerNecessity一对多 modelBuilder.Entity<Activity>() .HasMany(a => a.VolunteerNecessities) .WithOne(vn => vn.Activity) .HasForeignKey(vn => vn.ActivityId) .OnDelete(DeleteBehavior.Cascade); // Activity与SalesItemNecessity一对多 modelBuilder.Entity<Activity>() .HasMany(a => a.SalesItemNecessities) .WithOne(sn => sn.Activity) .HasForeignKey(sn => sn.ActivityId) .OnDelete(DeleteBehavior.Cascade); // VolunteerNecessity与Volunteer多对一 modelBuilder.Entity<VolunteerNecessity>() .HasOne(vn => vn.Volunteer) .WithMany() .HasForeignKey(vn => vn.VolunteerId); // VolunteerNecessity与VolunteerActivity多对一 modelBuilder.Entity<VolunteerNecessity>() .HasOne(vn => vn.VolunteerActivity) .WithMany() .HasForeignKey(vn => vn.VolunteerActivityId); // SalesItemNecessity与SalesItem多对一 modelBuilder.Entity<SalesItemNecessity>() .HasOne(sn => sn.SalesItem) .WithMany() .HasForeignKey(sn => sn.SalesItemId); } }
2. 正确收集现有项
收集现有Volunteer、SalesItem等数据时必须用AsNoTracking(),避免EF跟踪这些实体,防止添加Activity时误判为要插入新实体:
protected override async Task OnInitializedAsync() { using var context = _dbContext.CreateDbContext(); Volunteers = await context.Volunteers.Where(x => !x.Deleted).AsNoTracking().ToListAsync(); SalesItems = await context.SalesItems.Where(x => !x.Deleted).AsNoTracking().ToListAsync(); VolunteerActivities = await context.VolunteerActivities.AsNoTracking().ToListAsync(); } // 组件内变量 private List<Volunteer> Volunteers { get; set; } = new(); private List<SalesItem> SalesItems { get; set; } = new(); private List<VolunteerActivity> VolunteerActivities { get; set; } = new();
3. Razor页面(EditForm实现)
创建ActivityForm.razor页面,支持Activity的创建和更新:
@page "/activity-form/{Id:int?}" @inject IDbContextFactory<AppDbContext> DbContextFactory @inject NavigationManager NavManager <EditForm Model="@ActivityModel" OnValidSubmit="@HandleSubmit"> <DataAnnotationsValidator /> <ValidationSummary /> <!-- Activity基本信息 --> <div class="mb-3"> <label class="form-label">活动名称</label> <InputText @bind-Value="ActivityModel.Name" class="form-control" /> </div> <!-- 志愿者需求列表 --> <h4>志愿者需求</h4> @foreach (var item in ActivityModel.VolunteerNecessities) { <div class="card mb-2"> <div class="card-body"> <div class="row"> <div class="col-md-4"> <label class="form-label">志愿者</label> <InputSelect @bind-Value="item.VolunteerId" class="form-control"> <option value="">选择志愿者</option> @foreach (var volunteer in Volunteers) { <option value="@volunteer.Id">@volunteer.Name</option> } </InputSelect> </div> <div class="col-md-4"> <label class="form-label">志愿者活动类型</label> <InputSelect @bind-Value="item.VolunteerActivityId" class="form-control"> <option value="">选择活动类型</option> @foreach (var va in VolunteerActivities) { <option value="@va.Id">@va.Name</option> } </InputSelect> </div> <div class="col-md-3"> <label class="form-label">备注</label> <InputText @bind-Value="item.AdditionalInformation" class="form-control" /> </div> <div class="col-md-1 mt-auto"> <button type="button" class="btn btn-danger" @onclick="() => RemoveVolunteerNecessity(item)">删除</button> </div> </div> </div> </div> } <button type="button" class="btn btn-secondary mb-3" @onclick="AddVolunteerNecessity">添加志愿者需求</button> <!-- 销售物品需求列表 --> <h4>物品需求</h4> @foreach (var item in ActivityModel.SalesItemNecessities) { <div class="card mb-2"> <div class="card-body"> <div class="row"> <div class="col-md-4"> <label class="form-label">物品</label> <InputSelect @bind-Value="item.SalesItemId" class="form-control"> <option value="">选择物品</option> @foreach (var item in SalesItems) { <option value="@item.Id">@item.Name</option> } </InputSelect> </div> <div class="col-md-3"> <label class="form-label">数量</label> <InputNumber @bind-Value="item.Amount" class="form-control" min="1" /> </div> <div class="col-md-3"> <label class="form-label">备注</label> <InputText @bind-Value="item.AdditionalInformation" class="form-control" /> </div> <div class="col-md-2 mt-auto"> <button type="button" class="btn btn-danger" @onclick="() => RemoveSalesItemNecessity(item)">删除</button> </div> </div> </div> </div> } <button type="button" class="btn btn-secondary mb-3" @onclick="AddSalesItemNecessity">添加物品需求</button> <button type="submit" class="btn btn-primary">保存</button> <button type="button" class="btn btn-secondary ms-2" @onclick="() => NavManager.NavigateTo("/activities")">取消</button> </EditForm>
4. 组件代码(.razor.cs)
public partial class ActivityForm { [Parameter] public int? Id { get; set; } private Activity ActivityModel { get; set; } = new(); protected override async Task OnInitializedAsync() { await LoadLookupData(); if (Id.HasValue) { // 编辑模式:加载现有Activity及子实体 using var context = DbContextFactory.CreateDbContext(); ActivityModel = await context.Activities .Include(a => a.VolunteerNecessities) .Include(a => a.SalesItemNecessities) .FirstOrDefaultAsync(a => a.Id == Id.Value); if (ActivityModel == null) { NavManager.NavigateTo("/activities"); return; } } } private async Task LoadLookupData() { using var context = DbContextFactory.CreateDbContext(); Volunteers = await context.Volunteers.Where(x => !x.Deleted).AsNoTracking().ToListAsync(); SalesItems = await context.SalesItems.Where(x => !x.Deleted).AsNoTracking().ToListAsync(); VolunteerActivities = await context.VolunteerActivities.AsNoTracking().ToListAsync(); } private void AddVolunteerNecessity() { ActivityModel.VolunteerNecessities.Add(new VolunteerNecessity()); } private void RemoveVolunteerNecessity(VolunteerNecessity item) { ActivityModel.VolunteerNecessities.Remove(item); } private void AddSalesItemNecessity() { ActivityModel.SalesItemNecessities.Add(new SalesItemNecessity()); } private void RemoveSalesItemNecessity(SalesItemNecessity item) { ActivityModel.SalesItemNecessities.Remove(item); } private async Task HandleSubmit() { using var context = DbContextFactory.CreateDbContext(); if (Id.HasValue) { // 更新模式:获取数据库中的Activity及子实体 var existingActivity = await context.Activities .Include(a => a.VolunteerNecessities) .Include(a => a.SalesItemNecessities) .FirstAsync(a => a.Id == Id.Value); // 更新主实体字段 existingActivity.Name = ActivityModel.Name; // 处理VolunteerNecessities的增删改 UpdateChildEntities(existingActivity.VolunteerNecessities, ActivityModel.VolunteerNecessities, context); // 处理SalesItemNecessities的增删改 UpdateChildEntities(existingActivity.SalesItemNecessities, ActivityModel.SalesItemNecessities, context); } else { // 创建模式:只添加Activity,子实体自动被EF跟踪 // 注意:不要给子实体的导航属性(如Volunteer、SalesItem)赋值,只设置Id context.Activities.Add(ActivityModel); } await context.SaveChangesAsync(); NavManager.NavigateTo("/activities"); } // 通用子实体更新方法:对比现有和提交的子实体,处理增删改 private void UpdateChildEntities<T>(List<T> existingItems, List<T> submittedItems, AppDbContext context) where T : class, IHasId { // 删除不在提交列表中的现有项 var itemsToRemove = existingItems.Where(e => !submittedItems.Any(s => s.Id == e.Id)).ToList(); foreach (var item in itemsToRemove) { context.Remove(item); } // 新增或更新提交的项 foreach (var submittedItem in submittedItems) { var existingItem = existingItems.FirstOrDefault(e => e.Id == submittedItem.Id); if (existingItem == null) { context.Add(submittedItem); } else { context.Entry(existingItem).CurrentValues.SetValues(submittedItem); } } } } // 统一子实体主键接口 public interface IHasId { int Id { get; set; } } // 子实体实现接口 public class VolunteerNecessity : IHasId { /* 已有代码 */ } public class SalesItemNecessity : IHasId { /* 已有代码 */ }
5. 关键注意事项
- 不要给子实体的导航属性赋值:比如不要设置
VolunteerNecessity.Volunteer,只需要赋值VolunteerId,否则EF会尝试插入新的Volunteer,触发标识插入错误。 - 使用DbContextFactory:Blazor中不要长期持有DbContext,每次操作创建新实例,避免跟踪冲突。
- 更新子实体要对比现有数据:直接替换子实体列表会导致跟踪错误,需手动处理增删改。
- 初始化子实体列表:在Activity中给子实体列表赋值空列表,避免空引用异常。
内容的提问来源于stack exchange,提问作者Jeremy
相关产品推荐
相关产品推荐

