Search code examples
c#asp.net-mvcasp.net-identity-2ef-database-first

Add relationships to the ApplicationUser class in ASP.NET Identity (Database First)


I'm using ASP.NET Identity (Database First) in my ASP.NET MVC application. I followed the instructions here, to set up the ASP.NET Identity with database first approach.

My AspNetUsers table has a relationship with the Employee table (The Employee table has a UserId foreign key, and the AspNetUsers entity has an ICollection<Employee> property).

I would like to add the ICollection<Employee> property to the ApplicationUser, like below:

public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>
{
    public ICollection<Employee> Employees { get; set; }

    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, int> manager)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        return userIdentity;
    }
}

But, when I do that, I get the following error message:

EntityType 'AspNetUserLogins' has no key defined. Define the key for this EntityType. AspNetUserLogins: EntityType: EntitySet 'AspNetUserLogins' is based on type 'AspNetUserLogins' that has no keys defined.

Why am I getting this error message? How can I fix that?


Solution

  • I cannot reproduce the issue, even when I create the tables in another database without keys and relations. So I'm sure that there is a problem with your model. Unfortunately you didn't add code which I can compare, so I can't tell what is different and answer the question directly. The only thing I can do is to show what works for me. However, first I have some remarks.


    I think you shouldn't follow the article. As there is no reason to add the context to an existing database.

    Like Ivan Stoev mentioned you are not supposed to mix contexts. The Identity context is meant to authenticate the user. It stores the credentials, the roles of the user and claims. Where claims are meant to add identity information about the user.

    In fact, the default Hometown field of the ApplicationUser template can be removed, as it is an identity claim which should be stored in the AspNetUserClaims table. Not something you need to extend the ApplicationUser for. Actually I can't think of any reason to extend the ApplicationUser.

    About the roles, these are not really claims, as they tell nothing about the identity but rather are used for authorization. That's why it's fine that they are stored in the AspNetUserRoles table. Unfortunately roles are added to the identity as role claims, which makes things confusing.

    Please note that the Identity information is present in the claims. This means that the application doesn't have to call the Identity context. E.g. User.IsInRole checks the role claims of the current identity, not the roles stored in the table.

    About the different contexts, the other context (which I usually call the business model) has nothing in common with the Identity context. Email and other fields are not part, nor have meaning to the business model. You may think that those fields are redundant, but in fact they are not. I could login using a google account, but for the business use my work email address.

    There are several reasons to keep the context seperated.

    • Seperation of concerns. Suppose you want to exchange the authentication framework in the future with another one. Like implement IdentityServer in case you want to support single sign-on (SSO).
    • You can't move the users table to another database if another application needs the same logins. So you'll end up adding other contexts as well to the database.
    • Trouble with migrations. If you mix the contexts then migrations will fail.
    • It'll makes things far more easier. This is the first problem you've encountered, not the last.

    As also mentioned in the article:

    At this point if you need to add any relationships (E.g. foreign keys) from your own tables to these tables you are welcome to do so but do not modify any of the Entity Framework 2.0 tables directly or later on any of their POCO classes. Doing so will result in errors based upon feedback I’ve received.

    So how to manage the information if you shouldn't access the identity context from your application?

    For the current user you don't need to access the users table. All the information is present in the identity claims. The only reason to access the identity context is to allow a user to login. Besides user management.

    You can suffice by adding a reference to the user (the userid). If you need to show information of other users (like name) in a report, then create a user table in your business context to store the information. You can add relations to this table, as it is part of the same context.

    Please let me know if you have questions about this approach.


    Now the code that works for me. Like others have mentioned, it is not likely that adding the line:

    public ICollection<Employee> Employees { get; set; }
    

    is the cause. Without the virtual keyword I think it is even ignored (remains null).

    When I follow the steps of the article then I end up with the following model:

    public class ApplicationUser : IdentityUser
    {
        public string Hometown { get; set; }
    
        //public virtual ICollection<Employee> Employees { get; set; }
    
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }
    }
    
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
            // Disable migrations
            //Database.SetInitializer<ApplicationDbContext>(null);
        }
    
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }
    

    And then I add the Employee class and uncomment the line in the ApplicationUser class above:

    public class Employee
    {
        public int Id { get; set; }
    
        public string Name { get; set; }
    
        //public virtual ApplicationUser ApplicationUser { get; set; }
    
        public string ApplicationUserId { get; set; }
    }
    

    In the database I added the table:

    CREATE TABLE [dbo].[Employees](
        [Id] [int] NOT NULL,
        [Name] [varchar](50) NOT NULL,
        [ApplicationUserId] [nvarchar](128) NOT NULL,
    PRIMARY KEY CLUSTERED 
    (
        [Id] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    

    You can use the [ForeignKey] attribute to use a different field name.

    You can try this or choose to keep both contexts seperated instead.