You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);

方案优势与原代码问题分析

为什么这个方案可行

  1. 动态生成的匿名类型结构和C#编译器自动生成的匿名类型完全一致,EF Core能识别其属性映射,将Select和Distinct直接翻译为数据库层面的SELECT DISTINCT语句,性能最优。
  2. 动态生成的Equals和GetHashCode方法逻辑和匿名类型默认实现一致,确保Distinct的比较逻辑准确。

原代码的问题

  • 创建Dictionary并调用Add的操作属于内存集合操作,EF Core无法将其翻译为SQL,触发客户端评估错误。
  • 自定义DictionaryComparer的Distinct方法无法被EF Core翻译,只能在客户端加载全量数据后再去重,数据量大时性能极差。

内容的提问来源于stack exchange,提问作者Alex S.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 14:55:01