EF Core中使用非匿名类型作为外/内键选择器的问题
我的数据模型里有多个带复合主键的多对多连接表:比如room-document表对应Room和Document实体,包含room_id和document_id;product-document表对应Product和Document实体,包含product_id和document_id。
现在要基于EF Core 8(搭配Npgsql.EntityFrameworkCore.PostgreSQL)写一个通用服务,处理所有这类xxx-document类型的连接表。非泛型版本(针对Room)用匿名类型做外/内键选择器能正常运行,但泛型场景下需要动态构建Lambda表达式——因为匿名类型没法作为方法返回值,所以自定义了Composite记录类型。
但用这个自定义类型后出现InvalidOperationException:LINQ表达式无法被EF翻译。尝试用configurationBuilder.Properties<CompositeKey>().HaveConversion配置转换也没解决,求可行方案。
非泛型可运行代码
IQueryable<(int MainEntityId, int DocumentId)> someBaseQuery = /* 省略实现 */; var result = await someBaseQuery .LeftJoin // LeftJoin是IQueryable<T>的GroupJoin+SelectMany扩展方法 ( context.Set<RoomDocument>(), left => new { left.MainEntityId, left.DocumentId }, // 外键选择器 right => new { MainEntityId = right.RoomId, right.DocumentId } // 内键选择器 ) .ToListAsync();
自定义Composite类型
record Composite { public int MainEntityId { get; init; } public int DocumentId { get; init; } }
泛型版本代码(出现问题的版本)
.LeftJoin ( context.Set<TEntityDocumentMapping>(), left => new Composite { MainEntityId = left.MainEntityId, DocumentId = left.DocumentId }, GetKeySelectorExpression<TEntityDocumentMapping>() // 返回Expression<Func<TEntityDocumentMapping, Composite>>类型 )
可行解决方案
方案1:用值元组替代自定义Composite类型
EF Core原生支持值元组作为连接键选择器,元组可以直接作为方法返回值,完美适配泛型场景。
修改泛型代码:
// 调整GetKeySelectorExpression返回Expression<Func<TEntityDocumentMapping, (int MainEntityId, int DocumentId)>> .LeftJoin ( context.Set<TEntityDocumentMapping>(), left => (left.MainEntityId, left.DocumentId), GetKeySelectorExpression<TEntityDocumentMapping>() )
动态构建Lambda表达式的示例:
private static Expression<Func<TMapping, (int MainEntityId, int DocumentId)>> GetKeySelectorExpression<TMapping>() { var parameter = Expression.Parameter(typeof(TMapping), "x"); // 按命名约定获取主实体ID属性(比如RoomDocument取RoomId) var mainIdProperty = typeof(TMapping).GetProperty(GetMainEntityIdPropertyName<TMapping>()); var docIdProperty = typeof(TMapping).GetProperty(nameof(DocumentId)); var mainIdExpr = Expression.Property(parameter, mainIdProperty); var docIdExpr = Expression.Property(parameter, docIdProperty); // 构建元组初始化表达式 var tupleConstructor = typeof(ValueTuple<int, int>).GetConstructor(new[] { typeof(int), typeof(int) }); var tupleExpr = Expression.New(tupleConstructor, mainIdExpr, docIdExpr); return Expression.Lambda<Func<TMapping, (int MainEntityId, int DocumentId)>>(tupleExpr, parameter); } // 辅助方法:根据连接表类型获取主实体ID属性名 private static string GetMainEntityIdPropertyName<TMapping>() { var mappingTypeName = typeof(TMapping).Name; return mappingTypeName.Replace("Document", "") + "Id"; }
方案2:动态构建匿名类型Lambda表达式
如果坚持要用匿名类型,可以在泛型方法内动态创建匿名类型的Lambda表达式(返回Expression<Func<T, object>>),避免直接返回匿名类型。核心是确保左右两边的匿名类型结构完全一致(属性名、类型、顺序都相同)。
示例代码:
private static Expression<Func<TMapping, object>> GetAnonymousKeySelector<TMapping>() { var parameter = Expression.Parameter(typeof(TMapping), "x"); var mainIdProperty = typeof(TMapping).GetProperty(GetMainEntityIdPropertyName<TMapping>()); var docIdProperty = typeof(TMapping).GetProperty(nameof(DocumentId)); // 创建匿名类型实例获取类型信息 var anonymousType = new { MainEntityId = 0, DocumentId = 0 }.GetType(); // 构建匿名类型初始化表达式 var newExpr = Expression.New( anonymousType.GetConstructor(new[] { typeof(int), typeof(int) }), new[] { Expression.Property(parameter, mainIdProperty), Expression.Property(parameter, docIdProperty) }, anonymousType.GetProperties() ); return Expression.Lambda<Func<TMapping, object>>(Expression.Convert(newExpr, typeof(object)), parameter); }
方案3:将Composite配置为EF Core复杂类型(EF Core 8+支持)
EF Core 8开始支持自定义类型作为复杂类型映射,让EF能正确识别并翻译表达式。
首先修改Composite为只读结构体(或保持record,需配置为复杂类型):
public readonly struct Composite { public int MainEntityId { get; } public int DocumentId { get; } public Composite(int mainEntityId, int documentId) { MainEntityId = mainEntityId; DocumentId = documentId; } }
然后在DbContext的OnModelCreating中配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置连接表主键 modelBuilder.Entity<RoomDocument>().HasKey(r => new { r.RoomId, r.DocumentId }); modelBuilder.Entity<ProductDocument>().HasKey(p => new { p.ProductId, p.DocumentId }); // 配置Composite为复杂类型 modelBuilder.ComplexType<Composite>() .Property(c => c.MainEntityId) .HasColumnName("main_entity_id"); modelBuilder.ComplexType<Composite>() .Property(c => c.DocumentId) .HasColumnName("document_id"); }
之后确保动态构建的Lambda返回Composite类型,EF即可正确翻译连接条件。
内容的提问来源于stack exchange,提问作者balazska

