Deleting in EF Code first causes navigational prop

2019-02-23 18:40发布

I noticed something interesting when I was performing a delete using EF code first. I use the following domain model:

public class User
{
    public virtual long Id { get; set; }
    public virtual string Name { get; set; }
    public virtual ICollection<Playlist> Playlists { get; set; }
}

public class Playlist
{
    public virtual long Id { get; set; }
    public virtual string Title { get; set; }
    public virtual User User { get; set; }
    public virtual ICollection<Track> Tracks { get; set; }
}

public class Track
{
    public virtual long Id { get; set; }
    public virtual string Title { get; set; }
    public virtual Playlist Playlist { get; set; }
}

The model is configured using:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>().HasMany(x => x.Playlists).WithRequired(x => x.User).Map(x => x.MapKey("UserId"));
    modelBuilder.Entity<Playlist>().HasMany(x => x.Tracks).WithRequired(x => x.Playlist).Map(x => x.MapKey("PlaylistId"));
}

I use a generic repository:

public virtual void Delete(T entity)
{
    Database.Set<T>().Remove(entity);
}

I also have a dto that looks like:

public class PlaylistDTO
{
    public PlaylistDTO(Playlist playlist)
    {
        Id = playlist.Id;
        Title = playlist.Title;
        User = playlist.User.Name;
    }
}

In one of my services I am trying to do the following:

public PlaylistDTO Delete(long id)
{
    Playlist playlist = playlistRepository.GetById(id);
    playlistRepository.Delete(playlist);
    unitOfWork.Commit();

    return PlaylistDTO(playlist);
}

This code fails. When I stepped through the debugger I noticed something interesting. The moment I call playlistRepository.Delete the navigational properties (User and Tracks) get set to null and empty respectively. Playlist however stays in memory. So when I pass in the playlist to the DTO the code will fail when it is trying to access playlist.User.Name. I wanted to pass this data to the client to display a verification.

Is this behavior correct? Is this by design?

1条回答
放荡不羁爱自由
2楼-- · 2019-02-23 19:40

This is how EF works. The problem is that your Playlist forms entity graph with other relations and EF uses very simple rule for tracking entity graphs: All entities in the graph must be tracked - there cannot be reference to entity which is not tracked. I don't give you reference to description of this rule, it is just my observation but I didn't find any single exception to this rule.

Edit: Updated version - I just checked internal implementation and relations are indeed nulled during calling Delete

So what happened in your code.

  • You marked your Playlist as deleted
  • EF passes delete operation to the state manager which does the fixup - it will null all relations
  • You saved changes to the database
  • Because there are no cascade deletes from Playlist all related objects remain undeleted
  • Once you saved changes EF internally accepted them and set change tracker to current state
  • Because the current state of Playlist is non existing (deleted in the database) it was detached from the context
  • Detaching has broken entity graph and EF fixed it by modifying navigation properties on both ends

The code responsible for nulling from System.Data.Objects.EntityEntry.Delete(doFixup) (doFixup is true) - the class is internal:

if (doFixup && (base.State != EntityState.Deleted))
{
    this.RelationshipManager.NullAllFKsInDependentsForWhichThisIsThePrincipal();
    this.NullAllForeignKeys();
    this.FixupRelationships();
}

In your scenario this should have simple workaround - create DTO before you delete entity.

查看更多
登录 后发表回答