#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 для магических строк… ненавидел это, но в то время должен был двигаться вперед, и это сработало, так как у меня не было полей идентификаторов. Не уверен, был ли этот змеиный случай чем-то особенным, когда я начинал, но я обязательно проверю его….это облегчило бы дело. Что касается составного идентификатора, я решил сделать это таким образом, хотя знаю, что этот состав можно использовать. Когда я добавляю таблицу, которая ссылается на это, я должен перенести оба идентификатора в новую таблицу. Я просто предпочитаю такой подход.