efcore: Setting a Nullable Foreign Key property to Null triggers cascade delete
Using Entity Framework Core 6.0.1
The following code should create a Module model with the HelpCard Foreign Key set, then set the Help Card Foreign Key to Null and save the model to the database, but instead the Cascade Delete is being triggered and the record is deleted from the database. This worked fine in Entity Framework Core 5;
Removing .OnDelete(DeleteBehavior.Cascade) from either of the Configurations does not affect this behavior; removing it from both of the Configurations makes it behave as expected (but I need the Cascade).
Edit: Nullable Reference Types are set to Disable for the project, according to documentation, this should maintain the existing behavior.
// create a model with the ForeignKey set
var m = new Models.Module() {
Name = "Test",
HelpCardId = DatabaseContext.HelpCard.Select(r => r.Id).First()
};
DatabaseContext.Add(m);
DatabaseContext.SaveChanges(true);
// set the ForeignKey to null
m.HelpCardId = null;
// the model state is now marked as Deleted
var state = DatabaseContext.Entry(m).State;
// after saving, the record is deleted from the database instead of being saved with the ForeignKey set to Null
DatabaseContext.SaveChanges(true);
Models
public partial class Module : Entity, IEntity<Guid>
{
private HelpCard _HelpCard;
public Module()
{
}
private Module(ILazyLoader lazyLoader) : this()
{
LazyLoader = lazyLoader;
}
private ILazyLoader LazyLoader { get; set; }
[Key]
[Required()]
public virtual Guid Id { get; set; }
[StringLength(100)]
[Required()]
public virtual string Name { get; set; }
public virtual Guid? HelpCardId { get; set; }
public virtual HelpCard HelpCard
{
get
{
return LazyLoader?.Load(this, ref _HelpCard);
}
set
{
this._HelpCard = value;
}
}
}
public partial class HelpCard : Entity, IEntity<Guid>, ICloneable
{
private IList<Module> _Modules;
public HelpCard()
{
this._Modules = new List<Module>();
}
private HelpCard(ILazyLoader lazyLoader) : this()
{
LazyLoader = lazyLoader;
}
private ILazyLoader LazyLoader { get; set; }
[Key]
[Required()]
public virtual Guid Id { get; set; }
[StringLength(100)]
[Required()]
public virtual string Name { get; set; }
public virtual IList<Module> Modules
{
get
{
return LazyLoader?.Load(this, ref _Modules);
}
set
{
this._Modules = value;
}
}
}
Configuration
public partial class ModuleConfiguration : IEntityTypeConfiguration<Module>
{
public void Configure(EntityTypeBuilder<Module> builder)
{
builder.ToTable(@"Module", @"dbo");
builder.Property(x => x.Id).HasColumnName(@"Id").HasColumnType(@"uniqueidentifier").IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql(@"newid()");
builder.Property(x => x.Name).HasColumnName(@"Name").HasColumnType(@"varchar(100)").IsRequired().ValueGeneratedNever().HasMaxLength(100);
builder.Property(x => x.HelpCardId).HasColumnName(@"HelpCardId").HasColumnType(@"uniqueidentifier").ValueGeneratedNever();
builder.HasKey(@"Id");
builder.HasOne(x => x.HelpCard).WithMany(op => op.Modules).OnDelete(DeleteBehavior.Cascade).HasForeignKey(@"HelpCardId").IsRequired(false);
CustomizeConfiguration(builder);
}
}
public partial class HelpCardConfiguration : IEntityTypeConfiguration<HelpCard>
{
public void Configure(EntityTypeBuilder<HelpCard> builder)
{
builder.ToTable(@"HelpCard", @"dbo");
builder.Property(x => x.Id).HasColumnName(@"Id").HasColumnType(@"uniqueidentifier").IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql(@"newid()");
builder.Property(x => x.Name).HasColumnName(@"Name").HasColumnType(@"varchar(100)").IsRequired().ValueGeneratedNever().HasMaxLength(100);
builder.HasKey(@"Id");
builder.HasMany(x => x.Modules).WithOne(op => op.HelpCard).OnDelete(DeleteBehavior.Cascade).HasForeignKey(@"HelpCardId").IsRequired(false);
CustomizeConfiguration(builder);
}
}
EF Core version: 6.00 & 6.01 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 6.0 Operating system: Windows 10 IDE: Visual Studio 2022 17.0.2
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 20 (8 by maintainers)
Commits related to this issue
- [release/6.0] Revert change to delete dependent when optional FK with cascade delete is set to null Fixes #27174. — committed to dotnet/efcore by ajcvickers 2 years ago
@failwyn @m-azyoksul @powermetal63 I’ve been pondering on this over the weekend, and I think you folks are right. Cascade delete is typically used with required relationships, where a dependent cannot exist in the database without a principal. In this case, delete orphans behavior makes sense. However, if cascade delete is used with optional relationships, then the dependent can still exist without the principal, which means it probably doesn’t make sense to delete orphans automatically.
In addition, setting orphan timing to Never definitely should not throw when attempting to save a null FK for optional relationships.
I will investigate and discuss with the team what we can do here. We can likely revert this particular change in a patch (although that will require Director-level approval, so I can’t promise), but we also need to be conscious of making breaking changes here if other behaviors around orphan deletion are changed.
@ajcvickers Why cascade delete / delete orphans options affect the operation of the dependent entity? This is counterintuitive to everything we know about cascade delete - defines what to do with dependents when deleting the principal. Here we are not deleting the principal. We are updating the dependent. No relational database deletes the record when you update the nullable cascade delete enforced FK to NULL.
Also does not match the behavior explained in Optional relationships
This is exactly what one would expect for optional dependent, regardless of the cascade delete option.
So not sure what was fixed in 6.0, but the current behavior definitely sounds like bug to me.
And the worse is that there is no workaround - the orphan timing option you are suggesting is irrelevant, as we are not trying to reparent the entity, but simply disassociate it from the parent (after all, it is possible to be created without parent and should be able to be updated to have no parent).
I agree with everything @powermetal63 said. This does seem like a regression.
I am using the following workaround.
and in the
OnModelCreatingThis correctly sets up an optional FK relationship with cascade delete enabled.
However, when I set
myUser.Department = null, the user state becomesDeleted. What I do is just to set the state of the entity back toModifiedmanually.At the end of the day, I feel like this behavior is confusing. When an optional foreign key with cascade delete enabled is set to null, the dependant entity state is set to
Deleted, when I think it should be set toModified.@failwyn
Why so?
I filed #27218.
@failwyn
I will investigate this.