Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EF Core marks entities removed From child collections as modified #10093

Closed
timmhagen opened this issue Oct 17, 2017 · 19 comments
Closed

EF Core marks entities removed From child collections as modified #10093

timmhagen opened this issue Oct 17, 2017 · 19 comments

Comments

@timmhagen
Copy link

timmhagen commented Oct 17, 2017

If you are using EF core 2 for SQL Server, and you remove an entity from a child collection in a Parent,, EF sets the state of the deleted entity to be Modified, but it does delete the entity from the DB when you call SaveChanges. I am hooking into the SaveChanges event to do the check.

       protected override void MyDbContext_SavingChanges(object sender, EntityEventArgs e)
        {
            if (e.Entity is TEntity)
            {
                var action = e.State != EntityState.Deleted ? 0 : 1;                
            }

            base.ECaseDbContext_SavingChanges(sender, e);
        }

        public override int SaveChanges()
        {
            var entries = ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged).ToList();
            foreach (var entry in entries)
            {
                var args = new EntityEventArgs(entry.Entity, entry.State);
                OnSavingChanges(args);
            }

            return base.SaveChanges();
        }

public partial class Parent
{
           public ICollection<Child> Children{ get; set; }
}

var child = Parent.Children.Find(x => x.Id ==1);
Parent.Children.Remove(child);

dbContext.SaveChanges();

I expect that the state would be Deleted for the Child, but when checking state in my Event hook for SaveChanges, it shows as modified, not deleted.

Further technical details

PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0"
PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0"
PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0"

Operating system: Windows 7
IDE: Visual Studio 2017 CE

@ajcvickers
Copy link
Contributor

@timmhagen You don't show what the Child class looks like, but I expect that the FK on the child is nullable such that when the parent is deleted the child FKs are nulled out, resulting in their state being deleted. (Note that if there is no FK property in your class, then EF creates a "shadow" property which is by default nullable.)

To get a required relationship (non-nullable FK), use the .IsRequired() call in the fluent API when mapping the relationship. For more details see the docs here: https://docs.microsoft.com/en-us/ef/core/modeling/relationships

@timmhagen
Copy link
Author

@ajcvickers The code was generated using DB First, due to existing database. There is a mapping in the database for the Parent/Child relationship. I looked at Child (Dossier) and it didn't have the .IsRequired().
I tried adding the IsRequired() call but it still came through as Modified instead of Deleted.

In this case my Parent is Event and the Child is Dossier.

modelBuilder.Entity(entity =>
{
//Properties/fields removed for easier read
entity.HasOne(d => d.Event)
.WithMany(p => p.Dossier)
.IsRequired()
.HasForeignKey(d => d.EventId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("Event_Dossier_FK1");
});

In the Event Entity Class there is a property public ICollection Dossier { get; set; }

@ajcvickers
Copy link
Contributor

@timmhagen At this point I think we will need a full project or code listing that reproduces what you are seeing. From the snippets you have posted, the entities should change from Modified to Deleted while SaveChanges is running and the deletes should be sent to the database. (Note that the code you have above will not show the entities as Deleted because the cascade won't happen until base.SaveChanges is called, but you should still see delete statements sent to the database.)

@timmhagen
Copy link
Author

@ajcvickers I tried to recreate the issue with the Northwind database and I don't get the same behavior when deleting collections, in my Demo app the state comes back Deleted. I will have to dig more into the differences between on how Order and OrderDetails relate compared to how my Database Entities Event and Dossier relate. I thought maybe it had to do with changing from ClientSetNull to Cascade but I tried the same thing with Northwind and it still show Deleted state.

Thanks for your help, sorry it ended up being something goofy in my configuration.

@timmhagen
Copy link
Author

@ajcvickers Ok. I have narrowed it down to the difference and was able to replicate it in a demo project. The issue is when a Parent has a Child who has a Child of its own. I tried to incorporate my Demo using the Northwind Database so my tables and demo code are pretty rough and crude but demonstrates what I am seeing.

In my Demo I had to change up my relationships from above to kinda make sense in my head and to work with the Northwind database. I have Parent of Orders with many Events who in turns has many Locations. In my Demo I pull an Order, then grab an Event, then Remove that item from the Order's Event Collection. I expected the state to be Deleted for Event Entity but it came back as Modified. I think this has to do with the Cascading Delete of Locations on the Event Entity.

Demo DELETE Endpoint
http://localhost:62383/api/demo/1

Demo.zip

@ajcvickers
Copy link
Contributor

ajcvickers commented Oct 18, 2017

@timmhagen Can you clarify whether the problem you are describing is that the entities don't show up as "Deleted" or that they don't actually get deleted?

What I am seeing with the repro code is this:

  • The relationship is configured with ClientSetNull, which basically means don't do cascade deletes.
  • This means that when the Event is removed from the Order's Events list, it will be marked as Modified because the FK has been marked as null.
  • When SaveChanges happens, the FK marked as null cannot actually be set to null in the database, so instead EF throws an exception
  • If I remove the ClientSetNull, then the relationship gets Cascade by default, since it is a required relationship
  • This means that when SaveChanges runs it sees the Event entity that has it's flag marked as null and rather than throwing it marks it as Deleted and then attempts to delete it from the database.

This is all seems to be working as expected in the repro code. If the issue is that you are just not seeing entities marked as Deleted, this is because cascade deletes happens while SaveChanges is running which means in your code will not see this state as it looks at the state before SaveChanges has executed. I have been intending for a while to file an enhancement issue to make it possible to configure this--I'll do that now.

@ajcvickers
Copy link
Contributor

See #10114

@timmhagen
Copy link
Author

@ajcvickers Sorry I forgot to check what EF DB First Scaffolded out in the Demo for the dbContext for that Entity and made the assumption I changed it to Cascade. In my apps code which I was trying to pull a demo out of it does a Cascade Delete.

To answer your question. The entities do get deleted in the database but when checking the change tracking, prior to the Base.SaveChanges, for cases where they are deleted I was only getting modified where I expected to see Deleted for the Entity I removed and any cascading ones.

From what I can gather in #10114 I believe that is related.

@timmhagen
Copy link
Author

timmhagen commented Oct 18, 2017

@ajcvickers In the case where ClientSetNull you made mention of still Event being set to Modified, I would have expected that Event would still be Deleted because the Entity was removed and that Orders would be marked Modified (if anything). If the entity being removed is marked Modified how would I know if it was deleted vs just edited?

@ajcvickers
Copy link
Contributor

@timmhagen There isn't any easy way to do this right now--that's why I filed #10114. You could take a look at the code in StateManager.GetInternalEntriesToSave() and InternalEntityEntry.CascadeDelete and replicate this logic in your code.

@joshjreed
Copy link

We're hitting this exact issue. Has there been any updates on the possible fix? Not being able to reliably have the State of entities being processed by the database context is becoming a very large hurdle for us, and writing a custom solution seems silly when this behavior appears to be a bug in the implementation.

@ajcvickers
Copy link
Contributor

@joshjreed The only actionable item that came out of this issue is tracked by #10114, which is not currently scheduled for the 2.1 release. If you are seeing something that is not covered by that issue, then please file a new issue with a runnable project/solution or complete code listing that demonstrates the behavior you are seeing.

@joshjreed
Copy link

Thanks @ajcvickers. The issue we're seeing is that entities show with a "Modified" state but no changes, when in reality they are being deleted. Because we can't trust the state being shown, we can't correctly deal with those entities. Will we be able to see a correct state on these entities as a result of #10114? If so, that will cover our needs.

@ajcvickers
Copy link
Contributor

@joshjreed Yes, that issue will allow cascading deletes to happen before SaveChanges, so you will be able to see the final states at that time.

@jruizx
Copy link

jruizx commented Mar 22, 2018

Hi guys, I had the same issue. I have fixed with a couple of extension methods based on @ajcvickers recommendations.

        public static void ApplyCascadeDeletes<T>(this IEnumerable<EntityEntry<T>> entities) where T : class
        {
            foreach (var entry in entities.Where(
                e => (e.State == EntityState.Modified
                      || e.State == EntityState.Added)
                     && e.GetInternalEntityEntry().HasConceptualNull).ToList())
            {
                entry.GetInternalEntityEntry().HandleConceptualNulls();
            }

            foreach (var entry in entities.Where(e => e.State == EntityState.Deleted).ToList())
            {
                entry.GetInternalEntityEntry().CascadeDelete();
            }
        }
        public static InternalEntityEntry GetInternalEntityEntry<T>(this EntityEntry<T> entityEntry)  where T : class
        {
            var internalEntry = (InternalEntityEntry)entityEntry
                .GetType()
                .GetProperty("InternalEntry", BindingFlags.NonPublic  | BindingFlags.Instance)
                .GetValue(entityEntry);

            return internalEntry;
        }

@sellomkantjwa
Copy link

Seems like InternalEntry. CascadeDelete() has been removed from Ef core 2.1. So @jruizx 's workaround would not work as is.

@jruizx
Copy link

jruizx commented Oct 10, 2018

Hi @sellomkantjwa, on my website I have added a fix for EF Core 2.1 https://comment-it.co.uk/entity-framework-core-soft-delete-workaround/

@johnkwaters
Copy link

johnkwaters commented Jan 17, 2019

Hi, I don't know if this is the same issue, but seems very similar. I have a parent with a collection of child elements. I remove a child, and in SaveChanges, I iterate over changes. My code handles soft deletes by finding any entities with state Deleted and instead setting them to Modified with IsDeleted=true.
However, today I saw a call where an actual delete happened instead. Puzzled. I debugged it, and saw EF had the state of the removed child as Modified rather than Deleted.
Is this a bug or regression? I am pretty sure the child used to be Deleted when removed from its parent...
My child entity has children of its own.

BTW your fix, the 2.1 version above, @jruizx , fixes it... but is this an EF Core bug that will be fixed?

@deivydas321
Copy link

3 years have passed but the issue still exists in EF Core 3.1. The navigation property I want to delete is marked as Modified not Deleted. I am deleting by removing navigation entity from principal entity hashset collection. Navigation property also contains child properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants