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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 00:52:33