ASP.NET Core Identity ロール自動作成(シード)で「IdentityUserLogin に主キーが必要」例外を解決する方法

ASP.NET Core Identity でログインは動くのに、ロールを自動投入しようとした途端「IdentityUserLogin に主キーが必要」例外で登録が失敗することがあります。原因は多くの場合、Identity の関連テーブルから主キー設定を消してしまっていること。本記事では正しいロールのシード方法と落とし穴を整理します。

目次

症状:ロールをシードしたいだけなのに、登録処理が落ちる

ASP.NET Core Identity を導入すると、ユーザー登録・ログイン・パスワードリセットなどの基本機能は比較的スムーズに動きます。ところが運用を始めると「管理者(Admin)ロールを最初から用意したい」「権限ごとにページを分けたい」といった要件がすぐに出てきます。

そこで OnModelCreating() で IdentityRole.HasData(...) を使い、マイグレーションのシード機能で AspNetRoles にロールを投入しようとした瞬間、次のような例外に遭遇するケースがあります。

System.InvalidOperationException: The entity type 'IdentityUserLogin' requires a primary key to be defined

一見すると「Role を追加しただけなのに、なぜ UserLogin が?」と混乱しますが、原因はロールそのものではなく、Identity の内部テーブル設定(主キー定義)を壊してしまっていることがほとんどです。

例外メッセージの意味:IdentityUserLogin は“キーが前提”のエンティティ

Entity Framework Core(EF Core)では、通常のエンティティは主キーを持つことが前提です。主キーがあることで「どの行を更新するか」「同じ行を二重に追加していないか」「変更追跡(Tracking)をどう行うか」を判別できます。

そして ASP.NET Core Identity は、ユーザー・ロールだけでなく、外部ログイン(Google / Microsoft など)やトークン、ユーザーとロールの関連付けを複数のテーブルで管理します。これらの多くは複合主キーで構成され、キーが消えると登録やサインイン処理が成立しません。

Identity テーブル(代表)主キー(典型例)用途
AspNetUserLogins(LoginProvider, ProviderKey)外部ログインの識別(どのプロバイダのどのIDか)
AspNetUserRoles(UserId, RoleId)ユーザーとロールの関連付け(多対多)
AspNetUserTokens(UserId, LoginProvider, Name)リフレッシュトークン等の管理(キーで一意に識別)
AspNetUsersIdユーザー本体
AspNetRolesIdロール本体

このため、IdentityUserLogin に「主キーが必要」と言われたら、まず疑うべきはIdentity のモデル定義を上書きしてキーが消えたパターンです。

根本原因:Identity 関連エンティティに HasNoKey を付けてしまっている

例外の直接原因としてありがちなのが、次のような設定です。

  • 何らかの理由で OnModelCreating() を拡張し、Identity のテーブル(UserLogin / UserRole / UserToken など)に HasNoKey() を付けた
  • OnModelCreating() を override したが base.OnModelCreating(builder) を呼ばず、Identity 側のキー定義が丸ごと失われた

結論:Identity の標準テーブルに対して HasNoKey() を付けて主キーを消すのは NG です。キー無し(Keyless)エンティティは読み取り専用のビュー用途などに限定され、Identity のように頻繁に Insert/Update が発生する仕組みには適しません。

NG例(やらない)

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);


// ❌ Identity の内部テーブルからキーを消してしまう
builder.Entity<IdentityUserLogin<string>>().HasNoKey();
builder.Entity<IdentityUserRole<string>>().HasNoKey();
builder.Entity<IdentityUserToken<string>>().HasNoKey();


}

修正(まずは元に戻す)

protected override void OnModelCreating(ModelBuilder builder)
{
    // ✅ まず Identity の既定設定を適用
    base.OnModelCreating(builder);


// ✅ 追加の独自設定は “Identity のキー定義を壊さない範囲” で行う
// 例:独自エンティティの設定、インデックス追加など


}

この修正だけで、ユーザー登録・ログインで落ちる問題は大抵解消します。次に「ロールを自動投入する」実装へ進みます。

推奨アプローチ:RoleManager を使って起動時にロールをシードする

ロール投入には大きく2つの方法があります。

  • EF Core の HasData() によるマイグレーションシード
  • RoleManager(Identity API)による起動時シード

運用で壊れにくいのは、RoleManager で「存在確認 → なければ作成」する方法です。理由は次のとおりです。

  • 正規化(NormalizedName)や ConcurrencyStamp など、Identity が期待する値を API 側が自然に揃えてくれる
  • マイグレーションのスナップショットや Insert 固定値に依存しないため、環境差分に強い
  • アプリ起動時に必ず整備されるので、DB初期化や復元後も再現性が高い
観点HasData(マイグレーションシード)RoleManager(起動時シード)
安全性値の入れ忘れや更新で事故りやすいIdentity API が整合性を担保しやすい
運用マイグレーション適用が前提起動さえすれば整う(ただし権限管理は必要)
変更に強いロール名変更は migration 追加が必要になりがちコード側のリスト変更で追従しやすい
向いているケースマスタデータを DB で厳密管理したいまず確実にロールを作りたい(ほとんどのWebアプリ)

実装:AddRoles と RoleManager でロールを投入する

Identity でロール機能を有効化する

AddDefaultIdentity を使っている場合、ロールを使うには AddRoles<IdentityRole>() を追加します。これを忘れると RoleManager が DI から解決できず、起動時シードが書けません。

builder.Services
    .AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

SeedData:存在確認してなければ作成

最小構成はシンプルですが、実運用では複数ロールをまとめて作る・作成結果をログに残す・失敗時に例外を投げる、などの工夫を入れるとトラブルシュートが楽になります。

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;

public static class SeedData
{
    private static readonly string[] DefaultRoles = new[]
    {
        "Admin",
        "Manager",
        "User"
    };

    public static async Task InitializeAsync(IServiceProvider services)
    {
        var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();

        foreach (var roleName in DefaultRoles)
        {
            // すでにあれば何もしない(冪等)
            if (await roleManager.RoleExistsAsync(roleName))
            {
                continue;
            }

            var result = await roleManager.CreateAsync(new IdentityRole(roleName));

            if (!result.Succeeded)
            {
                var errors = string.Join(", ", result.Errors.Select(e => $"{e.Code}:{e.Description}"));
                throw new InvalidOperationException($"Failed to create role '{roleName}'. {errors}");
            }
        }
    }
}

Program.cs:app.Build() の後にスコープを作って実行

Identity の RoleManager はスコープサービスなので、起動時シードでは CreateScope()(または CreateAsyncScope())で DI スコープを作ってから呼び出します。

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    await SeedData.InitializeAsync(scope.ServiceProvider);
}

app.Run();

この形なら、ロールが存在しない状態でデプロイしても、起動と同時に AspNetRoles が整備されます。

発展:初期管理者ユーザーも自動作成してロールを付与する

ロールだけ作れても、最初の管理者アカウントが作れず詰まることがあります。開発・検証環境では、初期管理者ユーザーを自動作成して Admin を付与しておくと便利です(本番では運用ポリシーに合わせて無効化してください)。

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public static class SeedAdminUser
{
    public static async Task InitializeAsync(IServiceProvider services, IConfiguration config)
    {
        var userManager = services.GetRequiredService<UserManager<IdentityUser>>();
        var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();

        var email = config["SeedAdmin:Email"];
        var password = config["SeedAdmin:Password"];

        if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password))
        {
            // 設定がないなら作らない(環境で制御しやすい)
            return;
        }

        // Admin ロールが無ければ作成
        if (!await roleManager.RoleExistsAsync("Admin"))
        {
            var roleResult = await roleManager.CreateAsync(new IdentityRole("Admin"));
            if (!roleResult.Succeeded)
            {
                var errors = string.Join(", ", roleResult.Errors.Select(e => e.Description));
                throw new InvalidOperationException($"Failed to create Admin role. {errors}");
            }
        }

        var user = await userManager.FindByEmailAsync(email);
        if (user is null)
        {
            user = new IdentityUser
            {
                UserName = email,
                Email = email,
                EmailConfirmed = true
            };

            var createResult = await userManager.CreateAsync(user, password);
            if (!createResult.Succeeded)
            {
                var errors = string.Join(", ", createResult.Errors.Select(e => e.Description));
                throw new InvalidOperationException($"Failed to create admin user. {errors}");
            }
        }

        // Admin ロール付与(すでに付いていれば何もしない)
        if (!await userManager.IsInRoleAsync(user, "Admin"))
        {
            var addRoleResult = await userManager.AddToRoleAsync(user, "Admin");
            if (!addRoleResult.Succeeded)
            {
                var errors = string.Join(", ", addRoleResult.Errors.Select(e => e.Description));
                throw new InvalidOperationException($"Failed to add Admin role to user. {errors}");
            }
        }
    }
}

appsettings.Development.json に次のような設定を置いておけば、開発環境でだけ初期管理者を作れます。

{
  "SeedAdmin": {
    "Email": "[email protected]",
    "Password": "ChangeMe_123!"
  }
}

そして Program.cs 側でロールのシード後に呼び出します。

using (var scope = app.Services.CreateScope())
{
    await SeedData.InitializeAsync(scope.ServiceProvider);
    await SeedAdminUser.InitializeAsync(scope.ServiceProvider, app.Configuration);
}

どうしても HasData でロールを入れたい場合のチェックポイント

チーム方針や監査要件で「マイグレーションでロールも確実に投入したい」こともあります。その場合でも、Identity のキー定義は壊さず、ロールデータを正しく用意してください。特に落とし穴になりやすいのが NormalizedName と ConcurrencyStamp です。

列意味注意点
Idロールの主キー固定値にする(GUID文字列など)。毎回変えると差分が出続ける
Name表示名(ロール名)認可属性 [Authorize(Roles="...")] と一致させる
NormalizedName正規化名一般に大文字化(例:ADMIN)。空や不一致は検索に影響
ConcurrencyStamp同時実行制御用任意の文字列で良いが、未設定だと更新時の挙動が揺れることがある

例として、OnModelCreating() で次のように投入します。

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Entity<IdentityRole>().HasData(
        new IdentityRole
        {
            Id = "3f7e3c9e-2b2e-4b8a-9a9c-7b7c2a0d5c01",
            Name = "Admin",
            NormalizedName = "ADMIN",
            ConcurrencyStamp = "1"
        },
        new IdentityRole
        {
            Id = "d9c0e7a3-6a09-4d6f-9b2d-3b0c5f4c9c02",
            Name = "User",
            NormalizedName = "USER",
            ConcurrencyStamp = "1"
        }
    );
}

重要:この方法は「マイグレーションを追加した時点のデータ」を差分管理するため、ロール追加・変更のたびにマイグレーションが増えます。運用での柔軟性を優先するなら、やはり RoleManager シードの方が扱いやすいです。

OnModelCreating を拡張するときの安全策

Identity を使う DbContext を拡張する場合、次の原則を守るだけで事故率が大きく下がります。

  • 最初に必ず base.OnModelCreating(builder) を呼ぶ(Identity のキー・インデックス・制約を確実に適用)
  • Identity 既定テーブル(AspNetUsers 等)への変更は最小限にする
  • 独自テーブルの設定は Identity 設定の後にまとめる
  • 「ビュー用にキー無しで読みたい」場合は、別 DbContext や別エンティティで扱う(Identity エンティティを keyless にしない)
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        // ✅ Identity の既定マッピングを壊さない
        base.OnModelCreating(builder);

        // ✅ ここから下にアプリ独自の設定を書く
        // builder.Entity<YourEntity>()....
    }
}

よくある落とし穴と確認ポイント

最後に、現場でハマりやすいポイントを「症状→原因→対処」にまとめます。

症状原因の例対処
IdentityUserLogin requires a primary keyOnModelCreating で base を呼んでいない/Identity テーブルに HasNoKey を付けたbase.OnModelCreating を呼ぶ。HasNoKey を削除
RoleManager が解決できないAddRoles を追加していないservices.AddDefaultIdentity(…).AddRoles(…).AddEntityFrameworkStores(…)
同名ロール作成で失敗するシードが冪等になっていないRoleExistsAsync / FindByNameAsync で存在確認する
HasData で投入したのに認可が効かないNormalizedName が空、または大文字化されていないNormalizedName を適切に設定(例:ADMIN)
環境によってロールが増減するマイグレーション適用漏れ、DB初期化手順の差起動時シードで揃える/CI/CDで migration を統一

運用で安定させるためのベストプラクティス

  • シードは必ず冪等にする:「なければ作る」だけにし、毎回削除・再作成はしない
  • 失敗時は握りつぶさない:ロール作成失敗は認可全体に影響するため例外を投げて気づけるようにする
  • ログを出す:初期化で何が作られたかが分かると、障害時に復旧が早い
  • 本番の初期管理者は慎重に:起動時に固定パスワードを入れる方式は避け、Secrets 管理や手動登録フローも検討する

まとめ:Identity のキーを守り、RoleManager で堅実にロールを用意する

「IdentityUserLogin に主キーが必要」という例外は、ロール投入が原因に見えて、実際は Identity のモデル定義(特に主キー)を壊していることが引き金になっている場合が大半です。

まずは HasNoKey() を削除し、base.OnModelCreating(builder) を確実に呼んで Identity の既定設定を守ってください。その上でロールは RoleManager を使い、アプリ起動時に「存在確認→作成」を行うと、環境差分にも強く、運用での事故を最小化できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次