65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 表达式的修改

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2014年12月5日

CPOL

9分钟阅读

viewsIcon

19617

downloadIcon

102

在本文中,我将向您展示如何修改用于创建 Entity Framework 查询的表达式。

引言

我想我们许多人以以下方式编写简单的 Web API REST 服务:使用服务方法的参数来构造 Entity Framework LINQ 查询并返回结果。但有时,我们不直接返回 EF 实体对象,而是将它们重新打包成其他对象。

public PersonInfo[] GetPersons()
{
    return _dbContext.Persons.Select(p => new PersonInfo { Id = p.PersonId, Name = p.Name }).ToArray();
}

重新打包的原因可能有很多

  • 也许您只想将 EF 实体对象的部分属性发送给消费者
  • 也许您为消费者构建的对象来自多个 EF 实体对象
  • 也许您想将 EF 实体对象的一些属性按某些其他类表示的组进行排列
  • 等等。

在任何情况下,最终您都可能面临过滤问题。您的方法可以返回数百或数千个对象,但消费者可能只想要其中一部分满足某些条件的对象。如何实现这种过滤?

当然,可能存在简单的情况。如果消费者只想按名称过滤人员,您可以编写如下代码:

public PersonInfo[] GetPersons(string startOfName)
{
    return _dbContext.Persons.Where(p => p.Name.StartsWith(startOfName))
        .Select(p => new PersonInfo { Id = p.PersonId, Name = p.Name }).ToArray();
}

但通常,消费者可能希望通过 `PersonInfo` 类的任何属性从您的服务中过滤数据。此外,过滤条件可能多种多样。假设 `PersonInfo` 类有一个 `Age` 属性。在这种情况下,以下是一些过滤变体:

personInfo.Age = 30
personInfo.Age < 50
personInfo.Age >= 20 && personInfo.Age < 60

如何支持所有这些变体?一种方法是创建特殊的过滤器类

public class FilterCondition
{
    public string PropertyName { get; set; }
    public OperationType Operation { get; set; }
    public object Value { get; set; }
}

public enum OperationType
{
    Equals,
    Less,
    Greater,
}

在这种情况下,消费者可以这样调用我们的服务:

service.GetPersons(new []{ 
        new FilterCondition { PropertyName = "Age", Operation = OperationType.Less, Value = 50 } 
    });

服务方法将如下所示:

public PersonInfo[] GetPersons(FilterCondition[] conditions)
{
    var persons = _dbContext.Persons;

    foreach(var condition in conditions)
    {
         var value = condition.Value;
         if(condition.PropertyName == "Age")
         {
             switch(condition.Operation)
             {
                 case OperationType.Less:
                     persons = persons.Where(p => p.Age < (int)value);
                     break;
                 ....
             }
         }
         ....
    }
   
    return persons.Select(p => new PersonInfo { Id = p.PersonId, Name = p.Name }).ToArray();
}

在我看来,这种实现有几个严重的缺点

  • 首先,服务方法 `GetPersons` 的实现过于复杂。我们应该为每个我们想要过滤的属性编写单独的案例。当然,有时我们可以通过使用 Reflection 来改善这种情况。但也有复杂的情况。例如,`PersonInfo` 类的属性 `Location.Town` 可以从 `Person` EF 实体类的属性 `Address.City` 中填充。
  • 如果我们要添加对某些新属性的过滤,我们必须更改 `GetPersons` 方法。
  • `OperationType enum` 中的操作列表可能不完整。总会有消费者想要应用另一种类型的过滤器。在这种情况下,我们将不得不更改 `GetPersons` 方法以支持新操作。
  • 对于消费者来说,创建 `FilterCondition` 对象列表不是很方便。
  • 重命名 `Person` 和 `PersonInfo` 类的属性并不容易。它们与 `FilterCondition` 类的 `PropertyName` 属性的值绑定,而 `PropertyName` 只是一个 `string`。因此,重命名后必须手动检查 `PropertyName` 属性是否包含正确的值。

解决方案

那么,什么是理想的解决方案呢?如果消费者能够像这样编写任何过滤表达式呢?

var persons = service.GetPersons(p => p.Name.StartsWith("D") && p.Age < 30);

我认为这是一种非常方便的方法。消费者可以使用任何函数编写任何过滤器,编译器将检查属性名称及其类型的正确性,Visual Studio 将在需要时自动重命名属性。

但这个解决方案可行吗?嗯,部分可行。

如您所知,EF 方法(如 `Where`)可以接受 `Expression` 对象作为参数。这些 `Expression` 对象表示我们代码的对象模型。任何函数都可以转换为 `Expression` 对象

Expression<Func<PersonInfo, bool>> expr = p => p.Name.StartsWith("D") && p.Age < 30;

我们可以这样定义 `GetPersons` 方法:

public PersonInfo[] GetPersons(Expression<Func<PersonInfo, bool>> filter)

并在内部使用此 `filter` 表达式。

但存在 2 个主要障碍。

障碍 1:传输

如果我们有 Web API REST 服务,那么我们应该以某种方式将 `Expression` 对象从消费者传输到服务。不幸的是,`Expression` 类不可序列化。但是有一些第三方库可以做到这一点(expressiontree.codeplex.com)。此外,为了能够在服务侧反序列化此对象,此表达式中的所有构造都应在服务侧已知。这意味着消费者不能像这样编写他们的过滤器:

Expression<Func<PersonInfo, bool>> expr = p => p.Age < GetDesiredAge();

因为服务侧不知道 `GetDesiredAge` 函数。相反,应该使用类似以下的内容:

var age = GetDesiredAge();
Expression<Func<PersonInfo, bool>> expr = p => p.Age < age;

在筛选器表达式中使用类也有类似的限制。

障碍 2:重写

还有一个障碍,在我看来比前面提到的更重要。如您所见,我们有一个 `Expression>` 对象,而 Entity Framework 需要 `Expression>` 对象。此时,表达式重写就发挥作用了。您可以从本文下载表达式重写器的源代码。接下来,我们将讨论它是如何工作的。

重写的实现

粗略地说,我们需要一个函数,它接受 `Expression>` 并返回 `Expression>`。但要做到这一点,我们需要一些额外的信息。我们需要知道 `PersonInfo` 类的 `Location.Town` 属性应该替换为 `Person` 类的 `Address.City` 属性,等等。

var rewriter = new ExpressionRewriter();
rewriter.ChangeArgumentType<PersonInfo>().To<Person>();
rewriter.ChangeProperty<PersonInfo>(pi => pi.Status).To<Person>(p => p.FamilyStatus);
rewriter.ChangeProperty<PersonInfo>(pi => pi.Country).To<Person>(p => p.Address.Country);
rewriter.ChangeProperty<PersonInfo>(pi => pi.Location.Town).To<Person>(p => p.Address.City);

如您所见,此信息可以使用 `ChangeProperty` 方法提供。此外,`ChangeArgumentType` 方法表示我们表达式所代表函数参数中的 `PersonInfo` 类应替换为 `Person` 类。

好的,现在我们有了所有必需的信息,可以开始重写表达式了。实际上,“重写”一词并不完全正确。`Expression` 类的所有子孙都是只读的,不能修改。所以我们需要基于现有表达式创建一个新表达式。幸运的是,Microsoft 提供了 `ExpressionVisitor` 类,这使得这项工作更容易。您可以从该类继承您的表达式更改器

class RewritingVisitor : ExpressionVisitor

您将初始表达式传递给 `Visit` 方法,它会返回更改后的表达式。如果您不覆盖 `ExpressionVisitor` 的任何方法,则返回的表达式将保持不变。但我们会覆盖。

更改过滤函数的类型

如输入中所述,我们有一个 `Expression>`,但我们需要 `Expression>`。因此,`Func` 必须更改为 `Func`。这在 `VisitLambda` 方法的重写中完成

protected override Expression VisitLambda<T>(Expression<T> node)
{
    var body = Visit(node.Body);
    var parameters = VisitAndConvert(node.Parameters, "VisitLambda");

    if (body == node.Body && parameters == node.Parameters)
    { return node; }

    Type delegateType;

    var funcGenericTypes = new List<Type>(parameters.Select(p => p.Type));

    funcGenericTypes.Add(body.Type);

    var funcType = typeof(Func<>).Assembly.GetTypes()
            .Where(t => t.Name.StartsWith("Func`"))
            .Where(t => t.IsGenericType)
            .FirstOrDefault(t => t.GetGenericArguments().Length == funcGenericTypes.Count);

    if (funcType == null)
    {
        throw new InvalidOperationException("Can't find corresponding Func<> type");
    }

    delegateType = funcType.MakeGenericType(funcGenericTypes.ToArray());
    return Expression.Lambda(delegateType, body, parameters);
}

首先,我们重写方法的body和参数。如果在重写过程中它们没有改变,那么我们返回初始表达式。但如果它们改变了,那么我们为重写的表达式创建新的委托类型 `delegateType`,并为这个新函数返回新的表达式。

这里,我们可能有一个问题。我们假设输入表达式的类型是 `Expression<**Func<...>**>`。但通常情况下,它可能是 `Expression<**AnyDelegate**>`。很难理解 `AnyDelegate` 应该如何处理。因此,为了简单起见,也为了在 Entity Framework 中作为过滤表达式的有限使用,我们将坚持之前的假设。

那么我们如何改变函数的参数呢?

改变参数类型

这通过重写 `VisitParameter` 方法来完成。

protected override Expression VisitParameter(ParameterExpression node)
{
    if (_argumentTypeChanges.ContainsKey(node.Type))
    {
        if (_argumentSubstitutions.ContainsKey(node.Name))
        {
            return _argumentSubstitutions[node.Name];
        }
                
        var substitutionParameter = Expression.Parameter(_argumentTypeChanges[node.Type], node.Name);
        _argumentSubstitutions[node.Name] = substitutionParameter;
        return substitutionParameter;
    }

    return base.VisitParameter(node);
}

这里,我们有一个 `_argumentTypeChanges` 字典,其中包含参数的初始类型及其替换类型。此字典是使用 `ChangeArgumentType` 方法构建的。当参数类型在该字典中时,我们必须创建一个具有不同类型的新参数表达式。问题是,同一个参数可能在方法的正文中的许多地方使用。而在所有这些地方,都必须是 `ParameterExpression` 对象的相同实例。因此,当我们创建 `ParameterExpression` 对象的新实例时,我们会将其缓存到 `_argumentSubstitutions` 字典中,下次需要具有此名称的参数时,就从该缓存中获取。

唯一剩下的工作是重写属性序列(`.Location.Town -> .Address.City`)

重写属性序列

这部分工作在 `VisitMember` 方法的重写中完成

protected override Expression VisitMember(MemberExpression node)

此方法的实现由两部分组成。第一部分尝试使用通过 `ChangeProperty` 方法调用收集的信息来重写属性序列

var propertiesChange = _propertiesChanges.FirstOrDefault(pc => pc.SourceCorrespondsTo(node));
if (propertiesChange != null)
{
    Expression sequenceOrigin = propertiesChange.GetSequenceOriginExpression(node);
    Expression newSequenceOrigin = Visit(sequenceOrigin);
    return propertiesChange.GetNewPropertiesSequence(newSequenceOrigin);
}

首先,我们尝试理解应该重写哪些属性序列。如果找到此序列(`propertiesChange` 不为 `null`),我们处理它。例如,我们有函数

personInfo => personInfo.Location.Town.StartsWith("L")

我们想将属性序列 `。Location.Town` 替换为 `。Address.City`。我们首先找到此序列的起源(`sequenceOrigin`),即应用此序列的表达式。在这种情况下,它是参数 `personInfo`。然后,我们使用 `Visit` 方法处理此起源(在我们的例子中,`personInfo` 的类型将从 `PersonInfo` 更改为 `Person`)。然后,我们根据新的起源(`GetNewPropertiesSequence`)构造新的属性序列。

稍后我们将讨论 `propertiesChange` 对象的实现。现在,让我们考虑 `VisitMember` 方法的第二部分。假设 `PersonInfo` 和 `Person` 类都具有几个等效属性(例如 `Name`、`Age`、...),包含相同的信息。为每个这样的属性编写如下代码不是很方便

rewriter.ChangeProperty<PersonInfo>(pi => pi.Name).To<Person>(p => p.Name);

默认情况下处理这些属性会更好。这就是 `VisitMember` 方法的第二部分所做的工作。

Expression expression = Visit(node.Expression);
if (expression == node.Expression)
{
    return node;
}

if (expression.Type == node.Member.DeclaringType)
{
    return Expression.MakeMemberAccess(expression, node.Member);
}

var newMember = expression.Type.GetMember(node.Member.Name)
    .FirstOrDefault(m => m.MemberType == MemberTypes.Property || m.MemberType == MemberTypes.Field);
if (newMember == null)
{
    throw new InvalidOperationException(string.Format("Type '{0}' does not contain field 
                          or property '{1}'", expression.Type, node.Member.Name));
}

return Expression.MakeMemberAccess(expression, newMember);

`VisitMember` 方法中处理的 `MemberExpression` 对象包含 `System.Reflection.MemberInfo` 实例,用于指示应调用哪个成员。以前,此 `MemberInfo` 实例表示我们调用了 `PersonInfo` 类的 `Name` 属性。现在我们应该调用 `Person` 类的 `Name` 属性。这就是此代码的作用。它创建新的 `MemberInfo` 实例(`newMember`)和基于它的新的 `MemberExpression` 实例。

替换属性序列

最后但同样重要的是,让我们考虑如何用另一个属性序列(`.Address.City`)替换一个属性序列(`.Location.Town`)。一切都始于调用 `ChangeProperty`

rewriter.ChangeProperty<PersonInfo>(pi => pi.Location.Town).To<Person>(p => p.Address.City);

此时,创建了两个 `PropertiesSequence` 类型的对象:一个(`source`)用于 `pi => pi.Location.Town`,另一个(`target`)用于 `p => p.Address.City`。每个 `PropertiesSequence` 对象包含属性列表及其起源类型(应用此序列的对象)。因此对于源对象

properties = [ "Town", "Location" ]
originType = PersonInfo // type of pi

以及目标对象

properties = [ "Cities", "Address" ]
originType = Person // type of p

这两个 `PropertiesSequence` 类型的对象被打包到 `PropertiesChange` 对象中。该对象有几个辅助方法可以完成所需的工作。

首先,`SourceCorrespondsTo` 方法检查作为该方法参数传递的表达式是否确实与 `source` 对象中的属性序列相对应。这意味着表达式表示相同属性的相同调用序列,并且它们的来源具有与 `source` 对象中存储的来源类型相同的类型。

public bool SourceCorrespondsTo(Expression expression)
{
    expression = GetSequenceOriginExpression(expression);

    return expression != null && expression.Type == _source.SequenceOriginType;
}

public Expression GetSequenceOriginExpression(Expression expression)
{
    foreach (var propertyInfo in _source.Properties)
    {
        var memberExpression = expression as MemberExpression;
        if (memberExpression == null)
        { return null; }

        if (memberExpression.Member.Name != propertyInfo.Name)
        { return null; }
        if (memberExpression.Type != propertyInfo.ResultType)
        { return null; }

        expression = memberExpression.Expression;
    }

    return expression;
}

这里的 `GetSequenceOriginExpression` 方法返回属性序列的起源(如果它与 `source` 对象对应),否则返回 `null`。

让我提醒您这些方法在 `RewritingVisitor` 类的 `VisitMember` 方法中是如何使用的

var propertiesChange = _propertiesChanges.FirstOrDefault(pc => pc.SourceCorrespondsTo(node));
if (propertiesChange != null)
{
    Expression sequenceOrigin = propertiesChange.GetSequenceOriginExpression(node);
    Expression newSequenceOrigin = Visit(sequenceOrigin);
    return propertiesChange.GetNewPropertiesSequence(newSequenceOrigin);
}

我们找到其源属性序列与当前表达式对应的 `PropertiesChange` 对象。然后我们从当前表达式中获取此序列的起源,并使用 `Visit(sequenceOrigin)` 调用对其进行重写。最后,我们使用 `GetNewPropertiesSequence` 方法构造新的属性序列

public Expression GetNewPropertiesSequence(Expression sequenceOrigin)
{
    if (sequenceOrigin == null) throw new ArgumentNullException("sequenceOrigin");

    if(sequenceOrigin.Type != _target.SequenceOriginType)
        throw new ArgumentException("Type of rewritten properties sequence is incorrect.", 
                                    "sequenceOrigin");

    foreach (var propertyInfo in _target.Properties.Reverse())
    {
        var memberInfo = sequenceOrigin.Type.GetMember(propertyInfo.Name)
            .FirstOrDefault(m => m.MemberType == MemberTypes.Field || 
                            m.MemberType == MemberTypes.Property);
        if(memberInfo == null)
            throw new InvalidOperationException("Unable to create rewritten properties sequence");

        sequenceOrigin = Expression.MakeMemberAccess(sequenceOrigin, memberInfo);
    }

    return sequenceOrigin;
}

首先,它检查新来源的类型是否与 `target` 对象中存储的来源类型相同。然后它基于这个新来源重建目标属性序列的表达式。

结论

现在让我总结一下本文的结果。这里描述的方法允许我们的服务消费者使用他们编程语言中众所周知的语法编写过滤器。他们可以在他们的过滤器中使用许多标准函数和运算符。消费者可以在他们的过滤器中使用服务返回的类的任何属性。此外,编译器会检查过滤器代码的正确性,并且属性的重命名是安全操作。

但与此同时,这里也存在很多问题

  • 如果我们的服务提供了 .NET 客户端,则可以使用所描述的方法。从 JavaScript 中使用它不方便(如果可能的话)。
  • 从客户端到服务传输过滤表达式存在问题。
  • 仍然需要在相关类(`Person` 和 `PersonInfo`)发生某些更改时配置表达式重写器。
  • 一些需要更改结果类型的过滤表达式转换尚未实现。

我将本文中描述的工作视为概念验证,而不是一个现成的解决方案。我希望它能对您有所帮助,并且您能够改进它并根据您的需求进行调整。

历史

修订 日期 注释
1.0 04.12.2014 初始修订版
© . All rights reserved.