Search code examples
c#asp.net-coreentity-framework-coreasp.net-identityazure-cosmosdb

EF Core with Cosmos DB provider, UserManager AddLoginAsync gives ConcurrencyFailure


Creating a new user, fetching the user again with usermanager for testing, and then using the method AddLoginAsync with the recently fetched user gives the error

ConcurrencyFailure, Optimistic concurrency failure, object has been modified.

When fetching the user the "ConcurrencyStamp" has the correct etag, but after the "AddLoginAsync" I can see the user object has an invalid etag, the ConcurrencyStamp is a GUID.

I have followed the documentation and added this to the IdentityUser model configuration:

//builder: EntityTypeBuilder<ApplicationUser>
builder.Property(d => d.ConcurrencyStamp)
       .IsETagConcurrency();

Startup:

services.AddDbContextPool<ApplicationDbContext>(option =>
{
    var connectionConfig = new DbConnectionString("DefaultConnection", Configuration);
    option.UseCosmos(connectionConfig.ConnectionString, connectionConfig.Database);
});

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

In the Cosmos DB Emulator I can see two documents created:

{
    "Id": "b66b520e-60d2-4ba3-8347-6c168be9a1e5",
    "AccessFailedCount": 0,
    "_etag": "\"00000000-0000-0000-d54b-cffa72ea01d6\"",
    "Created": "2020-12-18T14:41:03.962769Z",
    "Discriminator": "ApplicationUser",
    "Email": "@email.com",
    "EmailConfirmed": false,
    "Encounters": 0,
    "LockoutEnabled": true,
    "LockoutEnd": null,
    "Nickname": null,
    "NormalizedEmail": "@EMAIL.COM",
    "NormalizedUserName": "@EMAIL.COM",
    "OrderNum": 0,
    "PasswordHash": null,
    "PhoneNumber": null,
    "PhoneNumberConfirmed": false,
    "SecurityStamp": "PPFUV4JXJ72KBYNXBPPV3FVJMIG",
    "TwoFactorEnabled": false,
    "UserName": "@email.com",
    "id": "ApplicationUser|b66b520e-60d2-4ba3-8347-6c168be9a1e5",
    "_rid": "VA5JAKKvHtQNAAAAAAAAAA==",
    "_self": "dbs/VA5JAA==/colls/VA5JAKKvHtQ=/docs/VA5JAKKvHtQNAAAAAAAAAA==/",
    "_attachments": "attachments/",
    "_ts": 1608302464 }

{
    "LoginProvider": "Microsoft",
    "ProviderKey": "***",
    "Discriminator": "IdentityUserLogin<string>",
    "ProviderDisplayName": "Microsoft",
    "UserId": "b66b520e-60d2-4ba3-8347-6c168be9a1e5",
    "id": "IdentityUserLogin<string>|Microsoft|***",
    "_rid": "VA5JAKKvHtQOAAAAAAAAAA==",
    "_self": "dbs/VA5JAA==/colls/VA5JAKKvHtQ=/docs/VA5JAKKvHtQOAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-d54b-d4ad146f01d6\"",
    "_attachments": "attachments/",
    "_ts": 1608302472
}

How to fix the issue?


Solution

  • I could fix the issue by creating a new UserStore,

       public class ApplicationUserStore 
            : UserStore<ApplicationUser,ApplicationRole,ApplicationDbContext>
        {
            public ApplicationUserStore(ApplicationDbContext context, IdentityErrorDescriber describer = null)
                : base(context, describer)
            {
            }
    
            public override async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                ThrowIfDisposed();
                if (user == null)
                {
                    throw new ArgumentNullException(nameof(user));
                }
    
                Context.Attach(user);
                Context.Update(user);
                try
                {
                    await SaveChanges(cancellationToken);
                }
                catch (DbUpdateConcurrencyException)
                {
                    return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
                }
    
                return IdentityResult.Success;
            }
        }
    

    But I am now facing another issue when using signinmanager:

    InvalidOperationException: The LINQ expression 'DbSet<IdentityUserRole>() .Join( inner: DbSet(), outerKeySelector: i => i.RoleId, innerKeySelector: a => a.Id, resultSelector: (i, a) => new TransparentIdentifier<IdentityUserRole, ApplicationRole>( Outer = i, Inner = a ))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

    I found the solution by overriding another method in the UserStore:

    public override async Task<IList<string>> GetRolesAsync(ApplicationUser user, CancellationToken cancellationToken = default)
    {
        string userId = user.Id;
        var roleIds = await Context.UserRoles.Where(ur => ur.UserId == userId)
            .Select(ur => ur.RoleId)
            .ToArrayAsync();
    
        return await Context.Roles.Where(r => roleIds.Contains(r.Id))
            .Select(r => r.Name)
            .ToListAsync();
    }