将谓词表达式树变异以针对另一种类型

51

介绍

在我目前开发的应用程序中,每个业务对象都有两种类型: "ActiveRecord" 类型和 "DataContract" 类型。例如,可能会有:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

数据库访问层负责在不同的类族之间进行转换:你可以要求它更新一个DataContract.Widget对象,它会神奇地创建一个具有相同属性值的ActiveRecord.Widget对象并将其保存。

当尝试重构此数据库访问层时,出现了问题。

问题

我想像下面这样在数据库访问层中添加方法:

// Widget is DataContract.Widget

interface IDbAccessLayer {
    IEnumerable<Widget> GetMany(Expression<Func<Widget, bool>> predicate);
}
上面是一个简单通用的“获取”方法,带有自定义谓词。唯一值得注意的是,在 IDbAccessLayer 内部查询 IQueryable<ActiveRecord.Widget>。为了高效地执行此操作(类似于 LINQ to SQL),我需要传递一个表达式树,因此该方法要求刚好如此。
问题在于,参数需要从 Expression<Func<DataContract.Widget, bool>> 魔法般地转换为 Expression<Func<ActiveRecord.Widget, bool>>
尝试的解决方案: 我想在 GetMany 中做的是:
IEnumerable<DataContract.Widget> GetMany(
    Expression<Func<DataContract.Widget, bool>> predicate)
{
    var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
        predicate.Body,
        predicate.Parameters);

    // use lambda to query ActiveRecord.Widget and return some value
}

这样做行不通,因为在典型情况下,例如如果:

predicate == w => w.Id == 0;

表达式树包含一个MemberAccessExpression实例,该实例具有描述DataContract.Widget.IdMemberInfo类型的属性。

在表达式树及其参数集合(predicate.Parameters)中还有ParameterExpression实例描述DataContract.Widget。所有这些都将导致错误,因为可查询体不包含该类型的widget,而是ActiveRecord.Widget

经过一番搜索,我找到了System.Linq.Expressions.ExpressionVisitor(其源代码可以在此处找到),它提供了一种方便的方法来修改表达式树。在.NET 4中,这个类被默认包括。

使用它,我实现了一个访问器。这个简单的访问器只负责更改成员访问和参数表达式中的类型,但这已足够用于操作w => w.Id == 0的谓词。

internal class Visitor : ExpressionVisitor
{
    private readonly Func<Type, Type> typeConverter;

    public Visitor(Func<Type, Type> typeConverter)
    {
        this.typeConverter = typeConverter;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var dataContractType = node.Member.ReflectedType;
        var activeRecordType = this.typeConverter(dataContractType);

        var converted = Expression.MakeMemberAccess(
            base.Visit(node.Expression),
            activeRecordType.GetProperty(node.Member.Name));

        return converted;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var dataContractType = node.Type;
        var activeRecordType = this.typeConverter(dataContractType);

        return Expression.Parameter(activeRecordType, node.Name);
    }
}

有了这个访问者,GetMany 就变成了:

IEnumerable<DataContract.Widget> GetMany(
    Expression<Func<DataContract.Widget, bool>> predicate)
{
    var visitor = new Visitor(...);
    var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
        visitor.Visit(predicate.Body),
        predicate.Parameters.Select(p => visitor.Visit(p));

    var widgets = ActiveRecord.Widget.Repository().Where(lambda);

    // This is just for reference, see below
    Expression<Func<ActiveRecord.Widget, bool>> referenceLambda = 
        w => w.Id == 0;

    // Here we 'd convert the widgets to instances of DataContract.Widget and
    // return them -- this has nothing to do with the question though.
}

结果

好消息是 lambda 构造正确。坏消息是当我尝试使用它时,它无法正常工作;并且异常信息根本没有帮助。

我已经检查了我的代码生成的 lambda 和一个硬编码的 lambda,它们看起来完全一样。我在调试器中花了几个小时尝试找到一些差异,但是我找不到。

当谓词为 w => w.Id == 0 时,lambda 看起来和 referenceLambda 完全一样。但后者可以与例如 IQueryable<T>.Where 一起使用,而前者不能; 我在调试器的立即窗口中尝试过。

我还应该提到,当谓词为 w => true 时,一切都正常工作。因此,我认为我在访问者中没有做足够的工作,但我找不到更多的线索跟进。

最终解决方案

在考虑了问题的正确答案之后(以下有两个答案;一个较短,另一个含有代码),问题得到了解决;我将代码连同一些重要说明放在了独立的答案中,以避免这个长问题变得更长。

感谢大家的回答和评论!


2
你是否遇到了“InvalidOperationException”异常,其错误信息为“变量 'w' 的类型为 'ConsoleApplication1.Product2',被引用于作用域 '' 中,但它没有被定义”,或者其他的异常? - TcKs
你介意在你的代码中加入Diego V建议的更改吗?这是一个很好的问题,它会帮助很多人。 - David Robbins
很棒的问题,也是一个很好的解决方案。 - Paul Suart
1
@Lorenzo:只需删除任何对它们的引用;您将使用此重载Expression.Lambda,该重载(逻辑上)不需要这些参数。 - Jon
我刚刚在这里添加了我的答案,这是一个类似的问题:https://dev59.com/HlvUa4cB1Zd3GeqPr0np#7425211 - luksan
显示剩余6条评论
6个回答

15

看起来你在 VisitMember() 中生成参数表达式两次:

var converted = Expression.MakeMemberAccess(
    base.Visit(node.Expression),
    activeRecordType.GetProperty(node.Member.Name));

我想,由于base.Visit()最终将在VisitParameter中结束,并在GetMany()本身中调用:

var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
    visitor.Visit(predicate.Body),
    predicate.Parameters.Select(p => visitor.Visit(p));
如果在Lambda表达式的主体中使用ParameterExpression,那么它必须是与Lambda声明的那个实例相同(而不仅仅是相同的类型和名称)。 我以前就遇到过这种情况的问题,尽管我认为结果只是我无法创建该表达式,它会抛出异常。无论如何,您可以尝试重用参数实例,看看是否有帮助。

你的回答看起来非常有前途,因为简单的 w => w.Id == 0 在主体中确实使用了一个 ParameterExpression,而我完全没有想到它们可能需要是同一个实例(文档上的什么鬼???)。我会调查一下并回复你。谢谢! - Jon
@diegov - +1-我认为这就是出错的地方。然而,我的做法是在构造函数中将新的ParameterExpression传入Visitor,而永远不会在Visitor内部构造新的ParameterExpression。每当它发现“旧”的参数实例时,它都会用新的参数替换它。 - Andras Zoltan
@diegov - 确认:这就是问题所在!谢谢!顺便问一下:你是怎么知道它必须是同一个实例的呢? :-) - Jon
我刚刚检查了一下,实际上这主要是运气问题,问题出在编译表达式上,而不仅仅是像我错误地回忆的那样构建它,我猜测subsonic没有编译它。但它可能是基于相同实例的假设工作的,这可能是神秘异常的原因。 - Diego Veralli
幸运的是,这是一个非常敏锐的观察!你是对的,SubSonic不会编译表达式——相反,它会将其拆开以构建SQL,这就是整个重点。 - Jon
谢谢你帮我节省了下午的时间。我完全不知道发生了什么。 - mkedobbs

14
事实证明,棘手的部分只是新lambda表达式树中存在的ParameterExpression实例必须与传递给Expression.Lambda的IEnumerable参数中的实例相同。
请注意,在TransformPredicateLambda内部,我将t => typeof(TNewTarget)作为“类型转换器”函数;这是因为在这种特定情况下,我们可以假设所有参数和成员访问都是那个特定类型。更高级的场景可能需要额外的逻辑。
代码:
internal class DbAccessLayer {
    private static Expression<Func<TNewTarget, bool>> 
    TransformPredicateLambda<TOldTarget, TNewTarget>(
    Expression<Func<TOldTarget, bool>> predicate)
    {
        var lambda = (LambdaExpression) predicate;
        if (lambda == null) {
            throw new NotSupportedException();
        }

        var mutator = new ExpressionTargetTypeMutator(t => typeof(TNewTarget));
        var explorer = new ExpressionTreeExplorer();
        var converted = mutator.Visit(predicate.Body);

        return Expression.Lambda<Func<TNewTarget, bool>>(
            converted,
            lambda.Name,
            lambda.TailCall,
            explorer.Explore(converted).OfType<ParameterExpression>());
    }


    private class ExpressionTargetTypeMutator : ExpressionVisitor
    {
        private readonly Func<Type, Type> typeConverter;

        public ExpressionTargetTypeMutator(Func<Type, Type> typeConverter)
        {
            this.typeConverter = typeConverter;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            var dataContractType = node.Member.ReflectedType;
            var activeRecordType = this.typeConverter(dataContractType);

            var converted = Expression.MakeMemberAccess(
                base.Visit(node.Expression), 
                activeRecordType.GetProperty(node.Member.Name));

            return converted;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            var dataContractType = node.Type;
            var activeRecordType = this.typeConverter(dataContractType);

            return Expression.Parameter(activeRecordType, node.Name);
        }
    }
}

/// <summary>
/// Utility class for the traversal of expression trees.
/// </summary>
public class ExpressionTreeExplorer
{
    private readonly Visitor visitor = new Visitor();

    /// <summary>
    /// Returns the enumerable collection of expressions that comprise
    /// the expression tree rooted at the specified node.
    /// </summary>
    /// <param name="node">The node.</param>
    /// <returns>
    /// The enumerable collection of expressions that comprise the expression tree.
    /// </returns>
    public IEnumerable<Expression> Explore(Expression node)
    {
        return this.visitor.Explore(node);
    }

    private class Visitor : ExpressionVisitor
    {
        private readonly List<Expression> expressions = new List<Expression>();

        protected override Expression VisitBinary(BinaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitBinary(node);
        }

        protected override Expression VisitBlock(BlockExpression node)
        {
            this.expressions.Add(node);
            return base.VisitBlock(node);
        }

        protected override Expression VisitConditional(ConditionalExpression node)
        {
            this.expressions.Add(node);
            return base.VisitConditional(node);
        }

        protected override Expression VisitConstant(ConstantExpression node)
        {
            this.expressions.Add(node);
            return base.VisitConstant(node);
        }

        protected override Expression VisitDebugInfo(DebugInfoExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDebugInfo(node);
        }

        protected override Expression VisitDefault(DefaultExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDefault(node);
        }

        protected override Expression VisitDynamic(DynamicExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDynamic(node);
        }

        protected override Expression VisitExtension(Expression node)
        {
            this.expressions.Add(node);
            return base.VisitExtension(node);
        }

        protected override Expression VisitGoto(GotoExpression node)
        {
            this.expressions.Add(node);
            return base.VisitGoto(node);
        }

        protected override Expression VisitIndex(IndexExpression node)
        {
            this.expressions.Add(node);
            return base.VisitIndex(node);
        }

        protected override Expression VisitInvocation(InvocationExpression node)
        {
            this.expressions.Add(node);
            return base.VisitInvocation(node);
        }

        protected override Expression VisitLabel(LabelExpression node)
        {
            this.expressions.Add(node);
            return base.VisitLabel(node);
        }

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            this.expressions.Add(node);
            return base.VisitLambda(node);
        }

        protected override Expression VisitListInit(ListInitExpression node)
        {
            this.expressions.Add(node);
            return base.VisitListInit(node);
        }

        protected override Expression VisitLoop(LoopExpression node)
        {
            this.expressions.Add(node);
            return base.VisitLoop(node);
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMember(node);
        }

        protected override Expression VisitMemberInit(MemberInitExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMemberInit(node);
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMethodCall(node);
        }

        protected override Expression VisitNew(NewExpression node)
        {
            this.expressions.Add(node);
            return base.VisitNew(node);
        }

        protected override Expression VisitNewArray(NewArrayExpression node)
        {
            this.expressions.Add(node);
            return base.VisitNewArray(node);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            this.expressions.Add(node);
            return base.VisitParameter(node);
        }

        protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node)
        {
            this.expressions.Add(node);
            return base.VisitRuntimeVariables(node);
        }

        protected override Expression VisitSwitch(SwitchExpression node)
        {
            this.expressions.Add(node);
            return base.VisitSwitch(node);
        }

        protected override Expression VisitTry(TryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitTry(node);
        }

        protected override Expression VisitTypeBinary(TypeBinaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitTypeBinary(node);
        }

        protected override Expression VisitUnary(UnaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitUnary(node);
        }

        public IEnumerable<Expression> Explore(Expression node)
        {
            this.expressions.Clear();
            this.Visit(node);
            return expressions.ToArray();
        }
    }
}

1
@Lorenzo:你会想要阅读这篇文章 - Jon
你好,我发现在 ExpressionTreeExplorer 中存在一个小问题,当输入表达式中有多个条件时会出现问题。比如说你有这样一个表达式:m => m.Field1 == 123 && m.Field2 == "xyz"。在这种情况下,explorer.Explore(converted).OfType<ParameterExpression>() 将返回两个 ParameterExpression,这将导致一个带有错误信息的 ArgumentExceptionIncorrect number of parameters supplied for lambda declaration。请注意,这两个 ParameterExpression 都绑定到 TNewTarget 类型。我该如何消除这个错误? - Lorenzo
@Lorenzo:你需要用更复杂的方法来替换简单的.OfType<ParameterExpression>(),以识别多个参数表达式引用同一个参数(在你的例子中是m),并且只返回一个副本。Sinelaw下面的答案可以做到这一点,尽管我只是浏览代码,无法确定它有多可靠。总的来说,这个问题非常复杂,所以即使这里的答案看起来很复杂,实际上它们只能处理简单的部分。 - Jon
好的,我能理解。我只有一个问题:假设我使用了Sinelaw的代码,并将其适配到了.NET 3.5,现在不需要使用ExpressionTreeExplorer,那么我该如何创建一个Lambda对象来返回呢?非常感谢您的帮助。 - Lorenzo
如果你只重写Visit(Expression)方法,而不重写其他方法,我认为你可以大大简化ExpressionTreeExplorer.Visitor - svick
显示剩余5条评论

6

我尝试了对表达式 p => p.Id == 15 进行变异的简单(不完整)实现(代码如下)。有一个名为“CrossMapping”的类定义了原始类型和“新”类型以及类型成员之间的映射。

每个表达式类型都有几种名为 Mutate_XY_Expression 的方法,用于创建新的变异表达式。这些方法需要原始表达式 (MemberExpression originalExpression) 作为表达式模型、参数表达式列表 (IList<ParameterExpression> parameterExpressions),这些参数由“父级”表达式定义,并且应该被“父级”的主体所使用,以及定义类型和成员之间映射关系的映射对象 (CrossMapping mapping)。

对于完整的实现,您可能需要从父级表达式中获取更多信息而不仅仅是参数。但是模式应该是相同的。

示例没有实现访问者模式,因为这样做会增加复杂度。但是转换到访问者模式并不困难。

希望这能有所帮助。

代码(C# 4.0):

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

namespace ConsoleApplication1 {
    public class Product1 {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Weight { get; set; }
    }

    public class Product2 {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Weight { get; set; }
    }

    class Program {
        static void Main( string[] args ) {
            // list of products typed as Product1
            var lst1 = new List<Product1> {
                new Product1{ Id = 1, Name = "One" },
                new Product1{ Id = 15, Name = "Fifteen" },
                new Product1{ Id = 9, Name = "Nine" }
            };

            // the expression for filtering products
            // typed as Product1
            Expression<Func<Product1, bool>> q1;
            q1 = p => p.Id == 15;

            // list of products typed as Product2
            var lst2 = new List<Product2> {
                new Product2{ Id = 1, Name = "One" },
                new Product2{ Id = 15, Name = "Fifteen" },
                new Product2{ Id = 9, Name = "Nine" }
            };

            // type of Product1
            var tp1 = typeof( Product1 );
            // property info of "Id" property from type Product1
            var tp1Id = tp1.GetProperty( "Id", BindingFlags.Public | BindingFlags.Instance );
            // delegate type for predicating for Product1
            var tp1FuncBool = typeof( Func<,> ).MakeGenericType( tp1, typeof( bool ) );

            // type of Product2
            var tp2 = typeof( Product2 );
            // property info of "Id" property from type Product2
            var tp21Id = tp2.GetProperty( "Id", BindingFlags.Public | BindingFlags.Instance );
            // delegate type for predicating for Product2
            var tp2FuncBool = typeof( Func<,> ).MakeGenericType( tp2, typeof( bool ) );

            // mapping object for types and type members
            var cm1 = new CrossMapping {
                TypeMapping = {
                    // Product1 -> Product2
                    { tp1, tp2 },
                    // Func<Product1, bool> -> Func<Product2, bool>
                    { tp1FuncBool, tp2FuncBool }
                },
                MemberMapping = {
                    // Product1.Id -> Product2.Id
                    { tp1Id, tp21Id }
                }
            };

            // mutate express from Product1's "enviroment" to Product2's "enviroment"
            var cq1_2 = MutateExpression( q1, cm1 );

            // compile lambda to delegate
            var dlg1_2 = ((LambdaExpression)cq1_2).Compile();

            // executing delegate
            var rslt1_2 = lst2.Where( (Func<Product2, bool>)dlg1_2 ).ToList();

            return;
        }

        class CrossMapping {
            public IDictionary<Type, Type> TypeMapping { get; private set; }
            public IDictionary<MemberInfo, MemberInfo> MemberMapping { get; private set; }

            public CrossMapping() {
                this.TypeMapping = new Dictionary<Type, Type>();
                this.MemberMapping = new Dictionary<MemberInfo, MemberInfo>();
            }
        }
        static Expression MutateExpression( Expression originalExpression, CrossMapping mapping ) {
            var ret = MutateExpression(
                originalExpression: originalExpression,
                parameterExpressions: null,
                mapping: mapping
            );

            return ret;
        }
        static Expression MutateExpression( Expression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            Expression ret;

            if ( null == originalExpression ) {
                ret = null;
            }
            else if ( originalExpression is LambdaExpression ) {
                ret = MutateLambdaExpression( (LambdaExpression)originalExpression, parameterExpressions, mapping );
            }
            else if ( originalExpression is BinaryExpression ) {
                ret = MutateBinaryExpression( (BinaryExpression)originalExpression, parameterExpressions, mapping );
            }
            else if ( originalExpression is ParameterExpression ) {
                ret = MutateParameterExpression( (ParameterExpression)originalExpression, parameterExpressions, mapping );
            }
            else if ( originalExpression is MemberExpression ) {
                ret = MutateMemberExpression( (MemberExpression)originalExpression, parameterExpressions, mapping );
            }
            else if ( originalExpression is ConstantExpression ) {
                ret = MutateConstantExpression( (ConstantExpression)originalExpression, parameterExpressions, mapping );
            }
            else {
                throw new NotImplementedException();
            }

            return ret;
        }

        static Type MutateType( Type originalType, IDictionary<Type, Type> typeMapping ) {
            if ( null == originalType ) { return null; }

            Type ret;
            typeMapping.TryGetValue( originalType, out ret );
            if ( null == ret ) { ret = originalType; }

            return ret;
        }
        static MemberInfo MutateMember( MemberInfo originalMember, IDictionary<MemberInfo, MemberInfo> memberMapping ) {
            if ( null == originalMember ) { return null; }

            MemberInfo ret;
            memberMapping.TryGetValue( originalMember, out ret );
            if ( null == ret ) { ret = originalMember; }

            return ret;
        }
        static LambdaExpression MutateLambdaExpression( LambdaExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpression ) { return null; }

            var newParameters = (from p in originalExpression.Parameters
                                 let np = MutateParameterExpression( p, parameterExpressions, mapping )
                                 select np).ToArray();

            var newBody = MutateExpression( originalExpression.Body, newParameters, mapping );

            var newType = MutateType( originalExpression.Type, mapping.TypeMapping );

            var ret = Expression.Lambda(
                delegateType: newType,
                body: newBody,
                name: originalExpression.Name,
                tailCall: originalExpression.TailCall,
                parameters: newParameters
            );

            return ret;
        }
        static BinaryExpression MutateBinaryExpression( BinaryExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpression ) { return null; }

            var newExprConversion = MutateExpression( originalExpression.Conversion, parameterExpressions, mapping );
            var newExprLambdaConversion = (LambdaExpression)newExprConversion;
            var newExprLeft = MutateExpression( originalExpression.Left, parameterExpressions, mapping );
            var newExprRigth = MutateExpression( originalExpression.Right, parameterExpressions, mapping );
            var newType = MutateType( originalExpression.Type, mapping.TypeMapping );
            var newMember = MutateMember( originalExpression.Method, mapping.MemberMapping);
            var newMethod = (MethodInfo)newMember;

            var ret = Expression.MakeBinary(
                binaryType: originalExpression.NodeType,
                left: newExprLeft,
                right: newExprRigth,
                liftToNull: originalExpression.IsLiftedToNull,
                method: newMethod,
                conversion: newExprLambdaConversion
            );

            return ret;
        }
        static ParameterExpression MutateParameterExpression( ParameterExpression originalExpresion, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpresion ) { return null; }

            ParameterExpression ret = null;
            if ( null != parameterExpressions ) {
                ret = (from p in parameterExpressions
                       where p.Name == originalExpresion.Name
                       select p).FirstOrDefault();
            }

            if ( null == ret ) {
                var newType = MutateType( originalExpresion.Type, mapping.TypeMapping );

                ret = Expression.Parameter( newType, originalExpresion.Name );
            }

            return ret;
        }
        static MemberExpression MutateMemberExpression( MemberExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpression ) { return null; }

            var newExpression = MutateExpression( originalExpression.Expression, parameterExpressions, mapping );
            var newMember = MutateMember( originalExpression.Member, mapping.MemberMapping );

            var ret = Expression.MakeMemberAccess(
                expression: newExpression,
                member: newMember
            );

            return ret;
        }
        static ConstantExpression MutateConstantExpression( ConstantExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpression ) { return null; }

            var newType = MutateType( originalExpression.Type, mapping.TypeMapping );
            var newValue = originalExpression.Value;

            var ret = Expression.Constant(
                value: newValue,
                type: newType
            );

            return ret;
        }
    }
}

感谢您抽出时间编写自己的小程序来进行说明。遗憾的是,我不能接受您和diegov的回答...... diegov先回复且正确,所以我接受了他的回答。如果可以的话,我会给你们点赞的 :) - Jon
这对于C# 3.0也有效吗? - David Robbins
2
@David:如果你在方法调用中替换命名参数(var ret = MutateExpression(originalExpression: originalExpression, parameterExpressions: null, mapping: mapping);)为(var ret = MutateExpression(originalExpression, null, mapping);),那么你就可以在C# 3.0中使用它了。 - TcKs

5

Jon的回答已经很好了,我将其扩展以处理方法调用、常量表达式等,因此现在它也适用于如下表达式:

x => x.SubObjects
      .AsQueryable()
      .SelectMany(y => y.GrandChildObjects)
      .Any(z => z.Value == 3)

我还取消了ExpressionTreeExplorer,因为我们只需要参数表达式。

这是代码(更新:完成转换后清除缓存)

public class ExpressionTargetTypeMutator : ExpressionVisitor
{
    private readonly Func<Type, Type> typeConverter;
    private readonly Dictionary<Expression, Expression> _convertedExpressions 
        = new Dictionary<Expression, Expression>();

    public ExpressionTargetTypeMutator(Func<Type, Type> typeConverter)
    {
        this.typeConverter = typeConverter;
    }

    // Clear the ParameterExpression cache between calls to Visit.
    // Not thread safe, but you can probably fix it easily.
    public override Expression Visit(Expression node)
    {
        bool outermostCall = false;
        if (false == _isVisiting)
        {
            this._isVisiting = true;
            outermostCall = true;
        }
        try
        {
            return base.Visit(node);
        }
        finally
        {
            if (outermostCall)
            {
                this._isVisiting = false;
                _convertedExpressions.Clear();
            }
        }
    }


    protected override Expression VisitMember(MemberExpression node)
    {
        var sourceType = node.Member.ReflectedType;
        var targetType = this.typeConverter(sourceType);

        var converted = Expression.MakeMemberAccess(
            base.Visit(node.Expression),
            targetType.GetProperty(node.Member.Name));

        return converted;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        Expression converted;
        if (false == _convertedExpressions.TryGetValue(node, out converted))
        {
            var sourceType = node.Type;
            var targetType = this.typeConverter(sourceType);
            converted = Expression.Parameter(targetType, node.Name);
            _convertedExpressions.Add(node, converted);
        }
        return converted;
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.IsGenericMethod)
        {
            var convertedTypeArguments = node.Method.GetGenericArguments()
                                                    .Select(this.typeConverter)
                                                    .ToArray();
            var genericMethodDefinition = node.Method.GetGenericMethodDefinition();
            var newMethod = genericMethodDefinition.MakeGenericMethod(convertedTypeArguments);
            return Expression.Call(newMethod, node.Arguments.Select(this.Visit));
        }
        return base.VisitMethodCall(node);
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        var valueExpression = node.Value as Expression;
        if (null != valueExpression)
        {
            return Expression.Constant(this.Visit(valueExpression));
        }
        return base.VisitConstant(node);
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        return Expression.Lambda(this.Visit(node.Body), node.Name, node.TailCall, node.Parameters.Select(x => (ParameterExpression)this.VisitParameter(x)));
    }
}

我尝试将您的代码适应于.NET 3.5,结果只是删除了VisitLambda<T>()方法。在这个过程中,我发现这段代码和被接受的答案一样,如果原始表达式像这个例子:x => x.Property1.Property2.Property3 == 1,那么就会遇到问题。当MakeMemberAccess()方法尝试将“导航”类型(例如Property2)转换为原始类型时,由于其typeConverter成员变量,问题会发生。由于我在表达式方面并不是很强,所以您能否指导我解决这个问题? - Lorenzo

-3

ExecuteTypedList是否能够实现您想要的功能?SubSonic将填充您的DTO/POCO。来自Rob Connery的博客:

ExecuteTypedList<>尝试将返回的列的名称与传入类型的属性名称匹配。在这个例子中,它们完全匹配 - 这并不完全符合实际情况。您可以通过为列设置别名来解决此问题 - 就像别名SQL调用一样:

return Northwind.DB.Select("ProductID as 'ID'", "ProductName as 'Name'", "UnitPrice as 'Price'")
            .From<Northwind.Product>().ExecuteTypedList<Product>();

这是 Rob 的链接 使用 SubSonic 2.1 编写解耦、可测试的代码


“ExecuteTypedList<>”仅存在于SubSonic 2中,我正在使用3。SS2甚至没有lambda表达式...另外,您提出的解决方案需要重新审查代码库,并将一些(但不是全部;有时我们在需要整个结果集的情况下使用LINQ到对象)Func<DataContract,bool>表达式更改为Func<ActiveRecord,bool>,并且我们放弃任何自定义的“变异”逻辑和选项。我不只是想让一个语句编译。 - Jon
好的 - 我想我明白了。你基本上想写出这样的东西:DataContract.WidgetList().AsQueryable().Where(predicate),其中谓词是DataContract类型,但要转换为ActiveRecord。 - David Robbins
那完美地概括了它,是的。 - Jon

-4

我认为如果你正确编写查询,Linq-To-SQL 将生成期望的 SQL。在这种情况下,使用 IQueryable 和延迟执行,您可以避免返回所有 ActiveRecord.Widget 记录。

IEnumerable<DataContract.Widget> GetMany( 
    Func<DataContract.Widget, bool> predicate) 
{ 
    // get Widgets
    IQueryable<DataContract.Widget> qry = dc.Widgets.Select(w => TODO: CONVERT_TO_DataContract.Widget);

    return qry.Where(predicate);
}

抱歉,但这个答案既没有帮助,也是错误的。我不确定你想表达什么,但问题显然都是关于表达式树的,而你的代码中甚至还不到一半。 - Jon
1
@Jon,我认为你的主要目标是解决你的问题!我建议采用一种不使用表达式树的不同方法,因为它从一开始就避免了性能问题。请确认在这种情况下生成的SQL,你会发现只返回所需的记录。 - bruno conde
我仍然不明白你想表达什么。根据我的理解,你的代码是说“使用这个谓词过滤DataContract.Widget的可查询集合”。问题(1):不存在“DataContract.Widget”的IQueryProvider。问题(2):即使我们编写了一个,它该如何实现?(a)通过转换查询表达式并在ActiveRecord.Widget上进行查询?【问题(3):你的代码没有使用查询表达式而是使用lambda表达式】这就是我正在尝试做的事情!还是(b)通过获取所有记录,转换它们并在之后进行筛选?这正是我想要避免的。 - Jon
1
我的代码表示:(1)获取ActiveRecord.Widget记录并进行转换(将它们转换为DataContract.Widget)。(2)对转换后的IQueryable应用所需的过滤器。虽然看起来你正在执行两个操作,但实际上,Linq-to-SQL只会使用一个查询来进行转换和过滤。因此,只有所需的记录被返回。 - bruno conde
我认为你的第一步实际上会拉取所有记录,但是我不敢说这是事实。然而,另一个问题仍然存在:我没有一个 IQueryable<DataContract.Widget> 实现。 - Jon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接