I'm a novice in using Entity Framework 6 and I got stuck trying to update an entity. I'm using the Code First approach and my model, reduced to the classes in question, looks like this:
public class Document
{
public long Key { get; set; }
public string Name { get; set; }
}
public class Batch
{
public long Key { get; set; }
public virtual List<Document> Documents { get; private set; }
public void Add(Document document)
{
Documents.Add(document);
}
public void Remove(Document document)
{
Documents.RemoveAt(Documents.FindIndex(d => d.Key == document.Key));
}
}
There is a one to many relationship between the Batch and Document, modeled through independent associations, so there is no explicit FK. An important particularity is that the Document class doesn't know about the Batch. This is a difference from other similar questions on Stackoverflow, like this one for instance. Entity Framework generates a null-able column Batch_Key in the Document table.
Here is the context class:
public class MyDbContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Document>().HasKey(d => d.Key);
modelBuilder.Entity<Batch>().HasKey(b => b.Key);
modelBuilder.Entity<Batch>().HasMany(b => b.Documents).WithOptional().WillCascadeOnDelete(false);
base.OnModelCreating(modelBuilder);
}
}
I have a BatchRepository class with an Update() method that receives a Batch parameter and updates the batch in the database:
public class BatchRepository
{
public void Update(Batch item)
{
using (var dbCtx = new MyDbContext(DBContextName))
{
batchesContext.Batches.Attach(item);
batchesContext.Entry(item).State = EntityState.Modified;
batchesContext.SaveChanges();
}
}
}
Here is a unittest for removing a document from a batch, that uses the Update() method:
[TestMethod]
public void CheckRemoveDocument()
{
var batches = BatchRepository.FindAll().ToList();
var batch = batches[0];
var batchKey = batches[0].Key;
var doc = batch.Documents[0];
int batchNumberOfDocuments = batch.Documents.Count;
batch.Remove(doc);
BatchRepository.Update(batch);
batch = BatchRepository.FindBy(batchKey);
Assert.AreEqual(batchNumberOfDocuments - 1, batch.Documents.Count);
}
The test fails, the batchNumberOfDocuments is the same as before. If I implement a method to remove all the documents from a batch, like this:
public class BatchRepository
{
public void RemoveDocuments(Batch item)
{
using (var dbCtx = new MyDbContext(DBContextName))
{
var existingBatch = batchesContext.Batches.Find(item.Key);
batchesContext.Entry(existingBatch).Collection(b => b.Documents).Load();
existingBatch.Documents.RemoveAll(d => true);
batchesContext.SaveChanges();
}
}
}
the following test succeeds:
[TestMethod]
public void CheckRemoveAllDocuments()
{
var batches = BatchRepository.FindAll().ToList();
var batch = batches[0];
var batchKey = batches[0].Key;
BatchRepository.RemoveDocuments(batch);
batch = BatchRepository.FindBy(batchKey);
Assert.AreEqual(0, batch.Documents.Count);
}
So EF correctly tracks the relationship between the batch and the documents, setting the Batch_Key column to NULL for the documents removed. Why does this work in this scenario, but not in the one where I am updating? I assume this is because the Document entities linked to the batch are not attached to the context. The problem is that I don't know if the batch from the parameter of the Update() method has more documents, less documents, or an entirely different list of documents (also, it might be the case that something else than the Document list changed).
An implementation like this:
public class BatchRepository
{
public void Update(Batch item)
{
using (var dbCtx = new MyDbContext(DBContextName))
{
var existingBatch = batchesContext.Batches.Find(item.Key);
batchesContext.Entry(existingBatch).Collection(b => b.Documents).Load();
foreach (var doc in item.Documents)
{
batchesContext.Documents.Attach(doc);
batchesContext.Entry(doc).State = EntityState.Modified;
}
batchesContext.SaveChanges();
}
}
}
throws an exception:
System.InvalidOperationException: Attaching an entity of type 'Document' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.
How should the Update() method be implemented to perform the update properly?
Thank you and sorry if the question sounds confusing, it's all due to the confusion in my mind related to EF.