EntityType 'IdentityUserLogin' no tiene una clave definida. Defina la clave para este EntityType

105

Estoy trabajando con Entity Framework Code First y MVC 5. Cuando creé mi aplicación con autenticación de cuentas de usuario individual, me dieron un controlador de cuenta y, junto con él, todas las clases y el código necesarios para que la autenticación de cuentas de usuario indiv funcione. .

Entre el código que ya estaba en vigor estaba este:

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

    }

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

Pero luego seguí adelante y creé mi propio contexto usando código primero, así que ahora también tengo lo siguiente:

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, tengo el siguiente método de semilla para agregar algunos datos con los que trabajar mientras desarrollo:

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

Mi solución se compila bien, pero cuando intento acceder a un controlador que requiere acceso a la base de datos, aparece el siguiente error:

DX.DOMAIN.Context.IdentityUserLogin:: EntityType 'IdentityUserLogin' no tiene una clave definida. Defina la clave para este EntityType.

DX.DOMAIN.Context.IdentityUserRole:: EntityType 'IdentityUserRole' no tiene una clave definida. Defina la clave para este EntityType.

¿Qué estoy haciendo mal? ¿Es porque tengo dos contextos?

ACTUALIZAR

Después de leer la respuesta de Augusto, opté por la opción 3 . Así es como se ve mi clase DXContext ahora:

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

También agregué una User.csy una Role.csclase, se ven así:

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

No estaba seguro de si necesitaría una propiedad de contraseña en el usuario, ¡ya que el ApplicationUser predeterminado tiene eso y muchos otros campos!

De todos modos, el cambio anterior se construye bien, pero nuevamente aparece este error cuando se ejecuta la aplicación:

Nombre de columna no válido UserId

UserId es una propiedad entera en mi Artist.cs

J86
fuente

Respuestas:

116

El problema es que su ApplicationUser hereda de IdentityUser , que se define así:

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

y sus claves primarias se mapean en el método OnModelCreating de la clase 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");

y como su DXContext no se deriva de él, esas claves no se definen.

Si profundiza en las fuentes de Microsoft.AspNet.Identity.EntityFramework, comprenderá todo.

Me encontré con esta situación hace algún tiempo y encontré tres posibles soluciones (tal vez haya más):

  1. Use DbContexts separados contra dos bases de datos diferentes o la misma base de datos pero con tablas diferentes.
  2. Fusiona tu DXContext con ApplicationDbContext y usa una base de datos.
  3. Use DbContexts separados en la misma tabla y administre sus migraciones en consecuencia.

Opción 1: Ver actualización en la parte inferior.

Opción 2: Terminará con un DbContext como este:

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

Opción 3: Tendrá un DbContext igual a la opción 2. Vamos a llamarlo IdentityContext. Y tendrá otro DbContext llamado 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();
    }
}

donde el usuario es:

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 esta solución, estoy mapeando la entidad Usuario a la misma tabla que la entidad ApplicationUser.

Luego, con Code First Migrations, deberá generar las migraciones para IdentityContext y LUEGO para DXContext, siguiendo esta excelente publicación de Shailendra Chauhan: Code First Migrations with Multiple Data Contexts

Deberá modificar la migración generada para DXContext. Algo como esto dependiendo de qué propiedades se comparten entre ApplicationUser y 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));

y luego ejecutar las migraciones en orden (primero las migraciones de identidad) desde global.asax o cualquier otro lugar de su aplicación usando esta clase personalizada:

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 esta manera, mis entidades transversales de n niveles no terminan heredando de las clases AspNetIdentity y, por lo tanto, no tengo que importar este marco en todos los proyectos en los que las uso.

Perdón por la extensa publicación. Espero que pueda ofrecer alguna orientación al respecto. Ya he usado las opciones 2 y 3 en entornos de producción.

ACTUALIZACIÓN: Expandir la opción 1

Para los dos últimos proyectos, he usado la primera opción: tener una clase AspNetUser que se deriva de IdentityUser y una clase personalizada separada llamada AppUser. En mi caso, los DbContexts son IdentityContext y DomainContext respectivamente. Y definí el Id del AppUser así:

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 es una clase base abstracta personalizada que utilizo en el método SaveChanges anulado de mi contexto DomainContext)

Primero creo el AspNetUser y luego el AppUser. El inconveniente de este enfoque es que debe asegurarse de que su funcionalidad "CreateUser" sea transaccional (recuerde que habrá dos DbContexts llamando a SaveChanges por separado). El uso de TransactionScope no funcionó para mí por alguna razón, así que terminé haciendo algo feo, pero eso funciona para mí:

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

(Por favor, si alguien viene con una mejor manera de hacer esta parte, agradezco comentar o proponer una edición a esta respuesta)

Los beneficios son que no tiene que modificar las migraciones y puede usar cualquier jerarquía de herencia loca sobre AppUser sin meterse con AspNetUser . Y de hecho uso migraciones automáticas para mi IdentityContext (el contexto que se deriva de IdentityDbContext):

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

    protected override void Seed(IdentityContext context)
    {
    }
}

Este enfoque también tiene la ventaja de evitar que sus entidades transversales de n niveles hereden de las clases AspNetIdentity.

Augusto Barreto
fuente
Gracias @Augusto por el extenso post. ¿Se tienen que utilizar las migraciones para obtener la opción 3 para el trabajo? Hasta donde yo sé, ¿las migraciones de EF son para revertir cambios? Si descarto mi base de datos y luego la vuelvo a crear y la siembro en cada nueva compilación, ¿tengo que hacer todas esas migraciones?
J86
No lo probé sin usar migraciones. No sé si puede lograr eso sin usarlos. Quizás sea posible. Siempre tuve que usar migraciones para retener los datos personalizados que se insertaron en la base de datos.
Augusto Barreto
Una cosa para señalar, si usa Migraciones ... debe usar el AddOrUpdate(new EntityObject { shoes = green})también conocido como "upsert". en lugar de simplemente agregar al contexto, de lo contrario, solo creará información de contexto de entidad duplicada / redundante.
Chef_Code
Quiero trabajar con la tercera opción, pero no la entiendo. ¿Puede alguien decirme cómo debería verse exactamente IdentityContext? ¡porque no puede ser exactamente como en la opción 2! ¿Me puedes ayudar @AugustoBarreto? Hice un hilo sobre algo similar, tal vez puedas ayudarme allí
Arianit
¿Cómo se ve su 'TrackableEntity'?
Ciaran Gallagher
224

En mi caso, había heredado de IdentityDbContext correctamente (con mis propios tipos personalizados y clave definida) pero sin darme cuenta eliminé la llamada al OnModelCreating de la clase base:

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

Lo que luego corrigió mis índices faltantes de las clases de identidad y luego pude generar migraciones y habilitar las migraciones de manera adecuada.

El senador
fuente
Tenía el mismo problema "eliminado la línea". Tu solución funcionó. :) ty.
Desarrollador Marius Žilėnas
2
Esto solucionó mi problema, donde tuve que anular el método OnModelCreating para incluir un mapeo personalizado usando una API fluida para una relación de entidad compleja. Resulta que olvidé agregar la línea en la respuesta antes de declarar mi mapeo, ya que estoy usando el mismo contexto que Identity. Salud.
Dan
Funciona si no hay 'override void OnModelCreating' pero si anula necesita agregar 'base.OnModelCreating (modelBuilder);' a la anulación. Arreglado mi problema.
Joe
13

Para aquellos que usan ASP.NET Identity 2.1 y han cambiado la clave principal de la predeterminada stringa into Guid, si aún obtiene

EntityType 'xxxxUserLogin' no tiene una clave definida. Defina la clave para este EntityType.

EntityType 'xxxxUserRole' no tiene una clave definida. Defina la clave para este EntityType.

probablemente olvidó especificar el nuevo tipo de clave en IdentityDbContext:

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

Si solo tienes

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

o incluso

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

Obtendrá el error "sin clave definida" cuando intente agregar migraciones o actualizar la base de datos.

David Liang
fuente
También estoy tratando de cambiar el ID a un Int y tengo este problema, sin embargo, he cambiado mi DbContext para especificar el nuevo tipo de clave. ¿Hay algún otro lugar que deba consultar? Pensé que estaba siguiendo las instrucciones con mucho cuidado.
Kyle
1
@Kyle: ¿Estás intentando cambiar el ID de todas las entidades a int, es decir, AppRole, AppUser, AppUserClaim, AppUserLogin y AppUserRole? Si es así, es posible que también deba asegurarse de haber especificado el nuevo tipo de clave para esas clases. Como 'clase pública AppUserLogin: IdentityUserLogin <int> {}'
David Liang
1
Este es el documento oficial sobre cómo personalizar el tipo de datos de las claves primarias: docs.microsoft.com/en-us/aspnet/core/security/authentication/…
AdrienTorris
1
Sí, mi problema fue que heredé de la clase general DbContext en lugar de IdentityDbContext <AppUser>. Gracias, esto ayudó mucho
yibe
13

Cambiando el DbContext como se muestra a continuación;

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

Simplemente agregando la OnModelCreatingllamada al método a base.OnModelCreating (modelBuilder); y se pone bien. Estoy usando EF6.

Agradecimiento especial a #El Senador

Muhammad Usama
fuente
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
fuente
0

Mi problema fue similar: tenía una nueva tabla que estaba creando para vincularla a los usuarios de identidad. Después de leer las respuestas anteriores, se dio cuenta de que tenía que ver con IsdentityUser y las propiedades heredadas. Ya tenía la identidad configurada como su propio contexto, por lo que para evitar unir inherentemente los dos, en lugar de usar la tabla de usuario relacionada como una verdadera propiedad EF, configuré una propiedad no mapeada con la consulta para obtener las entidades relacionadas. (DataManager está configurado para recuperar el contexto actual en el que 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);
            }
        }
    }
Miguel
fuente