efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Replace multi-level relationship in Entity Framework Core 6 vs 7

Open iheb719 opened this issue 2 years ago • 3 comments

Ask a question

I have the following model

public partial class Parent
{
    public int IdParent { get; set; }

    public virtual ICollection<FirstChild> FirstChild{ get; set; } = new List<FirstChild>();
}

public partial class FirstChild
{
    public int IdFirstChild { get; set; }

    public virtual ICollection<SecondChild> SecondChild { get; set; } = new List<SecondChild>();
}

public partial class SecondChild 
{
    public int IdSecondChild  { get; set; }

    public virtual ICollection<ThirdChild> ThirdChild{ get; set; } = new List<ThirdChild>();
}

public partial class ThirdChild
{
    public int IdThirdChild { get; set; }

    public String SomeProperty{ get; set; }
}

I update Parent with all it's relationships it this way :

var parentDB = _context.Parent.Single(x => x.IdParent == {id})
.Include(x => x.FirstChild)
.ThenInclude(x => x.SecondChild)
.ThenInclude(x => x.ThirdChild);

parentDb.FirstChild = {newCollectionValuesList}

_context.SaveChanges();

This works with EF Core 6 :

  • If in {newCollectionValuesList}, there is an element that already exists in parentDb.FirstChild (same ID), it get updated (with all its hierarchy)
  • If if does not exists, it will be added
  • If it exists but was not specified in {newCollectionValuesList}, it get deleted

When I upgraded to EF Core 7, I started to have this error :

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

I tried to add .AsNoTracking() in the query to resolve this, but my entities won't be updated

I don't want to loop manually through all the relationship and manually add the conditions to update the child elements

I didn't find anything related to that in What's new in EF7 nor in Breaking changes in EF7

This is a sample project with integration tests to reproduce the problem

Is this a bug ? because it works in EF6 Same result using dotnet6 and dotnet7

iheb719 avatar Jan 25 '23 04:01 iheb719

Note for triage: I am able to reproduce this. In 6.0, the existing instances are cascade-deleted before the new instances are tracked. In 7.0, the identity conflict happens before the cascade delete. Could be related to #30122.

FirstChild (Shared) {IdFirstChild: 11} Added
  IdFirstChild: 11 PK
  FirstChildName: 'firstChild1'
  IdParent: 1 FK
  IdParentNavigation: {IdParent: 1}
  SecondChild: [{IdSecondChild: 111}, {IdSecondChild: 111}]
FirstChild (Shared) {IdFirstChild: 11} Deleted
  IdFirstChild: 11 PK
  FirstChildName: 'firstChild1'
  IdParent: 1 FK
  IdParentNavigation: <null>
  SecondChild: [{IdSecondChild: 111}]
FirstChild {IdFirstChild: 12} Added
  IdFirstChild: 12 PK
  FirstChildName: 'firstChild2'
  IdParent: 1 FK
  IdParentNavigation: {IdParent: 1}
  SecondChild: []
Parent {IdParent: 1} Unchanged
  IdParent: 1 PK
  ParentName: 'parent'
  FirstChild: [{IdFirstChild: 11}, {IdFirstChild: 12}]
SecondChild (Shared) {IdSecondChild: 111} Added
  IdSecondChild: 111 PK
  IdFirstChild: 11 FK
  SecondChildName: 'secondChild1'
  IdFirstChildNavigation: {IdFirstChild: 11}
  ThirdChild: [{IdThirdChild: 1111}, {IdThirdChild: 1111}]
SecondChild (Shared) {IdSecondChild: 111} Deleted
  IdSecondChild: 111 PK
  IdFirstChild: 11 FK
  SecondChildName: 'secondChild1'
  IdFirstChildNavigation: {IdFirstChild: 11}
  ThirdChild: [{IdThirdChild: 1111}]
ThirdChild (Shared) {IdThirdChild: 1111} Added
  IdThirdChild: 1111 PK
  IdSecondChild: 111 FK
  ThirdChildName: 'thirdChild1'
  IdSecondChildNavigation: {IdSecondChild: 111}
ThirdChild (Shared) {IdThirdChild: 1111} Deleted
  IdThirdChild: 1111 PK
  IdSecondChild: 111 FK
  ThirdChildName: 'thirdChild1'
  IdSecondChildNavigation: {IdSecondChild: 111}

ajcvickers avatar Jan 25 '23 23:01 ajcvickers

@iheb719 Workaround is to for the deletion before replacing the collection:

public void UpdateParentChild(Parent parentUpdate)
{
    var existingParent = _context.Parent.Local.FirstOrDefault(x => x.IdParent == parentUpdate.IdParent);
    existingParent?.FirstChild.Clear();
    _context.ChangeTracker.DetectChanges();
    
    var parentBd = _context.Parent
        .Include(x => x.FirstChild)
        .ThenInclude(x => x.SecondChild)
        .ThenInclude(x => x.ThirdChild)
        .Single(x => x.IdParent == parentUpdate.IdParent);

    parentBd.ParentName = parentUpdate.ParentName;
    parentBd.FirstChild = parentUpdate.FirstChild;

    _context.SaveChanges();
}

ajcvickers avatar Jan 25 '23 23:01 ajcvickers

Thanks @ajcvickers the workaround is working

iheb719 avatar Jan 26 '23 03:01 iheb719