Search code examples
asp.net-mvcentity-framework-6asp.net-identityentity-relationshipdbcontext

How to link one-to-one relationship when fk is different from pk - EntityFramework


I have a custom users table in my database and i want to create a one-to-one relationship with aspnetusers table so that when i register a new customer, applicationuser class through UserManager should add the username, email, password and school code to the users table and add an fk in its own table. IS there any tutorial/example implementing this kind of scenario?

I am trying to add a one-to-one relationship with my users table but the fk is different than pk

  • Users Table [columns]
    • username
    • password
    • school code
    • userId [pk]

ASPNETUsers Table [standard columns]

  • Id, [pk]
  • UserName,
  • PasswordHash,
  • SecurityStamp,
  • Discriminator,
  • Email,
  • EmailConfirmed,
  • PhoneNumber,
  • PhoneNumberConfirmed,
  • TwoFactorEnabled,
  • LockoutEndDateUtc,
  • LockoutEnabled,
  • AccessFailedCount
  • userId [fk to users table]

I have added property inside ApplicationUser class

public class ApplicationUser : IdentityUser
{
    public virtual User user { get; set; }
    public int UserID { get; set; }
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>()
          .HasRequired(i => i.user).WithRequiredDependent(i => i.appUser);
      }

}

 public class User
{
    public int UserID { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public virtual ApplicationUser appUser { get; set; }
}

What else do i need to do and is it correct? How does entityframework work with this one?

I have tried to explain My application structure here: enter image description here

UPDATE On running it gives me error MySql.Data.MySqlClient.MySqlException: Unknown column 'Extent8.appUser_Id' in 'on clause'

If I define

modelBuilder.Entity<ApplicationUser>().HasKey(u => u.UserID);

inside OnModelCreating then it tries to link AspNetRoles and Claims etc tables through this UserID and ofcourse crashes.

ApplicationUser_Claims_Source_ApplicationUser_Claims_Target: : The types of all properties in the Dependent Role of a referential constraint must be the same as the corresponding property types in the Principal Role. The type of property 'UserId' on entity 'IdentityUserClaim' does not match the type of property 'UserID' on entity 'ApplicationUser' in the referential constraint 'ApplicationUser_Claims'. ApplicationUser_Logins_Source_ApplicationUser_Logins_Target: : The types of all properties in the Dependent Role of a referential constraint must be the same as the corresponding property types in the Principal Role. The type of property 'UserId' on entity 'IdentityUserLogin' does not match the type of property 'UserID' on entity 'ApplicationUser' in the referential constraint 'ApplicationUser_Logins'. ApplicationUser_Roles_Source_ApplicationUser_Roles_Target: : The types of all properties in the Dependent Role of a referential constraint must be the same as the corresponding property types in the Principal Role. The type of property 'UserId' on entity 'IdentityUserRole' does not match the type of property 'UserID' on entity 'ApplicationUser' in the referential constraint 'ApplicationUser_Roles'.


Solution

  • How EF's one-to-one relationship works, in short...

    When working with one-to-one relationship, Entity Framework uses the parent's Id column in the child as the FK to the parent, it won't create a second column for FK because that would be considered one-to-many relationship, because that second column would allow a parent Id to exist in many child entries, whereas the PK as FK wouldn't.

    How should I configure both keys?

    Since your ApplicationUser inherits from IdentityUser, it will use it's existing properties and relationships. It already defines an Id column, so creating UserId is not necessary, as well as trying to make UserId the key property. You should use the existing Id column, which by default is string and it looks your existing User table has an integer PK, so they don't match.

    However, this is not the end of the world. Identity's built-in implementation uses string as key for their classes (AspNetUsers, AspNetRoles, etc...) but they allows us to use another type of primary key.

    So...what should I do?

    Identity defines two classes for each table, so there are two IdentityUser classes, two IdentityRole, and so on. One is the base generic class, which accepts some generics. The other is the built-in implementation of that base class, which supplies those generic types.

    This base class looks like this:

    public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
            where TLogin : IdentityUserLogin<TKey>
            where TRole : IdentityUserRole<TKey>
            where TClaim : IdentityUserClaim<TKey>
    

    The first generic type is TKey, which defines the key's type, which is string. The second is a TLogin, which is a IdentityUserLogin<TKey>, which means it should be some class inheriting IdentityUserLogin using the same TKey type used as key (maybe string, maybe an int).

    Meanwhile, the built-in implementation for these base classes looks like this:

    public class IdentityUser 
        : IdentityUser<string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUser, IUser<string> {}
    

    Which defines TKey as a string to be used as key. The other generics like IdentityUserLogin and IdentityUserRole, are built-in classes implementing the base classes which a string key. Take a look how IdentityUserRole looks like:

    public class IdentityUserRole  IdentityUserRole<string> {}
    

    See? string as well. Many other classes use string as default for TKey.

    So if you were to change your class like this:

    public class ApplicationUser : IdentityUser<int, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
    {
        public virtual User user { get; set; }
    }
    

    It wouldn't work yet, because we changed TKey to be int, but the built-in IdentityUserLogin still uses string. So we need to create our own IdentityUserLogin like this:

    public class MyNiceUserLogin : IdentityUserLogin<int>
    {
    }
    

    ...and use it like this:

    public class ApplicationUser : IdentityUser<int, MyNiceUserLogin, IdentityUserRole, IdentityUserClaim>
    {
        public virtual User user { get; set; }
    }
    

    ...but we still had to do the same with the other classes like IdentityUserRole and IdentityUserClaim.

    I configured it to use int. Now what about one-to-one relationship?

    Your code here:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    
        modelBuilder.Entity<ApplicationUser>()
          .HasRequired(i => i.user).WithRequiredDependent(i => i.appUser);
    }
    

    ...works fine. You're configuring in a way both must exist, there is no ApplicationUser without a User and vice-versa.


    Now, I recommend you read this Stack Overflow answer to know exactly what steps to follow in order to change Identity's classes to use int as key.

    I also strongly recommend you to read this post by John Atten, so you can understand deeply about how to customize/extend Identity.