Migration difference on Delete: No Foreign Key Property generating a Shadow Property vs Fully Defined Relationships
See original GitHub issueIs the following a bug or per design? In case of a design constraint, it will be great to have an explanation and perhaps update the documentation on why there is a difference between FK set as Shadow Properties instead of declared in a CLR class when it comes to deletion.
Here are the articles that I followed No Foreign Key Property vs Fully Defined Relationships Shadow Properties Cascade Delete video uploaded on Aug 2015
The last link is a 1 year old video and I wonder if the scope of shadow properties has changed since then. In particular, at 6.30 min Seth asks the question if adding a shadow property and doing migrations would change anything and Rowan answers that it does nothing to the migrations pipeline and that it behaves exactly the same, as if this was defined on a CLR class. However, the code generated by the migration in .Net Core 1.1 is different when it comes to delete operations. Hence my question on whether this is per design or a bug.
The issue
Without Shadow Property using EF conventions (Fully Defined Relationships)
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
add-migration init
This gives as a constraint in xxx.init.cs
onDelete: ReferentialAction.Cascade;
I can then query, remove and save like this
var blog = db.Blogs.Find(id);
db.Remove(blog);
var count = db.SaveChanges();
or like that
var blog = db.Blogs.Find(id);
var blog = db.Blogs.Include(b => b.Posts)
.FirstOrDefault(b => b.BlogId == id);
db.Remove(blog);
var count = db.SaveChanges();
With Shadow Property (No Foreign Key Property)
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
add-migration init
This gives a different constraint in xxx.init.cs
onDelete: ReferentialAction.Restrict;
This will not be an issue if I query with an Include, remove a blog and save, but will throw an error if I do not use .Include
, with an error
The DELETE statement conflicted with the REFERENCE constraint “FK_Posts_Blogs_BlogId”.
Switching
onDelete: ReferentialAction.Restrict
to
onDelete: ReferentialAction.Cascade
will make it work again. Alternatively, I would have had to delete all posts before deleting the blog.
Even though I can see some sort of coherence in the essence of a shadow property, I still do not understand why the delete constraint has been set to onDelete: ReferentialAction.Restrict.
Further technical details
EF Core version: .Net Core 1.1 Operating system: W10 Visual Studio version: VS 2017 RC
Issue Analytics
- State:
- Created 7 years ago
- Comments:18 (9 by maintainers)
@Ponant http://stackoverflow.com/questions/851625/foreign-key-constraint-may-cause-cycles-or-multiple-cascade-paths
@Ponant I think maybe the confusion comes from understanding which are EF behaviors and which are database behaviors. For optional relationships, EF will always attempt to set null for any FK property in a tracked entity regardless of the cascading action. This is considered a normal part of fixup–previous versions of EF also worked this way and its not something we wanted to change. For required relationships, EF will only delete tracked dependents if cascade delete is configured. For both these cases EF does not suffer from the cycle issues that SQL Server does. So if all entities are loaded, then EF will have the correct behavior regardless of what the database is capable of.
When using Migrations to create a database, EF attempts to set the database to mimic EF behavior. This can allow an optimization in application code where dependent entities are not loaded but the effect is the same as if they were. Unfortunately, this doesn’t work well with SQL Server because of the cycles issue. Because of this we decided to not set null by default because it makes pretty much every model built for SQL Server fail and it is a less common behavior to want in the database. For deletes, we attempt to set it in the database, but if you have a cycle, then you must make an explicit decision to either forgo the database optimization or stop doing cascade deletes altogether.