ChrisBint ChrisBint - 1 year ago 79
C# Question

Adding Cascade Delete to EntityFramework causes Invalid Cast exception to unchanged code

I have the following classes defined with my Entity Framework Code First;

public class Parent: Entity<int>
{
public Guid Reference { get; set; } = Guid.NewGuid();
public int UserId { get; set; }
public virtual ICollection<Alert> Child1 { get; set; } = new HashSet<Child1>();
public virtual ICollection<History> Child2 { get; set; } = new HashSet<Child2>();
}

public class Child1: Entity<int>
{
public int ParentId { get; set; }
public Parent Parent { get; set; }
public DateTime Date { get; set; }
}

public class Child2 : Entity<int>
{
public int ParentId{ get; set; }
public Parent Parent { get; set; }
public string Title { get; set; }
}


These are referenced in my
DataContext
as follows;

public virtual DbSet<Parent> Parents{ get; set; }
public virtual DbSet<Child1> Child1 { get; set; }
public virtual DbSet<Child2> Child2 { get; set; }


I have disabled Cascade Delete for everything explicitly, by overriding OnModelCreating and adding;

modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();


At this point, all works well. I publish my database and test using the following;

var parent = new Parent {UserId = 1};

parent.Child1.Add(new Child1 { Date = DateTime.Now});
parent.Child1.Add(new Child1 { Date = DateTime.Now});
parent.Child1.Add(new Child1 { Date = DateTime.Now});

parent.Child2.Add(new Child2 { Title = "Title" });
parent.Child2.Add(new Child2 { Title = "Title" });
parent.Child2.Add(new Child2 { Title = "Title" });
parent.Child2.Add(new Child2 { Title = "Title" });

context.Monitors.Add(monitor);
context.SaveChanges();


Looking into the database, I can see the records and all correct.

If I then add the following to the same code;

context.Monitors.Remove(monitor);
context.SaveChanges();


I get an error, as I would expect;

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

At this point I looked to enabled Cascade Delete for these specific classes, leaving all the others in my project as explicit delete. I added the following to my datacontext;

modelBuilder.Entity<Parent>()
.HasOptional(p => p.Child1)
.WithMany()
.WillCascadeOnDelete(true);

modelBuilder.Entity<Parent>()
.HasOptional(p => p.Child2)
.WithMany()
.WillCascadeOnDelete(true);


I reset my migrations, added an initial migration, deleted and published the database entirely at this point.

I can see the following added to my Initial MIgration;

.ForeignKey("Parent.Child1", t => t.Child1_Id, cascadeDelete: true)
.ForeignKey("Parent.Child2", t => t.Child2_Id, cascadeDelete: true)


Now, when I run the same code as above to add records, I now get the following error;


System.InvalidCastException occurred HResult=0x80004002

Message=Unable to cast object of type
'System.Collections.Generic.HashSet
1[Child1]' to type 'Child1'.

Source=EntityFramework StackTrace: at
System.Data.Entity.Core.Objects.DataClasses.EntityReference
1.AddToLocalCache(IEntityWrapper
wrappedEntity, Boolean applyConstraints) at
System.Data.Entity.Core.Objects.EntityEntry.TakeSnapshotOfSingleRelationship(RelatedEnd
relatedEnd, NavigationProperty n, Object o) at
System.Data.Entity.Core.Objects.EntityEntry.TakeSnapshotOfRelationships()
at
System.Data.Entity.Core.Objects.Internal.EntityWrapperWithoutRelationships
1.TakeSnapshotOfRelationships(EntityEntry
entry) at
System.Data.Entity.Core.Objects.ObjectContext.AddSingleObject(EntitySet
entitySet, IEntityWrapper wrappedEntity, String argumentName) at
System.Data.Entity.Core.Objects.ObjectContext.AddObject(String
entitySetName, Object entity) at
System.Data.Entity.Internal.Linq.InternalSet
1.<>c__DisplayClassd.b__c()
at System.Data.Entity.Internal.Linq.InternalSet
1.ActOnSet(Action
action, EntityState newState, Object entity, String methodName) at
System.Data.Entity.Internal.Linq.InternalSet
1.Add(Object entity)

at System.Data.Entity.DbSet`1.Add(TEntity entity) at
Kinexus.Tools.TestApplication.Program.Main(String[] args) in
D:\Personal\Kinexus_TFS\Toolset\Kinexus.Tools\Kinexus.Tools.TestApplication\Program.cs:line
180


The error occurs at this line of code, which has not changed;

context.Monitors.Add(monitor);


I get what the error is saying, but I am struggling to understand why and what to do to resolve.

Answer Source

The fluent configuration does not match your model, which is causing EF to assume additional relationships, hence to create additional FK columns.

In order to produce the intended relationship mappings with fluent API, it's crucial to use the correct overloads which describe the presence/absence of the navigation and/or FK property. In your case, the parameterless WithMany() calls are telling EF that the relationship is unidirectional (i.e. without collection navigation property), hence when EF discovers the unmapped collection navigation properties, it creates another unidirectional relationship, this time without reference navigation property and with default FK column name EntityName_Id.

Also looking at your mappings, you've totally reversed the reference and collection property configuration. Has/WithOptional should specify the reference while Has/WithMany - the collection. And since the FK is not nullable, you should use Has/WithRequired instead of Has/WithOptional.

With that being said, to fix the issue simply apply the correct mappings:

modelBuilder.Entity<Parent>()
    .HasMany(p => p.Child1)
    .WithRequired(c => c.Parent)
    .HasForeignKey(c => c.ParentId)
    .WillCascadeOnDelete(true);

modelBuilder.Entity<Parent>()
    .HasMany(p => p.Child2)
    .WithRequired(c => c.Parent)
    .HasForeignKey(c => c.ParentId)
    .WillCascadeOnDelete(true);
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download