dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.78k stars 3.19k forks source link

New entity is not saved under certain conditions #26937

Closed tikhomirovp closed 2 years ago

tikhomirovp commented 2 years ago

We have a similar descendant from the DbContext:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;

namespace DetachedEntity {
    public class OneToManyDbContext : DbContext {
        public DbSet<OneToMany_Many> OneToMany_Many { get; set; }
        public DbSet<OneToMany_One> OneToMany_One { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder) {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<OneToMany_Many>()
                .HasOne(p => p.One)
                .WithMany(p => p.Collection)
                .OnDelete(DeleteBehavior.ClientCascade);
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder.UseInMemoryDatabase("InMemory");
        }
    }
    public class OneToMany_One {
        public OneToMany_One() {
            Collection = new List<OneToMany_Many>();
        }
        public int Id { get; set; }
        public List<OneToMany_Many> Collection { get; set; }
    }
    public class OneToMany_Many {
        public int Id { get; set; }
        public OneToMany_One One { get; set; }
    }
}

In the program, we are trying to create an entity as follows:

using System.Linq;

namespace DetachedEntity {
    class Program {
        static void Main(string[] args) {
            using(OneToManyDbContext context = new OneToManyDbContext()) {
                OneToMany_One one = new OneToMany_One();
                OneToMany_Many many = new OneToMany_Many();
                context.Attach(many);
                many.One = one;                
                var entry1 = context.Entry(many);
                //entry1.State == Microsoft.EntityFrameworkCore.EntityState.Added
                many.One = null;
                 var entry2 = context.Entry(many);
                //entry2.State == Microsoft.EntityFrameworkCore.EntityState.Detached
                context.SaveChanges(); // the many entity is not saved
            }
            using(OneToManyDbContext context = new OneToManyDbContext()) {
                var manyCount = context.OneToMany_Many.Count(); //0
                var oneCount = context.OneToMany_One.Count(); //1
            }
        }
    }
}

Current behavior: The entity 'Many' is not saved, the entity 'One' is saved.

Expected behavior: The entity 'Many' is saved, the entity 'One' is not saved.

If you do not explicitly detect change after changing the reference property 'One', everything works as expected:

using System.Linq;

namespace DetachedEntity {
    class Program {
        static void Main(string[] args) {
            using(OneToManyDbContext context = new OneToManyDbContext()) {
                OneToMany_One one = new OneToMany_One();
                OneToMany_Many many = new OneToMany_Many();
                context.Attach(many);
                many.One = one;                
                many.One = null;
                context.SaveChanges(); // the many entity is saved
            }
            using(OneToManyDbContext context = new OneToManyDbContext()) {
                var manyCount = context.OneToMany_Many.Count(); //1
                var oneCount = context.OneToMany_One.Count(); //0
            }
        }
    }
}

This behavior is reproduced in version 5.0.12.

ajcvickers commented 2 years ago

@tikhomirovp This is a case where a full DetectChanges is needed for correct behavior. However, it may be a place where we can in the future enhance local change detection to be sufficient.

With full DetectChanges like so:

context.Attach(many);
many.One = one;
context.ChangeTracker.DetectChanges();

many.One = null;
context.ChangeTracker.DetectChanges();

context.SaveChanges(); // the many entity is not saved

the sequence of events is:

OneToMany_Many {Id: -2147482647} Added
  Id: -2147482647 PK Temporary
  OneId: -2147482647 FK Temporary
  One: {Id: -2147482647}
OneToMany_One {Id: -2147482647} Added
  Id: -2147482647 PK Temporary
  Collection: [{Id: -2147482647}]
OneToMany_One {Id: -2147482647} Added
  Id: -2147482647 PK Temporary
  Collection: []

In your second example, the change to set the navigation property to null is never seen by EF, and hence there are no changes of state.

ajcvickers commented 2 years ago

In the current code base, local DetectChanges is sufficient for this case; will add a test.