EntityType 'IdentityUserLogin' tidak memiliki kunci yang ditentukan. Tentukan kunci untuk EntityType ini

105

Saya bekerja dengan Entity Framework Code First dan MVC 5. Ketika saya membuat aplikasi saya dengan Otentikasi Akun Pengguna Individual, saya diberi pengontrol Akun dan bersama dengan itu semua kelas dan kode yang diperlukan yang diperlukan untuk membuat otentikasi Akun Pengguna Indiv berfungsi .

Di antara kode yang sudah ada adalah ini:

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

    }

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

Tapi kemudian saya melanjutkan dan membuat konteks saya sendiri menggunakan kode terlebih dahulu, jadi saya sekarang memiliki yang berikut ini juga:

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

Akhirnya saya memiliki metode seed berikut untuk menambahkan beberapa data untuk saya kerjakan sambil mengembangkan:

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

Solusi saya dibangun dengan baik, tetapi ketika saya mencoba dan mengakses pengontrol yang memerlukan akses ke database saya mendapatkan kesalahan berikut:

DX.DOMAIN.Context.IdentityUserLogin:: EntityType 'IdentityUserLogin' tidak memiliki kunci yang ditentukan. Tentukan kunci untuk EntityType ini.

DX.DOMAIN.Context.IdentityUserRole:: EntityType 'IdentityUserRole' tidak memiliki kunci yang ditentukan. Tentukan kunci untuk EntityType ini.

Apa yang saya lakukan salah? Apakah karena saya memiliki dua konteks?

MEMPERBARUI

Setelah membaca jawaban Augusto, saya memilih Opsi 3 . Inilah tampilan kelas DXContext saya sekarang:

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

Saya juga menambahkan sebuah User.csdan Role.cskelas, tampilannya seperti ini:

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

Saya tidak yakin apakah saya memerlukan properti kata sandi pada pengguna, karena ApplicationUser default memiliki itu dan banyak bidang lainnya!

Bagaimanapun, perubahan di atas berjalan dengan baik, tetapi sekali lagi saya mendapatkan kesalahan ini ketika aplikasi dijalankan:

Nama kolom UserId tidak valid

UserId adalah properti integer di my Artist.cs

J86
sumber

Jawaban:

116

Masalahnya adalah ApplicationUser Anda mewarisi dari IdentityUser , yang didefinisikan seperti ini:

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

dan kunci utama mereka dipetakan dalam metode OnModelCreating kelas 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");

dan karena DXContext Anda tidak diturunkan darinya, kunci tersebut tidak dapat didefinisikan.

Jika Anda menggali ke dalam sumber-sumber dari Microsoft.AspNet.Identity.EntityFramework, Anda akan memahami segalanya.

Saya menemukan situasi ini beberapa waktu lalu, dan saya menemukan tiga kemungkinan solusi (mungkin ada lebih banyak):

  1. Gunakan DbContext terpisah terhadap dua database berbeda atau database yang sama tetapi tabel berbeda.
  2. Gabungkan DXContext Anda dengan ApplicationDbContext dan gunakan satu database.
  3. Gunakan DbContext terpisah pada tabel yang sama dan kelola migrasinya sesuai kebutuhan.

Opsi 1: Lihat perbarui bagian bawah.

Opsi 2: Anda akan berakhir dengan DbContext seperti ini:

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

Opsi 3: Anda akan memiliki satu DbContext yang sama dengan opsi 2. Beri nama IdentityContext. Dan Anda akan memiliki DbContext lain yang disebut 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();
    }
}

dimana Pengguna berada:

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

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

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

Dengan solusi ini saya memetakan Pengguna entitas ke tabel yang sama dengan entitas ApplicationUser.

Kemudian, dengan menggunakan Migrasi Pertama Kode, Anda harus menghasilkan migrasi untuk IdentityContext dan LALU untuk DXContext, mengikuti posting hebat ini dari Shailendra Chauhan: Migrasi Pertama Kode dengan Beberapa Konteks Data

Anda harus mengubah migrasi yang dihasilkan untuk DXContext. Sesuatu seperti ini tergantung pada properti mana yang dibagikan antara ApplicationUser dan Pengguna:

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

dan kemudian menjalankan migrasi secara berurutan (pertama, Migrasi identitas) dari global.asax atau tempat lain aplikasi Anda menggunakan kelas khusus ini:

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

Dengan cara ini, entitas lintas sektor n-tier saya tidak akan mewarisi dari kelas AspNetIdentity, dan oleh karena itu saya tidak perlu mengimpor kerangka kerja ini di setiap proyek tempat saya menggunakannya.

Maaf untuk posting yang luas. Saya berharap ini bisa menawarkan beberapa panduan tentang ini. Saya telah menggunakan opsi 2 dan 3 di lingkungan produksi.

UPDATE: Perluas Opsi 1

Untuk dua proyek terakhir saya telah menggunakan opsi 1: memiliki kelas AspNetUser yang berasal dari IdentityUser, dan kelas khusus terpisah yang disebut AppUser. Dalam kasus saya, DbContexts masing-masing adalah IdentityContext dan DomainContext. Dan saya mendefinisikan Id AppUser seperti ini:

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 adalah kelas dasar abstrak kustom yang saya gunakan dalam metode SaveChanges yang diganti dari konteks DomainContext saya)

Saya pertama kali membuat AspNetUser dan kemudian AppUser. Kekurangan dari pendekatan ini adalah Anda harus memastikan bahwa fungsionalitas "CreateUser" Anda transaksional (ingat bahwa akan ada dua DbContext yang memanggil SaveChanges secara terpisah). Menggunakan TransactionScope tidak berhasil untuk saya karena beberapa alasan, jadi saya akhirnya melakukan sesuatu yang buruk tetapi berhasil untuk saya:

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

(Tolong, jika seseorang datang dengan cara yang lebih baik untuk melakukan bagian ini, saya menghargai mengomentari atau mengusulkan edit untuk jawaban ini)

Manfaatnya adalah Anda tidak perlu mengubah migrasi dan Anda dapat menggunakan hierarki warisan gila apa pun di AppUser tanpa mengotak-atik AspNetUser . Dan sebenarnya saya menggunakan Migrasi Otomatis untuk IdentityContext saya (konteks yang berasal dari IdentityDbContext):

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

    protected override void Seed(IdentityContext context)
    {
    }
}

Pendekatan ini juga memiliki keuntungan untuk menghindari entitas lintas sektor n-tier Anda mewarisi dari kelas AspNetIdentity.

Augusto Barreto
sumber
Terima kasih @Augusto untuk posting ekstensifnya. Apakah seseorang harus menggunakan Migrasi agar Opsi 3 berfungsi? Sejauh yang saya tahu, Migrasi EF adalah untuk mengembalikan perubahan? Jika saya menghapus database saya dan kemudian membuatnya kembali dan melakukan seeding pada setiap build baru, apakah saya perlu melakukan semua hal migrasi itu?
J86
Saya tidak mencobanya tanpa menggunakan migrasi. Saya tidak tahu apakah Anda dapat mencapai itu tanpa menggunakannya. Mungkin itu mungkin. Saya selalu harus menggunakan migrasi untuk mempertahankan data khusus apa pun yang dimasukkan ke database.
Augusto Barreto
Satu hal yang perlu diperhatikan, jika Anda menggunakan Migrasi ... Anda harus menggunakan yang AddOrUpdate(new EntityObject { shoes = green})juga dikenal sebagai "upsert". menentang hanya menambahkan ke konteks, jika tidak Anda hanya akan membuat info konteks entitas duplikat / berlebihan.
Chef_Code
Saya ingin bekerja dengan opsi ke-3, tetapi saya agak tidak mengerti. dapatkah seseorang memberi tahu saya bagaimana sebenarnya IdentityContext seharusnya terlihat? karena tidak bisa persis seperti di opsi 2! Bisakah Anda membantu saya @AugustoBarreto? Saya telah membuat utas tentang sesuatu yang serupa, mungkin Anda dapat membantu saya di sana
Arianit
Seperti apa 'TrackableEntity' Anda?
Ciaran Gallagher
224

Dalam kasus saya, saya telah mewarisi dari IdentityDbContext dengan benar (dengan jenis dan kunci kustom saya sendiri yang ditentukan) tetapi secara tidak sengaja telah menghapus panggilan ke OnModelCreating kelas dasar:

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

Yang kemudian memperbaiki indeks saya yang hilang dari kelas identitas dan saya kemudian dapat menghasilkan migrasi dan mengaktifkan migrasi dengan tepat.

Senator
sumber
Punya masalah yang sama "menghapus garis". Solusi Anda berhasil. :) ty.
Pengembang Marius Žilėnas
2
Ini memperbaiki masalah saya, di mana saya harus mengganti metode OnModelCreating untuk menyertakan Pemetaan kustom menggunakan api yang lancar untuk hubungan entitas yang kompleks. Ternyata saya lupa menambahkan baris di jawaban sebelum mendeklarasikan pemetaan saya karena saya menggunakan konteks yang sama dengan Identity. Bersulang.
Dan
Ini berfungsi jika tidak ada 'override void OnModelCreating' tetapi jika Anda menimpa, Anda perlu menambahkan 'base.OnModelCreating (modelBuilder);' untuk menimpa. Memperbaiki masalah saya.
Joe
13

Bagi mereka yang menggunakan ASP.NET Identity 2.1 dan telah mengubah kunci utama dari default stringke salah satu intatau Guid, jika Anda masih mendapatkan

EntityType 'xxxxUserLogin' tidak memiliki kunci yang ditentukan. Tentukan kunci untuk EntityType ini.

EntityType 'xxxxUserRole' tidak memiliki kunci yang ditentukan. Tentukan kunci untuk EntityType ini.

Anda mungkin lupa menentukan jenis kunci baru pada IdentityDbContext:

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

Jika Anda baru saja

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

atau bahkan

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

Anda akan mendapatkan kesalahan 'tidak ada kunci yang ditentukan' saat Anda mencoba menambahkan migrasi atau memperbarui database.

David Liang
sumber
Saya juga mencoba mengubah ID menjadi Int dan saya mengalami masalah ini, namun saya telah mengubah DbContext saya untuk menentukan jenis kunci baru. Apakah ada tempat lain yang harus saya periksa? Saya pikir saya mengikuti instruksi dengan sangat hati-hati.
Kyle
1
@Kyle: Apakah Anda mencoba mengubah semua ID entitas menjadi int, yaitu AppRole, AppUser, AppUserClaim, AppUserLogin dan AppUserRole? Jika demikian, Anda mungkin juga perlu memastikan bahwa Anda telah menentukan jenis kunci baru untuk kelas tersebut. Seperti 'kelas publik AppUserLogin: IdentityUserLogin <int> {}'
David Liang
1
Ini adalah dokumen resmi tentang menyesuaikan jenis data kunci utama: docs.microsoft.com/en-us/aspnet/core/security/authentication/…
AdrienTorris
1
Ya, masalah saya adalah, saya mewarisi dari kelas DbContext umum, bukan IdentityDbContext <AppUser>. Terima kasih, ini sangat membantu
yibe
13

Dengan Mengubah DbContext Seperti Di Bawah Ini;

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

Hanya menambahkan OnModelCreatingpanggilan metode ke base.OnModelCreating (modelBuilder); dan itu menjadi baik. Saya menggunakan EF6.

Terima Kasih Khusus Untuk #Senator

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

Masalah saya serupa - Saya memiliki tabel baru yang saya buat ahd itu untuk mengikat pengguna identitas. Setelah membaca jawaban di atas, menyadari itu ada hubungannya dengan IsdentityUser dan properti yang diwarisi. Saya sudah menyiapkan Identitas sebagai Konteksnya sendiri, jadi untuk menghindari secara inheren mengikat keduanya, daripada menggunakan tabel pengguna terkait sebagai properti EF yang sebenarnya, saya menyiapkan properti yang tidak dipetakan dengan kueri untuk mendapatkan entitas terkait. (DataManager disiapkan untuk mengambil konteks saat ini di mana OtherEntity ada.)

    [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
sumber