ASP.NET Core MVC にて、Entity Frameworkを使わずにASP.NET Identityを利用する Part.2

前回のあらすじ

IUserStoreが必要やで。

mrgchr.hatenablog.com

IUserStoreを実装する

IUserStoreインタフェースを実装することで、UserManagerにてユーザーの作成、読み取り、更新、削除など、いわゆるCRUDを利用可能になります。

また、併せてIUserXxxStoreを実装することで、UserManagerがサポートする機能を追加することができます。
今回はパスワード機能を追加したいので、IUserPasswordStoreを併せて実装します。

IUserPasswordStoreのほかにも、IUserTwoFactorStoreや、IUserLockOutStore、IUserSecurityStampStoreなど色々とあります。

public class InMemoryUserStore :
  IUserStore<ApplicationUser>,
  IUserPasswordStore<ApplicationUser>
{
  private static ConcurrentDictionary<Guid, ApplicationUser> Users { get; } = new ConcurrentDictionary<Guid, ApplicationUser>();

  private static IdentityErrorDescriber IdentityErrorDescriber = new IdentityErrorDescriber();

  public Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    var result = Users.TryAdd(user.Id, user);

    if(!result)
    {
      return Task.FromResult(IdentityResult.Failed(IdentityErrorDescriber.ConcurrencyFailure()));
    }

    return Task.FromResult(IdentityResult.Success);
  }

  public Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    ApplicationUser removedUser;
    var result = Users.TryRemove(user.Id, out removedUser);

    if (!result)
    {
      return Task.FromResult(IdentityResult.Failed(IdentityErrorDescriber.ConcurrencyFailure()));
    }

    return Task.FromResult(IdentityResult.Success);
  }

  public Task<ApplicationUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (string.IsNullOrEmpty(userId)) throw new ArgumentNullException(nameof(userId));

    Guid id = Guid.Parse(userId);

    ApplicationUser user;
    return Task.FromResult(Users.TryGetValue(id, out user) ? user : null);
  }

  public Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (string.IsNullOrEmpty(normalizedUserName)) throw new ArgumentNullException(nameof(normalizedUserName));

    return Task.FromResult(Users.Values.FirstOrDefault(u => u.NormalizedLoginName.Equals(normalizedUserName)));
  }

  public Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    return Task.FromResult(user.NormalizedLoginName);
  }

  public Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    return Task.FromResult(user.UserId);
  }

  public Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    return Task.FromResult(user.LoginName);
  }

  public Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    user.LoginName = userName;
    return Task.CompletedTask;
  }

  public Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));
      
    user.NormalizedLoginName = normalizedName;
    return Task.CompletedTask;
  }

  public async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    var existingUser = await FindByIdAsync(user.UserId, cancellationToken);

    var result = Users.TryUpdate(user.Id, user, existingUser);

    if (!result)
    {
      return IdentityResult.Failed(IdentityErrorDescriber.ConcurrencyFailure());
    }

    return IdentityResult.Success;
  }

  public Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    return Task.FromResult(user.PasswordHash != null);
  }

  public Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    return Task.FromResult(user.PasswordHash);
  }

  public Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) throw new ArgumentNullException(nameof(user));

    user.PasswordHash = passwordHash;
    return Task.CompletedTask;
  }

  public void Dispose()
  {
    _disposed = true;
  }

  protected void ThrowIfDisposed()
  {
    if (_disposed)
    {
      throw new ObjectDisposedException(GetType().Name);
    }
  }

  private bool _disposed;
}

今回は外部RDBを用いずにインメモリオブジェクトにてユーザー管理をするので、private staticオブジェクトで管理します。
GetUserIdAsyncがstringを返さないといけないので、前回のUserIdプロパティを用いました。無くても良かったなと思いました。

ちなみに、同様にIRoleStoreを実装しないとSignInManaer利用時にエラーが出ます。
IRoleStore自体は今回は全く利用しないのですが。

public class InMemoryRoleStore : IRoleStore<ApplicationRole>
{
  public void Dispose()
  {
  }

  public Task<IdentityResult> CreateAsync(ApplicationRole role, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task<IdentityResult> DeleteAsync(ApplicationRole role, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task<ApplicationRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task<ApplicationRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task<string> GetNormalizedRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task<string> GetRoleIdAsync(ApplicationRole role, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task<string> GetRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task SetNormalizedRoleNameAsync(ApplicationRole role, string normalizedName, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task SetRoleNameAsync(ApplicationRole role, string roleName, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }

  public Task<IdentityResult> UpdateAsync(ApplicationRole role, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }
}

これらをStarup.csのConfigureServicesに登録して準備完了です。

//Starup.cs ConfigureServices

services.AddTransient<IUserStore<ApplicationUser>, InMemoryUserStore>();
services.AddTransient<IRoleStore<ApplicationRole>, InMemoryRoleStore>();

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

使ってみよう

では使ってみましょう。
HomeControllerに下記のように追加しました。
ViewModelやViewを作成するのが面倒だったのですべてGetメソッドで実装しました。

public async Task<IActionResult> Register([FromServices]UserManager<ApplicationUser> userManager)
{
  var user = new ApplicationUser { LoginName = "foo", Email = "foo@bar.com", ScreenName="Foo Bar" };
  var password = "Pa$$w0rd";
  var result = await userManager.CreateAsync(user, password);

  return View("Index");
}

public async Task<IActionResult> LogIn([FromServices]SignInManager<ApplicationUser> signInManager)
{
  var loginName = "foo";
  var password = "Pa$$w0rd";
  var result = await signInManager.PasswordSignInAsync(loginName, password, false, false);

  return View("Index");
}

public async Task<IActionResult> LogOut([FromServices]SignInManager<ApplicationUser> signInManager)
{
  await signInManager.SignOutAsync();
  return View("Index");
}

Registerメソッドにブレイクポイントを貼ってから、/Home/Registerにアクセスすると、初回は下記のようにユーザーの作成に成功します。

f:id:mrgchr:20161127213441p:plain

ですが、2回目にアクセスすると、下記のようにDuplicatedUserNameと失敗します。

f:id:mrgchr:20161127213519p:plain

/Home/Registerにアクセスした後に、/Home/LogInにアクセスすると、ログインに成功します。

ここでパスワードを変えてからアクセスすると、下記のようにログインに失敗します。

f:id:mrgchr:20161127213659p:plain

また、/Home/Registerでユーザー登録する前にログインしても当然失敗します。

今日はここまで

次回はログイン後のユーザー情報取得などについて書きます。