23

For an application using Code First EF 5 beta I have:

public class ParentObject
{
    public int Id {get; set;}
    public virtual List<ChildObject> ChildObjects {get; set;}
    //Other members
}

and

public class ChildObject
{
    public int Id {get; set;}
    public int ParentObjectId {get; set;}
    //Other members
}

The relevant CRUD operations are performed by repositories, where necessary.

In

OnModelCreating(DbModelBuilder modelBuilder)

I have set them up:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
            .WithOptional()
            .HasForeignKey(c => c.ParentObjectId)
            .WillCascadeOnDelete();

So if a ParentObject is deleted, its ChildObjects are too.

However, if I run:

parentObject.ChildObjects.Clear();
_parentObjectRepository.SaveChanges(); //this repository uses the context

I get the exception:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

This makes sense as the definition of the entities includes the foreign key constraint which is being broken.

Can I configure the entity to "clear itself up" when it gets orphaned or must I manually remove these ChildObjects from the context (in this case using a ChildObjectRepository).

StuperUser
  • 10,555
  • 13
  • 78
  • 137
  • Thankfully, the EF team [knows about this](http://blog.oneunicorn.com/2012/06/02/deleting-orphans-with-entity-framework/) and will likely come up with a built-in solution that doesn't require modifying the internals – PinnyM Jun 08 '12 at 18:22

7 Answers7

34

It is actually supported but only when you use Identifying relation. It works with code first as well. You just need to define complex key for your ChildObject containing both Id and ParentObjectId:

modelBuilder.Entity<ChildObject>()
            .HasKey(c => new {c.Id, c.ParentObjectId});

Because defining such key will remove default convention for auto incremented Id you must redefine it manually:

modelBuilder.Entity<ChildObject>()
            .Property(c => c.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

Now calling to parentObject.ChildObjects.Clear() deletes dependent objects.

Btw. your relation mapping should use WithRequired to follow your real classes because if FK is not nullable, it is not optional:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
            .WithRequired()
            .HasForeignKey(c => c.ParentObjectId)
            .WillCascadeOnDelete();
Community
  • 1
  • 1
Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • 1
    @Ladislav Mmka what about unidirectional case? When ChildObject doesn't know about the parent? – Davita Jul 14 '12 at 09:02
  • @Ladislav I'm using EF6 and tried to do as you have done in the examples. However calling Collection.Clear() and AddOrUpdate(parent) doesn't seem to work. Is there something I have to do on the context? – Jamez Nov 19 '13 at 15:08
  • @Jamez - Turns out I wasn't hydrating the child collection when calling clear. Loading the children fixes the issue. – Jamez Nov 19 '13 at 15:24
  • 2
    Many of the examples I have seen to fix this problem do not use object references. As in this code, you can create a foreign key because the parent property is an int, 'ParentObjectId'. Unfortunately it doesn't work if you are using full OO object references because you cannot define a foreign key using a reference. That leaves me with the same problem, orphans are not deleted. – Rob Kent Jul 30 '14 at 15:42
  • Saved my day! The code for "DatabaseGeneratedOption.Identity" wasn't included in other posts on this issue and IDENTITY OFF was preventing creation of new / replacement collection items – Xcheque Mar 14 '15 at 22:42
  • Can I have feedback for this too ? Thanks.http://stackoverflow.com/questions/35381783/the-relationship-could-not-be-changed-because-one-or-more-of-the-foreign-key-pro – Sampath Feb 13 '16 at 19:40
4

Update:

I found a way that doesn't need to add navigational properties from the child to the parent entity or to set up a complex key.

It's based on this article which uses the ObjectStateManager to find the deleted entities.

With a list ObjectStateEntry in hand, we can find a pair of EntityKey from each, which represents the relationship that was deleted.

At this point, I couldn't find any indication of which one had to be deleted. And contrary to the article's example, simply picking the second one would get the parent deleted in cases where the child had a navigation property back to the parent. So, in order to fix that, I track which types should be handled with the class OrphansToHandle.

The Model:

public class ParentObject
{
    public int Id { get; set; }
    public virtual ICollection<ChildObject> ChildObjects { get; set; }

    public ParentObject()
    {
        ChildObjects = new List<ChildObject>();
    }
}

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

The other classes:

public class MyContext : DbContext
{
    private readonly OrphansToHandle OrphansToHandle;

    public DbSet<ParentObject> ParentObject { get; set; }

    public MyContext()
    {
        OrphansToHandle = new OrphansToHandle();
        OrphansToHandle.Add<ChildObject, ParentObject>();
    }

    public override int SaveChanges()
    {
        HandleOrphans();
        return base.SaveChanges();
    }

    private void HandleOrphans()
    {
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;

        objectContext.DetectChanges();

        var deletedThings = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).ToList();

        foreach (var deletedThing in deletedThings)
        {
            if (deletedThing.IsRelationship)
            {
                var entityToDelete = IdentifyEntityToDelete(objectContext, deletedThing);

                if (entityToDelete != null)
                {
                    objectContext.DeleteObject(entityToDelete);
                }
            }
        }
    }

    private object IdentifyEntityToDelete(ObjectContext objectContext, ObjectStateEntry deletedThing)
    {
        // The order is not guaranteed, we have to find which one has to be deleted
        var entityKeyOne = objectContext.GetObjectByKey((EntityKey)deletedThing.OriginalValues[0]);
        var entityKeyTwo = objectContext.GetObjectByKey((EntityKey)deletedThing.OriginalValues[1]);

        foreach (var item in OrphansToHandle.List)
        {
            if (IsInstanceOf(entityKeyOne, item.ChildToDelete) && IsInstanceOf(entityKeyTwo, item.Parent))
            {
                return entityKeyOne;
            }
            if (IsInstanceOf(entityKeyOne, item.Parent) && IsInstanceOf(entityKeyTwo, item.ChildToDelete))
            {
                return entityKeyTwo;
            }
        }

        return null;
    }

    private bool IsInstanceOf(object obj, Type type)
    {
        // Sometimes it's a plain class, sometimes it's a DynamicProxy, we check for both.
        return
            type == obj.GetType() ||
            (
                obj.GetType().Namespace == "System.Data.Entity.DynamicProxies" &&
                type == obj.GetType().BaseType
            );
    }
}

public class OrphansToHandle
{
    public IList<EntityPairDto> List { get; private set; }

    public OrphansToHandle()
    {
        List = new List<EntityPairDto>();
    }

    public void Add<TChildObjectToDelete, TParentObject>()
    {
        List.Add(new EntityPairDto() { ChildToDelete = typeof(TChildObjectToDelete), Parent = typeof(TParentObject) });
    }
}

public class EntityPairDto
{
    public Type ChildToDelete { get; set; }
    public Type Parent { get; set; }
}

Original Answer

To solve this problem without setting up a complex key, you can override the SaveChanges of your DbContext, but then use ChangeTracker to avoid accessing the database in order to find orphan objects.

First add a navigation property to the ChildObject (you can keep int ParentObjectId property if you want, it works either way):

public class ParentObject
{
    public int Id { get; set; }
    public virtual List<ChildObject> ChildObjects { get; set; }
}

public class ChildObject
{
    public int Id { get; set; }
    public virtual ParentObject ParentObject { get; set; }
}

Then look for orphan objects using ChangeTracker:

public class MyContext : DbContext
{
    //...
    public override int SaveChanges()
    {
        HandleOrphans();
        return base.SaveChanges();
    }

    private void HandleOrphans()
    {
        var orphanedEntities =
            ChangeTracker.Entries()
            .Where(x => x.Entity.GetType().BaseType == typeof(ChildObject))
            .Select(x => ((ChildObject)x.Entity))
            .Where(x => x.ParentObject == null)
            .ToList();

        Set<ChildObject>().RemoveRange(orphanedEntities);
    }
}

Your configuration becomes:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
            .WithRequired(c => c.ParentObject)
            .WillCascadeOnDelete();

I did a simple speed test iterating 10.000 times. With HandleOrphans() enabled it took 1:01.443 min to complete, with it disabled it was 0:59.326 min (both are an average of three runs). Test code below.

using (var context = new MyContext())
{
    var parentObject = context.ParentObject.Find(1);
    parentObject.ChildObjects.Add(new ChildObject());
    context.SaveChanges();
}

using (var context = new MyContext())
{
    var parentObject = context.ParentObject.Find(1);
    parentObject.ChildObjects.Clear();
    context.SaveChanges();
}
Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62
2

Want to share another .net ef core solution that worked for me, may be somebody will find it usefull.

I had a child table with two foreign keys (either or), so the accepted solution didn't work for me. Based on the answer by Marcos Dimitrio I came up with the following:

In my custom DbContext:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
  {
    var modifiedEntities = this.ChangeTracker.Entries().Where(c => c.State == EntityState.Modified);
    foreach (var entityEntry in modifiedEntities)
    {
      if (entityEntry.Entity is ChildObject)
      {
         var fkProperty = entityEntry.Property(nameof(ChildObject.ParentObjectId));
         if (fkProperty.IsModified && fkProperty.CurrentValue == null && fkProperty.OriginalValue != null)
         {
           // Checked if FK was set to NULL
           entityEntry.State = EntityState.Deleted;
         }
      }
    }

    return await base.SaveChangesAsync(cancellationToken);
  }
chris31389
  • 8,414
  • 7
  • 55
  • 66
Maxim Zabolotskikh
  • 3,091
  • 20
  • 21
2

In EF Core, it can be done by Delete Orphans.

Like this:

dbContext.Children.Clear();
Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62
Adrita Sharma
  • 21,581
  • 10
  • 69
  • 79
2

This is my generic solution for Entity Framework 6.4.4, without knowledge of the particular schema.

Note that I start my search for orphan entities from modified entity entries, as in my case I could not find anything searching for deleted relationship entries like other answers suggest.

The logic behind the approach is that an entity removed from a collection of a required relationship will have its foreign key updated to null by the Entity Framework. So we search for all modified entities which have at least one relationship to an end with multiplicity 'One' but having the foreign key set to null.

Add this method to your DbContext subclass. You could override the SaveChanges / SaveChangesAsync methods to call this method automatically.

public void DeleteOrphanEntries()
{
  this.ChangeTracker.DetectChanges();

  var objectContext = ((IObjectContextAdapter)this).ObjectContext;

  var orphanEntityEntries =
    from entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified)
    where !entry.IsRelationship
    let relationshipManager = entry.RelationshipManager
    let orphanRelatedEnds = from relatedEnd in relationshipManager.GetAllRelatedEnds().OfType<EntityReference>()
                            where relatedEnd.EntityKey == null // No foreign key...
                            let associationSet = (AssociationSet)relatedEnd.RelationshipSet
                            let associationEndMembers = from associationSetEnd in associationSet.AssociationSetEnds
                                                        where associationSetEnd.EntitySet != entry.EntitySet // ... not the end pointing to the entry
                                                        select associationSetEnd.CorrespondingAssociationEndMember
                            where associationEndMembers.Any(e => e.RelationshipMultiplicity == RelationshipMultiplicity.One) // ..but foreign key required.
                            select relatedEnd
    where orphanRelatedEnds.Any()
    select entry;

  foreach (var orphanEntityEntry in orphanEntityEntries)
  {
    orphanEntityEntry.Delete();
  }
}

grammophone
  • 601
  • 6
  • 8
  • I've been picking this apart for a while today making sure I understand what all is happening. Am I correct that this will delete a child entity that has at least one required foreign key with a null value? Are there scenarios where this method can cause other constraint exceptions? – Logarr Aug 04 '22 at 03:50
1

Yes. The following works in EF Core:

Make sure you set the cascade behavior to Cascade like so:

entity.HasOne(d => d.Parent)
                    .WithMany(p => p.Children)
                    .HasForeignKey(d => d.ParentId)
                    .OnDelete(DeleteBehavior.Cascade);

Then set the Parent property to be null in all the child entities that are to be deleted like so:

var childrenToBeRemoved = parent.Children.Where(filter);
foreach(var child in childrenToBeRemoved)
{
    child.Parent = null;
}

Now, context.SaveAsync() should delete all the orphaned children entities.

Nimish David Mathew
  • 2,958
  • 6
  • 29
  • 45
  • Did you test this? – Gert Arnold Apr 19 '21 at 14:39
  • @GertArnold Yes. Is there any problem with this approach? – Nimish David Mathew Apr 19 '21 at 14:39
  • Well, I guess it depends on `ParentId` being required and on the way `childrenToBeRemoved` was queried. Maybe you should elaborate on that a bit. Apart from that, I wouldn't recommend this way of deleting records. It's EF-specific and not intuitive at all. I'd rather have EF not do this. – Gert Arnold Apr 19 '21 at 14:52
  • As per the Microsoft docs, this works with a non-nullable `ParentId` as well: https://learn.microsoft.com/en-us/ef/core/saving/cascade-delete#delete-orphans-examples – Nimish David Mathew Apr 19 '21 at 16:52
-2

This is not something that is supported automatically by EF right now. You can do it by overriding SaveChanges in your context and manually deleting an child objects that no longer have a parent. The code would be something like this:

public override int SaveChanges()
{
    foreach (var bar in Bars.Local.ToList())
    {
        if (bar.Foo == null)
        {
            Bars.Remove(bar);
        }
    }

    return base.SaveChanges();
}
Arthur Vickers
  • 7,503
  • 32
  • 26
  • 4
    Will this code not be executed for every SaveChanges()? – Elisabeth Jun 28 '15 at 22:07
  • 1
    Seeing as you work at MS on EF now... do you have a more reasonable solution here? This code 'works', but would never fly in a real code base. Does EF6 support this use case with entity references? – Ed S. Apr 01 '19 at 16:09