.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 的不同关系,具体取决于用户的角色。
- 教师角色中的用户link进入 1-N 学校
- Student 角色中的用户linked 到 1-N GroupOfStudents(但不是直接到学校)
- 扮演家长角色的用户link被 1-N 个学生(但不是学校)
- ...
另一个要求是用户必须能够担任 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。
- 用户table
- 学生table
- 老师table
- 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' 的字段。
关于在 .NET Core 2.1 项目中使用身份的基于角色的授权,我在找出适合我需求的最佳设计时遇到了问题。
我已经使用 ApplicationUser class 从 Identity 扩展了 User class。 我需要 5 个不同的角色来控制对应用程序不同功能的访问:
管理员、教师、学生、家长和主管
所有公共属性都保留在 User 和 ApplicationUser 中,但我仍然需要与其他 table 的不同关系,具体取决于用户的角色。
- 教师角色中的用户link进入 1-N 学校
- Student 角色中的用户linked 到 1-N GroupOfStudents(但不是直接到学校)
- 扮演家长角色的用户link被 1-N 个学生(但不是学校)
- ...
另一个要求是用户必须能够担任 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。
- 用户table
- 学生table
- 老师table
- Parent table
由于所有这些实体都继承自 ApplicationUser
,因此数据库将在它们与其 parent class.
要实现这一点,您需要修改 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' 的字段。