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 CLI。 有关详细信息,请参阅EF Core.NET 命令行工具
  • 应用在运行时,单击错误页上的“应用迁移”按钮。

ASP.NET Core 具有一个开发时错误页面处理程序。 应用在运行时,处理程序可以应用迁移。 生产应用通常从迁移生成 SQL 脚本,并将数据库更改作为受控应用和数据库部署的一部分进行部署。

创建使用 Identity 的新应用时,上面的步骤 1 和 2 已经完成。 也就是说,初始数据模型已存在,并且初始迁移已添加到项目中。 初始迁移仍需应用于数据库。 可以通过以下方法之一来应用初始迁移:

  • 在 PMC 中运行 Update-Database
  • 在命令 shell 中运行 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 为上面列出的每种实体类型定义默认公共语言运行时 (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 不是派生自 IdentityDbContext,则 AddEntityFrameworkStores 可能无法推断 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 项目中的基架 Identity。 因此,前面的代码需要调用 AddDefaultUI。 如果 Identity 基架用于将 Identity 文件添加到项目中,请删除对 AddDefaultUI 的调用。 有关详细信息,请参阅:

更改主键类型

在创建数据库后,对 PK 列的数据类型进行更改在许多数据库系统上都存在问题。 更改 PK 通常涉及删除并重新创建表。 因此,在创建数据库时,应在初始迁移中指定键类型。

按照以下步骤更改 PK 类型:

  1. 如果数据库是在 PK 更改之前创建的,请运行 Drop-Database(PMC) 或 dotnet ef database drop (.NET CLI) 将其删除。

  2. 确认删除数据库后,使用 Remove-Migration (PMC) 或 dotnet ef migrations remove (.NET 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 项目中的基架 Identity。 因此,前面的代码需要调用 AddDefaultUI。 如果 Identity 基架用于将 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 项目中的基架 Identity。 因此,前面的代码需要调用 AddDefaultUI。 如果 Identity 基架用于将 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 方法为空。

添加所有用户导航属性

以下示例使用上述部分作为指南,为用户的所有关系配置单向导航属性:

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();
        });
    }
}

添加用户和角色导航属性

以下示例使用上述部分作为指南,为用户和角色的所有关系配置导航属性:

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 联接实体,将多对多关系从用户导航到角色时需要该实体。
  • 请务必更改导航属性的类型,以反映现在正在使用 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 模型中延迟加载代理的支持。 延迟加载非常有用,因为它允许使用导航属性,而无需首先确保已加载这些属性。

可以通过多种方式使实体类型适用于延迟加载,如 EF Core 文档中所述。 为简单起见,请使用延迟加载代理,这需要:

下面的示例演示如何调用 Startup.ConfigureServices 中的 UseLazyLoadingProxies

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

有关将导航属性添加到实体类型的指导,请参阅前面的示例。

警告

本文介绍连接字符串的使用。 使用本地数据库时,用户无需进行身份验证,但在生产环境中,连接字符串有时包括进行身份验证的密码。 资源所有者密码凭据(ROPC)是在生产数据库中应避免的安全风险。 生产应用应使用可用的最安全的身份验证流。 有关部署到测试或生产环境的应用的身份验证的详细信息,请参阅 安全身份验证流

其他资源