SOLVED: It was because each of my service classes had their own context associated with them, I just stored the context in HttpContext and made did DI into the repository
I’ve got 3 classes:
public class Group : Entity
{
public Guid Id { get; set; }
//other simple props
public virtual GroupType GroupType { get; set; }
public virtual ICollection<User> Users { get; set; }
}
public class GroupType: Entity
{
public Guid Id { get; set; }
//other simple props
}
public class User: Entity
{
public Guid Id { get; set; }
//other simple props
public virtual ICollection<Group> Groups { get; set; }
}
And my fairly standard generic CRUD methods:
public void Insert(TClass entity)
{
if (_context.Entry(entity).State == EntityState.Detached)
_context.Set<TClass>().Attach(entity);
_context.Set<TClass>().Add(entity);
_context.SaveChanges();
}
public void Update(TClass entity)
{
DbEntityEntry<TClass> oldEntry = _context.Entry(entity);
if (oldEntry.State == EntityState.Detached)
_context.Set<TClass>().Attach(oldEntry.Entity);
oldEntry.CurrentValues.SetValues(entity);
_context.SaveChanges();
}
public bool Exists(TClass entity)
{
bool exists = false;
PropertyInfo info = entity.GetType().GetProperty(GetKeyName());
if (_context.Set<TClass>().Find(info.GetValue(entity, null)) != null)
exists = true;
return exists;
}
public void Save(TClass entity)
{
if (entity != null)
{
if (Exists(entity))
_repository.Update(entity);
else
_repository.Insert(entity);
}
}
I try and run this code:
public string TestCRUD()
{
UserService userService = UserServiceFactory.GetService();
User user = new User("Test", "Test", "Test", "TestUser") { Groups = new Collection<Group>() };
userService.Save(user);
GroupTypeService groupTypeService = GroupTypeServiceFactory.GetService();
GroupType groupType = new GroupType("TestGroupType2", null);
groupTypeService.Save(groupType);
GroupService groupService = GroupServiceFactory.GetService();
Group group = new Group("TestGroup2", null) { GroupType = groupType };
groupService.Save(group);
user.Groups.Add(group);
userService.Save(user);
}
When I get to the last line:
userService.Save(user);
I get this error:
Violation of PRIMARY KEY constraint ‘PK_GroupTypes_00551192′. Cannot insert duplicate key in object ‘dbo.GroupTypes’.
The statement has been terminated.
So it looks like it’s trying to insert a new groupType even though it was already inserted.
Edit: All my class constructors automatically set a new Guid to the Id property.
EDIT:
I just ran tests by calling .Save() twice on each object, so before it exists in the context/db and once after. The results seem strange:
- First Time I call
.Save()on groupType object: State is Detached (expected). - Second time w/no changes to it: State is Unchanged (expected and proves that change tracking is occurring on the in memory object even though I didn’t requery it from context).
- First Time I call
.Save()on group object (with GroupType set to
the already saved GroupType): Group State is Detached (expected),
GroupType state is Detached (not expected, why is it detached
here when I already saved it and it was being tracked?) - Second Time w/no changes: Both Group and GroupType stats are
unchanged (expected). - First Time I call
.Save()on user object: State is Detached
(expected). - Second Time after calling user.Groups.Add(group) with the saved
group: User is unchanged (expected), Group is Added (expected), GroupType is Added (not expected, why is this being marked as Added it’s been saved and supposedly is tracked both individually and as part of the group object?) - Finally I tried marking the groupType entry as unchanged, and the same duplicate key error occurred this time for the Group object, consistent since it’s also added but not expected.
So, 1) why do existing objects act like they aren’t being change-tracked when added to a collection navigation property, but in a simpler case, like calling it on the group object twice, the groupType nav prop is still change-tracked and not added twice, in both cases I never re-query to get the object from db.
Finally, 2) what is an efficient way around this behavior? I don’t really want to manually set nav properties entity states as unchanged because I can’t guarantee that they aren’t trying to add a non-existing entity, but at the same time it seems really inefficient to call .Save() on each group and each group type inside the user .Save() OR query each of them from the database.
I’m sure that if you were to look at the record in the database it would show an empty guid (only zeroes). To “fix” the problem you need to generate a guid and add it to your classes Id property. An easy way to fix this is to add an default constructor and add
Id = Guid.NewGuid();that way your classes will have a own guid and everything should work as expected.EDIT:
Since it it
GroupTypethat gets saved to the database multiple times with the same guid there are two scenarios I can think of.1) You created multiple groups and added the same
GroupTypeand try to save.2) You created the GroupType manually instead of getting it from the database in which 1) applies again.
Unless the entity
GroupTypehas anEntityStateofUnchangedorModifiedit will try to add the sameGroupTypeover and over again. I think you have two options:EDIT2: What happens if you were to run this?
I expect it to save the User the Group added to user and the GroupType added to the Group.
The reason why this would not cry over duplicates is because we only call
userService.Save(user);once. Now if we wanted to add more Groups to the user with the same groupType (for some reason?) we would now have to change our code a little.I expect that the following code should Save the User, Save the Groups and make the Groups point to the same GroupType.
As a last pointer I want to stress the fact that under no circumstances should both services use their own DbContext they Must Share the Same DbContext if you want it to track the entities.