If an entity is part of a hierarchy then I am unable to remove it by its PK only
See original GitHub issueI am trying to remove all items of a hierarchy by deleting all base items by their PK however however when I try to remove any other entity which has a FK pointing to a derived type then I get an internal casting exception.
My sample entities
public class Actor
{
public int Id { get; set; }
public string Name { get; set; }
}
public class User : Actor
{
public UserDetails Details { get; set; }
}
public class Group : Actor
{
}
public class UserDetails
{
public int ActorId { get; set; }
public User Actor { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
The deletion
var ctx = new MyDbContext();
var toBeDeleted = ctx
.Actors
.AsNoTracking()
.Select(e => new Actor
{
Id = e.Id
})
.ToList();
ctx.Actors.RemoveRange(toBeDeleted);
var toBeDeletedDetails = ctx
.UserDetails
.AsNoTracking()
.Select(e => new UserDetails
{
ActorId = e.ActorId
})
.ToList();
ctx.UserDetails.RemoveRange(toBeDeletedDetails);
ctx.SaveChanges();
The exception
Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'EfTests3.Actor' to type 'EfTests3.User'.
at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrPropertySetter`2.SetClrValue(Object instance, Object value)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.WritePropertyValue(IPropertyBase propertyBase, Object value)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetProperty(IPropertyBase propertyBase, Object value, Boolean setModified)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.set_Item(IPropertyBase propertyBase, Object value)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.SetNavigation(InternalEntityEntry entry, INavigation navigation, Object value)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.InitialFixup(InternalEntityEntry entry, ISet`1 handledForeignKeys, Boolean fromQuery)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.StateChanged(InternalEntityEntry entry, EntityState oldState, Boolean fromQuery)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.StateChanged(InternalEntityEntry entry, EntityState oldState, Boolean fromQuery)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean forceStateWhenUnknownKey)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode node)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph(EntityEntryGraphNode node, Func`2 handleNode)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState entityState, Boolean forceStateWhenUnknownKey)
at Microsoft.EntityFrameworkCore.DbContext.SetEntityState(InternalEntityEntry entry, EntityState entityState)
at Microsoft.EntityFrameworkCore.DbContext.RemoveRange(IEnumerable`1 entities)
at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.RemoveRange(IEnumerable`1 entities)
at EfTests3.Program.Main1(String[] args) in D:\code\tuxedo\Tests\EfBugTest3.cs:line 41
at EfTests3.Program.Main(String[] args) in D:\code\tuxedo\Tests\EfBugTest3.cs:line 10
Workaround 1 is to swap the deletion around then it works however this is just small piece of a big puzzle involving maps, reflection, dynamics and etc so the order cannot be guaranteed for every base class and derived class
var ctx = new MyDbContext();
// entites depending upon a derived class first
var toBeDeletedDetails = ctx
.UserDetails
.AsNoTracking()
.Select(e => new UserDetails
{
ActorId = e.ActorId
})
.ToList();
ctx.UserDetails.RemoveRange(toBeDeletedDetails);
// base entity
var toBeDeleted = ctx
.Actors
.AsNoTracking()
.Select(e => new Actor
{
Id = e.Id
})
.ToList();
ctx.Actors.RemoveRange(toBeDeleted);
Workaround 2 is to cast everything into derived entities first however some scenarios contains lots of derived entities so it is just a workaround if there isn’t a fix
var ctx = new MyDbContext();
var toBeDeleted = ctx
.Users
.AsNoTracking()
.Select(e => new User { Id = e.Id })
.Cast<Actor>()
.Union(ctx
.Groups
.AsNoTracking()
.Select(e => new Group { Id = e.Id })
)
.ToList();
ctx.Actors.RemoveRange(toBeDeleted);
var toBeDeletedDetails = ctx
.UserDetails
.AsNoTracking()
.Select(e => new UserDetails
{
ActorId = e.ActorId
})
.ToList();
ctx.UserDetails.RemoveRange(toBeDeletedDetails);
ctx.SaveChanges();
Full code
namespace EfTests3
{
class Program
{
static int Main(string[] args)
{
//Main1(args);
Main2(args);
//Main3(args);
return 0;
}
// This one breaks
static int Main1(string[] args)
{
var ctx = new MyDbContext();
var toBeDeleted = ctx
.Actors
.AsNoTracking()
.Select(e => new Actor
{
Id = e.Id
})
.ToList();
ctx.Actors.RemoveRange(toBeDeleted);
var toBeDeletedDetails = ctx
.UserDetails
.AsNoTracking()
.Select(e => new UserDetails
{
ActorId = e.ActorId
})
.ToList();
ctx.UserDetails.RemoveRange(toBeDeletedDetails);
ctx.SaveChanges();
return 0;
}
// Swapping the deletion around works
static int Main2(string[] args)
{
var ctx = new MyDbContext();
// entites depending upon a derived class first
var toBeDeletedDetails = ctx
.UserDetails
.AsNoTracking()
.Select(e => new UserDetails
{
ActorId = e.ActorId
})
.ToList();
ctx.UserDetails.RemoveRange(toBeDeletedDetails);
// base entity
var toBeDeleted = ctx
.Actors
.AsNoTracking()
.Select(e => new Actor
{
Id = e.Id
})
.ToList();
ctx.Actors.RemoveRange(toBeDeleted);
ctx.SaveChanges();
return 0;
}
// Forcing casting also works
static int Main3(string[] args)
{
var ctx = new MyDbContext();
var toBeDeleted = ctx
.Users
.AsNoTracking()
.Select(e => new User { Id = e.Id })
.Cast<Actor>()
.Union(ctx
.Groups
.AsNoTracking()
.Select(e => new Group { Id = e.Id })
)
.ToList();
ctx.Actors.RemoveRange(toBeDeleted);
var toBeDeletedDetails = ctx
.UserDetails
.AsNoTracking()
.Select(e => new UserDetails
{
ActorId = e.ActorId
})
.ToList();
ctx.UserDetails.RemoveRange(toBeDeletedDetails);
ctx.SaveChanges();
return 0;
}
}
public class Actor
{
public int Id { get; set; }
public string Name { get; set; }
}
public class User : Actor
{
public UserDetails Details { get; set; }
}
public class Group : Actor
{
}
public class UserDetails
{
public int ActorId { get; set; }
public User Actor { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Actor> Actors { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Group> Groups { get; set; }
public DbSet<UserDetails> UserDetails { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=localhost,1433;Database=ipl_tux;Integrated Security=True;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserDetails>()
.HasKey(e => e.ActorId);
base.OnModelCreating(modelBuilder);
}
}
}
To be sure to be sure, this issue isn’t a duplicate of neither #10179 nor #10180 however they were all found during my attempt to dynamically purge an entire hierarchy of objects.
EF Core version: 2.0
Issue Analytics
- State:
- Created 6 years ago
- Comments:7 (4 by maintainers)
@silarmani In general we don’t want to encourage users to supply the wrong type as there might be more places that won’t be able to handle it either now or in the future. Also we cannot make this work in all scenarios even now. A more robust way of accomplishing this would be with Batch CUD https://github.com/aspnet/EntityFrameworkCore/issues/795
Hi guys, just wondering why was this issue closed?