EntityType 'IdentityUserLogin' não tem chave definida. Defina a chave para este EntityType

105

Estou trabalhando com Entity Framework Code First e MVC 5. Quando criei meu aplicativo com autenticação de contas de usuário individual, recebi um controlador de conta e junto com ele todas as classes e códigos necessários para fazer a autenticação de contas de usuário Indiv funcionar .

Entre os códigos já implementados estava este:

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

    }

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

Mas então eu fui em frente e criei meu próprio contexto usando o código primeiro, então agora tenho o seguinte também:

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, tenho o seguinte método de semente para adicionar alguns dados para eu trabalhar durante o desenvolvimento:

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

Minha solução funciona bem, mas quando tento acessar um controlador que requer acesso ao banco de dados, recebo o seguinte erro:

DX.DOMAIN.Context.IdentityUserLogin:: EntityType 'IdentityUserLogin' não tem chave definida. Defina a chave para este EntityType.

DX.DOMAIN.Context.IdentityUserRole:: EntityType 'IdentityUserRole' não tem chave definida. Defina a chave para este EntityType.

O que estou fazendo de errado? É porque tenho dois contextos?

ATUALIZAR

Depois de ler a resposta de Augusto, escolhi a Opção 3 . Esta é a aparência da minha classe DXContext agora:

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

Eu também adicionei uma User.cse uma Role.csclasse, elas se parecem com isto:

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

Eu não tinha certeza se precisaria de uma propriedade de senha para o usuário, já que o ApplicationUser padrão tem isso e vários outros campos!

De qualquer forma, a alteração acima funciona bem, mas novamente recebo este erro quando o aplicativo é executado:

Nome de coluna inválido UserId

UserId é uma propriedade inteira no meu Artist.cs

J86
fonte

Respostas:

116

O problema é que seu ApplicationUser herda de IdentityUser , que é definido assim:

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 suas chaves primárias são mapeadas no método OnModelCreating da 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 como seu DXContext não deriva dele, essas chaves não são definidas.

Se você pesquisar as fontes de Microsoft.AspNet.Identity.EntityFramework, entenderá tudo.

Eu me deparei com essa situação há algum tempo e encontrei três soluções possíveis (talvez existam mais):

  1. Use DbContexts separados em dois bancos de dados diferentes ou no mesmo banco de dados, mas em tabelas diferentes.
  2. Combine seu DXContext com ApplicationDbContext e use um banco de dados.
  3. Use DbContexts separados na mesma tabela e gerencie suas migrações de acordo.

Opção 1: Veja a atualização na parte inferior.

Opção 2: você vai acabar com um 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);
    }
}

Opção 3: Você terá um DbContext igual à opção 2. Vamos chamá-lo de IdentityContext. E você terá outro DbContext chamado 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();
    }
}

onde o usuário é:

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

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

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

Com esta solução, estou mapeando a entidade User para a mesma tabela da entidade ApplicationUser.

Então, usando Code First Migrations você precisará gerar as migrações para o IdentityContext e ENTÃO para o DXContext, seguindo esta ótima postagem de Shailendra Chauhan: Code First Migrations with Multiple Data Contexts

Você terá que modificar a migração gerada para DXContext. Algo assim, dependendo de quais propriedades são compartilhadas entre 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, em seguida, executar as migrações em ordem (primeiro as migrações de identidade) do global.asax ou qualquer outro local de seu aplicativo usando esta classe 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);
    }
}

Dessa forma, minhas entidades de corte transversal de n camadas não acabam herdando de classes AspNetIdentity e, portanto, não tenho que importar esta estrutura em todos os projetos em que as uso.

Desculpe pela extensa postagem. Espero que possa oferecer alguma orientação sobre isso. Já usei as opções 2 e 3 em ambientes de produção.

ATUALIZAÇÃO: Expanda a opção 1

Nos últimos dois projetos, usei a primeira opção: ter uma classe AspNetUser que deriva de IdentityUser e uma classe personalizada separada chamada AppUser. No meu caso, os DbContexts são IdentityContext e DomainContext respectivamente. E eu defini o Id do AppUser assim:

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 é uma classe base abstrata personalizada que uso no método SaveChanges substituído do meu contexto DomainContext)

Primeiro crio o AspNetUser e depois o AppUser. A desvantagem dessa abordagem é que você deve garantir que a funcionalidade "CreateUser" seja transacional (lembre-se de que haverá dois DbContexts chamando SaveChanges separadamente). Usar o TransactionScope não funcionou para mim por algum motivo, então acabei fazendo algo feio, mas funciona para mim:

        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, se alguém vier com uma maneira melhor de fazer esta parte, agradeço comentar ou propor uma edição a esta resposta)

Os benefícios são que você não precisa modificar as migrações e pode usar qualquer hierarquia de herança maluca sobre o AppUser sem mexer com o AspNetUser . E, na verdade, eu uso migrações automáticas para meu IdentityContext (o contexto que deriva de IdentityDbContext):

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

    protected override void Seed(IdentityContext context)
    {
    }
}

Essa abordagem também tem o benefício de evitar que suas entidades cruzadas de n camadas herdem de classes AspNetIdentity.

Augusto Barreto
fonte
Obrigado @Augusto pela extensa postagem. É necessário usar migrações para fazer a opção 3 funcionar? Pelo que eu sei, as migrações EF são para reverter as alterações? Se eu estiver descartando meu banco de dados e, em seguida, recriá-lo e propagá-lo em cada nova construção, preciso fazer todas essas migrações?
J86 de
Não tentei sem usar migrações. Não sei se você pode fazer isso sem usá-los. Talvez seja possível. Sempre tive que usar migrações para reter quaisquer dados personalizados que foram inseridos no banco de dados.
Augusto Barreto
Uma coisa a apontar, se você usar migrações ... você deve usar o AddOrUpdate(new EntityObject { shoes = green})também conhecido como "upsert". em vez de apenas adicionar ao contexto, caso contrário, você estará apenas criando informações de contexto de entidade duplicadas / redundantes.
Chef_Code
Eu quero trabalhar com a terceira opção, mas eu meio que não entendo. alguém pode me dizer exatamente como o IdentityContext deve ser? porque não pode ser exatamente como na opção 2! Você pode me ajudar @AugustoBarreto?
Criei
Qual é a aparência do seu 'TrackableEntity'?
Ciaran Gallagher de
224

No meu caso, eu herdei do IdentityDbContext corretamente (com meus próprios tipos personalizados e chave definidos), mas removi inadvertidamente a chamada para OnModelCreating da classe base:

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

O que então corrigiu meus índices ausentes das classes de identidade e eu poderia gerar migrações e habilitá-las de forma adequada.

O senador
fonte
Tive o mesmo problema "retirou a linha". Sua solução funcionou. :) ty.
Desenvolvedor Marius Žilėnas,
2
Isso corrigiu meu problema, em que tive que substituir o método OnModelCreating para incluir um mapeamento personalizado usando uma API fluente para um relacionamento de entidade complexo. Acontece que esqueci de adicionar a linha na resposta antes de declarar meu mapeamento, pois estou usando o mesmo contexto de Identidade. Felicidades.
Dan,
Funciona se não houver 'override void OnModelCreating', mas se você substituir, será necessário adicionar 'base.OnModelCreating (modelBuilder);' para a substituição. Resolvido meu problema.
Joe
13

Para aqueles que usam ASP.NET Identity 2.1 e mudaram a chave primária do padrão stringpara intou Guid, se você ainda estiver recebendo

EntityType 'xxxxUserLogin' não tem chave definida. Defina a chave para este EntityType.

EntityType 'xxxxUserRole' não tem chave definida. Defina a chave para este EntityType.

você provavelmente apenas se esqueceu de especificar o novo tipo de chave em IdentityDbContext:

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

Se você apenas tiver

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

ou mesmo

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

você obterá o erro 'nenhuma chave definida' quando tentar adicionar migrações ou atualizar o banco de dados.

David Liang
fonte
Também estou tentando mudar o ID para Int e estou tendo esse problema, mas mudei meu DbContext para especificar o novo tipo de chave. Há algum outro lugar que eu deva verificar? Achei que estava seguindo as instruções com muito cuidado.
Kyle
1
@Kyle: Você está tentando alterar o ID de todas as entidades para int, ou seja, AppRole, AppUser, AppUserClaim, AppUserLogin e AppUserRole? Nesse caso, você também pode precisar ter certeza de ter especificado o novo tipo de chave para essas classes. Como 'public class AppUserLogin: IdentityUserLogin <int> {}'
David Liang
1
Este é o documento oficial sobre como personalizar o tipo de dados das chaves primárias: docs.microsoft.com/en-us/aspnet/core/security/authentication/…
AdrienTorris
1
Sim, meu problema era que herdei da classe geral DbContext em vez de IdentityDbContext <AppUser>. Obrigado, isso ajudou muito
yibe
13

Alterando o DbContext conforme abaixo;

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

Apenas adicionando a OnModelCreatingchamada de método a base.OnModelCreating (modelBuilder); e fica bem. Estou usando EF6.

Agradecimentos especiais a #O senador

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

Meu problema era semelhante - eu tinha uma nova tabela que estava criando para vincular aos usuários de identidade. Depois de ler as respostas acima, percebi que tinha a ver com IsdentityUser e as propriedades herdadas. Eu já tinha Identidade configurada como seu próprio Contexto, portanto, para evitar amarrar inerentemente os dois, em vez de usar a tabela de usuário relacionada como uma propriedade EF verdadeira, configurei uma propriedade não mapeada com a consulta para obter as entidades relacionadas. (DataManager é configurado para recuperar o contexto atual no qual 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
fonte