EntityType "IdentityUserLogin" non ha una chiave definita. Definisci la chiave per questo EntityType


105

Sto lavorando con Entity Framework Code First e MVC 5. Quando ho creato la mia applicazione con l' autenticazione degli account utente individuali, mi è stato fornito un controller dell'account e con esso tutte le classi e il codice necessari per far funzionare l'autenticazione degli account utente individuali .

Tra il codice già in atto c'era questo:

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

    }

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

Ma poi sono andato avanti e ho creato il mio contesto usando prima il codice, quindi ora ho anche quanto segue:

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

Finalmente ho il seguente metodo seed per aggiungere alcuni dati con cui lavorare durante lo sviluppo:

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

La mia soluzione funziona bene, ma quando provo ad accedere a un controller che richiede l'accesso al database ottengo il seguente errore:

DX.DOMAIN.Context.IdentityUserLogin:: EntityType 'IdentityUserLogin' non ha una chiave definita. Definisci la chiave per questo EntityType.

DX.DOMAIN.Context.IdentityUserRole:: EntityType 'IdentityUserRole' non ha una chiave definita. Definisci la chiave per questo EntityType.

Che cosa sto facendo di sbagliato? È perché ho due contesti?

AGGIORNARE

Dopo aver letto la risposta di Augusto, sono andato con l' opzione 3 . Ecco come appare la mia classe DXContext ora:

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

Ho anche aggiunto una User.cse una Role.csclasse, hanno questo aspetto:

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

Non ero sicuro se avrei avuto bisogno di una proprietà password per l'utente, dal momento che ApplicationUser predefinito ha quello e un sacco di altri campi!

Ad ogni modo, la modifica di cui sopra funziona correttamente, ma ancora una volta ottengo questo errore quando viene eseguita l'applicazione:

Nome colonna non valido UserId

UserId è una proprietà intera su my Artist.cs

Risposte:


116

Il problema è che il tuo ApplicationUser eredita da IdentityUser , che è definito in questo modo:

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

e le loro chiavi primarie sono mappate nel metodo OnModelCreating della classe 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");

e poiché il tuo DXContext non deriva da esso, quelle chiavi non vengono definite.

Se approfondisci le fonti diMicrosoft.AspNet.Identity.EntityFramework , capirete tutto.

Mi sono imbattuto in questa situazione qualche tempo fa e ho trovato tre possibili soluzioni (forse ce ne sono altre):

  1. Utilizzare DbContext separati per due database diversi o lo stesso database ma tabelle diverse.
  2. Unisci il tuo DXContext con ApplicationDbContext e usa un database.
  3. Utilizza DbContext separati rispetto alla stessa tabella e gestisci le migrazioni di conseguenza.

Opzione 1: vedere aggiornare la parte inferiore.

Opzione 2: ti ritroverai con un DbContext come questo:

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

Opzione 3: avrai un DbContext uguale all'opzione 2. Chiamiamolo IdentityContext. E avrai un altro DbContext chiamato 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();
    }
}

dove l'utente è:

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

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

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

Con questa soluzione sto mappando l'entità User sulla stessa tabella dell'entità ApplicationUser.

Quindi, utilizzando Code First Migrations dovrai generare le migrazioni per IdentityContext e THEN per DXContext, seguendo questo fantastico post di Shailendra Chauhan: Code First Migrations with Multiple Data Contexts

Dovrai modificare la migrazione generata per DXContext. Qualcosa di simile a seconda delle proprietà condivise tra ApplicationUser e 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));

e quindi eseguire le migrazioni in ordine (prima le migrazioni di identità) da global.asax o da qualsiasi altro luogo della tua applicazione utilizzando questa classe personalizzata:

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

In questo modo, le mie entità cross-cutting n-tier non finiscono per ereditare dalle classi AspNetIdentity, e quindi non devo importare questo framework in ogni progetto in cui le utilizzo.

Ci scusiamo per l'ampio post. Spero che possa offrire una guida su questo. Ho già utilizzato le opzioni 2 e 3 negli ambienti di produzione.

AGGIORNAMENTO: Espandi l'opzione 1

Per gli ultimi due progetti ho utilizzato la prima opzione: avere una classe AspNetUser che deriva da IdentityUser e una classe personalizzata separata chiamata AppUser. Nel mio caso, i DbContext sono rispettivamente IdentityContext e DomainContext. E ho definito l'ID dell'AppUser in questo modo:

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 è una classe base astratta personalizzata che utilizzo nel metodo SaveChanges sovrascritto del mio contesto DomainContext)

Creo prima AspNetUser e poi AppUser. Lo svantaggio di questo approccio è che devi assicurarti che la funzionalità "CreateUser" sia transazionale (ricorda che ci saranno due DbContext che chiamano SaveChanges separatamente). L'utilizzo di TransactionScope non ha funzionato per me per qualche motivo, quindi ho finito per fare qualcosa di brutto ma per me funziona:

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

(Per favore, se qualcuno arriva con un modo migliore di fare questa parte, apprezzo il commento o la proposta di una modifica a questa risposta)

I vantaggi sono che non devi modificare le migrazioni e puoi utilizzare qualsiasi gerarchia di ereditarietà folle su AppUser senza scherzare con AspNetUser . E in realtà utilizzo le migrazioni automatiche per il mio IdentityContext (il contesto che deriva da IdentityDbContext):

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

    protected override void Seed(IdentityContext context)
    {
    }
}

Questo approccio ha anche il vantaggio di evitare che le entità trasversali a n livelli ereditino dalle classi AspNetIdentity.


Grazie @Augusto per l'ampio post. Si fa devono usare migrazioni per ottenere l'opzione 3 per il lavoro? Per quanto ne so, le migrazioni EF servono per ripristinare le modifiche? Se sto eliminando il mio database e quindi ricreandolo e seminandolo su ogni nuova build, devo fare tutto ciò che riguarda le migrazioni?
J86

Non l'ho provato senza utilizzare le migrazioni. Non so se puoi farlo senza usarli. Forse è possibile. Ho sempre dovuto utilizzare le migrazioni per conservare i dati personalizzati inseriti nel database.
Augusto Barreto

Una cosa da sottolineare, se usi Migrations ... dovresti usare il AddOrUpdate(new EntityObject { shoes = green})noto anche come "upsert". al contrario della semplice aggiunta al contesto, altrimenti creerai solo informazioni sul contesto dell'entità duplicate / ridondanti.
Chef_Code

Voglio lavorare con la terza opzione, ma non la capisco. qualcuno può dirmi come dovrebbe apparire esattamente IdentityContext? perché non può essere esattamente come nell'opzione 2! Mi puoi aiutare @AugustoBarreto? Ho creato un thread su qualcosa di simile, forse puoi aiutarmi lì
Arianit

Che aspetto ha la tua "TrackableEntity"?
Ciaran Gallagher

224

Nel mio caso avevo ereditato correttamente da IdentityDbContext (con i miei tipi personalizzati e la chiave definita) ma avevo inavvertitamente rimosso la chiamata a OnModelCreating della classe base:

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

Che quindi ha corretto i miei indici mancanti dalle classi di identità e ho potuto quindi generare migrazioni e abilitare le migrazioni in modo appropriato.


Ha avuto lo stesso problema "rimosso la linea". La tua soluzione ha funzionato. :) ty.
Sviluppatore Marius Žilėnas

2
Questo ha risolto il mio problema, dove dovevo sovrascrivere il metodo OnModelCreating per includere una mappatura personalizzata utilizzando un'API fluente per una relazione di entità complessa. Si scopre che ho dimenticato di aggiungere la riga nella risposta prima di dichiarare la mia mappatura poiché sto usando lo stesso contesto di Identity. Saluti.
Dan

Funziona se non è presente "override void OnModelCreating" ma se si esegue l'override è necessario aggiungere "base.OnModelCreating (modelBuilder);" all'override. Risolto il mio problema.
Joe

13

Per coloro che utilizzano ASP.NET Identity 2.1 e hanno modificato la chiave primaria dal valore predefinito stringa into Guid, se stai ancora ottenendo

EntityType "xxxxUserLogin" non ha una chiave definita. Definisci la chiave per questo EntityType.

EntityType "xxxxUserRole" non ha una chiave definita. Definisci la chiave per questo EntityType.

probabilmente ti sei appena dimenticato di specificare il nuovo tipo di chiave su IdentityDbContext:

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

Se hai solo

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

o anche

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

riceverai l'errore "nessuna chiave definita" quando tenterai di aggiungere migrazioni o aggiornare il database.


Sto anche provando a cambiare l'ID in un Int e sto riscontrando questo problema, tuttavia ho cambiato il mio DbContext per specificare il nuovo tipo di chiave. C'è un altro posto che dovrei controllare? Pensavo di seguire le istruzioni con molta attenzione.
Kyle

1
@ Kyle: stai cercando di cambiare l'ID di tutte le entità in int, ovvero AppRole, AppUser, AppUserClaim, AppUserLogin e AppUserRole? In tal caso, potresti anche dover assicurarti di aver specificato il nuovo tipo di chiave per quelle classi. Come "public class AppUserLogin: IdentityUserLogin <int> {}"
David Liang,

1
Questo è il documento ufficiale sulla personalizzazione del tipo di dati delle chiavi primarie: docs.microsoft.com/en-us/aspnet/core/security/authentication/…
AdrienTorris

1
Sì, il mio problema era che ho ereditato dalla classe generale DbContext invece di IdentityDbContext <AppUser>. Grazie, questo ha aiutato molto
yibe

13

Modificando il DbContext come sotto;

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

Sto solo aggiungendo OnModelCreating chiamata al metodo a base.OnModelCreating (modelBuilder); e va bene. Sto usando EF6.

Un ringraziamento speciale a #Il senatore


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

Il mio problema era simile: avevo una nuova tabella che stavo creando e da collegare agli utenti di identità. Dopo aver letto le risposte di cui sopra, ho capito che aveva a che fare con IsdentityUser e le proprietà ereditate. Avevo già Identity impostato come proprio contesto, quindi per evitare di legare intrinsecamente i due insieme, piuttosto che utilizzare la tabella utente correlata come una vera proprietà EF, ho impostato una proprietà non mappata con la query per ottenere le entità correlate. (DataManager è impostato per recuperare il contesto corrente in cui esiste 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);
            }
        }
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.