Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected InvalidOperationException when trying to change relationship via property default value

In the sample code below I get the following exception when doing db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: 'The instance of entity type 'B' cannot be tracked because another instance with the key value '{Id: 0}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

Why doesn't it add instead of attach the instances of B?

Strangely the documentation for IsModified doesn't specify InvalidOperationException as a possible exception. Invalid documentation or a bug?

I know this code is strange, but I wrote it only to understand how ef core works in some weird egde cases. What I want is an explanation, not a work around.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    public class A
    {
        public int Id { get; set; }
        public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };
    }

    public class B
    {
        public int Id { get; set; }
    }

    public class Db : DbContext {
        private const string connectionString = @"Server=(localdb)\mssqllocaldb;Database=Apa;Trusted_Connection=True";

        protected override void OnConfiguring(DbContextOptionsBuilder o)
        {
            o.UseSqlServer(connectionString);
            o.EnableSensitiveDataLogging();
        }

        protected override void OnModelCreating(ModelBuilder m)
        {
            m.Entity<A>();
            m.Entity<B>();
        }
    }

    static void Main(string[] args)
    {
        using (var db = new Db()) {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();

            db.Add(new A { });
            db.SaveChanges();
        }

        using (var db = new Db()) {
            var a = db.Set<A>().Single();
            db.Entry(a).Collection(x => x.S).IsModified = true;
            db.SaveChanges();
        }
    }
}
like image 385
Supremum Avatar asked Feb 26 '20 13:02

Supremum


1 Answers

The reason of the error in the provided code is follows.

When you get created entity A from the database its property S is initialized with a collection that contains two new records B. Id of each of this new B entities is equal to 0.

// This line of code reads entity from the database
// and creates new instance of object A from it.
var a = db.Set<A>().Single();

// When new entity A is created its field S initialized
// by a collection that contains two new instances of entity B.
// Property Id of each of these two B entities is equal to 0.
public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };

After executing line of code var a = db.Set<A>().Single() collection S of entity A does not contain B entities from the database, because DbContext Db does not use lazy loading and there is no explicit loading of the collection S. Entity A only contains new B entities that were created during initialization of collection S.

When you call IsModifed = true for collection S entity framework tries to add those two new entites B into change tracking. But it fails because both new B entities have the same Id = 0:

// This line tries to add to change tracking two new B entities with the same Id = 0.
// As a result it fails.
db.Entry(a).Collection(x => x.S).IsModified = true;

You can see from the stack trace that entity framework tries to add B entities into the IdentityMap:

at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetPropertyModified(IProperty property, Boolean changeState, Boolean isModified, Boolean isConceptualNull, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(InternalEntityEntry internalEntityEntry, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(Object relatedEntity, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.set_IsModified(Boolean value)

And the error message also tells that it cannot track B entity with Id = 0 because another B entity with the same Id is already tracked.


How to resolve this problem.

To resolve this problem you should delete code that creates B entities when initializing S collection:

public ICollection<B> S { get; set; } = new List<B>();

Instead you should fill S collection at the place where A is created. For example:

db.Add(new A {S = {new B(), new B()}});

If you don't use lazy loading you should explicitly load S collection to add its items into change tracking:

// Use eager loading, for example.
A a = db.Set<A>().Include(x => x.S).Single();
db.Entry(a).Collection(x => x.S).IsModified = true;

Why doesn't it add instead of attach the instances of B?

In short, they are attached insted of being added because they have Detached state.

After executing line of code

var a = db.Set<A>().Single();

created instances of entity B have state Detached. It can be verified using the next code:

Console.WriteLine(db.Entry(a.S[0]).State);
Console.WriteLine(db.Entry(a.S[1]).State);

Then when you set

db.Entry(a).Collection(x => x.S).IsModified = true;

EF tries to add B entities to change tracking. From source code of EFCore you can see that this leads us to the method InternalEntityEntry.SetPropertyModified with the next argument values:

  • property - one of our B entities,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

This method with such arguments changes state of the Detached B entites to Modified, and then tries to start tracking for them (see lines 490 - 506). Because B entities now have state Modified this leads them to be attached (not added).

like image 94
Iliar Turdushev Avatar answered Oct 15 '22 20:10

Iliar Turdushev