The Confusing Behaviour Of EF Core OnDelete Restrict

I was recently helping another developer understand the various “OnDelete” behaviors of Entity Framework Core. That is, when a parent entity in a parent/child relationship is deleted, what should happen to the child?

I thought this was actually all fairly straight forward. The way I understood things was :

DeleteBehavior.Cascade – Delete the child when the parent is deleted (e.g. Cascading deletes)
DeleteBehavior.SetNull – Set the FK on the child to just be null (So allow orphans)
DeleteBehavior.Restrict – Don’t allow the parent to be deleted at all

I’m pretty sure if I asked 100 .NET developers what these meant, there is a fairly high chance that all of them would answer the same way. But in reality, DeleteBehavior.Restrict is actually dependant on what you’ve done in that DBContext up until the delete… Let me explain.

Setting Up

Let’s imagine that I have two models in my database, they look like so :

class BlogPost
{
public int Id { get; set; }
public string PostName { get; set; }
public ICollection<BlogImage> BlogImages { get; set; }
}

class BlogImage
{
public int Id { get; set; }
public int? BlogPostId { get; set; }
public BlogPost? BlogPost { get; set; }
public string ImageUrl { get; set; }
}

Then imagine the relationship in EF Core is set up like so :

modelBuilder.Entity<BlogImage>()
.HasOne(x => x.BlogPost)
.WithMany(x => x.BlogImages)
.OnDelete(DeleteBehavior.Restrict);

Any developer looking at this at first glance would say, if I delete a blog post that has images pointing to it, it should stop me from deleting the blog post itself. But is that true?

Testing It Out

Let’s imagine I have a simple set of code that looks like do :

var context = new MyContext();
context.Database.Migrate();

var blogPost = new BlogPost
{
PostName = “Post 1”,
BlogImages = new List<BlogImage>
{
new BlogImage
{
ImageUrl = “/foo.png”
}
}
};

context.Add(blogPost);
context.SaveChanges();

Console.WriteLine(“Blog Post Added”);

var getBlogPost = context.Find<BlogPost>(blogPost.Id);
context.Remove(getBlogPost);
context.SaveChanges(); //Does this error here? We are deleting the blog post that has images

Console.WriteLine(“Blog Post Removed”);

Do I receive an exception? The answer is.. No. When this code is run, and I check the database I end up with a BlogImage that looks like so :

So instead of restricting the delete, EF Core has gone ahead and set the BlogPostId to be null, and essentially given me an orphaned record. But why?!

Diving headfirst into the documentation we can see that DeleteBehavior.Restrict has the following description :

For entities being tracked by the DbContext, the values of foreign key properties in dependent entities are set to null when the related principal is deleted. This helps keep the graph of entities in a consistent state while they are being tracked, such that a fully consistent graph can then be written to the database. If a property cannot be set to null because it is not a nullable type, then an exception will be thrown when SaveChanges() is called.

Emphasis mine.

This doesn’t really make that much sense IMO. But I wanted to test it out further. So I used the following test script, which is exactly the same as before, except half way through I recreate the DB Context. Given the documentation, the entity I pull back for deletion will not have the blog images themselves being tracked.

And sure enough given this code :

var context = new MyContext();
context.Database.Migrate();

var blogPost = new BlogPost
{
PostName = “Post 1”,
BlogImages = new List<BlogImage>
{
new BlogImage
{
ImageUrl = “/foo.png”
}
}
};

context.Add(blogPost);
context.SaveChanges();

Console.WriteLine(“Blog Post Added”);

context = new MyContext(); // <– Create a NEW DB context

var getBlogPost = context.Find<BlogPost>(blogPost.Id);
context.Remove(getBlogPost);
context.SaveChanges();

Console.WriteLine(“Blog Post Removed”);

I *do* get the exception I was expecting all along :

SqlException: The DELETE statement conflicted with the REFERENCE constraint “FK_BlogImages_BlogPosts_BlogPostId”.

Still writing this, I’m struggling to understand the logic here. If by some chance you’ve already loaded the child entity (By accident or not), your delete restriction suddenly behaves completely differently. That doesn’t make sense to me.

I’m sure some of you are ready to jump through your screens and tell me that this sort of ambiguity is because I am using a nullable FK on my BlogImage type. Which is true, and does mean that I expect that a BlogImage entity *can* be an orphan. If I set this to be a non-nullable key, then I will always get an exception because it cannot set the FK to null. However, the point I’m trying to make is that if I have a nullable key, but I set the delete behavior to restrict, I should still see some sort of consistent behavior.

What About DeleteBehavior.SetNull?

Another interesting thing to note is that the documentation for DeleteBehavior.SetNull is actually identical to that of Restrict :

For entities being tracked by the DbContext, the values of foreign key properties in dependent entities are set to null when the related principal is deleted. This helps keep the graph of entities in a consistent state while they are being tracked, such that a fully consistent graph can then be written to the database. If a property cannot be set to null because it is not a nullable type, then an exception will be thrown when SaveChanges() is called.

And yet, in my testing, using SetNull does not depend on which entities are being tracked by the DbContext, and works the same every time (Although, I did consider that possibly this is a SQL Server function using the default value rather than EF Core doing the leg work).

I actually spent a long time using Google-Fu to try and find anyone talking about the differences between SetNull and Restrict but, many just go along with what I described in the intro. SetNull sets null when it came, and restrict always stops you from deleting.

Conclusion

Maybe I’m in the minority here, or maybe there is a really good reason for the restrict behavior acting as it does, but I really do think that for the majority of developers, when they use DeleteBehavior.Restrict, they are expecting the parent to be blocked from being deleted in any and all circumstances. I don’t think anyone expects an accidental load of an entity into the DbContext to suddenly change the behavior. Am I alone in that?

The post The Confusing Behaviour Of EF Core OnDelete Restrict appeared first on .NET Core Tutorials.

Leave a Reply

Your email address will not be published.