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.77k stars 3.18k forks source link

DbUpdateConcurrencyException exception when saving a master-detail graph where detail entity PK is Guid. (or Not)? #31042

Closed tzarot closed 1 year ago

tzarot commented 1 year ago

When saving a master-detail (one-many) graph, where the child/detail entity has a PK of Guid, that gets assigned from within the detail entity's constructor, and the master/principal entity is fetched from the database, I am getting a DbUpdateConcurrencyException. If however, I do not fetch the Principal/Master entity from the database. but instead I create a new one and also add the child entity to it, then all work without any problems.

Am I missing something here?

Also, if I remove the initializer, Guid.NewGuid(); from the Order Id property, all work without any errors.

However I want to point-out that the Order.Id property, in the migration, is declared as .ValueGeneratedOnAdd() so reading the documentation in https://learn.microsoft.com/en-us/dotnet/api/Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder-1.ValueGeneratedOnAdd?view=efcore-7.0&viewFallbackFrom=net-7.0, the approach taken should work.

public class Customer
{
    public int Id { get; private set; }

    public string Name { get; set; } = null!; 

    public IList<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
    public Guid Id { get; private set; } = Guid.NewGuid(); // if I remove this assignment, all work fine.

    public int CustomerId { get; set; }

    public Customer Customer { get; set; }

    public DateTime OrderDate { get; set; }

}

public class PlayDbContext:DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(
            "Server=.;Database=PlayAcmeDatabase;Trusted_Connection=true;TrustServerCertificate=true;");
    }
}

 // This throws a DbUpdateConcurrencyException exception!
    var order = new Order()
    {
        OrderDate = DateTime.Now,
    };

    var c = db.Customers.FirstOrDefault(c => c.Name == "A2"); // A2 customer exists in database
    if (c == null) return;

    c.Orders.Add(order);
    db.SaveChanges();

// This works perfectly !!!
    var c = new Customer()
    {
        Name = "A3"
    };
    var order = new Order()
    {
        OrderDate = DateTime.Now,
    };

    c.Orders.Add(order);
    db.Customers.Add(c);

    db.SaveChanges();

EF Core version: Database provider: (e.g. Microsoft.EntityFrameworkCore.SqlServer) Target framework: (e.g. .NET 7.0)

Attached the solution zip file. Play.EFCore.Master-Detail-Detail2.zip

ajcvickers commented 1 year ago

@tzarot When EF is configured to generate key values for an entity type, then any instance of that entity type is considered to already exist in the database when it already has a key value set before it is tracked. If you don't want to use auto-generated keys, then configure the entity type as such:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>().Property(e => e.Id).ValueGeneratedNever();
}
tzarot commented 1 year ago

@ajcvickers, Thank you for your answer. However I believe what you say, that was also initially my thought, is not totally accurate, since then I cannot explain why if I just create a simple entity i.e. Customer, with no detail entities whatsoever, and then again manually set the Id in the constructor to a Guid.NewGuid(), the process works fine without any exceptions!. Of course it works if I do not assign the Id also. (Attached this simpler solution also.)

So, to recap and possibly explain better: The entry.State of the newly added Customer (No detail entities), just before db.SaveChanges() is always Added, no matter if I assign the Id manually or not.

In the case as described, in my original question, the added Order that is a detail entity of Customer (when this Customer is fetched from database), when I have not assigned the Order.Id manually has a state of Added, where if I manually assign the Id in the constructor, then it has a state of Modified, hence the exception!. Then again if I also have added the Customer (a new Customer, not in database), then as mentioned also in my original article the entry.State of the Order is always Added, hence the process works.

public class Customer
{
    public Guid Id { get; private set; }  // I hust made this a Guid. In Migrations Snapshot file, it also gets .ValueGeneratedOnAdd()

    public string Name { get; set; } = null!; 

    public Customer()
    {
        Id = Guid.NewGuid();  // and I still assign this manually
    }
}

public class PlayDbContext:DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=.;Database=PlayAcmeDatabase3;Trusted_Connection=true;TrustServerCertificate=true;");
    }
}

// in Program.cs
using (var db = new PlayDbContext())
{
    CreateCustomer(db);
}

void CreateCustomer( PlayDbContext db )
{
    var c = new Customer()
    {
        Name = "John Doe"
    };
    db.Customers.Add(c);
    db.SaveChanges();
}

Play.EFCore.Master-Detail-Detail3.zip

ajcvickers commented 1 year ago

@tzarot Calling DbSet.Add or DbContext.Add always marks the pass entity as new, even if its key is set. This is essentially the way you tell EF to insert the entity even though it has a key value already set. Using DbSet.Attach/Update, DbContext.Attach/Update, or having EF detect the change, like you are doing in your first example, results in a state based on the key value. See Explicitly tracking entities for more info.