实体类型'IdentityUserLogin'未定义键。为此实体类型定义一个键。

115
我正在使用Entity Framework Code First和MVC 5进行工作。当我使用"个别用户账户身份验证(Individual User Accounts Authentication)"创建应用程序时,系统提供了一个账户控制器(Account controller),以及所有必要的类和代码,这些都是需要使得个别用户账户验证(token)实现的。
在已经存在的代码之中有以下内容:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext() : base("DXContext", throwIfV1Schema: false)
    {

    }

    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }
}

但是后来我使用Code First创建了自己的上下文,因此现在我有了以下内容:

public class DXContext : DbContext
{
    public DXContext() : base("DXContext")
    {
        
    }

    public DbSet<ApplicationUser> Users { get; set; }
    public DbSet<IdentityRole> Roles { get; set; }
    public DbSet<Artist> Artists { get; set; }
    public DbSet<Paintings> Paintings { get; set; }        
}

最后,我有以下种子方法来添加一些数据,让我在开发过程中使用:

protected override void Seed(DXContext context)
{
    try
    {

        if (!context.Roles.Any(r => r.Name == "Admin"))
        {
            var store = new RoleStore<IdentityRole>(context);
            var manager = new RoleManager<IdentityRole>(store);
            var role = new IdentityRole { Name = "Admin" };

            manager.Create(role);
        }

        context.SaveChanges();

        if (!context.Users.Any(u => u.UserName == "James"))
        {
            var store = new UserStore<ApplicationUser>(context);
            var manager = new UserManager<ApplicationUser>(store);
            var user = new ApplicationUser { UserName = "James" };

            manager.Create(user, "ChangeAsap1@");
            manager.AddToRole(user.Id, "Admin");
        }

        context.SaveChanges();

        string userId = "";

        userId = context.Users.FirstOrDefault().Id;

        var artists = new List<Artist>
        {
            new Artist { FName = "Salvador", LName = "Dali", ImgURL = "http://i62.tinypic.com/ss8txxn.jpg", UrlFriendly = "salvador-dali", Verified = true, ApplicationUserId = userId },
        };

        artists.ForEach(a => context.Artists.Add(a));
        context.SaveChanges();

        var paintings = new List<Painting>
        {
            new Painting { Title = "The Persistence of Memory", ImgUrl = "http://i62.tinypic.com/xx8tssn.jpg", ArtistId = 1, Verified = true, ApplicationUserId = userId }
        };

        paintings.ForEach(p => context.Paintings.Add(p));
        context.SaveChanges();
    }
    catch (DbEntityValidationException ex)
    {
        foreach (var validationErrors in ex.EntityValidationErrors)
        {
            foreach (var validationError in validationErrors.ValidationErrors)
            {
                Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage);
            }
        }
    }
    
}

我的解决方案编译成功,但当我尝试访问需要访问数据库的控制器时,出现以下错误:
DX.DOMAIN.Context.IdentityUserLogin::实体类型“IdentityUserLogin”未定义键。 为此实体类型定义键。
DX.DOMAIN.Context.IdentityUserRole::实体类型“IdentityUserRole”未定义键。 为此实体类型定义键。
我做错了什么?是因为我有两个上下文吗?
更新:
在阅读Augusto的回复后,我选择了Option 3。现在,我的DXContext类如下所示:
public class DXContext : DbContext
{
    public DXContext() : base("DXContext")
    {
        // remove default initializer
        Database.SetInitializer<DXContext>(null);
        Configuration.LazyLoadingEnabled = false;
        Configuration.ProxyCreationEnabled = false;

    }

    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }
    public DbSet<Artist> Artists { get; set; }
    public DbSet<Painting> Paintings { get; set; }

    public static DXContext Create()
    {
        return new DXContext();
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<User>().ToTable("Users");
        modelBuilder.Entity<Role>().ToTable("Roles");
    }

    public DbQuery<T> Query<T>() where T : class
    {
        return Set<T>().AsNoTracking();
    }
}

我还添加了一个 User.cs 和一个 Role.cs 类,它们的样子如下:
public class User
{
    public int Id { get; set; }
    public string FName { get; set; }
    public string LName { get; set; }
}

public class Role
{
    public int Id { set; get; }
    public string Name { set; get; }
}

我不确定用户是否需要密码属性,因为默认的ApplicationUser已经有了许多其他字段!

无论如何,上述更改构建良好,但是当应用程序运行时,我再次遇到了以下错误:

无效的列名“UserId”

UserId 是我Artist.cs上的整数属性。

6个回答

231

在我的情况下,我正确地继承了IdentityDbContext(使用自己定义的自定义类型和键),但不小心删除了对基类OnModelCreating方法的调用:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder); // I had removed this
    /// Rest of on model creating here.
}

接着,我从身份类中修复了缺失的索引,然后就能够生成并适当地启用迁移了。


遇到了同样的问题,"删除了那一行"。你的解决方案很好用。:) 谢谢。 - Developer Marius Žilėnas
2
这解决了我的问题,我不得不重写 OnModelCreating 方法以包括使用流畅的 api 进行自定义映射的复杂实体关系。结果发现,在声明我的映射之前,我忘记添加答案中的那一行,因为我正在使用与身份相同的上下文。干杯! - Dan
如果没有“override void OnModelCreating”,它就可以工作,但是如果你重写了这个方法,你需要在重写方法中添加“base.OnModelCreating(modelBuilder);”。这样修复了我的问题。 - Joe

124

问题在于你的ApplicationUser继承自IdentityUser,而IdentityUser被定义为:

IdentityUser : IdentityUser<string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUser
....
public virtual ICollection<TRole> Roles { get; private set; }
public virtual ICollection<TClaim> Claims { get; private set; }
public virtual ICollection<TLogin> Logins { get; private set; }

他们的主键在IdentityDbContext类的OnModelCreating方法中被映射:

modelBuilder.Entity<TUserRole>()
            .HasKey(r => new {r.UserId, r.RoleId})
            .ToTable("AspNetUserRoles");

modelBuilder.Entity<TUserLogin>()
            .HasKey(l => new {l.LoginProvider, l.ProviderKey, l.UserId})
            .ToTable("AspNetUserLogins");

由于您的DXContext没有从中派生出来,所以这些键没有被定义。

如果您深入研究Microsoft.AspNet.Identity.EntityFramework的源代码,您将理解一切。

我在一段时间前遇到了这种情况,并找到了三种可能的解决方案(也许还有更多):

  1. 使用针对两个不同数据库或相同数据库但不同表的单独 DbContext。
  2. 将您的 DXContext 与 ApplicationDbContext 合并并使用一个数据库。
  3. 使用针对同一表的单独 DbContext 并相应地管理它们的迁移。

选项1:请参见底部更新。

选项2:您最终将得到一个类似于下面这个 DbContext:

public class DXContext : IdentityDbContext<User, Role,
    int, UserLogin, UserRole, UserClaim>//: DbContext
{
    public DXContext()
        : base("name=DXContext")
    {
        Database.SetInitializer<DXContext>(null);// Remove default initializer
        Configuration.ProxyCreationEnabled = false;
        Configuration.LazyLoadingEnabled = false;
    }

    public static DXContext Create()
    {
        return new DXContext();
    }

    //Identity and Authorization
    public DbSet<UserLogin> UserLogins { get; set; }
    public DbSet<UserClaim> UserClaims { get; set; }
    public DbSet<UserRole> UserRoles { get; set; }
    
    // ... your custom DbSets
    public DbSet<RoleOperation> RoleOperations { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

        // Configure Asp Net Identity Tables
        modelBuilder.Entity<User>().ToTable("User");
        modelBuilder.Entity<User>().Property(u => u.PasswordHash).HasMaxLength(500);
        modelBuilder.Entity<User>().Property(u => u.Stamp).HasMaxLength(500);
        modelBuilder.Entity<User>().Property(u => u.PhoneNumber).HasMaxLength(50);

        modelBuilder.Entity<Role>().ToTable("Role");
        modelBuilder.Entity<UserRole>().ToTable("UserRole");
        modelBuilder.Entity<UserLogin>().ToTable("UserLogin");
        modelBuilder.Entity<UserClaim>().ToTable("UserClaim");
        modelBuilder.Entity<UserClaim>().Property(u => u.ClaimType).HasMaxLength(150);
        modelBuilder.Entity<UserClaim>().Property(u => u.ClaimValue).HasMaxLength(500);
    }
}

选项3: 您将拥有一个与选项2相等的DbContext。让我们称其为IdentityContext。您还将拥有另一个名为DXContext的DbContext:

public class DXContext : DbContext
{        
    public DXContext()
        : base("name=DXContext") // connection string in the application configuration file.
    {
        Database.SetInitializer<DXContext>(null); // Remove default initializer
        Configuration.LazyLoadingEnabled = false;
        Configuration.ProxyCreationEnabled = false;
    }

    // Domain Model
    public DbSet<User> Users { get; set; }
    // ... other custom DbSets
    
    public static DXContext Create()
    {
        return new DXContext();
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

        // IMPORTANT: we are mapping the entity User to the same table as the entity ApplicationUser
        modelBuilder.Entity<User>().ToTable("User"); 
    }

    public DbQuery<T> Query<T>() where T : class
    {
        return Set<T>().AsNoTracking();
    }
}

用户是:

public class User
{
    public int Id { get; set; }

    [Required, StringLength(100)]
    public string Name { get; set; }

    [Required, StringLength(128)]
    public string SomeOtherColumn { get; set; }
}

使用这种解决方案,我将实体User映射到与实体ApplicationUser相同的表中。

接下来,使用Code First Migrations需要为IdentityContext生成迁移,然后为DXContext生成迁移,可以参考Shailendra Chauhan的这篇文章:Code First Migrations with Multiple Data Contexts

您需要修改为DXContext生成的迁移,具体取决于ApplicationUser和User之间共享哪些属性:

        //CreateTable(
        //    "dbo.User",
        //    c => new
        //        {
        //            Id = c.Int(nullable: false, identity: true),
        //            Name = c.String(nullable: false, maxLength: 100),
        //            SomeOtherColumn = c.String(nullable: false, maxLength: 128),
        //        })
        //    .PrimaryKey(t => t.Id);
        AddColumn("dbo.User", "SomeOtherColumn", c => c.String(nullable: false, maxLength: 128));

然后按顺序运行迁移(首先是身份认证迁移),可以使用这个自定义类在 global.asax 或应用程序的任何其他地方运行迁移:

public static class DXDatabaseMigrator
{
    public static string ExecuteMigrations()
    {
        return string.Format("Identity migrations: {0}. DX migrations: {1}.", ExecuteIdentityMigrations(),
            ExecuteDXMigrations());
    }

    private static string ExecuteIdentityMigrations()
    {
        IdentityMigrationConfiguration configuration = new IdentityMigrationConfiguration();
        return RunMigrations(configuration);
    }

    private static string ExecuteDXMigrations()
    {
        DXMigrationConfiguration configuration = new DXMigrationConfiguration();
        return RunMigrations(configuration);
    }

    private static string RunMigrations(DbMigrationsConfiguration configuration)
    {
        List<string> pendingMigrations;
        try
        {
            DbMigrator migrator = new DbMigrator(configuration);
            pendingMigrations = migrator.GetPendingMigrations().ToList(); // Just to be able to log which migrations were executed

            if (pendingMigrations.Any())                
                    migrator.Update();     
        }
        catch (Exception e)
        {
            ExceptionManager.LogException(e);
            return e.Message;
        }
        return !pendingMigrations.Any() ? "None" : string.Join(", ", pendingMigrations);
    }
}

这样做,我的n层交叉实体就不会继承自AspNetIdentity类,因此我不必在使用它们的每个项目中导入这个框架。

很抱歉这篇文章比较冗长,但我希望它可以为您提供一些指导。我已经在生产环境中使用了选项2和3。

更新:扩展选项1

对于最近两个项目,我使用了第一种选项:有一个从IdentityUser派生出来的AspNetUser类和一个名为AppUser的单独的自定义类。在我的情况下,DbContext分别是IdentityContext和DomainContext。而且我定义了AppUser的Id如下:

public class AppUser : TrackableEntity
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    // This Id is equal to the Id in the AspNetUser table and it's manually set.
    public override int Id { get; set; }
是我在上重写的方法中使用的自定义抽象基类。我首先创建,然后创建。这种方法的缺点是确保您的“CreateUser”功能是事务性的(请记住,将有两个DbContext分别调用SaveChanges)。对我而言,使用TransactionScope没有起作用,所以最终我做了一些丑陋的但适用于我的事情。
        IdentityResult identityResult = UserManager.Create(aspNetUser, model.Password);

        if (!identityResult.Succeeded)
            throw new TechnicalException("User creation didn't succeed", new LogObjectException(result));

        AppUser appUser;
        try
        {
            appUser = RegisterInAppUserTable(model, aspNetUser);
        }
        catch (Exception)
        {
            // Roll back
            UserManager.Delete(aspNetUser);
            throw;
        }

如果有人能提出更好的方法,请在评论中或为此答案提出修改意见。

好处是您不必修改迁移,而且您可以在 AppUser 上使用任何疯狂的继承层次结构,而不会干扰 AspNetUser。实际上,我在 IdentityContext(从 IdentityDbContext 派生的上下文)中使用自动迁移:

public sealed class IdentityMigrationConfiguration : DbMigrationsConfiguration<IdentityContext>
{
    public IdentityMigrationConfiguration()
    {
        AutomaticMigrationsEnabled = true;
        AutomaticMigrationDataLossAllowed = false;
    }

    protected override void Seed(IdentityContext context)
    {
    }
}

这种方法的好处还在于避免让您的N层交叉实体继承自AspNetIdentity类。


感谢@Augusto的详细帖子。要让选项3起作用,是否必须使用迁移?据我所知,EF迁移是用于回滚更改的吗?如果我在每次新构建时都要删除数据库,然后重新创建并填充它,那么我需要做所有这些迁移工作吗? - J86
我没有尝试过不使用迁移来完成这个任务。我不知道是否可以在不使用迁移的情况下实现它。也许是可能的。我总是不得不使用迁移来保留插入到数据库中的任何自定义数据。 - Augusto Barreto
需要指出的一件事是,如果您使用迁移...您应该使用“AddOrUpdate(new EntityObject {shoes = green})”也称为“upsert”。与仅添加到上下文相反,否则您将只创建重复/冗余的实体上下文信息。 - Chef_Code
我想使用第三个选项进行工作,但是我有点不明白。有人可以告诉我IdentityContext应该长什么样子吗?因为它不能完全像选项2那样!@AugustoBarreto,你能帮我吗?我已经发布了一个类似的主题,也许你可以在那里帮助我。 - Arianit
你的“TrackableEntity”是什么样子的? - Ciaran Gallagher

14

通过以下更改 DbContext;

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
        modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
    }

只需要在 OnModelCreating 方法中添加调用 base.OnModelCreating(modelBuilder);,问题就解决了。我正在使用 EF6。

特别感谢 #The Senator。


13
对于那些使用ASP.NET Identity 2.1并将主键从默认的string更改为intGuid的人,如果您仍然收到以下错误信息:

EntityType 'xxxxUserLogin' has no key defined. Define the key for this EntityType.

EntityType 'xxxxUserRole' has no key defined. Define the key for this EntityType.

您可能只是忘记在IdentityDbContext上指定新的键类型。
public class AppIdentityDbContext : IdentityDbContext<
    AppUser, AppRole, int, AppUserLogin, AppUserRole, AppUserClaim>
{
    public AppIdentityDbContext()
        : base("MY_CONNECTION_STRING")
    {
    }
    ......
}

如果您只有

public class AppIdentityDbContext : IdentityDbContext
{
    ......
}

甚至更多

public class AppIdentityDbContext : IdentityDbContext<AppUser>
{
    ......
}

当您尝试添加迁移或更新数据库时,可能会遇到“未定义键”错误。


我也试图将ID更改为Int,但是我遇到了这个问题,不过我已经更改了我的DbContext以指定新的键类型。还有其他地方我应该检查吗?我认为我非常仔细地遵循了说明。 - Kyle
1
@Kyle:你是想把所有实体的ID改为int型,比如AppRole、AppUser、AppUserClaim、AppUserLogin和AppUserRole吗?如果是这样,你可能还需要确保已经为这些类指定了新的键类型。就像“public class AppUserLogin : IdentityUserLogin <int> { }”一样。 - David Liang
1
这是有关自定义主键数据类型的官方文档:https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-primary-key-configuration - AdrienTorris
1
是的,我的问题是,我继承了一般的DbContext类,而不是IdentityDbContext<AppUser>。谢谢,这帮了我很多。 - yibe
public class AppIdentityDbContext : IdentityDbContext<AppUser> 中,AppUser 是我创建的模型还是 Identity 拥有的模型? - I'm newbie
1
@I'mnewbie:所有这些5个类AppXXX都是您创建的自定义类。它们继承自Microsoft的IdentityXXX类。 - David Liang

1
 protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            //foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
            //    relationship.DeleteBehavior = DeleteBehavior.Restrict;

            modelBuilder.Entity<User>().ToTable("Users");

            modelBuilder.Entity<IdentityRole<string>>().ToTable("Roles");
            modelBuilder.Entity<IdentityUserToken<string>>().ToTable("UserTokens");
            modelBuilder.Entity<IdentityUserClaim<string>>().ToTable("UserClaims");
            modelBuilder.Entity<IdentityUserLogin<string>>().ToTable("UserLogins");
            modelBuilder.Entity<IdentityRoleClaim<string>>().ToTable("RoleClaims");
            modelBuilder.Entity<IdentityUserRole<string>>().ToTable("UserRoles");

        }
    }

0

我的问题类似——我正在创建一个新表,必须将其与身份用户相关联。在阅读了上面的答案后,我意识到与IsdentityUser和继承属性有关。我已经将Identity设置为自己的上下文,因此为了避免固有地将两者联系在一起,而不是将相关用户表用作真正的EF属性,我设置了一个非映射属性,并使用查询获取相关实体。(DataManager已经设置为检索OtherEntity所处的当前上下文。)

    [Table("UserOtherEntity")]
        public partial class UserOtherEntity
        {
            public Guid UserOtherEntityId { get; set; }
            [Required]
            [StringLength(128)]
            public string UserId { get; set; }
            [Required]
            public Guid OtherEntityId { get; set; }
            public virtual OtherEntity OtherEntity { get; set; }
        }

    public partial class UserOtherEntity : DataManager
        {
            public static IEnumerable<OtherEntity> GetOtherEntitiesByUserId(string userId)
            {
                return Connect2Context.UserOtherEntities.Where(ue => ue.UserId == userId).Select(ue => ue.OtherEntity);
            }
        }

public partial 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;
        }

        [NotMapped]
        public IEnumerable<OtherEntity> OtherEntities
        {
            get
            {
                return UserOtherEntities.GetOtherEntitiesByUserId(this.Id);
            }
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接