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...
What you describe seems expected behaviour to me.
What happens?
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.
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.
Upon second read I see that you do not have a user-id property in the group entity, so my solution would not work.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With