为什么我的 ASP.NET 身份的 Couchbase 自定义存储提供程序不持久更改?

Why does my Couchbase custom storage provider for ASP.NET Identity not persist changes?

上下文

我正在实施一个单点登录服务器,其中用户、角色、声明等将保留在 Couchbase 中。到目前为止,我的步骤是:

  1. 在 MVC 网络应用程序中实施 IdentityServer3(使用 v2.5.4 NuGet package)。
  2. 实施 IdentityServer3.AspNetIdentity 以使用身份作为用户和其他实体的后备存储(使用 v2.0.0 NuGet package)。
  3. 为简单的后端用户管理实施 IdentityManager (1.0.0-beta5-5 NuGet package) and IdentityManager.AspNetIdentity (1.0.0-beta5-1 NuGet package)UI。
  4. 实施已撤回的 couchbase-aspnet-identity Identity custom storage provider from Couchbase Labs. This is included as source (from GitHub) in my project as it was only ever released to NuGet as an incomplete developer preview

执行步骤 1-3 后一切正常。用户通常通过 EF 存储在 LocalDB 实例中 AspNetUsers table(IdentityServer3.AspNetIdentity 的默认实现),我可以使用 IdentityManager UI 添加、编辑和编辑删除用户。

第 4 步涉及 implementing custom store classes which are basically taken straight from the couchbase-aspnet-identity project on GitHub with some minor local changes (e.g. implementing the UserStore.Users getter which throws NotImplementedException in the original code)。

问题

现在,当我创建一个用户时,它会按预期存储在 Couchbase 中。如果我编辑添加到我的自定义 ApplicationUser class(例如 FirstNameAge)的自定义字段,更改会正确保留。到目前为止一切顺利。

但是,如果我编辑用户的密码、电子邮件地址或 phone 号码(在 IdentityManager UI 中),会发生以下情况:

单步执行代码我发现对我的 UserStore.UpdateAsync 方法进行了多次调用:

public async Task UpdateAsync(T user)
{
    await _bucket.UpdateAsync(user.Id, user);
}

每次调用时,用户都会正确保存到 Couchbase。所以实际上问题不在于更改根本没有持久化,而是它们被持久化然后被原始值覆盖。

例如,如果我更改用户的 phone 号码,该方法将被调用三次。将它从空改为123,我的user对象中的相关字段如下:

呼叫 1

呼叫 2

呼叫 3

(对象每次也有不同的 SecurityStamp 值,但对象值在其他方面是相同的。)

当我更改电子邮件地址或密码时,同样的事情发生了。 (对于密码,只有两次调用,但在每次调用中,最后一次调用都会将所有字段重置为其原始值。)

问题

是什么导致了对 UserStore.UpdateAsync() 的额外调用,我该如何解决?

不幸的是,我无法在调用堆栈中走得更高:我认为 UserStore 是从 UserManager 调用的,它是 Microsoft.AspNet.Identity.Core 的一部分,对此我没有有来源。

额外的细节

我的 UserStore class 实现了讨论的所有可选接口 here,即它看起来像这样:

public class UserStore<T> :
    IUserLoginStore<T>,
    IUserClaimStore<T>,
    IUserRoleStore<T>,
    IUserSecurityStampStore<T>,
    IQueryableUserStore<T>,
    IUserPasswordStore<T>,
    IUserPhoneNumberStore<T>,
    IUserStore<T>,
    IUserLockoutStore<T, string>,
    IUserTwoFactorStore<T, string>,
    IUserEmailStore<T>
    where T : IdentityUser
{
    // ...
}

这是我刚刚将 phone 数字更改为 123 时 IdentityManager UI 的屏幕截图。(请注意成功消息,但 Phone 字段为空。)

我设法弄清楚了。它适用于默认的 EF 实现(我的问题中的第 3 步),但当我将其换成 Couchbase 存储提供程序时失败了,这意味着我最初怀疑 Couchbase 提供程序有问题(尤其是因为它基于开发人员预览代码)。

实际上问题出在 IdentityManager.AspNetIdentity 包中,特别是方法 AspNetIdentityManagerService.SetUserPropertyAsync():

public virtual async Task<IdentityManagerResult> SetUserPropertyAsync(string subject, string type, string value)
{
    TUserKey key = ConvertUserSubjectToKey(subject);
    var user = await this.userManager.FindByIdAsync(key);

    // [...]

    var metadata = await GetMetadataAsync();
    var propResult = SetUserProperty(metadata.UserMetadata.UpdateProperties, user, type, value);
    if (!propResult.IsSuccess)
    {
        return propResult;
    }

    var result = await userManager.UpdateAsync(user);
    if (!result.Succeeded)
    {
        return new IdentityManagerResult(result.Errors.ToArray());
    }

    return IdentityManagerResult.Success;
}

如果你在 SetUserProperty() 调用中走得足够远,你会到达 AspNetIdentityManagerService.SetPhone(),它看起来像这样:

public virtual IdentityManagerResult SetPhone(TUser user, string phone)
{
    var result = this.userManager.SetPhoneNumber(user.Id, phone);

    // [...]
}

因为我们只将用户 ID 传递给 SetPhoneNumber()UserManagerUserStore 询问用户的另一个实例(通过调用 UserStore.FindByIdAsync() ),因此 PhoneNumber 属性 永远不会在传递到 SetUserProperty() 的实例上更新。因此,当我们在 SetUserPropertyAsync() 中到达对 userManager.UpdateAsync(user) 的调用时,我们传入了一个没有应用更改的陈旧对象。

据推测,EF 确保此处使用的两个实例是同一个实例,但其他提供商不会。我将 Couchbase 提供程序与其他更成熟的实现(例如 AspNet.Identity.Mongo)进行了比较,Couchbase 代码看起来不错。

我 运行 遇到了同样的问题,这个线程帮助我理解了为什么 IdentityManager 多次进行此调用。我也在将我的 DAL 换成非 sql 商店。

我的解决方案有些相似。我在更新后将用户模型存储在临时缓存中,此外,我使用类型为 Guid 的实例级变量作为缓存键的 t运行sactionId。我还将我的 UserManager 生命周期更改为 InstancePerHttpRequest,以确保对 Update 方法进行的两个并发调用(在本例中)来自相同的 http 请求,因此将共享相同的 t运行sactionId。我不会 运行 进入后续 http 请求和使用缓存的任何竞争条件。