Reputation: 101
I'm pretty new to EF and I've encountered a situation where Entity is not behaving the way I expected. I'd appreciate help in understanding whether its a bug in EF or a "bug" in my understanding.
I've included all the code at the end of my post; it should compile and run in a test project with Entity 6 installed.
Here's the situation.
I'm using Code-First.
I have two entities in a many-to-one relationship. Here they are:
public class OneSideEntity
{
public int OneSideEntityId { get; set; }
public string Name { get; set; }
public virtual List<ManySideEntity> MyManySideEntities { get; set; }
}
public class ManySideEntity
{
public int ManySideEntityId { get; set; }
public string Name { get; set; }
public virtual OneSideEntity MyOneSideEntity { get; set; }
}
Now suppose I create and save a ManySideEntity
to a variable many
where I dispose of the context after I do context.SaveChanges()
. Then I create and save a OneSideEntity
by performing a "set" to the MyOneSideEntity
property of many
and save, disposing of the context afterword. This works fine; the new OneSideEntity
instance is added to the database and the foreign key of many is correctly updated to the value of the primary key of the OneSideEntity
instance.
Here's where it starts to get interesting. If I now try to set many.MyOneSideEntity = null;
with a new context and save, the change is not pushed out to the database and the asserts in the unit test RelationshipRemovalFails
fail.
However, if I perform the removal by using the other end of the relationship, it works. That is, if I get the OneSideEntity
instance, call it one
for short, and remove it from the one-side navigation property like this one.MyManySideEntities.Remove( many );
and save, it does get pushed out to the database. Therefore the asserts in the unit test corresponding to this scenario (called RelationshipRemovalSucceeds1
in the code below) pass as the change was saved. Entity even correctly (according to my understanding) updates many.MyOneSideEntity
to null as a side-effect of the save.
Finally, if same unit test that fails is slightly changed to use the same DbContext for the add of one
and its later deletion via many.MyOneSideEntity = null;
, it also succeeds. This unit test is called RelationshipRemovalSucceeds2
in the code below.
I thought that the case that fails should work and that the one side entity's navigation property should be updated. Is there a way to set the navigation property to null and have Entity push the change out without having to keep the same DbContext around?
Full Code:
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using Epsilon.Toolbox.LINQ;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Tests
{
////////////////////////////////////////////////
// Two simple entity classes
////////////////////////////////////////////////
public class OneSideEntity
{
public int OneSideEntityId { get; set; }
public string Name { get; set; }
public virtual List<ManySideEntity> MyManySideEntities { get; set; }
}
public class ManySideEntity
{
public int ManySideEntityId { get; set; }
public string Name { get; set; }
public virtual OneSideEntity MyOneSideEntity { get; set; }
}
////////////////////////////////////////////////
// Fluent configuration classes for the entities
////////////////////////////////////////////////
public class ManySideEntityConfiguration : EntityTypeConfiguration<ManySideEntity>
{
public ManySideEntityConfiguration()
{
this.HasKey( x => x.ManySideEntityId );
}
}
public class OneSideEntityConfiguration : EntityTypeConfiguration<OneSideEntity>
{
public OneSideEntityConfiguration()
{
this.HasKey( x => x.OneSideEntityId );
}
}
////////////////////////////////////////////////
// DbContext
////////////////////////////////////////////////
public class RelationshipDeleteTestContext : DbContext
{
public DbSet<OneSideEntity> OneSideEntities { get; set; }
public DbSet<ManySideEntity> ManySideEntities { get; set; }
protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
// Entities
modelBuilder.Configurations.Add( new OneSideEntityConfiguration() );
modelBuilder.Configurations.Add( new ManySideEntityConfiguration() );
}
}
////////////////////////////////////////////////
// Fails to properly save the result of "manySideEntityX.MyOneSideEntity = null;"
////////////////////////////////////////////////
[TestClass]
public class EntityTest
{
[TestMethod]
[TestCategory( "EntityTests" )]
public void RelationshipRemovalFails()
{
Database.SetInitializer( new DropCreateDatabaseAlways<RelationshipDeleteTestContext>() );
// Add a ManySideEntity.
int manySideEntityXId;
using ( var context = new RelationshipDeleteTestContext() )
{
var manySideEntityX1 = new ManySideEntity() { Name = @"X" };
context.ManySideEntities.Add( manySideEntityX1 );
context.SaveChanges();
manySideEntityXId = manySideEntityX1.ManySideEntityId;
}
// Add a OnSideEntity by setting to the ManySide entity's
// navigation property to the newly created OneSideEntity.
int oneSideEntityIdA;
using ( var context = new RelationshipDeleteTestContext() )
{
var oneSideEntityA = new OneSideEntity()
{
Name = "A",
};
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
manySideEntityX.MyOneSideEntity = oneSideEntityA;
context.SaveChanges();
oneSideEntityIdA = oneSideEntityA.OneSideEntityId;
}
int oneSideEntityIdB;
using ( var context = new RelationshipDeleteTestContext() )
{
var oneSideEntityB = new OneSideEntity()
{
Name = "B",
};
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
manySideEntityX.MyOneSideEntity = oneSideEntityB;
context.SaveChanges();
oneSideEntityIdB = oneSideEntityB.OneSideEntityId;
}
using ( var context = new RelationshipDeleteTestContext() )
{
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
// Here is the statement that doesn't work; the database is not updated to null out the foreign key after SaveChanges.
manySideEntityX.MyOneSideEntity = null;
context.SaveChanges();
}
using ( var context = new RelationshipDeleteTestContext() )
{
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
var oneSideEntityB = context.OneSideEntities.Single( x => x.OneSideEntityId == oneSideEntityIdB );
var manySideCount = oneSideEntityB.MyManySideEntities.Count();
// Both Asserts fail since the foreign key in the ManySideEntities table has not been nulled out.
Assert.IsNull( manySideEntityX.MyOneSideEntity );
Assert.AreEqual( 0, manySideCount );
}
}
[TestMethod]
[TestCategory( "EntityTests" )]
public void RelationshipRemovalSucceeds1()
{
Database.SetInitializer( new DropCreateDatabaseAlways<RelationshipDeleteTestContext>() );
// Add a ManySideEntity.
int manySideEntityXId;
using ( var context = new RelationshipDeleteTestContext() )
{
var manySideEntityX = new ManySideEntity() { Name = @"X" };
context.ManySideEntities.Add( manySideEntityX );
context.SaveChanges();
manySideEntityXId = manySideEntityX.ManySideEntityId;
}
// Add a OnSideEntity by setting to the ManySide entity's
// navigation property to the newly created OneSideEntity.
int oneSideEntityIdA;
using ( var context = new RelationshipDeleteTestContext() )
{
var oneSideEntityA = new OneSideEntity()
{
Name = "A",
};
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
manySideEntityX.MyOneSideEntity = oneSideEntityA;
context.SaveChanges();
oneSideEntityIdA = oneSideEntityA.OneSideEntityId;
}
int oneSideEntityIdB;
using ( var context = new RelationshipDeleteTestContext() )
{
var oneSideEntityB = new OneSideEntity()
{
Name = "B",
};
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
manySideEntityX.MyOneSideEntity = oneSideEntityB;
context.SaveChanges();
oneSideEntityIdB = oneSideEntityB.OneSideEntityId;
}
using ( var context = new RelationshipDeleteTestContext() )
{
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
// Using the other side of the relationship DOES work!
var oneSideEntityB = manySideEntityX.MyOneSideEntity;
oneSideEntityB.MyManySideEntities.Remove( manySideEntityX );
context.SaveChanges();
}
using ( var context = new RelationshipDeleteTestContext() )
{
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
var oneSideEntityB = context.OneSideEntities.Single( x => x.OneSideEntityId == oneSideEntityIdB );
var manySideCount = oneSideEntityB.MyManySideEntities.Count();
// Asserts now succeed!
Assert.IsNull( manySideEntityX.MyOneSideEntity );
Assert.AreEqual( 0, manySideCount );
}
}
[TestMethod]
[TestCategory( "EntityTests" )]
public void RelationshipRemovalSucceeds2()
{
Database.SetInitializer( new DropCreateDatabaseAlways<RelationshipDeleteTestContext>() );
// Add a ManySideEntity.
int manySideEntityXId;
using ( var context = new RelationshipDeleteTestContext() )
{
var manySideEntityX = new ManySideEntity() { Name = @"X" };
context.ManySideEntities.Add( manySideEntityX );
context.SaveChanges();
manySideEntityXId = manySideEntityX.ManySideEntityId;
}
// Add a OnSideEntity by setting to the ManySide entity's
// navigation property to the newly created OneSideEntity.
int oneSideEntityIdA;
using ( var context = new RelationshipDeleteTestContext() )
{
var oneSideEntityA = new OneSideEntity()
{
Name = "A",
};
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
manySideEntityX.MyOneSideEntity = oneSideEntityA;
context.SaveChanges();
oneSideEntityIdA = oneSideEntityA.OneSideEntityId;
}
int oneSideEntityIdB;
using ( var context = new RelationshipDeleteTestContext() )
{
{
var oneSideEntityB = new OneSideEntity()
{
Name = "B",
};
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
manySideEntityX.MyOneSideEntity = oneSideEntityB;
context.SaveChanges();
oneSideEntityIdB = oneSideEntityB.OneSideEntityId;
}
{
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
// This now works when using the same context for an add the remove.
manySideEntityX.MyOneSideEntity = null;
context.SaveChanges();
}
}
using ( var context = new RelationshipDeleteTestContext() )
{
var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
var oneSideEntityB = context.OneSideEntities.Single( x => x.OneSideEntityId == oneSideEntityIdB );
var manySideCount = oneSideEntityB.MyManySideEntities.Count();
// Both Asserts now Succeed/
Assert.IsNull( manySideEntityX.MyOneSideEntity );
Assert.AreEqual( 0, manySideCount );
}
}
}
}
Upvotes: 4
Views: 2685
Reputation: 101
I've figured out what's going on. When I set the many.MyOneSideEntity = null;
, Entity's change tracker has captured the initial value of "null". From Entity's perspective therefore, there is no change to the property's value so the change tracker doesn't think the property needs to be updated even though the value of the property is different than what's in the database.
To fix the problem, just access the property via the getter. This causes the entity's state to be loaded into memory and the change tracker to be synced with these values. The change tracker then notices the change effected by setting the property to null and will then push the change out to the database when SaveChanges is called.
It's slightly weird that I have to do this, but it fits in with how Entity's change tracker works.
Hope this helps someone else!
Upvotes: 6