Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

entity cannot be referenced by multiple instances of IEntityChangeTracker

I get this error each time I try to attach an User to a Group (or creating a Group).

Here is the GroupController :

public class GroupsController : Controller
{
       private Context db = new Context();

        [HttpPost]
        public ActionResult Create(Group group)
        {
            if (ModelState.IsValid)
            {
                group.owner = db.Users.Attach((User)Session["user"]);
    //current user stored in session
                db.Groups.Add(group);
                db.SaveChanges();

                return RedirectToAction("Index", "Home");
            }
            return View(group);
        }
}

The variable Session["user"] is set at logon, here is the AccountController code :

public class AccountController : Controller
{
    private Context db = new Context();

    [HttpPost]
    public ActionResult LogOn(LoginTemplate login, string returnUrl)
    {

            User result = db.Users.Where(m => m.email == login.email).Where(m => m.password == login.password).SingleOrDefault();

            if (result != null)
            {
                Session["logged"] = true;
                Session["user"] = result;
                return RedirectToAction("Index", "Home");
            }
     }
 }

Here is my context :

public class Context : DbContext
{

    public Context() : base("name=myContext")
    {
    }
    private static Context _instance;

    public DbSet<User> Users { get; set; }
    public DbSet<Group> Groups { get; set; }

//I tried to use this method in order to always get the same context but does not work

    public static Context getContext()
    {
        if (_instance == null)
        {
            _instance = new Context();
        }

        return _instance;
    }
}

Here is User.cs

       public class User
{
    [Key]
    public int userId { get; set; }

    [Display(Name="Firstname")]
    public string firstname { get; set; }

    [Display(Name = "Lastname")]
    public string lastname { get; set; }

    public virtual ICollection<Group> membership { get; set;}

}

Here is group.cs :

        public class Group
{
    [Key]
    public int idGroup { get; set; }
    public string name { get; set; } 
    public User owner { get; set; }
    public virtual ICollection<User> members { get; set; }

}

I tried to do a "singleton" in my Context (to always reuse the same context in each controller) but when I do that, I get "Db closed" error message...

like image 805
Tang Avatar asked Oct 06 '22 22:10

Tang


2 Answers

INITIAL ANSWER

What you describe seems expected behaviour to me.

What happens?

  1. When you log on a user entity is loaded, and saved in the session. But since the context that is tracking the user entity has references to the user entity, and more importantly, the user entity has references to the context, the context is not disposed of, and even though your controller will be garbage collected, the context is not.

  2. The group-controller is instanced, and this has a separate context. Then you create a group entity, and at that point the group entity has no references yet to the context (or the other way around). So connecting the user entity to the group entity is fine. But when you attach the group entity to the new context, the context starts tracking this entity, and all other entities that are referenced by the group entity. So that includes the user entity. Since the original context is also tracking the user entity, this results in the exception.

I can see an easy solution, and that is to use the id of the user entity instead of the object itself, when you want to connect the user entity to the group entity. This way, no references are created between the group entity and the user entity, so that should work fine.

UPDATE

Upon second read I see that you do not have a user-id property in the group entity, so my solution would not work.

like image 168
Maarten Avatar answered Oct 12 '22 20:10

Maarten


The problem is caused by the virtual keyword of your entity's navigation properties. EF will create a lazy loading proxy then that is capable to load the navigation properties on the fly but to do this it needs to hold a reference to the context. When you attach the entity later to another context you get this exception because the proxy is still tracked by the old context it is pointing to.

You can probably fix the error by disabling proxy creation and tracking when you load the user to ensure that a pure (non-proxy) POCO entity is loaded that doesn't reference the context anymore:

public class AccountController : Controller
{
    private Context db = new Context();

    public AccountController()
    {
        db.Configuration.ProxyCreationEnabled = false;
    }

    [HttpPost]
    public ActionResult LogOn(LoginTemplate login, string returnUrl)
    {
        User result = db.Users.AsNoTracking()
            .Where(m => m.email == login.email)
            .Where(m => m.password == login.password)
            .SingleOrDefault();

        if (result != null)
        {
            Session["logged"] = true;
            Session["user"] = result;
            return RedirectToAction("Index", "Home");
        }
    }
}

It will disable proxies and lazy loading for the whole controller but you can replace it with eager loading (Include) when you need navigation properties to be loaded.

Even if that fixes your problem it is still not the best practice in my opinion to store an entity in session state. Better use a specialized UserForSessionModel class (that is not a database entity) or store only the Id and reload the user from the database when you need it.

Edit

You can alternatively use a "stub entity" in your Create action to avoid to attach the loaded user that is referenced by another context:

var userStub = new User { userId = ((User)Session["user"]).userId };
group.owner = db.Users.Attach(userStub);
db.Groups.Add(group);
db.SaveChanges();

The only thing you need from the user to establish a relationship is its primary key, so such an empty user with only the key populated is sufficient.

like image 38
Slauma Avatar answered Oct 12 '22 19:10

Slauma