Согласно обновленному примеру, вы хотите скрыть явный FK от открытого интерфейса класса сущностей и все же позволить ему быть видимым для EF Core и сопоставляться со столбцом FK в базе данных.
Первая проблема заключается в том, что явно реализованный элемент интерфейса не может быть непосредственно обнаружен EF. Также у него нет хорошего имени, поэтому соглашения по умолчанию не применяются.
Например, без плавной конфигурации EF Core правильно создаст одну-много ассоциаций между Parent
и Child
сущностями, но, поскольку он не обнаружит свойства int IChild<Parent>.ForeignKey { get; set; }
, он будет поддерживать значения свойств FK через ParentOneId
/ ParentTwoId
теневые свойства , а не через явные свойства интерфейса. Другими словами, эти свойства не будут заполняться EF Core, а также не будут учитываться трекером изменений.
Чтобы позволить EF Core использовать их, необходимо отобразить как свойство FK, так и имя столбца базы данных, используя соответственно HasForeignKey
и HasColumnName
плавные перегрузки метода API, принимающие string
имя свойства. Обратите внимание, что имя свойства строки должно быть полностью определено с пространством имен. Хотя Type.FullName
предоставляет эту строку для неуниверсальных типов, для универсальных типов, таких как IChild<ParentOne>
, такого свойства / метода не существует (результат должен быть "Namespace.IChild<Namespace.ParentOne>"
), поэтому давайте сначала создадим несколько помощников для этого:
static string ChildForeignKeyPropertyName<TParent>() where TParent : ParentEntity
=> $"{typeof(IChild<>).Namespace}.IChild<{typeof(TParent).FullName}>.{nameof(IChild<TParent>.ForeignKey)}";
static string ChildForeignKeyColumnName<TParent>() where TParent : ParentEntity
=> $"{typeof(TParent).Name}Id";
Следующим будет создание вспомогательного метода для выполнения необходимой конфигурации:
static void ConfigureRelationship<TChild, TParent>(ModelBuilder modelBuilder)
where TChild : ChildEntity, IChild<TParent>
where TParent : ParentEntity, IParent<TChild>
{
var childEntity = modelBuilder.Entity<TChild>();
var foreignKeyPropertyName = ChildForeignKeyPropertyName<TParent>();
var foreignKeyColumnName = ChildForeignKeyColumnName<TParent>();
var foreignKey = childEntity.Metadata.GetForeignKeys()
.Single(fk => fk.PrincipalEntityType.ClrType == typeof(TParent));
// Configure FK column name
childEntity
.Property<int>(foreignKeyPropertyName)
.HasColumnName(foreignKeyColumnName);
// Configure FK property
childEntity
.HasOne<TParent>(foreignKey.DependentToPrincipal.Name)
.WithMany(foreignKey.PrincipalToDependent.Name)
.HasForeignKey(foreignKeyPropertyName);
}
Как видите, я использую службы метаданных EF Core, чтобы найти имена соответствующих свойств навигации.
Но этот общий метод на самом деле показывает ограничение этого дизайна. Общие ограничения позволяют нам использовать
childEntity.Property(c => c.ForeignKey)
, который прекрасно компилируется, но не работает во время выполнения. Это не только для беглых методов API, но в основном для любого общего метода, включающего деревья выражений (например, запрос LINQ to Entities). Нет такой проблемы, когда свойство интерфейса неявно реализовано с открытым свойством.
Мы вернемся к этому ограничению позже. Чтобы завершить сопоставление, добавьте следующее к вашему OnModelCreating
переопределению:
ConfigureRelationship<ChildOne, ParentOne>(modelBuilder);
ConfigureRelationship<ChildOne, ParentTwo>(modelBuilder);
И теперь EF Core будет корректно загружать / учитывать ваши явно реализованные свойства FK.
Теперь вернемся к ограничениям. Нет проблем в использовании общих объектных сервисов, таких как ваш AdoptAll
метод или LINQ to Objects. Но вы не можете получить доступ к этим свойствам в общем случае в выражениях, используемых для доступа к метаданным EF Core или внутри запросов LINQ to Entities. В последнем случае вы должны получить к нему доступ через свойство навигации, или в обоих сценариях вы должны получить доступ по имени, возвращенному методом ChildForeignKeyPropertyName<TParent>()
. На самом деле запросы будут работать, но будут оцениваться локально , что приведет к проблемам с производительностью или неожиданному поведению.
* Э.Г. 1045. *
static IEnumerable<TChild> GetChildrenOf<TChild, TParent>(DbContext db, int parentId)
where TChild : ChildEntity, IChild<TParent>
where TParent : ParentEntity, IParent<TChild>
{
// Works, but causes client side filter evalution
return db.Set<TChild>().Where(c => c.ForeignKey == parentId);
// This correctly translates to SQL, hence server side evaluation
return db.Set<TChild>().Where(c => EF.Property<int>(c, ChildForeignKeyPropertyName<TParent>()) == parentId);
}
Если коротко, то это возможно, но используйте с осторожностью и убедитесь, что оно того стоит для ограниченных общих сценариев обслуживания, которые оно допускает. Альтернативные подходы будут использовать не интерфейсы, а (комбинацию) метаданных EF Core, отражения или Func<...>
/ Expression<Func<..>>
общих аргументов метода, аналогичных Queryable
методам расширения.
Редактировать: По второму вопросу редактировать, свободная конфигурация
modelBuilder.Entity<ChildOne>()
.HasOne(p => p.ParentOne)
.WithMany(c => c.ChildOnes)
.HasForeignKey(fk => ((IChild<ParentOne>)fk).ForeignKey);
modelBuilder.Entity<ChildOne>()
.HasOne(p => p.ParentTwo)
.WithMany(c => c.ChildOnes)
.HasForeignKey(fk => ((IChild<ParentTwo>)fk).ForeignKey);
производит следующую миграцию для ChildOne
migrationBuilder.CreateTable(
name: "ChildOne",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
ForeignKey = table.Column<int>(nullable: false),
Name = table.Column<string>(nullable: true),
Balance = table.Column<decimal>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChildOne", x => x.Id);
table.ForeignKey(
name: "FK_ChildOne_ParentOne_ForeignKey",
column: x => x.ForeignKey,
principalTable: "ParentOne",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ChildOne_ParentTwo_ForeignKey",
column: x => x.ForeignKey,
principalTable: "ParentTwo",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
Обратите внимание на один столбец ForeignKey
и попытку использовать его в качестве внешнего ключа для ParentOne
и ParentTwo
. Он сталкивается с теми же проблемами, что и прямое использование ограниченного свойства интерфейса, поэтому я предполагаю, что оно не работает.