我有一个非常基本的设置来测试和理解为什么EF Core 6在向已经保存的实体的多个末端添加新项时,默认情况下不保存相关实体.

  1. 有人能给我解释一下这个设置出了什么问题吗?
  2. 我如何才能让EF默认检测到更改?有什么配置可以让EF在不手动设置entry状态的情况下检测更改吗?

我也遵循了Microsoft docs中提供的样本,它给了我相同的结果(DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);)

using Microsoft.EntityFrameworkCore;

var db = new AuthorsDbContext();
db.Database.EnsureCreated();


var author = new Author();
author.Id = Guid.NewGuid();

db.Authors.Add(author);
db.SaveChanges();

// Not working
// var author1 = await db.Authors.FindAsync(new object?[] { author.Id }); 
// author1.Posts.Add(new Post() { Id = Guid.NewGuid(), Title = "test" });
// db.SaveChanges(); //-> DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

// Not working
// var author1 = await db.Authors.Include(a => a.Posts).FirstAsync(a => a.Id == author.Id);
// author1.Posts.Add(new Post() { Id = Guid.NewGuid(), Title = "test" });
// db.SaveChanges(); //-> DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

// Not working
// var author1 = await db.Authors.AsTracking().FirstAsync(a => a.Id == author.Id);
// author1.Posts.Add(new Post() { Id = Guid.NewGuid(), Title = "test" });
// db.SaveChanges(); //-> DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

// Not working
// var author1 = await db.Authors.Include(a=>a.Posts).AsTracking().FirstAsync(a => a.Id == author.Id);
// author1.Posts.Add(new Post() { Id = Guid.NewGuid(), Title = "test" });
// db.SaveChanges(); //-> DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

// this is the only way it works
// var author1 = await db.Authors.FindAsync(new object?[] { author.Id });
// var post = new Post() { Id = Guid.NewGuid(), Title = "test" };
// author1.Posts.Add(post);
// db.Entry(post).State = EntityState.Added;
// db.SaveChanges();

public class Author
{
  public Guid Id { get; set; }
  public List<Post> Posts { get; set; } = new();
}

public class Post
{
  public Guid Id { get; set; }
  public string Title { get; set; }
}

public class AuthorsDbContext : DbContext
{
  public DbSet<Author> Authors { get; set; }
  public DbSet<Post> Posts { get; set; }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    optionsBuilder.UseMySQL("Data Source=127.0.0.1;Initial Catalog=test-db-091;User Id=root;Password=DeV12345");
  }

protected override void OnModelCreating(ModelBuilder builder)
  {
    builder.Entity<Author>().HasKey(a => a.Id);
    builder.Entity<Post>().HasKey(a => a.Id);

    builder.Entity<Author>().HasMany(a => a.Posts).WithOne(a => a.Author).HasForeignKey(a => a.AuthorId);
    builder.Entity<Post>().HasOne(a => a.Author).WithMany().HasForeignKey(a => a.AuthorId);
  }
}

推荐答案

首先,发布的模型与流畅的配置不匹配,因此让我们更正这一点.根据FLUENT配置,Post同时具有引用导航属性和显式FK属性到Author:

public class Post
{
    //...
    public Author Author { get; set; }
    public Guid AuthorId { get; set; }
}

其次,这里存在关系配置错误

builder.Entity<Author>().HasMany(a => a.Posts).WithOne(a => a.Author).HasForeignKey(a => a.AuthorId);
builder.Entity<Post>().HasOne(a => a.Author).WithMany().HasForeignKey(a => a.AuthorId);

您配置了两次相同的关系,这通常是潜在问题的根源,因为在本例中,第二个配置(覆盖第一个)缺少WithMany中的集合导航属性,这反过来会导致2个关系和2个FK.所以go 掉它,只留下第一个(正确的)

builder.Entity<Author>()
    .HasMany(a => a.Posts)
    .WithOne(a => a.Author)
    .HasForeignKey(a => a.AuthorId);

通常,每个关系都使用一个流畅的配置,并将导航属性传递给Has/With调用(如果它们存在).


现在谈一谈主要话题--变化跟踪.当您直接调用更改跟踪API(Entry.StateDbContext/DbSet AddRemoveAttach)时,EF只是将您告诉它的任何内容与所提供实体的状态一起使用.然而,当某个实体没有被跟踪,而EF需要确定其状态(由SaveChanges和其他几个地方调用的ChangeTracker.DetectChanges)时,事情就变得棘手了,因为它不知道该实体是新的(因此应该被添加的)还是存在的并且需要更新.

在这里,他们使用一种简单的方法,这种方法在大多数情况下都有效.如果主键为not generated value或设置了主键(具有与CLR默认值不同的值),则实体被视为existing,否则被视为new.

这就是这里问题的原因.默认情况下,Guid个PK被认为是自动生成的(ValueGeneratedOnAdd),由于您提供的是显式值,因此Post实例被认为是existing,EFtry 更新(而不是插入),这当然会失败.

基本上有两种方法可以处理这一问题.首先是让默认的PK属性配置,不要为新的实体分配PK值(当实体进入变化跟踪器时,EF会生成并分配一个,但它也会知道它是自动生成的,实体必须被创建),也就是在你所有的"非工作"例子中,从new Post() { ... }条语句中删除Id = Guid.NewGuid(),条,它们应该可以正常工作.

第二种方法是(如果您想要使用显式Guid个PKs)保持用法不变,但更改FLUENT配置以告诉EF这些属性不是自动生成的,即

builder.Entity<Post>().Property(e => e.Id).ValueGeneratedNever();

Csharp相关问答推荐

使页面内容居中

有没有办法在WPF文本框中添加复制事件的处理程序?

静态对象构造顺序

UWP中的任务和界面

在静态模式下实例化配置

MigraDoc文档

在C#中,非静态接口方法的抽象和虚拟是冗余的吗?

如何将MemberInitExpression添加到绑定中其他Lambda MemberInitExpression

避免只读记录 struct 中的防御副本

GODOT 4向C#中的字符串参数发送信号以等待

C#动态设置ServerReport报表参数

如何在同一成员上组合[JsonPropertyName]和[ObservableProperty]?

MSI无法将快捷方式添加到启动文件夹

如何在使用属性 Select 器时判断是否可以为空

无效的Zip文件-Zip存档

SqlException:无法打开数据库.升级到Dotnet 8后-数据库兼容性版本-非EFCore兼容性级别

实例化列表时的集合表达式是什么?

持久函数似乎没有并行运行活动函数

在NET7 throws中为集成测试创建WebApplication固定装置

DocuSign签名者的可变数量