Problem Saving with Entity Framework (Need conceptual help)

1.5k Views Asked by At

Problem Summary: I have a Master and Detail entities. When I initialize a Master (myMaster), it creates an instance of Details (myMaster.Detail) and both appear to persist in the database when myMaster is added. However, when I reload the context and access myMasterReloaded.detail its properties are not initialized. However, if I pull the detail from the context directly, then this magically seems to initialize myMasterReloaded.detail. I've distilled this down with a minimal unit test example below. Is this a "feature" or am I missing some important conceptual detail?

//DECLARE CLASSES
public class Master
{
    [Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
    public Guid MasterId { get; set; }
    public Detail Detail { get; set; }
    public Master() { Detail = new Detail(); }
}

public class Detail
{
    [Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
    public Guid DetailId { get; set; }
    public Master MyMaster{ get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Master> Masters { get; set; }
    public DbSet<Detail> Details { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Master>()
            .HasOptional(x => x.Detail)
            .WithOptionalPrincipal(x => x.MyMaster)
            .WillCascadeOnDelete(true);
    }
}

//PERFORM UNIT TEST
[TestMethod]
public void UnitTestMethod()
{
    //Start with fresh DB
    var context = new MyDbContext();
    context.Database.Delete();
    context.Database.CreateIfNotExists();

    //Create and save entities
    var master = context.Masters.Create();            
    context.Masters.Add(master);
    context.SaveChanges();

    //Reload entity
    var contextReloaded = new MyDbContext();
    var masterReloaded = contextReloaded.Masters.First();

    //This should NOT Pass but it does..
    Assert.AreNotEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);

    //Let's say 'hi' to the instance of details in the db without using it.
    contextReloaded.Details.First();

    //By simply referencing the instance above, THIS now passes, contracting the first Assert....WTF??
    Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
}

(This is the sticking point for a more sophisticated entity set. I've simply distilled this down to its simplest case I can't simply replace details with a complex type).

Cheers, Rob

2

There are 2 best solutions below

0
On BEST ANSWER

Matt Hamilton was right (See above). The problem was:

  1. The Detail property should not be instantiated within the constructor, nor through the getters/setters via a backing member. If it's convenient to instantiate a Entity containing Properties in your new instance, then it may be helpful to have a separate initialize method which will not be automatically executed by the Entity Framework as it reconstructs objects from the database.
  2. The Detail property needs to be declared virtual in the Master class for this to work properly.

The following WILL PASS (As expected/hope)

  public class Master
{
    [Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
    public Guid MasterId { get; set; }

    //Key new step: Detail MUST be declared VIRTUAL
    public virtual Detail Detail { get; set; }
}

public class Detail
{
    [Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
    public Guid DetailId { get; set; }
    //Set this to be VIRTUAL as well
    public virtual Master MyMaster { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Master> Masters { get; set; }
    public DbSet<Detail> Details { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //This sets up a BI-DIRECTIONAL relationship
        modelBuilder.Entity<Master>()
            .HasOptional(x => x.Detail)
            .WithOptionalPrincipal(x => x.MyMaster)
            .WillCascadeOnDelete(true);
    }
}


[TestMethod]
public void UnitTestMethod()
{

   var context = new MyDbContext();
   context.Database.Delete();
   context.Database.CreateIfNotExists();

   //Create and save entities
   var master = context.Masters.Create();

   //Key new step: Detail must be instantiated and set OUTSIDE of the constructor
   master.Detail = new Detail();
   context.Masters.Add(master);
   context.SaveChanges();

   //Reload entity
   var contextReloaded = new MyDbContext();
   var masterReloaded = contextReloaded.Masters.First();

   //This NOW Passes, as it should
   Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);

   //This line is NO LONGER necessary
   contextReloaded.Details.First();

   //This shows that there is no change from accessing the Detail from the context
   Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
   }

Finally, it is also not necessary to have a bidirectional relationship. The reference to "MyMaster" can be safely removed from the Detail class and the following mapping can be used instead:

modelBuilder.Entity<Master>()
            .HasOptional(x => x.Detail)
            .WithMany()
            .IsIndependent();

With the above, performing context.Details.Remove(master.Detail), resulted in master.Detail == null being true (as you would expect/hope).

I think some of the confusion emerged from the X-to-many mapping where you can initialize a virtual list of entities in the constructor (For instance, calling myDetails = new List(); ), because you are not instantiating the entities themselves.

Incidentally, in case anyone is having some difficulties with a one-to-many unidirectional map from Master to a LIST of Details, the following worked for me:

 modelBuilder.Entity<Master>()
             .HasMany(x => x.Details)
             .WithMany()
             .Map((x) =>
                      {
                           x.MapLeftKey(m => m.MasterId, "MasterId");
                           x.MapRightKey(d => d.DetailId, "DetailId");
                       });

Cheers, Rob

5
On

I think it's because when you first reload the Master, you have not eager-loaded the Detail, so the Detail entity will not be in the Entity Framework "graph" (internal memory). The only thing in the graph will be a single Master entity.

But when you query the Detail ("Let's say hi"), it is loaded into the graph and the reference was resolved based on the FK association, therefore your final test passes as the Master is now related to the Detail.

I could be wrong though - but that's what it sounds like.

Do you have lazy-loading enabled? If not, you need to eager-load the relationships you need.

Instead of this:

var masterReloaded = contextReloaded.Masters.First();

Try this:

var masterReloaded = contextReloaded.Masters.Include(x => x.Detail).First();