4

I'm trying to update a user with a user repository service. I'm still using the IdentityUser, just modified it to use int as primary key instead of string/guid.

Everything works fine, until my Update throws an exception.

So this is my update method in my BaseRepository class:

public virtual void Update(T entity)
{
    EntityEntry dbEntityEntry = _context.Entry<T>(entity);
    dbEntityEntry.State = EntityState.Modified;
}

public virtual void Commit()
{
    _context.SaveChanges();
}

The call itself goes:

[HttpPost]
[Route("home/account/edit")]
public ActionResult Edit(User user)
{
    if (!ModelState.IsValid)
        return View(user);

    string id = _userManager.GetUserId(User);
    user.Id = id.To<int>();

    _userService.Update(user);
    _userService.Commit();

    return RedirectToAction("Index");
}

(nothing special I think)

So now, when it's in Commit to save my changes, it throws an exception:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

I thought, okay, I check the profiler, couldn't be that tricky. Profiler shows me the statement, entity framework sent him:

exec sp_executesql N'SET NOCOUNT ON;
UPDATE [AspNetUsers] SET [AccessFailedCount] = @p0, [Birthday] = @p1, [ConcurrencyStamp] = @p2, [Email] = @p3, [EmailConfirmed] = @p4, [FirstName] = @p5, [Language] = @p6, [LastName] = @p7, [LockoutEnabled] = @p8, [LockoutEnd] = @p9, [MiddleName] = @p10, [NormalizedEmail] = @p11, [NormalizedUserName] = @p12, [PasswordHash] = @p13, [PhoneNumber] = @p14, [PhoneNumberConfirmed] = @p15, [SecurityStamp] = @p16, [TwoFactorEnabled] = @p17, [UserName] = @p18, [VerificationToken] = @p19
WHERE [Id] = @p20 AND [ConcurrencyStamp] = @p21;
SELECT @@ROWCOUNT;
',N'@p20 int,@p0 int,@p1 datetime2(7),@p2 nvarchar(4000),@p21 nvarchar(4000),@p3 nvarchar(256),@p4 bit,@p5 nvarchar(4000),@p6 nvarchar(4000),@p7 nvarchar(4000),@p8 bit,@p9 nvarchar(4000),@p10 nvarchar(4000),@p11 nvarchar(256),@p12 nvarchar(256),@p13 nvarchar(4000),@p14 nvarchar(4000),@p15 bit,@p16 nvarchar(4000),@p17 bit,@p18 nvarchar(256),@p19 nvarchar(4000)',@p20=1,@p0=0,@p1='1991-06-10 00:00:00',@p2=N'24fe11c5-a831-45ce-8960-16aa78b280df',@p21=N'24fe11c5-a831-45ce-8960-16aa78b280df',@p3=N'my@email.ch',@p4=0,@p5=N'Matthias',@p6=N'de-DE',@p7=N'Burger',@p8=0,@p9=NULL,@p10=NULL,@p11=NULL,@p12=NULL,@p13=NULL,@p14=NULL,@p15=0,@p16=NULL,@p17=0,@p18=NULL,@p19=NULL;

So, [ConcurrencyStamp] = @p21 and @p21=N'24fe11c5-a831-45ce-8960-16aa78b280df'

Checked my record of that user...

ConcurrencyStamp is 22fa7d03-1382-4eee-8c17-878253fdf1c4

So my questions are:

  • What is that ConcurrencyStamp ?
  • Why is Entity Framework filtering for it? He gets my User ID. Isn't that enough?
  • Why has the user object another ConcurrencyStamp than the one in the database? When Entity Framework wants to filter for it, shouldn't they be the same?
  • Does anybody have an idea? How could/should I fix it? Or am I doing something wrong?

Update:

Model (for now viewmodel, too):

public class User : IdentityUser<int>, IEntity
{
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
    public string Language { get; set; }

    public ICollection<MailBox> MailBoxes { get; set; } = new List<MailBox>();
    public string VerificationToken { get; set; }
}
Matthias Burger
  • 5,549
  • 7
  • 49
  • 94
  • can you show us your model \ viewmodel please – Simon Price Oct 23 '16 at 20:56
  • @SimonPrice updated :) model is for now the viewmodel, too. it's more or less for testing now. – Matthias Burger Oct 23 '16 at 20:59
  • Your code is strange. You use the ID of the logged in user, but the data of the passed user? This code attempts to always creates a user with the logged in users id. Also you seem to never be attaching the model. Did you treid attaching the model before updating it? – Tseng Oct 23 '16 at 21:10
  • Yep, this is for updating the own profile-data. So the passed user IS the logged in user. – Matthias Burger Oct 23 '16 at 21:12
  • 2
    And do you pass the concurrency token (`ConcurrencyStamp`) back to action? It is used to check if the model you tried to update hasn't been updated from a different action. if the token is different, the update fails (because the concurrency token will become part of the where clause) – Tseng Oct 23 '16 at 21:17
  • What does `IdentityUser<>` look like? – StriplingWarrior Oct 23 '16 at 21:18
  • @StriplingWarrior: https://github.com/aspnet/Identity/blob/dev/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityUser.cs, it's open source and part of the Identity framework – Tseng Oct 23 '16 at 21:19
  • @Tseng - I see, I should really have created a viewmodel and mapped it later on to the User-class... Mhhh. Yes, the ConcurrencyStamp was not mapped, so it was a different one.. Now with the viewmodel it works, since the ConcurrencyStamp is the same one. - Thanks for the hints :) – Matthias Burger Oct 23 '16 at 21:23
  • @MatthiasBurger: You probably want to *include* the `ConcurrencyStamp` on your model, keep track of it in the UI, and then map it back when saving. Otherwise, two users could edit the object concurrently. – StriplingWarrior Oct 23 '16 at 21:34
  • @StriplingWarrior, yes that's what I'm doing now :) (now, I understood what it does) – Matthias Burger Oct 23 '16 at 21:36

1 Answers1

3

The ConcurrencyStamp is there to prevent two people from making two different changes to the same user without being aware of one another's changes.

    /// <summary>
    /// A random value that must change whenever a user is persisted to the store
    /// </summary>
    public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString();

In principle, this should be assigned once when creating a user, and (I'm assuming) there should be a mechanism like a trigger in the database that causes the value to change whenever the entry in the database gets updated.

From this answer:

If you look at the implementation of IdentityDbContext in the OnModelCreating() method, you'll find:

builder.Entity<TUser>(b =>
{
....
    b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();
....

So Entity Framework is aware that this is supposed to be treated as a concurrency token. It checks the property's value along with the ID to ensure that the update fails if the user has been changed since that user object was loaded out of the database.

As to why the value isn't the same, that's something you'll probably have to figure out through debugging. Here are a few theories you might check:

  1. Maybe the user object really was changed since it got loaded? If the user object gets loaded twice with its original values and then changed twice independently, then this would happen.
  2. Maybe you're constructing the user object based on values passed into a web request, but you're not mapping the ConcurrencyStamp token based on the value that was there when you pulled it from the database, so it's keeping the randomly-generated GUID that it gets when it's first constructed?
Community
  • 1
  • 1
StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • 1
    it was nearly the second one. my view sent a new user-object with the updated data, that I tried to update to the database. Here was no `ConcurrencyStamp` set (mapped from the original object, since views are resetting the model), therefore a new ConcurrencyStamp was generated and for sure they didn't match. – Matthias Burger Oct 23 '16 at 21:35