Entity Framework 核心迁移问题

Entity Framework Core migration issue

EF Core 3.1.16 中的多对多关系存在问题。

相当基本的概念:用户/user_role/角色。

使用 MS Identity,因此依赖于 IdentityUserRole class。由于 class 已经具有 UserId 和 RoleId 属性,因此 EF 正尝试按照约定生成这些字段。我还没有想出一种方法来告诉 EF 将 MS UserId 和 RoleId 字段视为与我想要在 UserRole 实体 [=69 上的对象中的 User.Id 和 Role.Id 字段相同的字段=].我们使用 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"
            });
        }
    }

USER_ROLE

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

角色

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

基本类型配置

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

输出

在下面的输出部分中,是迁移过程创建的内容。此示例在模型上为 user_role 生成正确的字段(即...单个 user_id 和 role_id 字段,再加上核心 id 字段),但是当您查看 FK refs它生成,两个字段都是重复的。我试过 运行 宁这个场景,它会 运行 迁移并会正确创建 tables,但是当你尝试 运行 插入 user_role table, EF抛出如下异常:

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

已创建数据库

您应该尝试像这样配置您的实体:

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”或“user_id”这样的名称。由于您将 属性 定义为例如“role_id”在您的 table 中,EF 将在内部处理所有其他事情并正确映射您的外键。但是在您提供的示例中,EF 无法理解它应该映射到哪个属性,这就是它生成额外列的原因。

您还应该考虑使用它来重命名数据库级别的所有属性:Naming Conventions for Entity Framework Core Tables and Columns

UserRole class 中的 Id 字段也是多余的,因为您只需要一个复合键 {UserId, RoleId}