EntityType 'IdentityUserLogin' не определен ключ. Определите ключ для этого EntityType
Я работаю с Entity Framework Code First и MVC 5. Когда я создавал свое приложение с Аутентификацией индивидуальных учетных записей пользователей, мне был предоставлен контроллер учетных записей, а также все необходимые классы и код, необходимые для работы аутентификации учетных записей отдельных пользователей.,
Среди кода, который уже был на месте, было это:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext() : base("DXContext", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
Но потом я пошел дальше и сначала создал свой собственный контекст, используя код, так что теперь у меня есть следующее:
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:: EntityType "IdentityUserLogin" не определен ключ. Определите ключ для этого EntityType.
DX.DOMAIN.Context.IdentityUserRole:: EntityType "IdentityUserRole" не имеет определенного ключа. Определите ключ для этого EntityType.
Что я делаю неправильно? Это потому что у меня два контекста?
ОБНОВИТЬ
Прочитав ответ Аугусто, я выбрал вариант 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 ответов
Проблема в том, что ваш ApplicationUser наследуется от 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; }
и их первичные ключи отображаются в методе OnModelCreating класса IdentityDbContext:
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
, вы все поймете.
Я столкнулся с этой ситуацией некоторое время назад, и я нашел три возможных решения (возможно, есть и другие):
- Используйте отдельные DbContexts для двух разных баз данных или одной и той же базы данных, но разных таблиц.
- Объедините ваш DXContext с ApplicationDbContext и используйте одну базу данных.
- Используйте отдельные DbContexts для одной и той же таблицы и соответственно управляйте их миграциями.
Вариант 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: у вас будет один DbContext, равный варианту 2. Давайте назовем его IdentityContext. И у вас будет другой DbContext с именем DXContext:
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 и THEN для DXContext, следуя этому замечательному сообщению от Shailendra Chauhan: Code First Migrations с несколькими контекстами данных
Вам придется изменить миграцию, созданную для 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));
и затем запускаем миграцию по порядку (сначала миграции Identity) из 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
Для последних двух проектов я использовал 1-й вариант: наличие класса AspNetUser, производного от IdentityUser, и отдельного пользовательского класса под названием AppUser. В моем случае DbContexts - это IdentityContext и DomainContext соответственно. И я определил идентификатор AppUser следующим образом:
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; }
(TrackableEntity - это пользовательский абстрактный базовый класс, который я использую в переопределенном методе SaveChanges моего контекста DomainContext)
Сначала я создаю AspNetUser, а затем AppUser. Недостаток этого подхода заключается в том, что вы должны убедиться, что ваша функциональность "CreateUser" является транзакционной (помните, что будет два DbContexts, вызывающих 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.
В моем случае я унаследовал от IdentityDbContext правильно (с определенными моими собственными пользовательскими типами и ключом), но случайно удалил вызов OnModelCreating базового класса:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder); // I had removed this
/// Rest of on model creating here.
}
Это затем исправило мои отсутствующие индексы из классов идентификаторов, и я мог затем генерировать миграции и включать миграции соответствующим образом.
Изменяя DbContext как показано ниже;
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
}
Просто добавив в метод OnModelCreating вызов метода base.OnModelCreating(modelBuilder); и его стало хорошо. Я использую EF6.
Особая благодарность сенатору
Для тех, кто использует ASP.NET Identity 2.1 и изменил первичный ключ по умолчанию string
либо int
или же Guid
, если вы все еще получаете
EntityType 'xxxxUserLogin' не определен ключ. Определите ключ для этого EntityType.
EntityType 'xxxxUserRole' не определен ключ. Определите ключ для этого 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>
{
......
}
вы получите эту ошибку "ключ не определен", когда вы попытаетесь добавить миграции или обновить базу данных.
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");
}
}
Моя проблема была похожа - у меня была новая таблица, которую я создавал, чтобы привязать ее к пользователям. Прочитав приведенные выше ответы, понял, что это связано с 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);
}
}
}