EntityType 'IdentityUserLogin' n'a pas de clé définie. Définir la clé pour cet EntityType

105

Je travaille avec Entity Framework Code First et MVC 5. Lorsque j'ai créé mon application avec l'authentification des comptes d'utilisateurs individuels, j'ai reçu un contrôleur de compte et avec lui toutes les classes et le code requis pour que l'authentification des comptes d'utilisateurs Indiv fonctionne .

Parmi les codes déjà en place, il y avait ceci:

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

    }

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

Mais ensuite, je suis allé de l'avant et j'ai d'abord créé mon propre contexte en utilisant du code, donc j'ai maintenant ce qui suit:

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

Enfin, j'ai la méthode de démarrage suivante pour ajouter des données avec lesquelles je pourrai travailler tout en développant:

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

Ma solution se construit bien, mais lorsque j'essaie d'accéder à un contrôleur qui nécessite l'accès à la base de données, j'obtiens l'erreur suivante:

DX.DOMAIN.Context.IdentityUserLogin:: EntityType 'IdentityUserLogin' n'a pas de clé définie. Définissez la clé de cet EntityType.

DX.DOMAIN.Context.IdentityUserRole:: EntityType 'IdentityUserRole' n'a pas de clé définie. Définissez la clé de cet EntityType.

Qu'est-ce que je fais mal? Est-ce parce que j'ai deux contextes?

METTRE À JOUR

Après avoir lu la réponse d'Augusto, j'ai opté pour l' option 3 . Voici à quoi ressemble ma classe DXContext maintenant:

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

J'ai également ajouté une classe User.cset une Role.csclasse, elles ressemblent à ceci:

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

Je n'étais pas sûr si j'aurais besoin d'une propriété de mot de passe sur l'utilisateur, car l'ApplicationUser par défaut a cela et un tas d'autres champs!

Quoi qu'il en soit, le changement ci-dessus se construit bien, mais encore une fois, j'obtiens cette erreur lorsque l'application est exécutée:

Nom de colonne non valide UserId

UserId est une propriété entière sur mon Artist.cs

J86
la source

Réponses:

116

Le problème est que votre ApplicationUser hérite de IdentityUser , qui est défini comme ceci:

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

et leurs clés primaires sont mappées dans la méthode OnModelCreating de la 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");

et comme votre DXContext n'en dérive pas, ces clés ne sont pas définies.

Si vous fouillez dans les sources de Microsoft.AspNet.Identity.EntityFramework, vous comprendrez tout.

Je suis tombé sur cette situation il y a quelque temps et j'ai trouvé trois solutions possibles (il y en a peut-être plus):

  1. Utilisez des DbContexts distincts sur deux bases de données différentes ou la même base de données mais des tables différentes.
  2. Fusionnez votre DXContext avec ApplicationDbContext et utilisez une base de données.
  3. Utilisez des DbContexts distincts sur la même table et gérez leurs migrations en conséquence.

Option 1: Voir mise à jour en bas.

Option 2: Vous vous retrouverez avec un DbContext comme celui-ci:

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

Option 3: Vous aurez un DbContext égal à l'option 2. Appelons-le IdentityContext. Et vous aurez un autre DbContext appelé 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();
    }
}

où l'utilisateur est:

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

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

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

Avec cette solution, je mappe l'entité User sur la même table que l'entité ApplicationUser.

Ensuite, en utilisant les migrations Code First, vous devrez générer les migrations pour IdentityContext et ALORS pour DXContext, à la suite de cet excellent article de Shailendra Chauhan: Migrations Code First avec plusieurs contextes de données

Vous devrez modifier la migration générée pour DXContext. Quelque chose comme ça en fonction des propriétés partagées entre ApplicationUser et 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));

puis exécutez les migrations dans l'ordre (d'abord les migrations d'identité) à partir du global.asax ou de tout autre endroit de votre application à l'aide de cette classe personnalisée:

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

De cette façon, mes entités transversales à n niveaux ne finissent pas par hériter des classes AspNetIdentity, et je n'ai donc pas à importer ce framework dans chaque projet où je les utilise.

Désolé pour le message complet. J'espère qu'il pourra offrir des conseils à ce sujet. J'ai déjà utilisé les options 2 et 3 dans des environnements de production.

MISE À JOUR: Développez l'option 1

Pour les deux derniers projets, j'ai utilisé la première option: avoir une classe AspNetUser qui dérive d'IdentityUser et une classe personnalisée distincte appelée AppUser. Dans mon cas, les DbContexts sont respectivement IdentityContext et DomainContext. Et j'ai défini l'identifiant de l'AppUser comme ceci:

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 est une classe de base abstraite personnalisée que j'utilise dans la méthode SaveChanges remplacée de mon contexte DomainContext)

Je crée d'abord AspNetUser, puis AppUser. L'inconvénient de cette approche est que vous devez vous assurer que votre fonctionnalité «CreateUser» est transactionnelle (rappelez-vous qu'il y aura deux DbContexts appelant SaveChanges séparément). Utiliser TransactionScope n'a pas fonctionné pour moi pour une raison quelconque, alors j'ai fini par faire quelque chose de laid mais cela fonctionne pour moi:

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

(S'il vous plaît, si quelqu'un propose une meilleure façon de faire cette partie, j'apprécie de commenter ou de proposer une modification à cette réponse)

Les avantages sont que vous n'avez pas à modifier les migrations et que vous pouvez utiliser n'importe quelle hiérarchie d'héritage folle sur AppUser sans jouer avec AspNetUser . Et en fait, j'utilise les migrations automatiques pour mon IdentityContext (le contexte qui dérive d'IdentityDbContext):

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

    protected override void Seed(IdentityContext context)
    {
    }
}

Cette approche a également l'avantage d'éviter que vos entités transversales à n niveaux héritent des classes AspNetIdentity.

Augusto Barreto
la source
Merci @Augusto pour le message détaillé. Doit - on avoir à utiliser l' option pour obtenir les migrations 3 au travail? Autant que je sache, les migrations EF sont destinées à annuler les modifications? Si je supprime ma base de données, puis la recrée et l'amorce à chaque nouvelle version, dois-je faire tout ce qui concerne les migrations?
J86
Je ne l'ai pas essayé sans utiliser les migrations. Je ne sais pas si vous pouvez accomplir cela sans les utiliser. C'est peut-être possible. J'ai toujours dû utiliser des migrations pour conserver les données personnalisées insérées dans la base de données.
Augusto Barreto
Une chose à souligner, si vous utilisez des migrations ... vous devriez utiliser le AddOrUpdate(new EntityObject { shoes = green})également connu sous le nom de "upsert". par opposition à simplement ajouter au contexte, sinon vous ne créerez que des informations de contexte d'entité dupliquées / redondantes.
Chef_Code
Je veux travailler avec la 3ème option, mais je ne comprends pas. quelqu'un peut-il me dire à quoi devrait ressembler exactement IdentityContext? car ça ne peut pas être exactement comme dans l'option 2! Pouvez-vous m'aider @AugustoBarreto? J'ai fait un fil sur quelque chose de similaire, peut-être que vous pouvez m'aider là
Arianit
À quoi ressemble votre «TrackableEntity»?
Ciaran Gallagher
224

Dans mon cas, j'avais hérité correctement de IdentityDbContext (avec mes propres types personnalisés et clé définis) mais j'avais supprimé par inadvertance l'appel à OnModelCreating de la classe de base:

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

Ce qui a ensuite corrigé mes index manquants dans les classes d'identité et j'ai pu alors générer des migrations et activer les migrations de manière appropriée.

Le sénateur
la source
Avait le même problème "supprimé la ligne". Votre solution a fonctionné. :) ty.
Développeur Marius Žilėnas
2
Cela a résolu mon problème, où je devais remplacer la méthode OnModelCreating pour inclure un mappage personnalisé utilisant une API fluide pour une relation d'entité complexe. Il s'avère que j'ai oublié d'ajouter la ligne dans la réponse avant de déclarer mon mappage car j'utilise le même contexte que l'identité. À votre santé.
Dan le
Cela fonctionne s'il n'y a pas de 'override void OnModelCreating' mais si vous remplacez, vous devez ajouter 'base.OnModelCreating (modelBuilder);' à la dérogation. Correction de mon problème.
Joe
13

Pour ceux qui utilisent ASP.NET Identity 2.1 et ont changé la clé primaire de la valeur par défaut stringà l'un intou l' autre Guid, si vous obtenez toujours

EntityType 'xxxxUserLogin' n'a pas de clé définie. Définissez la clé de cet EntityType.

EntityType 'xxxxUserRole' n'a pas de clé définie. Définissez la clé de cet EntityType.

vous avez probablement oublié de spécifier le nouveau type de clé sur IdentityDbContext:

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

Si tu as juste

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

ou même

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

vous obtiendrez cette erreur «aucune clé définie» lorsque vous essayez d'ajouter des migrations ou de mettre à jour la base de données.

David Liang
la source
J'essaie également de changer l'ID en un Int et j'ai ce problème, cependant j'ai changé mon DbContext pour spécifier le nouveau type de clé. Y a-t-il un autre endroit où je devrais vérifier? Je pensais suivre les instructions très attentivement.
Kyle
1
@Kyle: Essayez-vous de changer l'ID de toutes les entités en int, c'est-à-dire AppRole, AppUser, AppUserClaim, AppUserLogin et AppUserRole? Si tel est le cas, vous devrez peut-être également vous assurer que vous avez spécifié le nouveau type de clé pour ces classes. Comme 'public class AppUserLogin: IdentityUserLogin <int> {}'
David Liang
1
Ceci est la documentation officielle sur la personnalisation du type de données des clés primaires: docs.microsoft.com/en-us/aspnet/core/security/authentication/…
AdrienTorris
1
Oui, mon problème était que j'ai hérité de la classe générale DbContext au lieu d'IdentityDbContext <AppUser>. Merci, cela a beaucoup aidé
yibe
13

En modifiant le DbContext comme ci-dessous;

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

Il suffit d' ajouter à l' OnModelCreatingappel de méthode pour base.OnModelCreating (ModelBuilder); et ça devient bien. J'utilise EF6.

Remerciements spéciaux à #The Senator

Muhammad Usama
la source
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
la source
0

Mon problème était similaire - j'avais une nouvelle table que je créais et qui était liée aux utilisateurs d'identité. Après avoir lu les réponses ci-dessus, j'ai réalisé que cela avait à voir avec IsdentityUser et les propriétés héritées. J'avais déjà configuré Identity comme son propre contexte, donc pour éviter de lier intrinsèquement les deux, plutôt que d'utiliser la table utilisateur associée comme une véritable propriété EF, j'ai configuré une propriété non mappée avec la requête pour obtenir les entités associées. (DataManager est configuré pour récupérer le contexte actuel dans lequel OtherEntity existe.)

    [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);
            }
        }
    }
Mike
la source