How to set the referenced collection of many-to-many relationship?
-
21-12-2019 - |
Question
I have to entities which have many-to-many relationship.
public class M1 { M1Id int; ICollection<M2> M2s { get; set; } }
public class M2 { M2Id int; ICollection<M1> M1s { get; set; } }
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> {
public DbSet<M1> M1s{ get; set; }
public DbSet<M2> M2s{ get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<M1>().HasMany<M2>(e => e.M2s).WithMany(c => c.M1s)
.Map(c =>
{
c.MapLeftKey("M1");
c.MapRightKey("M2Id");
c.ToTable("M1AndM2");
});
And I need to create an M1
from VM in the post method.
var M1 = new M1
{
Id = M1Vm.M1Id,
// ....
M2s = db.M1.FirstOrDefault(x => x.M1Id == M1Vm.M1Id).M2s
if (ModelState.IsValid)
{
db.Entry(M1).State = EntityState.Modified; // Error
UpdateM2s(M1, M1Vm.NewM2s); // Update table M1AndM2
It will raise the following error when set the State
of the M1
.
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
The UpdateM2s
will run M1.M2s.Add(m2)
or M1.M2s.Remove(m2)
after the comparison. The following is the code.
private void UpdateM2s(M1 m1, IEnumerable<NewM2s> newM2s)
{
foreach (var newM2 in newM2s)
{
// Add many-to-many relationship
if (newM2.Assigned && !m1.M2s.Any(c => c.M2Id == newM2.M2Id))
{
var m2 = new M2 { M2Id = newM2.M2Id, .... };
db.M2.Attach(m2);
m1.M2s.Add(m2);
}
// Remove many-to-many relationship
else if (!newM2.Assigned && m1.M2s.Any(c => c.Id == assigned.Id))
{
var m2 = new M2 { M2Id = newM2.M2Id, .... };
db.M2.Attach(m2); // Same Error
m1.M2s.Remove(m2);
}
}
}
La solution
The line...
M2s = db.M1.FirstOrDefault(x => x.M1Id == M1Vm.M1Id).M2s
...loads the M1
entity with key M1Vm.M1Id
from the database and attaches it to the context. Then it loads its M2s
collection via lazy loading. At the same time you are creating a new entity M1
with the same key Id = M1Vm.M1Id
and attaching it to the context by setting its state with db.Entry(M1).State = EntityState.Modified
. So, you have two objects with the same key attached to the context which is what the exception is complaining about.
You could try to fix the problem by loading the M2s
only, without the parent:
M2s = db.M1.Select(m1 => m1.M2s).FirstOrDefault(x => x.M1Id == M1Vm.M1Id)
However, I believe the better approach would be to not create a new object at all, but load the original M1
from the database including the M2s
collection and then update the object graph with the view model:
if (ModelState.IsValid)
{
var m1 = db.M1.Include(m => m.M2s).FirstOrDefault(x => x.M1Id == M1Vm.M1Id);
db.Entry(m1).CurrentValues.SetValues(M1Vm);
UpdateM2(m1, M1Vm.NewM2s);
db.SaveChanges();
}
Edit
Now that I see the UpdateM2
method I suggest that you change that as well. In the else
part you are creating a new M2
with the same key as another m2 that already has been included and attached in the query for m1
. Hence you get the same exception about two attached objects with the same key again, this time just refering to M2
and not M1
. You can try to change the else
block like so:
else if (!newM2.Assigned)
{
var m2 = m1.M2s.SingleOrDefault(c => c.Id == newM2.Id);
if (m2 != null)
{
m1.M2s.Remove(m2);
}
}