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

解决EF6中Linq To Entities选择操作的类型转换失败问题

在Entity Framework 6中处理自定义Id类型的查询翻译问题

你可以通过自定义ExpressionVisitor结合查询扩展方法,自动处理Select投影中ToId()调用、Id显式转换或构造函数调用的翻译问题,无需手动拆分两次Select。以下是具体实现方案:

核心思路

通过ExpressionVisitor分析投影表达式,将EF无法翻译的Id转换操作拆分为两部分:

  1. 服务器端投影:仅提取EF能翻译的原始字段(如x.order_id),避免冗余数据传输
  2. 客户端转换:在内存中将原始字段转换为自定义Id类型,生成目标DTO

实现代码

1. 自定义ExpressionVisitor及查询扩展方法

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;

public static class QueryableExtensions
{
    public static IQueryable<TResult> SelectWithIdConversion<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector)
    {
        var serverVisitor = new ServerProjectionVisitor();
        var serverSelector = serverVisitor.Visit(selector) as LambdaExpression;
        
        var clientVisitor = new ClientProjectionVisitor(serverVisitor.MemberMappings);
        var clientSelector = clientVisitor.Visit(selector) as Expression<Func<object, TResult>>;

        return source.Select(serverSelector)
                     .AsEnumerable()
                     .Select(clientSelector.Compile())
                     .AsQueryable();
    }

    private class ServerProjectionVisitor : ExpressionVisitor
    {
        public Dictionary<MemberInfo, Expression> MemberMappings { get; } = new();

        protected override Expression VisitMemberInit(MemberInitExpression node)
        {
            var propertyTypes = new List<Type>();
            var bindings = new List<Expression>();

            foreach (var binding in node.Bindings.Cast<MemberAssignment>())
            {
                var valueExpr = Visit(binding.Expression);
                MemberMappings[binding.Member] = valueExpr;

                if (IsIdConversionExpression(valueExpr))
                {
                    var intExpr = ExtractIntExpression(valueExpr);
                    bindings.Add(intExpr);
                    propertyTypes.Add(typeof(int));
                }
                else
                {
                    bindings.Add(valueExpr);
                    propertyTypes.Add(valueExpr.Type);
                }
            }

            var anonType = CreateAnonymousType(propertyTypes.ToArray());
            var constructor = anonType.GetConstructor(propertyTypes.ToArray());
            return Expression.New(constructor, bindings);
        }

        private bool IsIdConversionExpression(Expression expr)
        {
            return expr switch
            {
                MethodCallExpression mce => mce.Method.DeclaringType == typeof(IdHelper) && mce.Method.Name == nameof(IdHelper.ToId),
                UnaryExpression ue => ue.NodeType == ExpressionType.Convert && ue.Type == typeof(Id),
                NewExpression ne => ne.Type == typeof(Id) && ne.Arguments.Count == 1 && ne.Arguments[0].Type == typeof(int),
                _ => false
            };
        }

        private Expression ExtractIntExpression(Expression expr)
        {
            return expr switch
            {
                MethodCallExpression mce => mce.Arguments[0],
                UnaryExpression ue => ue.Operand,
                NewExpression ne => ne.Arguments[0],
                _ => throw new InvalidOperationException("无效的Id转换表达式")
            };
        }

        private Type CreateAnonymousType(Type[] propertyTypes)
        {
            var assemblyName = new AssemblyName("DynamicAnonTypes");
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicAnonTypesModule");
            var typeBuilder = moduleBuilder.DefineType($"Anon_{Guid.NewGuid():N}", TypeAttributes.Public | TypeAttributes.Class);

            var fields = new List<FieldBuilder>();
            for (int i = 0; i < propertyTypes.Length; i++)
            {
                var field = typeBuilder.DefineField($"_prop{i}", propertyTypes[i], FieldAttributes.Private);
                fields.Add(field);

                var prop = typeBuilder.DefineProperty($"Prop{i}", PropertyAttributes.None, propertyTypes[i], null);
                var getter = typeBuilder.DefineMethod($"get_Prop{i}", MethodAttributes.Public | MethodAttributes.SpecialName, propertyTypes[i], Type.EmptyTypes);
                var getterIl = getter.GetILGenerator();
                getterIl.Emit(OpCodes.Ldarg_0);
                getterIl.Emit(OpCodes.Ldfld, field);
                getterIl.Emit(OpCodes.Ret);

                var setter = typeBuilder.DefineMethod($"set_Prop{i}", MethodAttributes.Public | MethodAttributes.SpecialName, null, new[] { propertyTypes[i] });
                var setterIl = setter.GetILGenerator();
                setterIl.Emit(OpCodes.Ldarg_0);
                setterIl.Emit(OpCodes.Ldarg_1);
                setterIl.Emit(OpCodes.Stfld, field);
                setterIl.Emit(OpCodes.Ret);

                prop.SetGetMethod(getter);
                prop.SetSetMethod(setter);
            }

            var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, propertyTypes);
            var ctorIl = ctorBuilder.GetILGenerator();
            for (int i = 0; i < propertyTypes.Length; i++)
            {
                ctorIl.Emit(OpCodes.Ldarg_0);
                ctorIl.Emit(OpCodes.Ldarg, i + 1);
                ctorIl.Emit(OpCodes.Stfld, fields[i]);
            }
            ctorIl.Emit(OpCodes.Ret);

            return typeBuilder.CreateType();
        }
    }

    private class ClientProjectionVisitor : ExpressionVisitor
    {
        private readonly Dictionary<MemberInfo, Expression> _memberMappings;

        public ClientProjectionVisitor(Dictionary<MemberInfo, Expression> memberMappings)
        {
            _memberMappings = memberMappings;
        }

        protected override Expression VisitMemberInit(MemberInitExpression node)
        {
            var bindings = new List<MemberBinding>();
            var paramExpr = Expression.Parameter(typeof(object));

            foreach (var binding in node.Bindings.Cast<MemberAssignment>())
            {
                var member = binding.Member;
                var propIndex = _memberMappings.Keys.ToList().IndexOf(member);
                var anonProp = Expression.Property(Expression.Convert(paramExpr, _memberMappings[member].Type.DeclaringType), $"Prop{propIndex}");

                if (member.PropertyType == typeof(Id))
                {
                    var convertExpr = Expression.Call(typeof(IdHelper), nameof(IdHelper.ToId), Type.EmptyTypes, anonProp);
                    bindings.Add(Expression.Bind(member, convertExpr));
                }
                else
                {
                    bindings.Add(Expression.Bind(member, anonProp));
                }
            }

            return Expression.Lambda<Func<object, TResult>>(Expression.MemberInit(node.NewExpression, bindings), paramExpr);
        }
    }
}

2. 使用方式

直接调用扩展方法替代原有的Select,语法保持一致:

var orders = db.Orders
    .SelectWithIdConversion(x => new Order() 
    { 
        OrderId = x.order_id.ToId(), 
        OrderDesc = x.order_desc 
    })
    .ToList();

方案优势

  • 减少冗余代码:无需手动编写两次Select,保持查询语法简洁
  • 降低内存占用:服务器端仅返回DTO所需的原始字段,避免传输不必要的数据
  • 兼容多种转换方式:自动处理ToId()调用、(Id)intValue显式转换、new Id(intValue)构造函数三种场景

内容的提问来源于stack exchange,提问作者Andrei

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 07:49:53