You should of course customize the Identity entities, as that is the whole reason for the creation of Identity: to allow greater extensibility. To have different types of "users", you should inherit from ApplicationUser
; importantly, not from IdentityUser
directly. This will assure that the core Identity relationships (roles, claims, logins, etc.) are all tied to a single "user" table, while you can then either extend that table or create other tables to hold additional user data.
public class ApplicationUser : IdentityUser
public class Student : ApplicationUser
public class Instructor : ApplicationUser
By default, this inheritance will be implemented by TPH (Table Per Hierarchy), also known as STI (Single Table Inheritance). What that means is that all properties from all derived classes will be represented by columns on a single database table. A Discriminator
column will also be added, which will hold the name of the actual class that was saved, i.e. "ApplicationUser", "Student", or "Instructor". EF will use this column when building object graphs from your queries to instantiate the right "user" type.
There's pros and cons with this approach. Since everything exists in a single table, queries are simple and quick. However, this approach necessitates that all properties on each derived class must be nullable at the database level. The obvious reason why is because if Instructor
has a required column, you would not be able to save Student
, since Student
would not have the property to fulfill that requirement. You can still enforce that properties be required at the view level, using view models. The actual column in the database must be nullable, though.
An alternative approach is to use what's called TPT (Table Per Type). In this inheritance strategy, a table will be created for the base class (ApplicationUser
) with all common properties. Then, a table will be created for each discreet derived class, with just the properties that exist on that class. A foreign key will be added to the table for the base class, which EF will then use to join the common data from that table to the specific data on your derived class' table. This approach allows you to enforce NOT NULL at the database level, but it of course requires a join to bring in all the data, which can slow down your queries.
To implement TPT, you need simply to add the [Table]
annotation to your derived class:
[Table("Students")]
public class Student : ApplicationUser
[Table("Instructors")]
public class Instructor : ApplicationUser
One final thing of note is how you'll need to utilize UserManager
. If you scaffolded your AccountController
, you'll notice that it sets up a UserManager
controller property, which is then utilized to create users, lookup users, change passwords, etc. This is actually an instance of UserManager<ApplicationUser>
, as it's a generic type. If you need to specifically work with Student
or Instructor
, you'll need to instantiate UserManager<Student>
and UserManager<Instructor>
, respectively. You can't use an instance of UserManager<ApplicationUser>
as it will upcast your derived type to ApplicationUser
. For example:
var student = new Student { ... };
await UserManager.CreateAsync(student);
Would actually result in ApplicationUser
being saved to the database. The student-specific data will be discarded and the Discriminator
column's value would be "ApplicationUser".