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

2

6

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?

Shoshone answered 20/8, 2018 at 16:9 Comment(12)
EntityType has no key defined - your AspNetUserLogins table probably has no primary key or just attached to wrong context. Can you show AspNetUserLogins class structure?Ousel
@TetsuyaYamamoto, no, the AspNetUserLogins table doesn't have any issues. I get the error only when I add the ICollection<Employee> property. And, this only happens when I add complex-type properties. I don't get an error message when I add string FirstName, for example.Shoshone
public ICollection<Employee> { get; set; } has no name, I doubt this compiles. Are we speaking about an entity? Why does it have a GenerateUserIdentityAsync?Sesquiplane
@ChristianGollhardt, sorry! I updated that. Why shouldn't have GenerateUserIdentityAsync?Shoshone
Not sure, uncommon to see it in entities. Are you sure, it is not the UserManager? That said take a look at the comment of @TetsuyaYamamoto. Entity Frameworks validates many things very late. Are you sure, you are able to create a new AspNetUserLogins and ctx.SaveChanges()?Sesquiplane
@ChristianGollhardt, this is not in an entity. This is part of the IdentityModels.cs file of ASP.NET Identity. And, yes, I'm sure. Everything works until I add the complex type property.Shoshone
So you validated it by creating a new AspNetUserLogins record via EntityFramework? Without the property you have had a new record in your database table? Then the error message is a bug and should be reported. When does the error throw?Sesquiplane
@ChristianGollhardt, no the error message isn't a bug. My guess is that adding the new property causes the entities not to be generated correctly, and AspNetUserLogins happens to be the first entity after the entity generation fails. That's why I get this error message. The question is why does the entity generation fail, even though the relationship between the AspNetUsers and Employee tables exists in the database, and the AspNetUsers entity already has the ICollection<Employee> property.Shoshone
So it is the T4 Template which throws?Sesquiplane
@ChristianGollhardt, Sorry, by entity generation, I was not referring to EF entities. I used that term for the lack of any better word, perhaps because I don't know much about how ASP.NET Identity generates the ApplicationUser and the other authentication entities. That's what I meant.Shoshone
By my understanding you are not supposed to mix the identity context entities (Code First) with your edmx (Database First) context entities. They should be in separate projects and should not reference the other.Aldehyde
@IvanStoev So, you're saying there is no solution?Shoshone
L
10

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.

Lignify answered 24/8, 2018 at 16:31 Comment(1)
Thank you so much for such a great answer, and sorry for my late response! I think you're right about the separation of concerns. I decided to keep the contexts separated. I can always use my EF context to access the same data. Cheers!Shoshone
V
1

Concern:


I know exactly what your burden is here. Yes, Microsoft, an esoteric cult, did very poor job in providing the information to this matter that is creating a relationship with the Identity (Entity Framework).

Contribution:
Ruard van Elburg post on Aug 24 at 16:31 gives good insight on this matter; however, there was one key component that I noticed was missing in his code which was DbSet that needed to be placed in the DBContext of IdentityModels.

Tech Stack:
I provide my tech stack so that if this does not work with older versions of software, you will know what I used to get this issue resolved.
  • Visual Studio 2017 MVC 5. FYI, MVC 5 is built in into most recent VS.
  • SQL Server 17
  • MS SQL Management Studio 17


Solution:


Disclaimer!!! I understand that the concern is for database first; however, this solution is only for code first approach. But hey, it works!

Here I provide a walk through on how to do this. Please make sure you have all the dependencies in top margin of your code.

Step 1: Add public virtual DbSet<ModelNameOfInterest> ModelNameOfInterest { get; set; } to public class ApplicationDbContext : IdentityDbContext<ApplicationUser>{} as seen in code below.

using System.Data.Entity;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
    using System.ComponentModel.DataAnnotations.Schema;

namespace AwesomeCode.Models
{
    // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
    public class ApplicationUser : IdentityUser
    {

        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)
        {
        }
        //A virtul DbSet in order to interact with the autogenerated code the identity framewrok produces.
        public virtual DbSet<ModelNameOfInterest> ModelNameOfInterest { get; set; }

        public static ApplicationDbContext Create()
        {

            return new ApplicationDbContext();
        }



    }
}

Step 2: Add public virtual ApplicationUser ApplicationUser { get; set; } to your model that you want to create a relationship with as seen code below.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Web;

namespace AwesomeCode.Models
{
    public class WorkExp
    {
        [Key]
        public int Id { get; set; }
        public string JobTitle { get; set; }

        //Create foreign key with reference to ApplicationUser_Id that was auto-generated by entity framework.
        public virtual ApplicationUser ApplicationUser { get; set; }
    }
}

Step 3: Given that you set up your connection string for your database, you need to produce a migration. Path to Package Manager Console: Tools->NuGet Packer Manager->Package Manager Console

  • Enable a migration if migration folder is absent in root: After PM>, type Enable-Migrations you should see a migration folder with two files.
  • Once migration is enabled: After PM>, type Update-Database You should see tables in your database now.
  • To add another migration: After PM>, type Add-Migration After Name:, type InitialCreate or Your model of interest You should see tables in your database now. You should see tables in your database now.


Step 4: Double check that the model of interest's foreign key is properly referenced to the AspNetUser table. In MS Management Studio, you can create a relational diagram to show the references. You can find how to do that on google.

Step 5: As always stay cool, calm, and collected.

Vomitory answered 21/11, 2018 at 15:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.