.NET Core 2.1 Identity:为每个 Role + bridge M:M table 创建一个 table

.NET Core 2.1 Identity : Creating a table for each Role + bridge M:M table

关于在 .NET Core 2.1 项目中使用身份的基于角色的授权,我在找出适合我需求的最佳设计时遇到了问题。

我已经使用 ApplicationUser class 从 Identity 扩展了 User class。 我需要 5 个不同的角色来控制对应用程序不同功能的访问:

管理员、教师、学生、家长和主管

所有公共属性都保留在 User 和 ApplicationUser 中,但我仍然需要与其他 table 的不同关系,具体取决于用户的角色。

另一个要求是用户必须能够担任 1-N 角色。

对我而言,最佳做法是什么?

我是否缺少 Identity 的功能?

我最初的想法是使用可为空的 FK,但是随着角色数量的增加,所有这些记录都有这么多空字段看起来不是一个好主意。

我正在考虑为每个角色使用 "bridge table" 到 link 用户到其他 table。 在 ApplicationUser 和网桥 table 之间存在多对多关系,在网桥 table 和每个角色的个人 table 之间存在 0-1 关系。但这也无济于事,因为每条记录都会产生相同数量的空字段。

我对 .NET Core 尤其是 Identity 还很陌生,我可能遗漏了一些关键字来进行有效的研究,因为在我看来它是一个非常基本的系统(需求中没有什么特别的)。

感谢阅读!

编辑: 我现在并没有真正的错误,因为我试图在深入项目之前找出最佳实践。由于这是我第一次遇到这种要求,因此我试图找到有关 pros/cons.

的文档

我遵循了 Marco 的想法并将继承用于我的基于角色的模型,因为这是我的第一个想法。我希望它能帮助理解我的担忧。

public class ApplicationUser : IdentityUser
{
    public string CustomTag { get; set; }
    public string CustomTagBis { get; set; }
}
    public class Teacher : ApplicationUser
{
    public string TeacherIdentificationNumber { get; set; }
    public ICollection<Course> Courses { get; set; }
}
public class Student : ApplicationUser
{
    public ICollection<StudentGroup> Groups { get; set; }
}
public class Parent : ApplicationUser
{
    public ICollection<Student> Children { get; set; }
}
public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Category { get; set; }
}
public class StudentGroup
{
    public int Id { get; set; }
    public string Name { get; set; }
}

这会为包含所有属性的用户创建一个大 table 的数据库:

用户table生成

我可以使用它,它会起作用。 如果用户需要担任不同的角色,则他可以填写这些可为空的字段中的任何一个。

我担心的是,对于每条记录,我都会有大量 "inappropriate fields" 保持为空。 假设在 1000 个用户中,80% 的用户是学生。 包含 800 行的结果是什么: - 一个空的 ParentId FK - 一个空的 TeacherIdentificationNumber

而这只是模型内容的一小部分。 "feel" 不对,我错了吗?

难道没有更好的方法来设计实体,使 table 用户只包含所有用户的公共属性(正如它应该的那样?)并且仍然能够 link 每个用户到另一个 table,这将 link 用户到 1-N tables Teacher/Student/Parent/... table?

Table-Per-Hierarchy 方法的图表

编辑 2: 使用 Marco 的回答,我尝试使用 Table-Per-Type 方法。 在修改我的上下文以实现 Table-Per-Type 方法时,我在想要添加迁移时遇到了这个错误:

"The entity type 'IdentityUserLogin' requires a primary key to be defined."

我相信这是因为我删除了 :

base.OnModelCreating(builder);

导致此代码:

protected override void OnModelCreating(ModelBuilder builder)
{
        //base.OnModelCreating(builder);
        builder.Entity<Student>().ToTable("Student");
        builder.Entity<Parent>().ToTable("Parent");
        builder.Entity<Teacher>().ToTable("Teacher");
}

我相信那些身份密钥映射在 base.OneModelCreating 中。 但是即使我取消注释该行,我也会在我的数据库中保留相同的结果。

经过一些研究,我发现 this article 帮助我完成了创建 Table-per-type 模型并应用迁移的过程。

使用这种方法,我有一个如下所示的架构: Table-按类型方法

如有不妥请指正,但两种技术都符合我的要求,更多的是设计偏好?它对架构和身份特征没有太大影响?


对于第三种选择,我想使用不同的方法,但我不太确定。

这样的设计是否符合我的要求,是否有效? 通过有效,我的意思是,link 教师实体对角色而不是用户感觉很奇怪。但在某种程度上,教师实体代表了用户在担任教师角色时需要的功能。

对实体的作用

我还不太确定如何使用 EF 核心实现此功能以及覆盖 IdentityRole class 将如何影响身份功能。我在做,但还没弄明白。

你做的完全符合你的要求。您当前实现的称为 Table-Per-Hierarchy。这是默认方法,Entity Framework 在发现其模型时会这样做。

另一种方法是 Table-Per-Type。在这种情况下,Entity Framework 将创建 4 个 table。

  1. 用户table
  2. 学生table
  3. 老师table
  4. Parent table

由于所有这些实体都继承自 ApplicationUser,因此数据库将在它们与其 parent class.

之间生成 FK 关系

要实现这一点,您需要修改 DbContext:

public class FooContext : DbContext
{
    public DbSet<ApplicationUser> Users { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>().ToTable("Students");
        modelBuilder.Entity<Parent>().ToTable("Parents");
        modelBuilder.Entity<Teacher>().ToTable("Teachers");
    }
}

这应该是最规范的做法了。然而,还有第三种方法,您最终会得到 3 tables,并且 parent ApplicationUser class 将被映射到其具体实现中。但是,我从来没有用 Asp.Net Identity 实现过这个,所以我不知道它是否会起作用,也不知道你是否会 运行 陷入一些关键冲突。

我建议您利用 asp.net 核心的新功能和新的身份框架。有很多关于 security.

的文档

可以用policy based security, but in your case resource-based security 好像比较合适

最好的方法是不要混合上下文。 保持关注点分离:身份上下文(使用 UserManager)和业务上下文(学校,您的 DbContext)。

因为将 ApplicationUser table 放在您的 'business context' 中意味着您正在直接访问身份上下文。这不是您应该使用 Identity 的方式。使用 UserManager 进行 IdentityUser 相关查询。

为了使其正常工作,不要继承 ApplicationUser table,而是在您的学校环境中创建一个用户 table。它不是副本,而是新的 table。事实上,唯一的共同点是 UserId 字段。

查看我的回答 以了解有关更详细设计的想法。

将 TeacherIdentificationNumber 等字段移出 ApplicationUser。您可以将此作为声明添加到用户 (AspNetUserClaims table):

new Claim("http://school1.myapp.com/TeacherIdentificationNumber", 123);

或将其存储在学校环境中。

此外,考虑使用声明而不是角色,您可以在其中通过类型名称区分声明(例如 http://school1.myapp.com/role):

new Claim("http://school1.myapp.com/role", "Teacher");
new Claim("http://school2.myapp.com/role", "Student");

虽然我认为在你的情况下,将信息存储在学校环境中可能更好。

最重要的是,保持 Identity 上下文不变,并将 tables 添加到 school 上下文中。您不必创建两个数据库,只是不要添加跨上下文关系。唯一绑定两者的是 UserId。但是您不需要实际的数据库关系。

使用 UserManager 等进行身份查询和您的应用程序的学校上下文。如果不用于身份验证,则不应使用身份上下文。


现在开始设计,创建一个用户 table,该用户具有与 link 当前用户匹配的 UserId 字段。仅当您想要显示时(在报告中)添加名称等字段。

在使用复合键的地方为学生、教师等添加 table:School.Id、User.Id。或者添加一个公共 Id 并对 School.Id、User.Id.

的组合使用唯一约束

当用户出现在 table 中时,这意味着该用户是学校 x 的学生或学校 y 的老师。身份上下文中不需要角色。

使用导航属性,您可以轻松确定 'role' 并访问 'role' 的字段。