EF Core 6:实现基于输入字段列表的泛型去重查询方法
解决EF Core动态字段Distinct的“Linq cannot be translated”错误
你遇到的核心问题是EF Core无法将Dictionary的创建和Add操作翻译为SQL,同时自定义DictionaryComparer的Distinct也只能在客户端执行,既触发翻译错误又影响性能。而固定字段时用匿名类型能正常工作,是因为EF能识别匿名类型的属性映射,将Distinct翻译为数据库层面的去重逻辑。
解决方案:动态构建匿名类型的Select表达式
通过动态生成匿名类型的表达式,让EF Core可以正常翻译为SQL,同时利用匿名类型默认的相等比较逻辑完成Distinct,无需自定义比较器。
完整代码实现
public static Expression<Func<TSource, object>> BuildDynamicSelectExpression<TSource>(List<string> fieldNames) { var sourceType = typeof(TSource); var sourceParam = Expression.Parameter(sourceType, "x"); // 收集需要选择的属性表达式 var propertyBindings = fieldNames.Select(fieldName => { var property = sourceType.GetProperty(fieldName); if (property == null) { throw new ArgumentException($"类型 {sourceType.Name} 不存在属性 {fieldName}"); } var propertyExpr = Expression.Property(sourceParam, property); // 构建匿名类型的属性绑定(属性名和字段名一致) return Expression.Bind(property.Name, propertyExpr); }).ToArray(); // 创建匿名类型的初始化表达式 var anonymousType = AnonymousTypeBuilder.Build(fieldNames.ToArray()); var initExpr = Expression.MemberInit(Expression.New(anonymousType.GetConstructor(Type.EmptyTypes)), propertyBindings); // 转换为object类型(因为匿名类型是密封类,转换安全) return Expression.Lambda<Func<TSource, object>>(Expression.Convert(initExpr, typeof(object)), sourceParam); } // 辅助类:动态创建匿名类型 public static class AnonymousTypeBuilder { public static Type Build(params string[] propertyNames) { var assemblyName = new AssemblyName("DynamicAnonymousTypes"); var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); // 生成类似C#匿名类型的命名格式 var typeName = $"<>f__AnonymousType{propertyNames.Length}_{Guid.NewGuid():N}"; var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, typeof(object)); // 添加属性和对应的私有字段 foreach (var propName in propertyNames) { var fieldBuilder = typeBuilder.DefineField($"<{propName}>i__Field", typeof(object), FieldAttributes.Private); var propBuilder = typeBuilder.DefineProperty(propName, PropertyAttributes.None, typeof(object), null); // 生成属性get方法 var getMethodBuilder = typeBuilder.DefineMethod($"get_{propName}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(object), Type.EmptyTypes); var getIl = getMethodBuilder.GetILGenerator(); getIl.Emit(OpCodes.Ldarg_0); getIl.Emit(OpCodes.Ldfld, fieldBuilder); getIl.Emit(OpCodes.Ret); // 生成属性set方法 var setMethodBuilder = typeBuilder.DefineMethod($"set_{propName}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { typeof(object) }); var setIl = setMethodBuilder.GetILGenerator(); setIl.Emit(OpCodes.Ldarg_0); setIl.Emit(OpCodes.Ldarg_1); setIl.Emit(OpCodes.Stfld, fieldBuilder); setIl.Emit(OpCodes.Ret); propBuilder.SetGetMethod(getMethodBuilder); propBuilder.SetSetMethod(setMethodBuilder); } // 添加无参构造函数 typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes) .GetILGenerator().Emit(OpCodes.Ret); // 生成和C#匿名类型一致的Equals和GetHashCode方法 GenerateEqualsMethod(typeBuilder, propertyNames); GenerateGetHashCodeMethod(typeBuilder, propertyNames); return typeBuilder.CreateType()!; } private static void GenerateEqualsMethod(TypeBuilder typeBuilder, string[] propertyNames) { var equalsMethod = typeBuilder.DefineMethod("Equals", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, typeof(bool), new[] { typeof(object) }); var il = equalsMethod.GetILGenerator(); // 引用相等直接返回true il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ceq); var labelEqual = il.DefineLabel(); il.Emit(OpCodes.Brtrue_S, labelEqual); // 参数为null返回false il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Ceq); var labelNull = il.DefineLabel(); il.Emit(OpCodes.Brtrue_S, labelNull); // 检查类型是否匹配 il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Isinst, typeBuilder); var castObj = il.DeclareLocal(typeBuilder); il.Emit(OpCodes.Stloc, castObj); il.Emit(OpCodes.Ldloc, castObj); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Ceq); il.Emit(OpCodes.Brtrue_S, labelNull); // 逐个比较属性值 foreach (var propName in propertyNames) { var field = typeBuilder.GetField($"<{propName}>i__Field", BindingFlags.NonPublic | BindingFlags.Instance)!; il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, field); il.Emit(OpCodes.Ldloc, castObj); il.Emit(OpCodes.Ldfld, field); il.Emit(OpCodes.Call, typeof(object).GetMethod("Equals", new[] { typeof(object), typeof(object) })!); il.Emit(OpCodes.Brfalse_S, labelNull); } // 所有属性相等返回true il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Ret); il.MarkLabel(labelEqual); il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Ret); il.MarkLabel(labelNull); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Ret); } private static void GenerateGetHashCodeMethod(TypeBuilder typeBuilder, string[] propertyNames) { var getHashCodeMethod = typeBuilder.DefineMethod("GetHashCode", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, typeof(int), Type.EmptyTypes); var il = getHashCodeMethod.GetILGenerator(); // 初始哈希值17 il.Emit(OpCodes.Ldc_I4_S, 17); foreach (var propName in propertyNames) { var field = typeBuilder.GetField($"<{propName}>i__Field", BindingFlags.NonPublic | BindingFlags.Instance)!; // hash = hash * 23 + (value?.GetHashCode() ?? 0) il.Emit(OpCodes.Ldc_I4_S, 23); il.Emit(OpCodes.Mul); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, field); var labelHasValue = il.DefineLabel(); il.Emit(OpCodes.Brtrue_S, labelHasValue); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Br_S, labelAdd); il.MarkLabel(labelHasValue); il.Emit(OpCodes.Callvirt, typeof(object).GetMethod("GetHashCode", Type.EmptyTypes)!); il.MarkLabel(labelAdd); il.Emit(OpCodes.Add); } il.Emit(OpCodes.Ret); } }
调用示例
var selectExpression = BuildDynamicSelectExpression<MyEntityType>(fieldNames); var distinctRecords = await _context.MyEntityDbSet .Select(selectExpression) .Distinct() .ToListAsync(cancellationToken);
方案优势与原代码问题分析
为什么这个方案可行
- 动态生成的匿名类型结构和C#编译器自动生成的匿名类型完全一致,EF Core能识别其属性映射,将
Select和Distinct直接翻译为数据库层面的SELECT DISTINCT语句,性能最优。 - 动态生成的
Equals和GetHashCode方法逻辑和匿名类型默认实现一致,确保Distinct的比较逻辑准确。
原代码的问题
- 创建
Dictionary并调用Add的操作属于内存集合操作,EF Core无法将其翻译为SQL,触发客户端评估错误。 - 自定义
DictionaryComparer的Distinct方法无法被EF Core翻译,只能在客户端加载全量数据后再去重,数据量大时性能极差。
内容的提问来源于stack exchange,提问作者Alex S.
相关产品推荐
相关产品推荐

