I was writing my Entity Framework 5 Code First data models one day when I received a dramatic sounding error: "Introducing FOREIGN KEY constraint 'FK_dbo.Days_dbo.Weeks_WeekID' on table 'Days' may cause cycles or multiple cascade paths." I was instructed to "Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints." And then simply the message, "Could not create constraint." So what happened with my Foreign Key that makes it cyclic?"
First a spot of code that can nicely demonstrate this scenario:
public class Year
{
public int ID { get; set; }
public string Name { get; set; }
public ICollection<Month> Months { get; set; }
}
public class Month
{
public int ID { get; set; }
public string Name { get; set; }
public Year Year { get; set; }
public ICollection<Day> Days { get; set; }
}
public class Day
{
public int ID { get; set; }
public string Name { get; set; }
public Month Month { get; set; }
public Week Week { get; set; }
}
//problem time
public class Week
{
public int ID { get; set; }
public string Name { get; set; }
public Year Year { get; set; }
public ICollection<Day> Days { get; set; }
}
So, let's explain this. A Year has Months and a Month has Days - all is well at this point and it will build and generate all your tables happily. The problem comes when you add the highlighted parts to add Weeks into the data model.
By making those Navigation Properties you have implicitly instructed EF Code First to create Foreign Key constraints for you and each of these will have cascading deletes on them. This means, upon deleting a Year, the database will cascade that delete to that Year's Months and in turn those Month's Days. This is to, quite rightly, hold referential integrity; you will not have Yearless Months.
So if you think about it, now that we have added a Week entity that also has a relationship with Days this too will cascade deletes. So as well as the cascading deletes that occur when you delete a Year as I described above, now deleting a Year will also delete its Weeks and those Weeks will delete the Days. But those Days would have already been deleted by the cascade that went via Months. So who wins? Who gets there first? Don't know... that's why you need to design an answer to this conundrum.
Removing the multiple cascade paths
The way I found to solve this issue is you need to remove one of the cascades, since there is nothing wrong with the Foreign Keys, it is only the multiple cascade paths. So lets remove the cascading delete on the Week -> Days relationship since that delete will be taken care of by each Month cascading its deletes to their Days.
public class CalenderContext : DbContext
{
public DbSet<Year> Years{ get; set; }
public DbSet<Month> Months { get; set; }
public DbSet<Day> Days { get; set; }
public DbSet<Week> Weeks { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Day>()
.HasRequired(d => d.Week)
.WithMany(w => w.Days)
.WillCascadeOnDelete(false);
base.OnModelCreating(modelBuilder);
}
}
If you already have a ForeignKey field explicitly written into your code model (as I usually do - I removed them from the above example for clarity) you will need to specify this existing Foreign Key in the code just written. So if your Day entity looked like this:
public class Day
{
public int ID { get; set; }
public string Name { get; set; }
[ForeignKey("Month")]
public int MonthID { get; set; }
public Month Month { get; set; }
[ForeignKey("Week")]
public int WeekID { get; set; }
public Week Week { get; set; }
}
You will need your OnModelCreating method to look like this:
modelBuilder.Entity<Day>()
.HasRequired(d => d.Week)
.WithMany(w => w.Days)
.HasForeignKey(d => d.WeekID)
.WillCascadeOnDelete(false);
Have fun deleting safely!