Основная проблема миграции Entity Framework

#c# #entity-framework-core #entity-framework-migrations #npgsql

Вопрос:

Возникли проблемы с отношениями «многие ко многим» в ядре EF 3.1.16.

Довольно простая концепция: пользователь / роль пользователя / роль.

Используя MS Identity, поэтому имейте зависимость от класса IdentityUserRole. Поскольку у этого класса уже есть свойства userId и RoleId, EF пытается создать эти поля по соглашению. Я не придумал, как сказать EF, чтобы он обрабатывал поля MS userId и RoleId как те же поля, что и пользователь.Идентификатор и роль.Поля идентификаторов объектов, которые мне нужны в классе сущностей UserRole. Мы используем экземпляры IEntityTypeConfiguration для создания схемы базы данных, которую использует EF, а не подход, основанный на соглашениях. Код для создания схемы приведен ниже.

пользователь

     public class User : IdentityUser<int>, IEntityCustom
    {
        // Custom field for testing purpose only...wouldn't actually store this.
        public int Age { get; set; }

        public List<UserRole> UserRoles { get; set; }
    }


    public class UserTypeConfiguration : BaseTypeConfiguration<User>, IEntityTypeConfiguration<User>
    {
        public UserTypeConfiguration() : base("permissions", "user") { }

        public override void Configure(EntityTypeBuilder<User> builder)
        {
            base.Configure(builder);

            #region "Hiding other properties"
            // Other fields are here that I omitted as they are just lowercasing only
            // and dont have relation to what is going on in the question.   user_name
            // field below is same as all the code omitted just with that field name pair.
            #endregion
            builder.Property(x => x.UserName).HasColumnName("user_name");


            builder.HasMany(x => x.UserRoles).WithOne(x => x.User).HasForeignKey("user_id");



            builder.HasData(new User
            {
                Id = 1,
                AccessFailedCount = 0,
                Age = 12,
                ConcurrencyStamp = Guid.NewGuid().ToString(),
                Email = "test@yopmail.com",
                EmailConfirmed = true,
                LockoutEnabled = false,
                NormalizedEmail = "test@yopmail.com",
                NormalizedUserName = "test",
                PasswordHash = "hash",
                PhoneNumber = "5551231234",
                PhoneNumberConfirmed = true,
                SecurityStamp = Guid.NewGuid().ToString(),
                TwoFactorEnabled = false,
                UserName = "test"
            });
        }
    }
 

РОЛЬ ПОЛЬЗОВАТЕЛЯ

     public class UserRole : IdentityUserRole<int>, IEntityCustom
    {
        public int Id { get; set; }

        public User User { get; set; }
        public Role Role { get; set; }


        public UserRole() { }

        public UserRole(User user, Role role)
        {
            User = user;
            Role = role;
        }
    }


    public class UserRoleTypeConfiguration : BaseTypeConfiguration<UserRole>, IEntityTypeConfiguration<UserRole>
    {
        public UserRoleTypeConfiguration() : base("permissions", "user_role") { }

        public override void Configure(EntityTypeBuilder<UserRole> builder)
        {
            base.Configure(builder);


            // Gens the right schema, but fails at runtime on 
            // insert to user_role table with duplicate "role_id" column exists on table "user_role"
            builder.Property<int>("UserId").HasColumnName("user_id");
            builder.Property<int>("RoleId").HasColumnName("role_id");

            // These 2 lines of code seem dumb, but they fix the schema
            // Only way I could find to accomplish it tho...
            builder.Property<int>("user_id").HasColumnName("user_id");
            builder.Property<int>("role_id").HasColumnName("role_id");

            builder.HasOne(x => x.User).WithMany(x => x.UserRoles).HasForeignKey("user_id");
            builder.HasOne(x => x.Role).WithMany(x => x.UserRoles).HasForeignKey("role_id");


            #region Region with all the ways I tried that didnt work.
            // Removed all the stuff that I tried that didn't get the model right.
            // ie....duplicate fields, etc.
            #endregion
        }
    }
 

ROLE

     public class Role : IdentityRole<int>, IEntityCustom
    {
        // Another custom field for testing.  eg.  Meaning = "SYSTEMADMIN"
        public string Meaning { get; set; }


        // I really dont want this navigation and my real code doesnt have it currently.
        // I couldnt get it working without, so I finally just added to see if I could get it 
        // working as full many to many relation and would remove later.
        public List<UserRole> UserRoles { get; set; }
    }



    public class RoleTypeConfiguration : BaseTypeConfiguration<Role>, IEntityTypeConfiguration<Role>
    {
        public RoleTypeConfiguration() : base("permissions", "role") { }

        public override void Configure(EntityTypeBuilder<Role> builder)
        {
            base.Configure(builder);

            builder.Property(x => x.Name).HasColumnName("name");
            builder.Property(x => x.ConcurrencyStamp).HasColumnName("concurrency_stamp");
            builder.Property(x => x.NormalizedName).HasColumnName("normalized_name");
            builder.Property(x => x.Meaning).HasColumnName("meaning");



            builder.HasMany(x => x.UserRoles).WithOne(x => x.Role).HasForeignKey("role_id");



            builder.HasData(new Role
            {
                Id = 1,
                ConcurrencyStamp = Guid.NewGuid().ToString(),
                Meaning = "SYSADMIN",
                Name = "System Admin",
                NormalizedName = "system admin"
            });


            builder.HasData(new Role
            {
                Id = 2,
                ConcurrencyStamp = Guid.NewGuid().ToString(),
                Meaning = "CONTENTADMIN",
                Name = "Content Admin",
                NormalizedName = "content admin"
            });
        }
    }
 

BASE TYPE CONFIGURATION

     public class BaseTypeConfiguration<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : class, IEntityCustom
    {
        private readonly string _schemaName;
        private readonly string _tableName;

        public BaseTypeConfiguration(string schemaName, string tableName)
        {
            _schemaName = schemaName;
            _tableName = tableName;
        }

        public virtual void Configure(EntityTypeBuilder<TEntity> builder)
        {
            Console.WriteLine("schema:"   _schemaName);
            Console.WriteLine("table:"   _tableName);

            builder.Metadata.SetSchema(_schemaName);
            builder.ToTable(_tableName);

            builder.HasKey(x => x.Id);

            builder.Property(x => x.Id)
                .HasColumnName("id")
                .ValueGeneratedOnAdd();
        }
    }
 

IENTITYCUSTOM

 public interface IEntityCustom
    {
        int Id { get; set; }
    }
 

OUTPUT

In the output section below, is what the migration process creates. This examples generates the right fields on the model for user_role (ie…single user_id and role_id fields, coupled with the core id field), but when you look at the FK refs it generates, both fields are duplicated. I tried running this scenario, and it does run the migration and will create the tables correctly, but when you try to run an insert against the user_role table, EF throws the following exception:

«42701: столбец role_id указан более одного раза».

 migrationBuilder.CreateTable(
                name: "user_role",
                schema: "permissions",
                columns: table => new
                {
                    id = table.Column<int>(nullable: false)
                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
                    user_id = table.Column<int>(nullable: false),
                    role_id = table.Column<int>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_user_role", x => x.id);
                    table.ForeignKey(
                        name: "FK_user_role_role_role_id",
                        column: x => x.role_id,
                        principalSchema: "permissions",
                        principalTable: "role",
                        principalColumn: "id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_user_role_user_user_id",
                        column: x => x.user_id,
                        principalSchema: "permissions",
                        principalTable: "user",
                        principalColumn: "id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_user_role_role_role_id1",
                        column: x => x.role_id,
                        principalSchema: "permissions",
                        principalTable: "role",
                        principalColumn: "id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_user_role_user_user_id1",
                        column: x => x.user_id,
                        principalSchema: "permissions",
                        principalTable: "user",
                        principalColumn: "id",
                        onDelete: ReferentialAction.Cascade);
                });
 

СОЗДАННАЯ БАЗА ДАННЫХ

введите описание изображения здесь

Комментарии:

1. Я могу оценить ваше желание дать предысторию и как можно больше информации, но это скорее роман, чем вопрос. В будущем постарайтесь максимально сократить свой вопрос, чтобы он был более доступным.

Ответ №1:

Вы должны попытаться настроить свои сущности следующим образом:

 public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasMany(x => x.UserRoles)
            .WithOne(x => x.User)
            .HasForeignKey(x => x.UserId);
    }
}

public class RoleConfiguration : IEntityTypeConfiguration<Role>
{
    public void Configure(EntityTypeBuilder<Role> builder)
    {
        builder.HasMany(x => x.UserRoles)
            .WithOne(x => x.Role)
            .HasForeignKey(x => x.RoleId);
    }
}

public void Configure(EntityTypeBuilder<UserRole> builder)
{
    builder.Property(x => x.RoleId).HasColumnName("role_id");
    builder.HasKey(x => new {x.UserId, x.RoleId});
}
 

При указании внешних ключей следует использовать свойства модели вместо указания таких имен, как «role_id» или «идентификатор пользователя». Поскольку вы определили это свойство, например, как «role_id» в своей таблице, EF будет обрабатывать все остальное внутри и правильно сопоставит ваши внешние ключи. Но в приведенном вами примере EF не может понять, с какой поперти он должен сопоставляться, поэтому он генерирует дополнительные столбцы.

Вам также следует рассмотреть возможность использования этого для переименования всех ваших свойств на уровне базы данных: Соглашения об именовании основных таблиц и столбцов Entity Framework.

Id Поле в вашем UserRole классе также является избыточным, так как вам нужен только составной ключ {UserId, RoleId}

Комментарии:

1. Спасибо! Я попробовал это сделать несколько месяцев назад, но так и не смог заставить его работать, поэтому в конце концов отказался от лямбд при вызове HasForeignKey для магических строк… ненавидел это, но в то время должен был двигаться вперед, и это сработало, так как у меня не было полей идентификаторов. Не уверен, был ли этот змеиный случай чем-то особенным, когда я начинал, но я обязательно проверю его….это облегчило бы дело. Что касается составного идентификатора, я решил сделать это таким образом, хотя знаю, что этот состав можно использовать. Когда я добавляю таблицу, которая ссылается на это, я должен перенести оба идентификатора в новую таблицу. Я просто предпочитаю такой подход.