Tell DbContext to not add whole object graph?
I have a class in my model that can refer to the same child class from two different FK associations/fields. Both of these references are populated with the same instance of the child object when the parent is created, and then later, one of the two children can be updated or changed (this does not always happen), and the original is kept because the other of the children is never touched. I hope that makes sense.
When a Parent is created or pulled from the database that has the same Child referenced twice, when you try to add the Parent to the DbContext we're seeing the dreaded error: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
This is being thrown because the DbContext is trying to add the entire object graph to its change tracker, i.e. the two Child references pointing to the same Child object.
We don't need change tracking. We don't mind throwing a fully populated UPDATE statement at the database. Is there a way to force the DbContext to not add the entire object graph, to only add the single instance we're telling it to add? If so, what functionality are we going to lose if we disable this globally?
EDIT: Updated code sample.
EDIT 2: Updated code sample to include serialization to mimic web service interaction.
[TestClass]
public class EntityFrameworkTests
{
[TestMethod]
public void ObjectGraphTest()
{
Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());
string connectionString = String.Format("Data Source={0}\\EntityFrameworkTests.sdf", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
MyDbContext context = new MyDbContext(connectionString);
Child child = new Child() { ID = 1, SomeProperty = "test value" };
//context.Entry<Child>(child).State = EntityState.Added;
Parent parent = new Parent()
{
ID = 1,
SomeProperty = "some value",
OriginalChild = child,
ChangeableChild = child
};
context.Entry<Parent>(parent).State = EntityState.Added;
context.SaveChanges();
context = new MyDbContext(connectionString);
//parent = context.Set<Parent>().AsNoTracking().Include(p => p.OriginalChild).Include(p => p.ChangeableChild).FirstOrDefault();
parent = context.Set<Parent>().Include(p => p.OriginalChild).Include(p => p.ChangeableChild).FirstOrDefault();
// mimic receiving object via a web service
SaveToStorage(parent);
parent = GetSavedItem(1);
parent.SomeProperty = "some new value";
context = new MyDbContext(connectionString);
context.Entry<Parent>(parent).State = EntityState.Modified; // error here
context.SaveChanges();
}
}
Serialization methods to mimic web service interaction:
private void SaveToStorage(Parent parent)
{
string savedFilePath = String.Format("{0}\\Parent{1}.xml", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), parent.ID);
using (FileStream fileStream = new FileStream(savedFilePath, FileMode.Create, FileAccess.Write))
{
using (XmlWriter writer = XmlWriter.Create(fileStream))
{
DataContractSerializer serializer = new DataContractSerializer(typeof(Parent));
serializer.WriteObject(writer, parent);
}
}
}
private Parent GetSavedItem(int parentID)
{
string savedFilePath = String.Format("{0}\\Parent{1}.xml", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), parentID);
using (FileStream fileStream = new FileStream(savedFilePath, FileMode.Open, FileAccess.Read))
{
using (XmlDictionaryReader xmlReader = XmlDictionaryReader.CreateTextReader(fileStream, new XmlDictionaryReaderQuotas()))
{
DataContractSerializer serializer = new DataContractSerializer(typeof(Parent));
Parent savedItem = (Parent)serializer.ReadObject(xmlReader, true);
return savedItem;
}
}
}
Classes used (updated for serialization):
[DataContract]
internal class Child
{
[DataMember]
public int ID { get; set; }
[DataMember]
public string SomeProperty { get; set; }
}
[DataContract]
internal class Parent
{
[DataMember]
public int ID { get; set; }
[DataMember]
public string SomeProperty { get; set; }
[DataMember]
public int OriginalChildID { get; set; }
[DataMember]
public Child OriginalChild { get; set; }
[DataMember]
public int ChangeableChildID { get; set; }
[DataMember]
public Child ChangeableChild { get; set; }
}
internal class MyDbContext : DbContext开发者_如何学Go
{
public DbSet<Parent> Parents { get; set; }
public DbSet<Child> Children { get; set; }
public MyDbContext(string connectionString)
: base(connectionString) { }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
}
}
The ugly solution:
Child originalChild = parent.OriginalChild;
Child changeableChild = parent.ChangeableChild;
parent.OriginalChild = null;
parent.ChangeableChild = null;
context.Entry<Parent>(parent).State = EntityState.Modified;
context.SaveChanges();
parent.OriginalChild = originalChild;
parent.ChangeableChild = changeableChild;
I you don't need the parent
with the child objects anymore after saving setting the children to null
would be sufficient of course.
Another and much better solution: Pull the original parent from the database again - without the children, since you know that you only want to save changed parent properties:
var originalParent = context.Set<Parent>()
.Where(p => p.ID == parent.ID)
.FirstOrDefault();
context.Entry(originalParent).CurrentValues.SetValues(parent);
context.SaveChanges();
You have to load the parent first (with active change tracking!) from the database but on the other hand the UPDATE command issued here will only contain the changed properties. Since you say that you don't mind to send a full UPDATE command (by setting the state to Modified
) I guess you don't have performance issues. So, loading the original and then sending a small UPDATE with only the changed properties might not be worse or much worse performance-wise than sending a full UPDATE command.
精彩评论