一尘不染

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

c#

介绍

在我当前正在处理的应用程序中,每种业务对象都有两种:“ ActiveRecord”类型和“ DataContract”类型。因此,例如,将有:

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

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

数据库访问层负责在族之间进行转换:您可以告诉它更新a
DataContract.Widget,它将神奇地创建一个ActiveRecord.Widget具有相同属性值的an 并将其保存。

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

问题

我想将以下方法添加到数据库访问层:

// 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为describe
DataContract.Widget.IdParameterExpression在表达式树及其参数集合(predicate.Parameters)中也有实例描述DataContract.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 == 0lambda看起来完全像referenceLambda。但是后者适用于例如IQueryable<T>.Where,而前者则不适用;我已经在调试器的即时窗口中尝试过此操作。

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

最终解决方案

在考虑了问题的正确答案之后(以下两个);一个简短的问题,一个带有代码的问题得以解决;我将代码和一些重要说明放在一个单独的答案中,以防止这个漫长的问题变得更长。

感谢大家的回答和评论!


阅读 227

收藏
2020-05-19

共1个答案

一尘不染

似乎您在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声明的实例具有相同的实例(不仅仅是相同的类型和名称)。在这种情况下,我曾经遇到过问题,尽管我认为结果是我无法创建表达式,但它只会引发异常。无论如何,您都可以尝试重用参数实例,以查看是否有帮助。

2020-05-19