はじめに
ドメイン駆動設計のパターンの一つである値オブジェクトを実践するようになってから、ASP.NET Core Identity でユーザーの ID の型 が string
なことに恐怖を感じるようになった。ユーザー ID を表す値オブジェクトじゃないと安心してコード書けない。
そういうわけで、ASP.NET Core Identity を使うときはユーザー ID の型に値オブジェクトを使うようにしているので、その手順をメモしておく。
ユーザー ID を表す値オブジェクトを構造体で作成
ASP.NET Core Identity の ID の型として使うためには、IEquatable<T>
を実装する必要がある。
#nullable enable using System; using System.Diagnostics.CodeAnalysis; namespace HelloIdentity.Models { public readonly struct UserId : IEquatable<UserId> { readonly string value; public UserId(string value) => this.value = value; public static UserId NewUserId() => new UserId(Guid.NewGuid().ToString()); public override string ToString() => value; public override int GetHashCode() => value.GetHashCode(); public override bool Equals(object? obj) { if (obj is UserId other) { return value == other.value; } return false; } public bool Equals([AllowNull] UserId other) => value == other.value; public static bool operator ==(UserId left, UserId right) => left.Equals(right); public static bool operator !=(UserId left, UserId right) => !(left == right); } }
IdentityDbContext から派生して ApplicationDbContext を作成
TKey
には先ほど作成した UserId
を指定する。データベースには文字列として保存するので、HasConversion
を使って UserId
と string
相互に変換するように指示。
#nullable enable using HelloIdentity.Models; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace HelloIdentity.Data { public class ApplicationDbContext : IdentityDbContext<IdentityUser<UserId>, IdentityRole<UserId>, UserId> { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<IdentityUser<UserId>>(b => { b.Property(x => x.Id) .HasConversion(x => x.ToString(), x => new UserId(x)); }); builder.Entity<IdentityRole<UserId>>(b => { b.Property(x => x.Id) .HasConversion(x => x.ToString(), x => new UserId(x)); }); } } }
UserStore から派生した ApplicationUserStore を作成
UserStore
はそのままだと内部で string
を UserId
に変換できず、実行時に例外が発生してしまう。
ConvertIdFromString
をオーバーライドして、UserId
に変換する処理を記述しておく。
あとついでに、新規登録時に新しい UserId
を割り当てるようにもしておく。
#nullable enable using System.Threading; using System.Threading.Tasks; using HelloIdentity.Data; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; namespace HelloIdentity.Models { public class ApplicationUserStore : UserStore<IdentityUser<UserId>, IdentityRole<UserId>, ApplicationDbContext, UserId> { public ApplicationUserStore(ApplicationDbContext context, IdentityErrorDescriber? describer = null) : base(context, describer) { } public override UserId ConvertIdFromString(string id) => new UserId(id); public override Task<IdentityResult> CreateAsync(IdentityUser<UserId> user, CancellationToken cancellationToken = default) { user.Id = UserId.NewUserId(); return base.CreateAsync(user, cancellationToken); } } }
Startup で ASP.NET Core Identity を構成
AddIdentity
でユーザーとロールの型を登録する際に、TKey
に UserId
を指定する。AddUserStore
で ApplicationUserStore
も忘れずに登録。
#nullable enable using HelloIdentity.Data; using HelloIdentity.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace HelloIdentity { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services .AddIdentity<IdentityUser<UserId>, IdentityRole<UserId>>( options => options.SignIn.RequireConfirmedAccount = false) .AddEntityFrameworkStores<ApplicationDbContext>() .AddUserStore<ApplicationUserStore>() .AddDefaultUI() .AddDefaultTokenProviders(); services.AddControllersWithViews(); services.AddRazorPages(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); } else { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); endpoints.MapRazorPages(); }); } } }
ASP.NET Core Identity の Default UI 修正
_LoginPartial.cshtml
で SignInManager<IdentityUser>
と UserManager<IdentityUser>
を記述している箇所があるので、SignInManager<IdentityUser<UserId>>
と UserManager<IdentityUser<UserId>>
に書き換える。
@using Microsoft.AspNetCore.Identity @inject SignInManager<IdentityUser<UserId>> SignInManager @inject UserManager<IdentityUser<UserId>> UserManager <ul class="navbar-nav"> @if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a> </li> <li class="nav-item"> <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })"> <button type="submit" class="nav-link btn btn-link text-dark">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a> </li> } </ul>
ASP.NET Core Identity でユーザー ID に値オブジェクトを使うための作業は以上。
おわりに
ASP.NET Core Identity のユーザー ID に string
ではなく値オブジェクトを使えるようになったので、
安心してコードが書けるようになった。めでたい。
IdentityRole<TKey>
の TKey
も UserId
になってしまうのが玉に瑕だけど、今のところ ID よりも名前を指定して取得することの方が多いので許容範囲内かと。