4

I've a very basic setup to test and understand why EF Core 6 don't save related entities by default when adding new item to the many end of an already saved entity.

  1. Can someone explain to me what is wrong with this setup?
  2. And how can I make EF detect changes by default? is there any configuration that I can do in order to make EF detect changes without setting entry state manually?

I also followed the sample provided in Microsoft docs and it is giving me the same result (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);
  }
}
Mustafa Magdy
  • 1,230
  • 5
  • 28
  • 44
  • Where have you define the relation between `Post` & `Author`. Please add Foreign Key and then try again. It should work. – Karan Aug 10 '22 at 05:03
  • I tried with all explicit relation configuration, it has the same behavior. I will update the sample for reference. – Mustafa Magdy Aug 10 '22 at 05:20
  • Try removing `builder.Entity().HasMany(a => a.Posts).WithOne(a => a.Author).HasForeignKey(a => a.AuthorId);` – Karan Aug 10 '22 at 05:43
  • `DbUpdateConcurrencyException` indicates that EF Core is trying to `update`, because it is assuming that your new post object represents an existing record, because you have provided an explicit primary key value. `context.Posts.Add(...)` provides a clear signal to EF Core that you really want to insert this new object. – Jeremy Lakeman Aug 10 '22 at 06:02

1 Answers1

9

First, the posted model does not match the fluent configuration, so lets correct that. According to the fluent configuration, the Post has both reference navigation property and explicit FK property to Author:

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

Second, there is relationship misconfiguration here

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);

You are configuring one and the same relationship twice, which in general is a source of potential problems, as in this case the second configuration (which overrides the first) is missing the collection navigation property in WithMany, which in turn leads to 2 relationships and 2 FKs. So remove it and leave just the first (correct) one

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

and in general always use one fluent configuration per relationship and pass the navigation properties to Has / With calls when they exist.


Now on the main subject - change tracking. When you call change tracking APIs directly (Entry.State or DbContext / DbSet Add, Remove, Attach), then EF just uses whatever you tell it with the state for the provided entity. However, when some entity is not tracked, and EF needs to determine its state (ChangeTracker.DetectChanges which is called by SaveChanges and several other places), then the things become tricky, because it doesn;t know if the entity is new (thus supposed to be added) or existing and needs to be updated.

Here they use a simple approach which works in most of the cases. If the PK is not generated value or if the PK is set (has value different from CLR default), then the entity is considered existing, otherwise new.

And this is the cause of the issue here. The Guid PKs by default are considered auto-generated (ValueGeneratedOnAdd), and since you are providing explicit value, the Post instance is considered existing and EF attempts to update (instead of insert) which of course fails.

There are basically two ways you can handle that. First is to let the default PK property configuration and don't assign PK value for the new entities (EF will generate and assign one when the entity enters the change tracker, but also it will know that it is auto generated and the entity must be created), i.e. in all your "non working" examples, remove Id = Guid.NewGuid(), from new Post() { ... } statements, and they should work correctly.

Second approach is (in case you want to use explicit Guid PKs) to leave the usage as is, but change the fluent configuration to tell EF that these properties are not auto generated, i.e.

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

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Thank you so much for the explanation, ```ValueGeneratedNever``` Was the missing piece, which causes the entry to be in Modified not Added state. Thanks. – Mustafa Magdy Aug 10 '22 at 15:32