web-dev-qa-db-fra.com

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

Je travaille avec Entity Framework Code First et MVC 5. Lors de la création de mon application avec Authentification des comptes utilisateur individuels, on m'a attribué un contrôleur de compte, ainsi que toutes les classes et le code nécessaires pour obtenir l'authentification des comptes d'utilisateurs individuels pour fonctionner.

Voici le code déjà en place:

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

    }

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

Mais ensuite, j’ai créé mon propre contexte en utilisant du code, j’ai donc aussi les éléments suivants:

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épart suivante pour ajouter des données avec lesquelles je peux 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 est correcte, mais lorsque j'essaie d'accéder à un contrôleur nécessitant un accès à la base de données, l'erreur suivante apparaît:

DX.DOMAIN.Context.IdentityUserLogin :: EntityType 'IdentityUserLogin' n'a pas de clé définie. Définissez la clé pour ce type d'entité.

DX.DOMAIN.Context.IdentityUserRole:: Le type d'entité 'IdentityUserRole' n'a pas de clé définie. Définissez la clé pour ce type d'entité.

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

UPDATE

Après avoir lu la réponse d'Augusto, je suis allé avec Option. Voici à quoi ressemble ma classe 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();
    }
}

J'ai également ajouté un User.cs et un Role.cs classe, ils 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 d'avoir besoin d'une propriété de mot de passe sur l'utilisateur, car ApplicationUser par défaut en dispose ainsi que de nombreux autres champs!

Quoi qu'il en soit, la modification ci-dessus fonctionne correctement, mais là encore, je reçois cette erreur lorsque l'application est exécutée:

Nom de colonne invalide UserId

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

95
J86

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

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 creusez dans le 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 (peut-être qu'il y en a d'autres):

  1. Utilisez des DbContexts distincts sur deux bases de données différentes ou la même base de données, mais sur des tables différentes.
  2. Fusionner votre DXContext avec ApplicationDbContext et utiliser 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 la mise à jour du bas.

Option 2: Vous allez vous retrouver 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. Nommez-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.

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

Vous devrez modifier la migration générée pour DXContext. Quelque chose comme cela dépend 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 en exécutant les migrations dans l'ordre (en premier lieu les migrations d'Identity) à partir de global.asax ou de tout autre emplacement 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);
    }
}

Ainsi, mes entités transversales à plusieurs niveaux n'héritent pas des classes AspNetIdentity et je n'ai donc pas besoin d'importer ce cadre dans tous les projets où je les utilise.

Désolé pour le long post. J'espère que cela pourrait offrir des conseils à ce sujet. J'ai déjà utilisé les options 2 et 3 dans des environnements de production.

UPDATE: développez l'option 1

Pour les deux derniers projets, j'ai utilisé la première option: avoir une classe AspNetUser dérivant d'IdentityUser et une classe personnalisée distincte appelée AppUser. Dans mon cas, les DbContexts sont IdentityContext et DomainContext respectivement. 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)

J'ai d'abord créé AspNetUser, puis AppUser. L'inconvénient de cette approche est que vous devez vous assurer que votre fonctionnalité "CreateUser" est transactionnelle (souvenez-vous qu'il y aura deux DbContexts appelant SaveChanges séparément). Utiliser TransactionScope ne fonctionnait pas 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 vient avec une meilleure façon de faire cette partie, j'apprécie de commenter ou de proposer une modification de cette réponse)

L'avantage est 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 IdentityContext (le contexte dérivé d'IdentityDbContext):

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

    protected override void Seed(IdentityContext context)
    {
    }
}

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

108
Augusto Barreto

Dans mon cas, j'avais hérité correctement d'IdentityDbContext (avec mes propres types personnalisés et ma clé définie), mais j'avais par inadvertance supprimé 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 à partir des classes d'identité et je pouvais alors générer des migrations et permettre les migrations de manière appropriée.

211
The Senator

Pour ceux qui utilisent ASP.NET Identity 2.1 et ont changé la clé primaire de la valeur par défaut string en int ou Guid, si vous obtenez toujours

EntityType 'xxxxUserLogin' n'a pas de clé définie. Définissez la clé pour ce type d'entité.

EntityType 'xxxxUserRole' n'a pas de clé définie. Définissez la clé pour ce type d'entité.

vous avez probablement juste 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 vous avez juste

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

ou même

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

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

12
David Liang

En modifiant le DbContext comme ci-dessous;

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

En ajoutant simplement dans la méthode OnModelCreating à la méthode base.OnModelCreating (modelBuilder); et ça va bien. J'utilise EF6.

Un merci spécial au sénateur

12
Muhammad Usama
 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");

        }
    }
1
Javier González

Mon problème était similaire - j'avais un nouveau tableau que je créais et qui était en lien avec les utilisateurs d'identité. Après avoir lu les réponses ci-dessus, réalisé que cela avait à voir avec IsdentityUser et les propriétés héritées. J'avais déjà défini Identity comme son propre contexte. Par conséquent, pour éviter de lier intrinsèquement les deux ensemble plutôt que d'utiliser la table d'utilisateurs associée comme 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 extraire le contexte actuel dans lequel existe 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);
            }
        }
    }
0
Mike