根据更新的示例,您希望在实体类公共接口中隐藏显式fk,并且仍然让它对ef core可见并映射到数据库中的fk列。
第一个问题是显式实现的接口成员不能被ef直接发现。而且它没有好名字,所以默认的约定不适用。
例如,w/o fluent configuration ef core将在
Parent
和
Child
实体,但因为它不会发现
int IChild<Parent>.ForeignKey { get; set; }
属性,它将通过
ParentOneId
/
ParentTwoId
shadow properties
而不是通过接口显式属性。换言之,这些属性不会由ef core填充,也不会被变更跟踪程序考虑。
要让ef core使用它们,您需要分别使用
HasForeignKey
和
HasColumnName
fluent 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)
它编译得很好,但在运行时不起作用。它不仅适用于fluent api方法,而且基本上适用于任何涉及表达式树的泛型方法(比如linq to entities查询)。如果使用公共属性隐式实现接口属性,则不存在此类问题。
我们稍后再讨论这个限制。要完成映射,请将以下内容添加到
OnModelCreating
覆盖:
ConfigureRelationship<ChildOne, ParentOne>(modelBuilder);
ConfigureRelationship<ChildOne, ParentTwo>(modelBuilder);
现在ef core将正确地加载/考虑显式实现的fk属性。
现在回到限制。使用类似于
AdoptAll
方法或对象的LINQ。但不能在用于访问ef核心元数据的表达式中或在linq到实体的查询中通用地访问这些属性。在后一种情况下,您应该通过导航属性访问它,或者在这两种情况下,您都应该通过从
ChildForeignKeyPropertyName<TParent>()
方法。实际上,查询将起作用,但将进行评估
locally
从而导致性能问题或意外行为。
例如。
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核心元数据、反射或
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
是的。它遇到的问题与直接使用受约束的接口属性相同,因此我假设它不起作用。