Situation sketch
I develop a system where there many materials (in code Matetiaal
) could be added to an advertisement (in code Zoeker
). The relationship is many to many. Here you got the structure of my classes.
+-------------------------------------+ many to many +-------------------------------+
| Zoeker | <--------------> | Materiaal |
+------------+------------------------+ +---------+---------------------+
| ID | int | | ID | int |
| Materialen | ICollection<Materiaal> | | Zoekers | ICollection<Zoeker> |
| Titel | string | | Naam | string |
+------------+------------------------+ +---------+---------------------+
I'll add new materials and advertisements using next pseudo code:
void Insert(Zoeker zoeker, string[] materialen)
{
foreach (string m in materialen)
{
Materiaal mat = materiaalRepo.GetByName(m);
if (mat == null)
{
mat = materiaalRepo.Insert(new Materiaal(m));
}
zoeker.Materialen.Add(mat);
}
zoekerRepo.Insert(zoeker);
}
In words when a new advertisement (Zoeker
) must be added, I'll check if the material already exist in the database. The method materialenRepo.GetByName(string)
returns null
if the material doesn't exists. If that is true, insert a new material into the database and returns the Materiaal
-object. After that or if materialenRepo.GetByName(string)
didn't return a null
, add that material to the Materialen
-property of the advertisement and insert that into the database.
To connect the database with my application I use Entity Framework.
My problem
When I use the code on heading below, I've got this exception:
System.InvalidOperationException
: An entity object cannot be referenced by multiple instances ofIEntityChangeTracker
.public virtual TEntity Insert(TEntity entity) { return dbSet.Add(entity); // <-- on this line }
The exception is thrown on the marked line when zoekerRepo.Insert(zoeker);
is called.
My code
Here you could find my code I've made.
Service
public class ZoekService
{
public int InsertZoeker(Zoeker zoeker, ApplicationUser user, string materialenString)
{
return InsertZoeker(zoeker, user, materialenString.Split(','));
}
public int InsertZoeker(Zoeker zoeker, ApplicationUser user, string[] materialen)
{
zoeker.Gebruiker = user;
zoeker.Materialen = new List<Materiaal>();
using (ApplicationDbContext ctx = new ApplicationDbContext())
{
ZoekerRepository zoekerRepo = new ZoekerRepository(ctx);
MateriaalRepository materiaalRepo = new MateriaalRepository(ctx);
// ^^^^ See update one
foreach (string m in materialen)
{
Materiaal materiaal = materiaalRepo.GetByNaam(m);
if (materiaal == null)
{
materiaal = materiaalRepo.Insert(new Materiaal(m));
// ^ See update three
}
zoeker.Materialen.Add(materiaal);
}
zoekerRepo.Insert(zoeker);
zoekerRepo.SaveChanges(); // <-- Pointer didn't come on this line.
return zoeker.ID;
}
}
}
Repositories
ZoekerRepository
extends GenericRepository<Zoeker>
public class ZoekerRepository : GenericRepository<Zoeker>
{
public ZoekerRepository()
{ }
public ZoekerRepository(ApplicationDbContext ctx): base(ctx)
{ }
// Some other methods I've add or will override.
}
MateriaalRepository
extends GenericRepository<Materiaal>
public class MateriaalRepository : GenericRepository<Materiaal>
{
public MateriaalRepository()
{ }
public MateriaalRepository(ApplicationDbContext ctx): base(ctx)
{ }
public Materiaal GetByNaam(string naam)
{
return (from m in dbSet
where m.Naam.Equals(naam)
select m).SingleOrDefault<Materiaal>();
}
// Some other methods I've add or will override.
}
GenericRepository<TEntity>
public class GenericRepo<TEntity> where TEntity : class
{
internal ApplicationDbContext context;
internal DbSet<TEntity> dbSet;
public GenericRepo()
{
context = new ApplicationDbContext();
dbSet = context.Set<TEntity>();
}
public GenericRepo(ApplicationDbContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
public virtual TEntity Insert(TEntity entity)
{
return dbSet.Add(entity); // <-- after `zoekerRepo.Insert(Zoeker)` the exception
// will be thrown on this line.
}
public void SaveChanges()
{
try
{
context.SaveChanges();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
What I've done
Of course I've tried other code but all code gives the same exception. I'll make my code so that the repository for the materials is separated with the repository from the advertisements repository using the same generic class named GenericRepo<TEntity>
(except from the thing I'll add or override). I'll reuse that as most as possible. Once for GenericRepo<Zoeker>
and an other time for GenericRepo<Materiaal>
.
My question
How could I solve this bug?
Update one
This question is not duplicate with entity object cannot be referenced by multiple instances of IEntityChangeTracker. while adding related objects to entity in Entity Framework 4.1 because I use the same context like the accepted answer said:
You can fix this by creating a context outside of the service classes and injecting and using it in both services:
EmployeeService es = new EmployeeService(context); CityService cs = new CityService(context); // same context instance
See the comments on code above.
Update two
Here you've a new part of my code I've made.
Controllers
public class DeelController : Controller
{
private UserManager<ApplicationUser> UserManager { get; set; }
private ZoekService _zoekservice = new ZoekService();
public DeelController()
{
this.UserManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(ApplicationDbContext));
}
[HttpGet]
[Authorize]
public ActionResult Nieuw()
{
return View(new Zoeker());
}
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Nieuw(Zoeker zoeker)
{
_zoekservice.InsertZoeker(zoeker, await UserManager.FindByNameAsync(User.Identity.Name), zoeker.MaterialenString);
return RedirectToAction(nameof(Index));
}
}
Classes
Zoeker
extends Post
public class Zoeker: Post
{
public virtual ICollection<Materiaal> Materialen { get; set; }
public virtual ICollection<ApplicationUser> Delers { get; set; }
[NotMapped]
public string MaterialenString { get; set; } // <-- contains a comma separated
// list of all materials
}
Post
public class Post
{
[Key]
public int ID { get; set; }
public string Titel { get; set; }
public string Omschrijving { get; set; }
public virtual ApplicationUser Gebruiker { get; set; }
public string GebruikerID { get; set; }
public DateTime StartPeriode { get; set; }
public DateTime? Verwijderd { get; private set; }
public DateTime? Opgelost { get; private set; }
public DateTime? Belangerijk { get; private set; }
public virtual ICollection<Antwoord> Antwoorden { get; set; }
}
Update three
I've also tried to replace the commented line from code above with this code but didn't solve this bug:
materiaal = /*materiaalRepo.Insert(*/ new Materiaal(m); //);
If I use a material that doesn't exists, using the code of update three, I've got the bug again.
Update four
If I use the watcher I see this:
Material[0]
got a field _entityWrapper
type of EntityWrapperWithoutRelationships<System.Data.Entity.DynamicProxies.Materiaal_E0DDEFDA63D33C75C64324662B40FDC414EC19CA4AD6BF18443B63FC60015374>
. Note: This material does already exists in the database. Also here I've got the same bug.
Update five
I've also replaced the `InsertZoeker(Zoeker
public int InsertZoeker(Zoeker zoeker, ApplicationUser user, string[] materialen)
{
Zoeker newZoeker = new Zoeker()
{
Gebruiker = user,
StartPeriode = DateTime.Now,
Materialen = new List<Materiaal>(),
Titel = zoeker.Titel,
Omschrijving = zoeker.Omschrijving
};
using (ApplicationDbContext ctx = new ApplicationDbContext())
{
ZoekerRepository zoekerRepo = new ZoekerRepository(ctx);
MateriaalRepository materiaalRepo = new MateriaalRepository(ctx);
foreach (string m in materialen)
{
Materiaal materiaal = materiaalRepo.GetMateriaalByNaam(m);
if (materiaal == null)
{
materiaal = /*materiaalRepo.Insert(*/ new Materiaal(m); //);
}
newZoeker.Materialen.Add(materiaal);
}
zoekerRepo.Insert(newZoeker);
zoekerRepo.SaveChanges();
return zoeker.ID;
}
}