ASP.NET Core 中的 Identity 模型自訂

作者:Arthur Vickers

ASP.NET Core Identity 提供一個架構,用於管理和儲存 ASP.NET Core 應用程式中的使用者帳戶。 當選取 [個別使用者帳戶] 作為驗證機制時,Identity 會新增至您的專案。 根據預設,Identity 會使用 Entity Framework (EF) Core 資料模型。 本文說明如何自訂 Identity 模型。

Identity 和 EF Core 移轉

在檢查模型之前,了解移轉 Identity 如何搭配 EF Core 移轉 來建立和更新資料庫會很有用。 進階程序為:

  1. 定義或更新程式碼中的資料模型
  2. 新增移轉,將此模型轉譯成可套用至資料庫的變更。
  3. 檢查移轉是否正確代表您的意圖。
  4. 套用移轉以更新要與模型同步的資料庫。
  5. 重複步驟 1 到 4,進一步調整模型,並讓資料庫保持同步。

使用下列其中一種方法來新增和套用移轉:

  • 如果使用的是 Visual Studio,請使用 [套件管理員主控台] (PMC) 視窗。 如需詳細資訊,請參閱 EF Core PMC 工具
  • 如果使用命令列,請使用 .NET Core CLI。 如需詳細資訊,請參閱 EF Core .NET 命令列工具
  • 在應用程式執行時,按一下錯誤頁面上的 [套用移轉] 按鈕。

ASP.NET Core 具有開發時間錯誤頁面處理常式。 處理常式可以在執行應用程式時套用移轉。 生產應用程式通常會從移轉產生 SQL 指令碼,並在受控制的應用程式和資料庫部署過程中部署資料庫變更。

在建立使用 Identity 的新應用程式時,上述步驟 1 和 2 已經完成。 也就是說,初始資料模型已經存在,且初始移轉已新增至專案。 仍然需要將初始移轉套用至資料庫。 可透過下列其中一種方法套用初始移轉:

  • 在 PMC 中執行 Update-Database
  • 在命令殼層中執行 dotnet ef database update
  • 在應用程式執行時,按一下錯誤頁面上的 [套用移轉] 按鈕。

對模型進行變更時,請重複前面的步驟。

Identity 模型

實體類型

Identity 模型包含下列實體類型。

實體類型 描述
User 代表使用者。
Role 代表角色。
UserClaim 代表使用者擁有的宣告。
UserToken 代表使用者的驗證權杖。
UserLogin 將使用者與登入產生關聯。
RoleClaim 代表授與角色內所有使用者的宣告。
UserRole 關聯使用者和角色的聯結實體。

實體類型關聯性

實體類型會以下列方式彼此相關:

  • 每個 User 可以擁有許多 UserClaims
  • 每個 User 可以擁有許多 UserLogins
  • 每個 User 可以擁有許多 UserTokens
  • 每個 Role 可以擁有許多相關聯的 RoleClaims
  • 每個 User 可以擁有許多相關聯的 Roles,而且每個 Role 可以與許多 Users 相關聯。 這就是多對多關聯性,它需要資料庫中的聯結資料表。 聯結資料表是由 UserRole 實體表示。

預設模型設定

Identity 定義繼承自 DbContext內容類別,以設定和使用模型。 可在內容類別的 OnModelCreating 方法中使用 EF Core Code First Fluent API 來完成此設定。 預設設定為:

builder.Entity<TUser>(b =>
{
    // Primary key
    b.HasKey(u => u.Id);

    // Indexes for "normalized" username and email, to allow efficient lookups
    b.HasIndex(u => u.NormalizedUserName).HasName("UserNameIndex").IsUnique();
    b.HasIndex(u => u.NormalizedEmail).HasName("EmailIndex");

    // Maps to the AspNetUsers table
    b.ToTable("AspNetUsers");

    // A concurrency token for use with the optimistic concurrency checking
    b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();

    // Limit the size of columns to use efficient database types
    b.Property(u => u.UserName).HasMaxLength(256);
    b.Property(u => u.NormalizedUserName).HasMaxLength(256);
    b.Property(u => u.Email).HasMaxLength(256);
    b.Property(u => u.NormalizedEmail).HasMaxLength(256);

    // The relationships between User and other entity types
    // Note that these relationships are configured with no navigation properties

    // Each User can have many UserClaims
    b.HasMany<TUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();

    // Each User can have many UserLogins
    b.HasMany<TUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();

    // Each User can have many UserTokens
    b.HasMany<TUserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();

    // Each User can have many entries in the UserRole join table
    b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
});

builder.Entity<TUserClaim>(b =>
{
    // Primary key
    b.HasKey(uc => uc.Id);

    // Maps to the AspNetUserClaims table
    b.ToTable("AspNetUserClaims");
});

builder.Entity<TUserLogin>(b =>
{
    // Composite primary key consisting of the LoginProvider and the key to use
    // with that provider
    b.HasKey(l => new { l.LoginProvider, l.ProviderKey });

    // Limit the size of the composite key columns due to common DB restrictions
    b.Property(l => l.LoginProvider).HasMaxLength(128);
    b.Property(l => l.ProviderKey).HasMaxLength(128);

    // Maps to the AspNetUserLogins table
    b.ToTable("AspNetUserLogins");
});

builder.Entity<TUserToken>(b =>
{
    // Composite primary key consisting of the UserId, LoginProvider and Name
    b.HasKey(t => new { t.UserId, t.LoginProvider, t.Name });

    // Limit the size of the composite key columns due to common DB restrictions
    b.Property(t => t.LoginProvider).HasMaxLength(maxKeyLength);
    b.Property(t => t.Name).HasMaxLength(maxKeyLength);

    // Maps to the AspNetUserTokens table
    b.ToTable("AspNetUserTokens");
});

builder.Entity<TRole>(b =>
{
    // Primary key
    b.HasKey(r => r.Id);

    // Index for "normalized" role name to allow efficient lookups
    b.HasIndex(r => r.NormalizedName).HasName("RoleNameIndex").IsUnique();

    // Maps to the AspNetRoles table
    b.ToTable("AspNetRoles");

    // A concurrency token for use with the optimistic concurrency checking
    b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();

    // Limit the size of columns to use efficient database types
    b.Property(u => u.Name).HasMaxLength(256);
    b.Property(u => u.NormalizedName).HasMaxLength(256);

    // The relationships between Role and other entity types
    // Note that these relationships are configured with no navigation properties

    // Each Role can have many entries in the UserRole join table
    b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();

    // Each Role can have many associated RoleClaims
    b.HasMany<TRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
});

builder.Entity<TRoleClaim>(b =>
{
    // Primary key
    b.HasKey(rc => rc.Id);

    // Maps to the AspNetRoleClaims table
    b.ToTable("AspNetRoleClaims");
});

builder.Entity<TUserRole>(b =>
{
    // Primary key
    b.HasKey(r => new { r.UserId, r.RoleId });

    // Maps to the AspNetUserRoles table
    b.ToTable("AspNetUserRoles");
});

模型泛型型別

Identity 會針對上面所列的每個實體類型,定義預設 Common Language Runtime (CLR) 型別。 這些型別前面都會有 Identity

  • IdentityUser
  • IdentityRole
  • IdentityUserClaim
  • IdentityUserToken
  • IdentityUserLogin
  • IdentityRoleClaim
  • IdentityUserRole

無法直接使用這些型別,而是可以做為應用程式本身型別的基底類別來使用。 Identity 所定義的 DbContext 型別是泛型,因此不同的 CLR 型別可用於模型中的一或多個實體類型。 這些泛型型別也允許變更 User 主索引鍵 (PK) 資料類型。

搭配角色支援使用 Identity 時,應該使用 IdentityDbContext 類別。 例如:

// Uses all the built-in Identity types
// Uses `string` as the key type
public class IdentityDbContext
    : IdentityDbContext<IdentityUser, IdentityRole, string>
{
}

// Uses the built-in Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityDbContext<TUser>
    : IdentityDbContext<TUser, IdentityRole, string>
        where TUser : IdentityUser
{
}

// Uses the built-in Identity types except with custom User and Role types
// The key type is defined by TKey
public class IdentityDbContext<TUser, TRole, TKey> : IdentityDbContext<
    TUser, TRole, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>,
    IdentityUserLogin<TKey>, IdentityRoleClaim<TKey>, IdentityUserToken<TKey>>
        where TUser : IdentityUser<TKey>
        where TRole : IdentityRole<TKey>
        where TKey : IEquatable<TKey>
{
}

// No built-in Identity types are used; all are specified by generic arguments
// The key type is defined by TKey
public abstract class IdentityDbContext<
    TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
    : IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>
         where TUser : IdentityUser<TKey>
         where TRole : IdentityRole<TKey>
         where TKey : IEquatable<TKey>
         where TUserClaim : IdentityUserClaim<TKey>
         where TUserRole : IdentityUserRole<TKey>
         where TUserLogin : IdentityUserLogin<TKey>
         where TRoleClaim : IdentityRoleClaim<TKey>
         where TUserToken : IdentityUserToken<TKey>

您也可以在沒有角色的情況下使用 Identity (僅限宣告),在此情況下,應該使用 IdentityUserContext<TUser> 類別:

// Uses the built-in non-role Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityUserContext<TUser>
    : IdentityUserContext<TUser, string>
        where TUser : IdentityUser
{
}

// Uses the built-in non-role Identity types except with a custom User type
// The key type is defined by TKey
public class IdentityUserContext<TUser, TKey> : IdentityUserContext<
    TUser, TKey, IdentityUserClaim<TKey>, IdentityUserLogin<TKey>,
    IdentityUserToken<TKey>>
        where TUser : IdentityUser<TKey>
        where TKey : IEquatable<TKey>
{
}

// No built-in Identity types are used; all are specified by generic arguments, with no roles
// The key type is defined by TKey
public abstract class IdentityUserContext<
    TUser, TKey, TUserClaim, TUserLogin, TUserToken> : DbContext
        where TUser : IdentityUser<TKey>
        where TKey : IEquatable<TKey>
        where TUserClaim : IdentityUserClaim<TKey>
        where TUserLogin : IdentityUserLogin<TKey>
        where TUserToken : IdentityUserToken<TKey>
{
}

自訂模型

模型自訂的起點是衍生自適當的內容型別。 請參閱模型泛型型別一節。 這個內容型別通常稱為 ApplicationDbContext,而且是由 ASP.NET Core 範本所建立。

可使用內容以兩種方式設定模型:

  • 提供泛型型別參數的實體和索引鍵類型。
  • 覆寫 OnModelCreating 以修改這些型別的對應。

覆寫 OnModelCreating 時,應該先呼叫 base.OnModelCreating;接下來應該呼叫覆寫設定。 EF Core 通常具有最後一個獲勝的設定原則。 例如,如果實體類型的 ToTable 方法會先以一個資料表名稱呼叫,然後再以不同的資料表名稱再次呼叫,則會使用第二個呼叫中的資料表名稱。

注意:如果 DbContext 不是衍生自 IdentityDbContextAddEntityFrameworkStores 可能無法為 TUserClaimTUserLoginTUserToken 推斷正確的 POCO 類型。 如果 AddEntityFrameworkStores 未推斷正確的 POCO 類型,因應措施是透過 services.AddScoped<IUser/RoleStore<TUser>UserStore<...>> 直接新增正確的類型。

自訂使用者資料

透過繼承 IdentityUser 支援自訂使用者資料。 通常將此型別命名為 ApplicationUser

public class ApplicationUser : IdentityUser
{
    public string CustomTag { get; set; }
}

使用 ApplicationUser 型別做為內容的泛型引數:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

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

不需要在 ApplicationDbContext 類別中覆寫 OnModelCreating。 EF Core 依慣例會對應 CustomTag 屬性。 不過,資料庫必須更新才能建立新的 CustomTag 資料行。 若要建立資料行,請新增移轉,然後依照 Identity 和 EF Core 移轉中所述更新資料庫。

更新 Pages/Shared/_LoginPartial.cshtml 並將 IdentityUser 取代為 ApplicationUser

@using Microsoft.AspNetCore.Identity
@using WebApp1.Areas.Identity.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

更新 Areas/Identity/IdentityHostingStartup.csStartup.ConfigureServices 並將 IdentityUser 取代為 ApplicationUser

services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();                                    

呼叫 AddDefaultIdentity 相當於下列程式碼:

services.AddAuthentication(o =>
{
    o.DefaultScheme = IdentityConstants.ApplicationScheme;
    o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(o => { });

services.AddIdentityCore<TUser>(o =>
{
    o.Stores.MaxLengthForKeys = 128;
    o.SignIn.RequireConfirmedAccount = true;
})
.AddDefaultUI()
.AddDefaultTokenProviders();

Identity 會以 Razor 類別庫的形式提供。 如需詳細資訊,請參閱 ASP.NET Core 專案中的 Scaffold Identity。 因此,上述程式碼需要呼叫 AddDefaultUI。 如果使用 Identity Scaffolder 將 Identity 個檔案新增至專案,請移除對 AddDefaultUI 的呼叫。 如需詳細資訊,請參閱

變更主索引鍵類型

在建立資料庫之後,PK 資料行的資料類型變更在許多資料庫系統上都會造成問題。 變更 PK 通常牽涉到卸除和重新建立資料表。 因此,建立資料庫時,應該在初始移轉中指定索引鍵類型。

請依照下列步驟變更 PK 型別:

  1. 如果在 PK 變更之前建立資料庫,請執行 Drop-Database (PMC) 或 dotnet ef database drop (.NET Core CLI) 將其刪除。

  2. 確認刪除資料庫之後,請使用 Remove-Migration (PMC) 或 dotnet ef migrations remove (.NET Core CLI) 移除初始移轉。

  3. 更新 ApplicationDbContext 類別以衍生自 IdentityDbContext<TUser,TRole,TKey>。 指定 TKey 的新索引鍵類型。 例如,若要使用 Guid 索引鍵類型:

    public class ApplicationDbContext
        : IdentityDbContext<IdentityUser<Guid>, IdentityRole<Guid>, Guid>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
    

    在上述程式碼中,必須指定泛型類別 IdentityUser<TKey>IdentityRole<TKey> 才能使用新的索引鍵類型。

    必須更新 Startup.ConfigureServices 才能使用一般使用者:

    services.AddDefaultIdentity<IdentityUser<Guid>>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
    
  4. 如果使用自訂 ApplicationUser 類別,請更新類別以繼承自 IdentityUser。 例如:

    using System;
    using Microsoft.AspNetCore.Identity;
    
    public class ApplicationUser : IdentityUser<Guid>
    {
        public string CustomTag { get; set; }
    }
    

    更新 ApplicationDbContext 以參考自訂 ApplicationUser 類別:

    public class ApplicationDbContext
        : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
    

    Startup.ConfigureServices 中新增 Identity 服務時,註冊自訂資料庫內容類別:

    services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
    

    主索引鍵的資料類型是藉由分析 DbContext 物件來推斷。

    Identity 會以 Razor 類別庫的形式提供。 如需詳細資訊,請參閱 ASP.NET Core 專案中的 Scaffold Identity。 因此,上述程式碼需要呼叫 AddDefaultUI。 如果使用 Identity Scaffolder 將 Identity 個檔案新增至專案,請移除對 AddDefaultUI 的呼叫。

  5. 如果使用自訂 ApplicationRole 類別,請更新類別以繼承自 IdentityRole<TKey>。 例如:

    using System;
    using Microsoft.AspNetCore.Identity;
    
    public class ApplicationRole : IdentityRole<Guid>
    {
        public string Description { get; set; }
    }
    

    更新 ApplicationDbContext 以參考自訂 ApplicationRole 類別。 例如,下列類別會參考自訂 ApplicationUser 和自訂 ApplicationRole

    using System;
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore;
    
    public class ApplicationDbContext :
        IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
    

    Startup.ConfigureServices 中新增 Identity 服務時,註冊自訂資料庫內容類別:

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });
    
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
    
        services.AddIdentity<ApplicationUser, ApplicationRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultUI()
                .AddDefaultTokenProviders();
    
        services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    

    主索引鍵的資料類型是藉由分析 DbContext 物件來推斷。

    Identity 會以 Razor 類別庫的形式提供。 如需詳細資訊,請參閱 ASP.NET Core 專案中的 Scaffold Identity。 因此,上述程式碼需要呼叫 AddDefaultUI。 如果使用 Identity Scaffolder 將 Identity 個檔案新增至專案,請移除對 AddDefaultUI 的呼叫。

新增導覽屬性

變更關聯性的模型設定比進行其他變更還困難。 必須小心取代現有的關聯性,而不是建立新的、額外的關聯性。 特別是,變更的關聯性必須指定與現有關聯性相同的外部索引鍵 (FK) 屬性。 例如,UsersUserClaims 之間的關聯性預設會指定如下:

builder.Entity<TUser>(b =>
{
    // Each User can have many UserClaims
    b.HasMany<TUserClaim>()
     .WithOne()
     .HasForeignKey(uc => uc.UserId)
     .IsRequired();
});

此關聯性的 FK 會指定為 UserClaim.UserId 屬性。 系統會在沒有引數的情況下呼叫 HasManyWithOne,以建立沒有導覽屬性的關聯性。

將導覽屬性新增至 ApplicationUser,以允許從使用者參考相關聯的 UserClaims

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
}

IdentityUserClaim<TKey>TKey 是針對使用者 PK 指定的型別。 在此案例中,因為使用預設值,所以 TKeystring。 它「不是」UserClaim 實體類型的 PK 類型

既然導覽屬性存在,則必須在 OnModelCreating 中設定:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

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

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();
        });
    }
}

請注意,關聯性設定的方式與之前完全相同,只是使用呼叫 HasMany 中指定的導覽屬性。

導覽屬性只存在於 EF 模型中,而非資料庫。 因為關聯性的 FK 尚未變更,因此這種模型變更不需要更新資料庫。 進行變更之後,可以藉由新增移轉來檢查此情況。 UpDown 方法是空的。

新增所有使用者導覽屬性

使用上一節做為指引,下列範例會針對 User 上的所有關聯性設定單向導覽屬性:

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
    public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
    public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
    public virtual ICollection<IdentityUserRole<string>> UserRoles { get; set; }
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

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

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne()
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne()
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne()
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });
    }
}

新增 User 和 Role 導覽屬性

使用上一節做為指引,下列範例會針對 User 和 Role 上的所有關聯性設定導覽屬性:

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
    public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
    public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationRole : IdentityRole
{
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationUserRole : IdentityUserRole<string>
{
    public virtual ApplicationUser User { get; set; }
    public virtual ApplicationRole Role { get; set; }
}
public class ApplicationDbContext
    : IdentityDbContext<
        ApplicationUser, ApplicationRole, string,
        IdentityUserClaim<string>, ApplicationUserRole, IdentityUserLogin<string>,
        IdentityRoleClaim<string>, IdentityUserToken<string>>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

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

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne()
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne()
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.User)
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });

        modelBuilder.Entity<ApplicationRole>(b =>
        {
            // Each Role can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.Role)
                .HasForeignKey(ur => ur.RoleId)
                .IsRequired();
        });

    }
}

注意:

  • 此範例也包含 UserRole 聯結實體,需要此實體才能將多對多關聯性從 Users 導覽至 Roles。
  • 請記得變更導覽屬性的型別,以反映目前正在使用 Application{...} 型別,而不是 Identity{...} 型別。
  • 請記得在泛型 ApplicationContext 定義中使用 Application{...}

新增所有導覽屬性

使用上一節做為指引,下列範例會針對所有實體類型上的所有關聯性設定導覽屬性:

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<ApplicationUserClaim> Claims { get; set; }
    public virtual ICollection<ApplicationUserLogin> Logins { get; set; }
    public virtual ICollection<ApplicationUserToken> Tokens { get; set; }
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationRole : IdentityRole
{
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
    public virtual ICollection<ApplicationRoleClaim> RoleClaims { get; set; }
}

public class ApplicationUserRole : IdentityUserRole<string>
{
    public virtual ApplicationUser User { get; set; }
    public virtual ApplicationRole Role { get; set; }
}

public class ApplicationUserClaim : IdentityUserClaim<string>
{
    public virtual ApplicationUser User { get; set; }
}

public class ApplicationUserLogin : IdentityUserLogin<string>
{
    public virtual ApplicationUser User { get; set; }
}

public class ApplicationRoleClaim : IdentityRoleClaim<string>
{
    public virtual ApplicationRole Role { get; set; }
}

public class ApplicationUserToken : IdentityUserToken<string>
{
    public virtual ApplicationUser User { get; set; }
}
public class ApplicationDbContext
    : IdentityDbContext<
        ApplicationUser, ApplicationRole, string,
        ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin,
        ApplicationRoleClaim, ApplicationUserToken>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

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

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne(e => e.User)
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne(e => e.User)
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne(e => e.User)
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.User)
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });

        modelBuilder.Entity<ApplicationRole>(b =>
        {
            // Each Role can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.Role)
                .HasForeignKey(ur => ur.RoleId)
                .IsRequired();

            // Each Role can have many associated RoleClaims
            b.HasMany(e => e.RoleClaims)
                .WithOne(e => e.Role)
                .HasForeignKey(rc => rc.RoleId)
                .IsRequired();
        });
    }
}

使用複合索引鍵

上述各節示範如何變更 Identity 模型中所使用的索引鍵類型。 不支援也不建議將 Identity 索引鍵模型變更為使用複合索引鍵。 搭配 Identity 使用複合索引鍵牽涉到變更 Identity 管理員程式碼與模型互動的方式。 此自訂超出此文件的範圍。

變更資料表/資料行名稱和 Facet

若要變更資料表和資料行的名稱,請呼叫 base.OnModelCreating。 然後,新增設定以覆寫任何預設值。 例如,若要變更所有 Identity 資料表的名稱:

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

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.ToTable("MyUsers");
    });

    modelBuilder.Entity<IdentityUserClaim<string>>(b =>
    {
        b.ToTable("MyUserClaims");
    });

    modelBuilder.Entity<IdentityUserLogin<string>>(b =>
    {
        b.ToTable("MyUserLogins");
    });

    modelBuilder.Entity<IdentityUserToken<string>>(b =>
    {
        b.ToTable("MyUserTokens");
    });

    modelBuilder.Entity<IdentityRole>(b =>
    {
        b.ToTable("MyRoles");
    });

    modelBuilder.Entity<IdentityRoleClaim<string>>(b =>
    {
        b.ToTable("MyRoleClaims");
    });

    modelBuilder.Entity<IdentityUserRole<string>>(b =>
    {
        b.ToTable("MyUserRoles");
    });
}

這些範例會使用預設 Identity 類型。 如果使用應用程式類型,例如 ApplicationUser,請設定該類型,而不是預設類型。

下列範例會變更一些資料行名稱:

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

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.Property(e => e.Email).HasColumnName("EMail");
    });

    modelBuilder.Entity<IdentityUserClaim<string>>(b =>
    {
        b.Property(e => e.ClaimType).HasColumnName("CType");
        b.Property(e => e.ClaimValue).HasColumnName("CValue");
    });
}

可以使用特定 facet (例如允許的最大 string 長度) 來設定某些類型的資料庫資料行。 下列範例會設定模型中數個 string 屬性的資料行長度上限:

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

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.Property(u => u.UserName).HasMaxLength(128);
        b.Property(u => u.NormalizedUserName).HasMaxLength(128);
        b.Property(u => u.Email).HasMaxLength(128);
        b.Property(u => u.NormalizedEmail).HasMaxLength(128);
    });

    modelBuilder.Entity<IdentityUserToken<string>>(b =>
    {
        b.Property(t => t.LoginProvider).HasMaxLength(128);
        b.Property(t => t.Name).HasMaxLength(128);
    });
}

對應至不同的結構描述

結構描述在資料庫提供者間的行為可能會不同。 若為 SQL Server,預設會在 dbo 結構描述中建立所有資料表。 可以在不同的結構描述中建立資料表。 例如:

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

    modelBuilder.HasDefaultSchema("notdbo");
}

消極式載入

在本節中,會新增 Identity 模型中延遲載入 Proxy 的支援。 延遲載入很有用,因為它允許在不先確定載入導覽屬性的情況下使用導覽屬性。

您可以透過數種方式,將實體類型設定為適合延遲載入,如 EF Core 文件所述。 為了簡單起見,請使用延遲載入 Proxy,使用需求如下:

下列程式碼範例示範如何在 Startup.ConfigureServices 呼叫 UseLazyLoadingProxies

services
    .AddDbContext<ApplicationDbContext>(
        b => b.UseSqlServer(connectionString)
              .UseLazyLoadingProxies())
    .AddDefaultIdentity<ApplicationUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

如需將導覽屬性新增至實體類型的指引,請參閱上述範例。

其他資源