asp.net-mvcentity-frameworkef-code-firstasp.net-identitytable-per-hierarchy

How to use TPH (Table Per Hierarchy) with ASP.NET Identity 2


I use ASP.NET Identity 2 in an MVC5 project and there are 2 type of user classes called Student and Coordinator as shown below. On the other hand, I tried to follow TPH (Table Per Hierarchy) approach so that using the same entity for both type of users.

public class Student : ApplicationUser
{
    public int? Number { get; set; }
}


public class Coordinator : ApplicationUser
{
    public string Room { get; set; }
}


public class ApplicationUser : IdentityUser<int, ApplicationUserLogin,
     ApplicationUserRole, ApplicationUserClaim>, IUser<int>
{
    //Custom Properties 
    public string Name { get; set; }

    public string Surname { get; set; }

    //code omitted for brevity
}

As ApplicationUser is already inherited from IdentityUser, I did not create it as an abstract class and did not add it to my DbContext as shown below:

public DbSet<ApplicationUser> ApplicationUsers { get; set; }

On the other hand, there is another entity having Student (not ApplicationUser) as navigation property shown below:

public class Grade
{
    [Key]
    public int Id { get; set; }

    public int Grade { get; set; }

    //Navigation properties ===========================
    public virtual Experiment Experiment { get; set; }

    public virtual Student Student { get; set; }

    //public virtual ApplicationUser User { get; set; }
    //=================================================
}

}

However, when adding DbSet to DbContext I encountered "Multiple object sets per type are not supported. The object sets 'ApplicationUsers' and 'Users' can both contain instances of type 'Xxxx.ApplicationUser'." error. So, is there any mistake regarding to the approach above? Should I use DbSet in DbContext and should I add ApplicationUser as navigation property to Grade entity? Any help would be appreciated...


Solution

  • You should be able to do it without adding an ApplicationUsers DbSet to your DbContext, just like this:

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<Student> Students { get; set; }
        public DbSet<Coordinator> Coordinators { get; set; }
        public DbSet<Grade> Grades { get; set; }
        ...
    }
    

    And it is okay to use the Student type in the navigation property of your Grade entity, you shouldn't have to use ApplicationUser.

    Check that your database is synchronized with your model. Are you using EF migrations? Your tables in the migration files should look like this: (note the Discriminator field in AspNetUsers table, and the Number and Room fields. Those are proof of TPH being applied to the table creation. Also, no tables for Student or Coordinator were included in the migration.)

            CreateTable(
                "dbo.AspNetUsers",
                c => new
                    {
                        Id = c.String(nullable: false, maxLength: 128),
                        Name = c.String(),
                        Surname = c.String(),
                        Email = c.String(maxLength: 256),
                        EmailConfirmed = c.Boolean(nullable: false),
                        PasswordHash = c.String(),
                        SecurityStamp = c.String(),
                        PhoneNumber = c.String(),
                        PhoneNumberConfirmed = c.Boolean(nullable: false),
                        TwoFactorEnabled = c.Boolean(nullable: false),
                        LockoutEndDateUtc = c.DateTime(),
                        LockoutEnabled = c.Boolean(nullable: false),
                        AccessFailedCount = c.Int(nullable: false),
                        UserName = c.String(nullable: false, maxLength: 256),
                        Room = c.String(),
                        Number = c.Int(),
                        Discriminator = c.String(nullable: false, maxLength: 128),
                    })
                .PrimaryKey(t => t.Id)
                .Index(t => t.UserName, unique: true, name: "UserNameIndex");
    
            CreateTable(
                "dbo.Grades",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        GradeValue = c.Int(nullable: false),
                        Student_Id = c.String(maxLength: 128),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.AspNetUsers", t => t.Student_Id)
                .Index(t => t.Student_Id);
    

    enter image description here

    If your tables don't look like these, try reverting to an empty database, deleting the migrations and generating them again, and finally updating the database with the good migrations.

    I created a dummy MVC project with your entities and just those three DbSets in the DbContext, generated the migrations and created the database tables, and run the following test code (from the default controller index method). The data were correctly saved in the database:

        public void Test()
        {
            using (var context = new ApplicationDbContext())
            {
                var student = new Student
                {
                    Id = "12345",
                    Name = "John",
                    Number = 123,
                    UserName = "Johnny",
                    Email = "some@email.com"
                };
                context.Students.Add(student);
                context.SaveChanges();
            }
    
            using (var context = new ApplicationDbContext())
            {
                var student = context.Students.Find("12345");
                var grade = new Grade { Id = 333, GradeValue = 90, Student = student };
                context.Grades.Add(grade);
                context.SaveChanges();
            }
        }
    

    Some questions:

    If you need it I can send the code of the dummy solution I used to test this.