EntityType „IdentityUserLogin” nie ma zdefiniowanego klucza. Zdefiniuj klucz dla tego EntityType

105

Pracuję z Entity Framework Code First i MVC 5. Kiedy utworzyłem aplikację z uwierzytelnianiem indywidualnych kont użytkowników , otrzymałem kontroler konta, a wraz z nim wszystkie wymagane klasy i kod, który jest potrzebny do uruchomienia uwierzytelnienia kont użytkowników Indiv .

Wśród już wprowadzonego kodu był następujący:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext() : base("DXContext", throwIfV1Schema: false)
    {

    }

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

Ale potem poszedłem do przodu i najpierw utworzyłem własny kontekst, używając kodu, więc mam teraz również:

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

Na koniec mam następującą metodę zarodka, aby dodać trochę danych do pracy podczas tworzenia:

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

Moje rozwiązanie działa dobrze, ale kiedy próbuję uzyskać dostęp do kontrolera, który wymaga dostępu do bazy danych, pojawia się następujący błąd:

DX.DOMAIN.Context.IdentityUserLogin:: EntityType „IdentityUserLogin” nie ma zdefiniowanego klucza. Zdefiniuj klucz dla tego EntityType.

DX.DOMAIN.Context.IdentityUserRole:: EntityType „IdentityUserRole” nie ma zdefiniowanego klucza. Zdefiniuj klucz dla tego EntityType.

Co ja robię źle? Czy to dlatego, że mam dwa konteksty?

AKTUALIZACJA

Po przeczytaniu odpowiedzi Augusto wybrałem opcję 3 . Oto jak wygląda teraz moja klasa 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();
    }
}

Dodałem też a User.csi Role.csklasę, wyglądają tak:

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

Nie byłem pewien, czy będę potrzebować właściwości hasła na użytkowniku, ponieważ domyślny ApplicationUser ma to i kilka innych pól!

W każdym razie powyższa zmiana kompiluje się dobrze, ale ponownie pojawia się ten błąd, gdy aplikacja jest uruchomiona:

Nieprawidłowa nazwa kolumny UserId

UserId jest własnością całkowitą na my Artist.cs

J86
źródło

Odpowiedzi:

116

Problem polega na tym, że ApplicationUser dziedziczy po IdentityUser , który jest zdefiniowany w następujący sposób:

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

a ich klucze podstawowe są mapowane w metodzie OnModelCreating klasy 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");

a ponieważ twój DXContext nie pochodzi z tego, te klucze nie są definiowane.

Jeśli kopać w źródłach o Microsoft.AspNet.Identity.EntityFramework, zrozumiesz wszystko.

Kiedyś natknąłem się na taką sytuację i znalazłem trzy możliwe rozwiązania (może jest ich więcej):

  1. Użyj oddzielnych DbContexts dla dwóch różnych baz danych lub tej samej bazy danych, ale różnych tabel.
  2. Połącz swój DXContext z ApplicationDbContext i używaj jednej bazy danych.
  3. Użyj oddzielnych DbContexts dla tej samej tabeli i odpowiednio zarządzaj ich migracjami.

Opcja 1: Zobacz aktualizację na dole.

Opcja 2: Skończysz z DbContext takim jak ten:

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

Opcja 3: Będziesz mieć jeden DbContext równy opcji 2. Nazwijmy go IdentityContext. Będziesz mieć inny DbContext o nazwie 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();
    }
}

gdzie użytkownik jest:

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

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

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

W tym rozwiązaniu mapuję jednostkę User do tej samej tabeli, co jednostka ApplicationUser.

Następnie, korzystając z Migracje Code First, musisz wygenerować migracje dla IdentityContext i THEN dla DXContext, zgodnie z tym świetnym postem od Shailendra Chauhan: Migracje Code First z wieloma kontekstami danych

Będziesz musiał zmodyfikować migrację wygenerowaną dla DXContext. Coś takiego w zależności od tego, które właściwości są współdzielone między ApplicationUser i 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));

a następnie uruchamianie migracji w kolejności (najpierw migracje tożsamości) z global.asax lub dowolnego innego miejsca aplikacji przy użyciu tej klasy niestandardowej:

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

W ten sposób moje n-warstwowe jednostki tnące nie dziedziczą po klasach AspNetIdentity, dlatego nie muszę importować tej struktury w każdym projekcie, w którym ich używam.

Przepraszamy za obszerny post. Mam nadzieję, że może dostarczyć wskazówek na ten temat. Użyłem już opcji 2 i 3 w środowiskach produkcyjnych.

AKTUALIZACJA: Rozwiń opcję 1

W ostatnich dwóch projektach użyłem pierwszej opcji: posiadanie klasy AspNetUser, która pochodzi od IdentityUser i oddzielnej klasy niestandardowej o nazwie AppUser. W moim przypadku DbContexts to odpowiednio IdentityContext i DomainContext. I zdefiniowałem identyfikator użytkownika aplikacji w następujący sposób:

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 to niestandardowa abstrakcyjna klasa bazowa, której używam w nadpisanej metodzie SaveChanges mojego kontekstu DomainContext)

Najpierw tworzę AspNetUser, a następnie AppUser. Wadą tego podejścia jest to, że masz pewność, że funkcja „CreateUser” jest transakcyjna (pamiętaj, że będą dwa DbContexts wywołujące osobno SaveChanges). Korzystanie z TransactionScope z jakiegoś powodu nie zadziałało, więc w końcu zrobiłem coś brzydkiego, ale to działa dla mnie:

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

(Proszę, jeśli ktoś znajdzie lepszy sposób wykonania tej części, doceniam komentowanie lub proponowanie zmian w tej odpowiedzi)

Korzyści polegają na tym, że nie musisz modyfikować migracji i możesz użyć dowolnej szalonej hierarchii dziedziczenia na AppUser bez bałaganu z AspNetUser . I faktycznie używam automatycznych migracji dla mojego IdentityContext (kontekst, który pochodzi z IdentityDbContext):

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

    protected override void Seed(IdentityContext context)
    {
    }
}

Takie podejście ma również tę zaletę, że pozwala uniknąć sytuacji, w których n-warstwowe jednostki przecinające dziedziczą po klasach AspNetIdentity.

Augusto Barreto
źródło
Dzięki @Augusto za obszerny post. Czy trzeba używać migracji, aby opcja 3 działała? O ile wiem, migracje EF służą do wycofywania zmian? Jeśli upuszczam bazę danych, a następnie tworzę ją ponownie i umieszczam w każdej nowej kompilacji, czy muszę robić to wszystko z migracjami?
J86
Nie próbowałem tego bez migracji. Nie wiem, czy możesz to osiągnąć bez ich używania. Może to możliwe. Zawsze musiałem używać migracji, aby zachować wszelkie niestandardowe dane wstawione do bazy danych.
Augusto Barreto
Jedna rzecz, na którą należy zwrócić uwagę, jeśli używasz migracji ... powinieneś używać AddOrUpdate(new EntityObject { shoes = green})również znanego jako „upsert”. w przeciwieństwie do zwykłego dodawania do kontekstu, w przeciwnym razie będziesz po prostu tworzyć zduplikowane / nadmiarowe informacje o kontekście encji.
Chef_Code
Chcę pracować z trzecią opcją, ale trochę jej nie rozumiem. czy ktoś może mi powiedzieć, jak dokładnie powinien wyglądać IdentityContext? bo nie może być dokładnie tak, jak w opcji 2! Czy możesz mi pomóc @AugustoBarreto? Zrobiłem wątek o czymś podobnym, może możesz mi tam pomóc
Arianit
Jak wygląda Twój „TrackableEntity”?
Ciaran Gallagher
224

W moim przypadku poprawnie odziedziczyłem po IdentityDbContext (z własnymi niestandardowymi typami i zdefiniowanymi kluczami), ale nieumyślnie usunąłem wywołanie OnModelCreating klasy bazowej:

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

Który następnie naprawił moje brakujące indeksy z klas tożsamości, a następnie mogłem wygenerować migracje i odpowiednio włączyć migracje.

Senator
źródło
Miałem ten sam problem „usunąłem linię”. Twoje rozwiązanie zadziałało. :) ty.
Deweloper Marius Žilėnas
2
To rozwiązało mój problem, w którym musiałem zastąpić metodę OnModelCreating, aby uwzględnić niestandardowe mapowanie przy użyciu interfejsu API Fluent dla złożonych relacji encji. Okazuje się, że zapomniałem dodać wiersz w odpowiedzi przed zadeklarowaniem mapowania, ponieważ używam tego samego kontekstu co Tożsamość. Twoje zdrowie.
Dan
Działa, jeśli nie ma 'override void OnModelCreating', ale jeśli zastąpisz, musisz dodać 'base.OnModelCreating (modelBuilder);' do zastąpienia. Naprawiłem mój problem.
Joe
13

Dla tych, którzy używają ASP.NET Identity 2.1 i zmienili klucz podstawowy z domyślnego stringna albo intlub Guid, jeśli nadal otrzymujesz

EntityType „xxxxUserLogin” nie ma zdefiniowanego klucza. Zdefiniuj klucz dla tego EntityType.

EntityType „xxxxUserRole” nie ma zdefiniowanego klucza. Zdefiniuj klucz dla tego EntityType.

prawdopodobnie zapomniałeś podać nowego typu klucza na IdentityDbContext:

public class AppIdentityDbContext : IdentityDbContext<
    AppUser, AppRole, int, AppUserLogin, AppUserRole, AppUserClaim>
{
    public AppIdentityDbContext()
        : base("MY_CONNECTION_STRING")
    {
    }
    ......
}

Jeśli tylko masz

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

lub nawet

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

podczas próby dodania migracji lub aktualizacji bazy danych pojawi się błąd „brak zdefiniowanego klucza”.

David Liang
źródło
Próbuję również zmienić identyfikator na Int i mam ten problem, jednak zmieniłem mój kontekst DbContext, aby określić nowy typ klucza. Czy jest jeszcze gdzieś, co powinienem sprawdzić? Myślałem, że bardzo dokładnie wykonuję instrukcje.
Kyle
1
@Kyle: Czy próbujesz zmienić identyfikator wszystkich podmiotów na int, tj. AppRole, AppUser, AppUserClaim, AppUserLogin i AppUserRole? Jeśli tak, być może trzeba będzie również upewnić się, że określono nowy typ klucza dla tych klas. Na przykład „public class AppUserLogin: IdentityUserLogin <int> {}”
David Liang
1
To jest oficjalny dokument dotyczący dostosowywania typu danych kluczy podstawowych: docs.microsoft.com/en-us/aspnet/core/security/authentication/ ...
AdrienTorris
1
Tak, mój problem polegał na tym, że odziedziczyłem po ogólnej klasie DbContext zamiast IdentityDbContext <AppUser>. Dzięki, to bardzo pomogło
yibe
13

Zmieniając DbContext, jak poniżej;

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

Wystarczy dodać OnModelCreatingwywołanie metody do base.OnModelCreating (modelBuilder); i będzie dobrze. Używam EF6.

Specjalne podziękowania dla #The Senator

Muhammad Usama
źródło
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");

        }
    }
Javier González
źródło
0

Mój problem był podobny - miałem nową tabelę, którą tworzyłem, ahd w celu powiązania z użytkownikami tożsamości. Po przeczytaniu powyższych odpowiedzi zdałem sobie sprawę, że ma to związek z IsdentityUser i odziedziczonymi właściwościami. Miałem już skonfigurowaną tożsamość jako własny kontekst, więc aby uniknąć nieodłącznego wiązania obu razem, zamiast używania powiązanej tabeli użytkownika jako prawdziwej właściwości EF, skonfigurowałem niepamapowaną właściwość z zapytaniem, aby uzyskać powiązane jednostki. (DataManager jest skonfigurowany do pobierania bieżącego kontekstu, w którym istnieje 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);
            }
        }
    }
Mikrofon
źródło