У EntityType 'IdentityUserLogin' не визначено жодного ключа. Визначте ключ для цього EntityType


105

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

Серед уже наявного коду був такий:

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

За допомогою цього рішення я відображаю сутність Користувача у ту саму таблицю, що і сутність ApplicationUser.

Потім, використовуючи Code First Migrations, вам потрібно буде генерувати міграції для IdentityContext та THEN для DXContext, дотримуючись цього чудового повідомлення від Shailendra Chauhan: Code First Migrations with Multiple Contexts

Вам доведеться змінити міграцію, створену для DXContext. Щось подібне залежно від того, які властивості поділяються між ApplicationUser та користувачем:

        //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)
    {
    }
}

Цей підхід також має перевагу уникнути того, щоб ваші n-ярусні наскрізні об'єкти успадковували від класів AspNetIdentity.


Дякуємо @Augusto за велику публікацію. Чи потрібно використовувати міграцію, щоб змусити роботу 3-го варіанту? Наскільки я знаю, міграція EF призначена для того, щоб змінити зміни? Якщо я скидаю свою базу даних, а потім відновлюю її та висіваю на кожну нову збірку, чи потрібно мені робити це, що стосується міграцій?
J86

Я не пробував цього без використання міграції. Я не знаю, чи зможете ви цього досягти, не використовуючи їх. Можливо, це можливо. Мені завжди доводилося використовувати міграції, щоб зберігати будь-які власні дані, які були вставлені в базу даних.
Augusto Barreto

Одне, що слід зазначити, якщо ви все ж використовуєте міграцію ... ви також повинні використовувати цю AddOrUpdate(new EntityObject { shoes = green})ноу-хау як "прикорм". на відміну від простого додавання до контексту, інакше ви просто створюватимете дублікатну / зайву інформацію про контекст сутності.
Chef_Code

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

Як виглядає ваша "TrackableEntity"?
Ciaran Gallagher

224

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

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

Потім виправляв мої відсутні індекси з класів ідентичності, і я міг генерувати міграції та включати міграції належним чином.


Якщо в тій же проблемі була "видалена лінія". Ваше рішення спрацювало. :) ти.
розробник Marius Žilėnas

2
Це вирішило мою проблему, коли мені довелося змінити метод OnModelCreating, щоб включити користувальницьку картографічну карту з використанням вільних api для складних відносин сутності. Виявляється, я забув додати рядок у відповідь, перш ніж заявляти про своє відображення, оскільки я використовую той же контекст, що і Identity. Ура.
Ден

Він працює, якщо немає "переосмислити недійсність 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: Ви намагаєтесь змінити ідентифікатор усіх організацій на int, тобто AppRole, AppUser, AppUserClaim, AppUserLogin та AppUserRole? Якщо це так, вам може знадобитися також переконатися, що ви вказали новий тип ключів для цих класів. Як і 'public class AppUserLogin: IdentityUserLogin <int> {}'
Девід Лян

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

1
Так, моя проблема полягала в тому, що я успадкував від загального класу DbContext замість IdentityDbContext <AppUser>. Спасибі, це дуже допомогло
підйому

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

Моя проблема була схожа - у мене була нова таблиця, я створював цей ahd, щоб прив’язатись до користувачів ідентичності. Прочитавши вищевказані відповіді, зрозумів, що це стосується 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.