Context
I have a project, where I am using the entity framework core. This project will have users which can register and, of course, sign in afterwards. They also have the possibility to edit their profile AFTERWARDS and this is where the problem arises. When trying to edit the user settings (data, he doesn’t enter when signin up, signin in) he can also set an Address, which is a dependent entity to the user. When trying to add the Address as well, it fails with
Optimistic concurrency failure, object has been modified
Flow
First, the user registers, simply, by providing an email address and a password. An email is sent to him to confirm his account. When he did, he is able to log in. When logging in, he retrieves a JWT, which he can use to perform his requests. The only other controller method right now, is to edit his profile settings, where he can add data, such as
- First name (string)
- Last name (string)
- Phone number (string)
- Address (Address)
- Street
- HouseNumber
- City
- ZipCode
Problem and relevant code
Before I go into detail, I want to show you how I did the relation
User.cs
public sealed class User : IdentityUser<Guid>
{
// Other fields...
[ProtectedPersonalData]
public Address Address { get; set; }
}
Address.cs
public sealed class Address
{
// Other fields...
public Guid AddressId { get; set; }
public Guid UserId { get; set; }
public User User { get; set; }
public Address()
{
AddressId = Guid.NewGuid();
}
}
DbContext.cs
public sealed class DbContext : IdentityDbContext<User, Role, Guid>
{
public DbSet<User> Users { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySql(
"server=localhost;database=db;user=root;password=example"
);
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<User>()
.HasOne(u => u.Address)
.WithOne(a => a.User)
.HasForeignKey<Address>(a => a.UserId);
base.OnModelCreating(builder);
}
}
When I now make a PUT request to edit the profile, I am sending this data
{
"firstname": "Austin",
"lastname": "Powers",
"Address": {
"Street": "Mr. Evil Way",
"HouseNumber": "20",
"ZipCode": "10115"
},
"PhoneNumber": "+492148 48484"
}
My controller, looks like this
public async Task<ActionResult<User>> EditProfile(EditProfileResource editProfileResource)
{
User user = await _userManager.GetUserAsync(User);
// _mapper is an automapper instance
user = _mapper.Map(editProfileResource, user);
user.ProfileSetup = true;
IdentityResult result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return NoContent();
}
return Problem(result.Errors.First().Description, null, StatusCodes.Status400BadRequest);
}
So what happens, is the following.
- The controller, automatically transforms the request body to an
EditProfileResourcewhich looks like this and has an AddressResource
public sealed class EditProfileResource
{
public string FirstName { get; set; }
public string LastName { get; set; }
public AddressResource Address { get; set; }
public string PhoneNumber { get; set; }
}
// Separate file of course
public sealed class AddressResource
{
public string Street { get; set; }
public string HouseNumber { get; set; }
public string ZipCode { get; set; }
public string City { get; set; }
}
So my editProfileResource has all the right data set! The problem? When I now save the user in the line with the contents IdentityResult result = await _userManager.UpdateAsync(user); I’m getting the error Optimistic concurrency failure, object has been modified. When, I remove the AddressResource from the EditProfileResource it works without any problems, but I want to edit the address as well.
But to understand correctly, I will show how the code is implemented and what is in there, which could cause the error, but I don’t know what the fix would be
User user = await _userManager.GetUserAsync(User);
-> In here, we have the user object, stored as in the database, no first name, no last name set, no address set
user = _mapper.Map(editProfileResource, user);
-> This now changes the user object, so that it now has a first name, last name and even an Address object. The address object also has the user object with the correct guid and user object. So everything is corerct.
IdentityResult result = await _userManager.UpdateAsync(user);
-> This throws the error, probably because the Address in the User also has a User object, right?
Tested solutions
So what I did next, was to try, to change the saving mechanism. Instead of doing IdentityResult result = await _userManager.UpdateAsync(user); I did
await using (DbContext context = new DbContext())
{
context.Addresses.Add(user.Address);
await context.SaveChangesAsync();
}
However, this failed, bercause of a foreign key constraint?
Cannot add or update a child row: a foreign key constraint fails (
db.Addresses, CONSTRAINTFK_Addresses_AspNetUsers_UserIdFOREIGN KEY (UserId) REFERENCESAspNetUsers(Id) ON DELETE CASCADE)
so I tried it without context.Addresses.Add(user.Address);
which works, but since I haven’t added it to the context, there’s no Address in the database. What am I doing wrong? :/
What is the correct way, to edit two entities at the same time and save them both into the database?
Updates
Update 1
I tried to split this into two methods, just to be sure, that it’s not related somehow to trying to update multiple data sets at once
[HttpPut("userdata")]
public async Task<ActionResult<User>> EditProfile(EditProfileResource editProfileResource)
{
User user = await _userManager.GetUserAsync(User);
_mapper.Map(editProfileResource, user);
user.ProfileSetup = user.ProfileSetup ? user.ProfileSetup : user.Address != null;
IdentityResult result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return NoContent();
}
return Problem(result.Errors.First().Description, null, StatusCodes.Status400BadRequest);
}
[HttpPut("address")]
public async Task<ActionResult> EditAddress(AddressResource addressResource)
{
User user = await _userManager.GetUserAsync(User);
Address address = _mapper.Map<AddressResource, Address>(addressResource);
address.User = user;
address.UserId = user.Id;
await using (DbContext context = new DbContext())
{
context.Addresses.Add(address);
await context.SaveChangesAsync();
}
return NoContent();
}
and again, editing the user works, but when I want to save the address it fails because of an already existing key. How can that be?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
Okay, it seems the problem had to do with the combination of using the UserManager and the DbContext in combination.
The solution looks like this
public async Task<ActionResult<User>> EditProfile(ProfileSetupResource editProfileResource)
{
Guid userId = Guid.Parse(_userManager.GetUserId(User));
await using (DbContext context = new DbContext())
{
User user = await context.Users.FindAsync(userId);
if (user.ProfileSetup)
{
return BadRequest("Profile already set up.");
}
user = _mapper.Map(editProfileResource, user);
user.ProfileSetup = true;
context.Users.Update(user);
await context.Addresses.AddAsync(user.Address);
await context.SaveChangesAsync();
}
return NoContent();
}
The problem was, that in the previous example, the DbContext noticed that the user has been changed, however, the changes were not managed by the DbContext, but instead by the UserManager since this was the code
// User changes managed by UserManager
User user = await _userManager.GetUserAsync(User);
user = _mapper.Map(editProfileResource, user);
// [other code...]
await using (DbContext context = new DbContext())
{
// Address changes managed by DbContext
context.Addresses.Add(address);
await context.SaveChangesAsync();
}
So basically, like I said, the solution was to let the DbContext manage everything. The UserManager should only be used, if only the user will be changed, and no other data. Since the usermanager also uses the DbContext behind the scenes, it was kind of a “war” which one wins and both had changes the other DbContext did not have. So yes. This was the solution for me.
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0