I am attempting to use EF 4.1 Code First to model a simple relationship of a User having a single Role. When I attempt to save an existing User with a new Role to a different context (using a different context to simulate a client-server round-trip), I get the following exception:
System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details. ---> System.Data.UpdateException: A relationship from the 'User_CurrentRole' AssociationSet is in the 'Added' state. Given multiplicity constraints, a corresponding 'User_CurrentRole_Source' must also in the 'Added' state.
What I expect is that a new Role is created and associated with the exising User.
What am I doing wrong, is this possible to achieve in EF 4.1 code first? The error message seems to suggest that it needs both the User and the Role to be in the added state, but I'm modifying an exising User, so how can that be?
Things to note: I'd like to avoid modifying the structure of the entities (eg by introducing foreign key properties visible on the entities), and in the database I'd like the User to have a foreign key pointing to Role (not the other way around). I'm also not prepared to move to Self Tracking Entities (unless there's no other way).
Here are the entities:
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public Role CurrentRole { get; set; }
}
public class Role
{
public int RoleId { get; set; }
public string Description { get; set; }
}
And here's the mapping I'm using:
public class UserRolesContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().HasKey(u => u.UserId);
modelBuilder.Entity<Role>().HasKey(r => r.RoleId);
modelBuilder.Entity<User>().HasRequired(u => u.CurrentRole);
}
}
I pre-populate the database with this:
public class UserInitializer : DropCreateDatabaseAlways<UserRolesContext>
{
protected override void Seed(UserRolesContext context)
{
context.Users.Add(new User() {Name = "Bob",
CurrentRole = new Role() {Description = "Builder"}});
context.SaveChanges();
}
}
And finally, here's the failing test:
[TestMethod]
public void CanModifyDetachedUserWithRoleAndReattach()
{
Database.SetInitializer<UserRolesContext>(new UserInitializer());
var context = new UserRolesContext();
// get the existing user
var user = context.Users.AsNoTracking().Include(c => c.CurrentRole).First(u => u.UserId == 1);
//modify user, and attach to a new role
user.Name = "MODIFIED_USERNAME";
user.CurrentRole = new Role() {Description = "NEW_ROLE"};
var newContext = new UserRolesContext();
newContext.Users.Attach(user);
// attachment doesn't mark it as modified, so mark it as modified manually
newContext.Entry(user).State = EntityState.Modified;
newContext.Entry(user.CurrentRole).State = EntityState.Added;
newContext.SaveChanges();
var verificationContext = new UserRolesContext();
var afterSaveUser = verificationContext.Users.Include(c => c.CurrentRole).First(u => u.UserId == 1);
Assert.AreEqual("MODIFIED_USERNAME", afterSaveUser.Name, "User should have been modified");
Assert.IsTrue(afterSaveUser.CurrentRole != null, "User used to have a role, and should have retained it");
Assert.AreEqual("NEW_ROLE", afterSaveUser.CurrentRole.Description, "User's role's description should have changed.");
}
}
}
Surely this is a scenario that's covered, I would guess it's something I'm missing in the way I've defined the model mapping?
We can configure a one-to-One relationship between entities using Fluent API where both ends are required, meaning that the Student entity object must include the StudentAddress entity object and the StudentAddress entity must include the Student entity object in order to save it.
A many-to-many relationship is defined in code by the inclusion of collection properties in each of the entities - The Categories property in the Book class, and the Books property in the Category class: public class Book. { public int BookId { get; set; }
You have broken EF state model. You mapped your entity with mandatory CurrentRole
so EF knows that you cannot have existing User
without the Role
. You have also used independent associations (no FK property exposed on your entity). It means that relation between role and user is another tracked entry which has its state. When you assign the role to existing user the relation entry has state set to Added
but it is not possible for existing User
(because it must have already role assigned) unless you mark the old relation as Deleted
(or unless you are working with a new user). Solving this in detached scenario is very hard and it leads to the code where you must pass information about old role during the roundtrip and manually play with state manager or with entity graph itself. Something like:
Role newRole = user.CurrentRole; // Store the new role to temp variable
user.CurrentRole = new Role { Id = oldRoleId }; // Simulate old role from passed Id
newContext.Users.Attach(user);
newCotnext.Entry(user).State = EntityState.Modified;
newContext.Roles.Add(newRole);
user.CurrentRole = newRole; // Reestablish the role so that context correctly set the state of the relation with the old role
newContext.SaveChanges();
The simplest solution is load the old state from the database and merge changes from the new state to the loaded (attached) one. This can be also avoided by exposing FK properties.
Btw. your model is not one to one but one to many where the role can be assigned to multiple users - in case of one-to-one it would be even more complicated because you will have to delete the old role prior to creating a new one.
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