将谓词的表达式树变换为目标另一种类型

时间:2010-05-09 09:54:46

标签: c# linq lambda expression-trees

简介

在我目前正在处理的应用程序中,每个业务对象有两种:“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);
}

以上是一个简单的通用“get”方法,带有自定义谓词。唯一感兴趣的是我传递的是表达式树而不是lambda,因为在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实例,该实例具有描述MemberInfo的{​​{1}}类型的属性。 表达式树及其参数集合(DataContract.Widget.Id)中也有ParameterExpression个实例,用于描述predicate.Parameters;所有这些都会导致错误,因为可查询主体不包含该类型的小部件,而是DataContract.Widget

在搜索了一下后,我发现System.Linq.Expressions.ExpressionVisitor(它的源代码可以在how-to的上下文中找到here),这提供了一种修改表达式树的便捷方法。在.NET 4中,这个类是开箱即用的。

有了这个,我实施了一个访客。这个简单的访问者只负责更改成员访问和参数表达式中的类型,但这足以使用谓词ActiveRecord.Widget

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时,一切正常。因此,我假设我在访问者中没有做足够的工作,但我找不到任何更多的潜在客户。

最终解决方案

考虑到问题的正确答案(下面两个;一个简短,一个带代码),问题解决了;我将代码与separate answer中的一些重要注释放在一起,以防止这个长问题变得更长。

感谢大家的回答和评论!

6 个答案:

答案 0 :(得分: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));

如果您在正文中使用ParameterExpression,则它必须是与为Lambda声明的实例相同的实例(不仅仅是相同的类型和名称)。 我之前遇到过这种情况的问题,虽然我认为结果是我只是无法创建表达式,它只会引发异常。在任何情况下,您可能会尝试重用参数实例,看看它是否有帮助。

答案 1 :(得分:13)

事实证明,棘手的部分只是新lambda的表达式树中存在的ParameterExpression实例必须与{{1>传递的相同的实例参数IEnumerable<ParameterExpression>

请注意,在Expression.Lambda内,我将TransformPredicateLambda作为“类型转换器”功能;那是因为在这种特定情况下,我们可以假设所有参数和成员访问都属于那一种特定类型。更高级的场景可能需要额外的逻辑。

代码:

t => typeof(TNewTarget)

答案 2 :(得分:6)

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

对于每种表达式类型,都有几个名为Mutate_XY_Expression的方法,这会产生新的变异表达式。方法输入需要原始表达式(MemberExpression originalExpression)作为表达式模型,列表或参数表达式(IList<ParameterExpression> parameterExpressions),它们是由“父”表达式定义的参数,应该由“父级”主体使用,以及定义类型和成员之间映射的映射对象(CrossMapping mapping)。

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

如你所知,

Sample没有实现Visitor模式 - 这是因为简单。但转换成它们没有障碍。

我希望,这会有所帮助。

代码(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;
        }
    }
}

答案 3 :(得分:5)

上面的

Jon's own answer很棒,所以我将它扩展为处理方法调用,常量表达式等,以便它现在也适用于以下表达式:

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

我也取消了ExpressionTreeExplorer,因为我们唯一需要的是ParameterExpressions。

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

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

答案 4 :(得分:-2)

ExecuteTypedList不能完成你想做的事吗? SubSonic将填充您的DTO / POCO。来自Rob Connery的博客:

  

ExecuteTypedList&LT;&GT;试图匹配   返回列的名称   的属性名称   传入式。在这个例子中他们   完全匹配 - 而事实并非如此   完全真实的世界。你可以得到   围绕这个通过别名列 -   以同样的方式为SQL命名   拨打:

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

这是Rob的Writing Decoupled, Testable code with SubSonic 2.1

的链接

答案 5 :(得分:-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);
}