EntityType 'IdentityUserLogin' не имеет определенного ключа. Определите ключ для этого EntityType


105

Я работаю с Entity Framework Code First и MVC 5. Когда я создавал свое приложение с аутентификацией индивидуальных учетных записей пользователей, мне был предоставлен контроллер учетной записи, а вместе с ним и все необходимые классы и код, необходимые для работы аутентификации Indiv User Accounts. .

Среди уже имеющихся кодов было следующее:

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

Ответы:


116

Проблема в том, что ваш 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 , вы все поймете.

Некоторое время назад я столкнулся с этой ситуацией и нашел три возможных решения (может быть, их больше):

  1. Используйте отдельные DbContexts для двух разных баз данных или одной и той же базы данных, но с разными таблицами.
  2. Объедините свой DXContext с ApplicationDbContext и используйте одну базу данных.
  3. Используйте отдельные 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, следуя этой замечательной публикации от Шайлендры Чаухан: 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));

а затем запускать миграции по порядку (сначала миграции идентификаторов) из 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)
    {
    }
}

Этот подход также имеет то преимущество, что вы избегаете наследования многоуровневых сквозных сущностей от классов AspNetIdentity.


Спасибо @Augusto за обширный пост. Есть ли один должен использовать Миграции , чтобы получить Вариант 3 для работы? Насколько я знаю, EF Migrations предназначены для отката изменений? Если я удаляю свою базу данных, а затем заново создаю ее и заполняю при каждой новой сборке, нужно ли мне делать все эти миграции?
J86

Без миграций не пробовал. Я не знаю, сможете ли вы добиться этого без их использования. Может, это возможно. Мне всегда приходилось использовать миграции для сохранения любых пользовательских данных, которые были вставлены в базу данных.
Аугусто Баррето,

Следует отметить, что если вы все-таки используете Migrations ... вы должны использовать AddOrUpdate(new EntityObject { shoes = green})также известный как «upsert». в отличие от простого добавления в контекст, иначе вы просто создадите дублирующую / избыточную информацию о контексте объекта.
Chef_Code

Я хочу работать с 3-м вариантом, но не понимаю. может кто-нибудь сказать мне, как именно должен выглядеть IdentityContext? потому что это не может быть в точности как в варианте 2! Вы можете мне помочь @AugustoBarreto? Я сделал ветку о чем-то подобном, может, вы мне поможете
Арианит

Как выглядит ваш TrackableEntity?
Кьяран Галлахер,

224

В моем случае я правильно унаследовал от IdentityDbContext (с определенными моими собственными типами и ключом), но случайно удалил вызов OnModelCreating базового класса:

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

Которая затем исправила мои недостающие индексы из классов идентичности, и я мог затем сгенерировать миграции и включить миграции соответствующим образом.


Была такая же проблема "удалили строку". Ваше решение сработало. :) ты.
Разработчик Мариус Жиленас

2
Это устранило мою проблему, когда мне пришлось переопределить метод OnModelCreating, чтобы включить настраиваемое сопоставление с использованием свободного API для сложных отношений сущностей. Оказывается, я забыл добавить строку в ответ перед объявлением своего сопоставления, поскольку я использую тот же контекст, что и Identity. Ура.
Дэн

Он работает, если нет «override void OnModelCreating», но если вы переопределите, вам нужно добавить «base.OnModelCreating (modelBuilder);» к отмене. Исправлена ​​моя проблема.
Джо

13

Для тех, кто использует 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>
{
    ......
}

вы получите ошибку «ключ не определен» при попытке добавить миграции или обновить базу данных.


Я также пытаюсь изменить идентификатор на Int, и у меня возникла эта проблема, однако я изменил свой DbContext, чтобы указать новый тип ключа. Есть ли еще что-нибудь, что мне следует проверить? Я думал, что очень внимательно следую инструкциям.
Кайл

1
@Kyle: Вы пытаетесь изменить ID всех сущностей на int, то есть AppRole, AppUser, AppUserClaim, AppUserLogin и AppUserRole? Если это так, вам также может потребоваться убедиться, что вы указали новый тип ключа для этих классов. Как «публичный класс AppUserLogin: IdentityUserLogin <int> {}»
Дэвид Лян,

1
Это официальный документ о настройке типа данных первичных ключей: docs.microsoft.com/en-us/aspnet/core/security/authentication/…
AdrienTorris

1
Да, моя проблема заключалась в том, что я унаследовал от общего класса DbContext вместо IdentityDbContext <AppUser>. Спасибо, это очень помогло
yibe

13

Изменив DbContext, как показано ниже;

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

Просто добавив OnModelCreatingвызов метода в base.OnModelCreating (modelBuilder); и становится нормально. Я использую EF6.

Особая благодарность # сенатору


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);
            }
        }
    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.