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

组合表达式以动态追加 LINQ where 子句中的条件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (20投票s)

2015年4月12日

CPOL

5分钟阅读

viewsIcon

66154

downloadIcon

598

本文展示了在构建搜索功能时,如何组合 LINQ 表达式以动态追加 where 子句条件。

引言

在LINQ的`.Where`方法中动态附加`AND`或`OR`条件子句并不是一件很直接的事情。本文将介绍一种方法来轻松实现这一点。

背景

你必须写过,我也必须写过,我们都必须写过。它被写得太多了,以至于几乎成了一个开发者的成年礼。给我做一个页面(或表单),能够搜索某个数据集,并提供特定的文本框让我按“<插入字段列表>”进行搜索。给我选择匹配所有或任何我指定的条件,如果我没有提供任何条件,则返回所有搜索结果。

然后我们来看代码。

如果提供了名字和姓氏的搜索条件,我们想要写出的代码看起来是这样的:

var results = people.Where(p => p.FirstName.Like(FirstNameCriteria) || 
p.LastName.Like(LastNameCriteria));

但如果姓氏没有被指定呢?如果有其他十个条件呢?如果没有任何条件被指定呢?如果实体对象的第一个名字是`null`而不是空的`string`呢?这样查询就会变得困难得多。

我们可以通过对`AND`操作的结果集进行交集,对`OR`操作的结果集进行并集来解决这个问题,但这样的话,当我们的查询执行时,我们会得到非常糟糕的性能,因为我们多次搜索数据集,而实际上只需要搜索一次。

这是我在多个项目上不得不多次解决的问题,我认为它的实现方式足够独特,值得分享。

代码

这个例子有一些数据支撑会更容易理解,所以我从这个维基百科文章中收集了所有美国总统、他们的任期以及政治党派的列表。这为我们提供了一个多样化的数据集,以及多个可以查询的属性。

此外,我还提供了ASP.NET MVC、WPF和Web API的示例。Web API是最简单的例子,所以我将在本文中使用它。至于ASP.NET MVC和WPF的实现,我留给你们自行练习。

让我们先来构建我们的扩展方法,它们将作为后续所有内容的基础。

public static class ObjectExtensions
{
    public static String ToStringInstance(this Object obj)
    {
        if(obj == null)
        { return String.Empty;}

        return obj.ToString();
    }
}

public static class StringExtensions
{
    public static Boolean HasValue(this String str)
    {
        return String.IsNullOrWhiteSpace(str) == false;
    }

    public static Boolean Like(this String str, String other)
    {
        return str.ToUpper().Contains(other.ToUpper());
    }        
}

结合使用`.ToStringInstance`和`.HasValue`方法,我们可以获得构建查询所需的语法糖,以便确定用户是否指定了任何条件。

我们还需要一种方法来将搜索条件属性与搜索模型中的其他所有内容(搜索操作符、非搜索条件的视图模型属性等)区分开来。让我们添加一个属性来标识这一点。

[AttributeUsage(AttributeTargets.Property, AllowMultiple=false)]
public class SearchCriteriaAttribute : Attribute
{}

让这一切都能工作的关键在于`.NET 4.0`中引入的ExpressionVisitor。本质上,我们在这里使用`ExpressionVisitor`来允许给定表达式的参数从左边的表达式流向右边的表达式。

例如,如果我们有两个表达式 `(foo) => foo == bar` 和 `(foo) => foo == baz`,我们希望使用`ExpressionVisitor`来生成一个表达式,使得在第一个表达式中使用的`foo`的值与第二个表达式中使用的值相同。

public class ExpressionParameterReplacer : ExpressionVisitor
{
    private IDictionary<ParameterExpression, ParameterExpression> ParameterReplacements { get; set; }

    public ExpressionParameterReplacer
    (IList<ParameterExpression> fromParameters, IList<ParameterExpression> toParameters)
    {
        ParameterReplacements = new Dictionary<ParameterExpression, ParameterExpression>();

        for(int i = 0; i != fromParameters.Count && i != toParameters.Count; i++)
        { ParameterReplacements.Add(fromParameters[i], toParameters[i]); }
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        ParameterExpression replacement;

        if(ParameterReplacements.TryGetValue(node, out replacement))
        { node = replacement; }

        return base.VisitParameter(node);
    }           
}

为了创建广泛搜索,我们需要一种方法将我们的表达式`OR`在一起。

/// <summary>
/// Creates a lambda expression that represents a conditional OR operation
/// </summary>
/// <param name="left">An expression to set the left property of the binary expression</param>
/// <param name="right">An expression to set the right property of the binary expression</param>
/// <returns>A binary expression that has the node type property equal to OrElse, 
/// and the left and right properties set to the specified values</returns>
public static Expression<Func<T, Boolean>> 
OrElse(Expression<Func<T, Boolean>> left, Expression<Func<T, Boolean>> right)
{
    Expression<Func<T, Boolean>> combined = Expression.Lambda<Func<T, Boolean>>(
        Expression.OrElse(
            left.Body,
            new ExpressionParameterReplacer(right.Parameters, left.Parameters).Visit(right.Body)
            ), left.Parameters);

    return combined;
}

我们在这里所做的是生成一个全新的lambda表达式,它结合了左侧和右侧的表达式。

如果我们说`Expression.OrElse(left, right)`,那将相当于说:

(foo=> foo == bar) || (foo => foo == baz)

相反,我们将构建一个全新的lambda,并得到以下结果:

foo => (foo == bar || foo == baz)

为了创建更精确的搜索,我们将构建`AND`表达式,并使用非常相似的代码。

/// <summary>
/// Creates a lambda expression that represents a conditional AND operation
/// </summary>
/// <param name="left">An expression to set the left property of the binary expression</param>
/// <param name="right">An expression to set the right property of the binary expression</param>
/// <returns>A binary expression that has the node type property equal to AndAlso, 
/// and the left and right properties set to the specified values</returns>
public static Expression<Func<T, Boolean>> 
AndAlso(Expression<Func<T, Boolean>> left, Expression<Func<T, Boolean>> right)
{
    Expression<Func<T, Boolean>> combined = Expression.Lambda<Func<T, Boolean>>(
        Expression.AndAlso(
            left.Body,
            new ExpressionParameterReplacer(right.Parameters, left.Parameters).Visit(right.Body)
            ), left.Parameters);

    return combined;
}

既然我们已经具备了组合表达式的能力,我们就需要一个搜索模型。让我们先添加所有允许用户搜索总统的条件,以及用户指定的搜索操作符。

我们的搜索操作符将允许用户指定是显示匹配所有条件的还是匹配任何条件的搜索结果。值应仅限于“ANY”或“ALL”。

public class SearchModel
{    
    [SearchCriteria]
    public String FirstName { get; set; }

    [SearchCriteria]
    public String LastName { get; set; }

    [SearchCriteria]
    public String PresidentNumber { get; set; }

    [SearchCriteria]
    public Nullable<DateTime> StartDate { get; set; }

    [SearchCriteria]
    public Nullable<DateTime> EndDate { get; set; }

    [SearchCriteria]
    public String TermCount { get; set; }

    [SearchCriteria]
    public Nullable<Boolean> Alive { get; set; }
        
    public String SearchOperator { get; set; }
}

您会注意到,我们允许用户搜索的条件都用我们之前构建的`[SearchCriteria]`属性进行了装饰。这允许我们在搜索模型上实现以下方法:

/// <summary>
/// Returns true, if this view model has criteria to search against
/// </summary>        
public Boolean HasCriteria()
{
    //get the properties of this object
    var properties = this.GetType().GetProperties(BindingFlags.Public | 
    BindingFlags.Instance | BindingFlags.FlattenHierarchy);
    var searchProperties = properties.Where(p => p.CustomAttributes.Select
    (a => a.AttributeType).Contains(typeof(SearchCriteriaAttribute)));

    return searchProperties.Any(sp => sp.GetValue(this).ToStringInstance().HasValue());
}

我们的`HasCriteria`方法使我们能够快速判断是否需要经过注入`where`子句的繁琐过程。

我们还需要一种方法来根据用户指定的内容附加`AND`和`OR`子句,所以让我们也构建它。

private Expression<Func<Model.President, Boolean>> AppendExpression
(Expression<Func<Model.President, Boolean>> left, 
Expression<Func<Model.President, Boolean>> right)
{
    Expression<Func<Model.President, Boolean>> result;

    switch (SearchOperator)
    {
        case "ANY":

            //the initial case starts off with a left expression as null. If that's the case,
            //then give the short-circuit operator something to trigger on for the right expression
            if (left == null)
            { left = model => false; }

            result = ExpressionExtension<Model.President>.OrElse(left, right);
            break;
        case "ALL":

            if (left == null)
            { left = model => true; }

            result = ExpressionExtension<Model.President>.AndAlso(left, right);
            break;
        default:
            throw new InvalidOperationException();
    }

    return result;
}

如果您还记得构建真值表,您就会知道从`false`开始会让`OrElse`继续求值,而从`true`开始会让`AndAlso`继续求值。

另外,您应该注意到这里我们处理的是`Expression<Func<T>>`,而不是`Func<T>`。这是一个重要的区别,因为`Func<T>`是指向方法实现的指针,而`Expression<Func<T>>`是描述`Func<T>`如何实现的结构。

如果打个比方,`Expression<Func<T>>`就像原始源代码,而`Func<T>`是其编译后的二进制等价物。

因此,通过传递表达式而不是委托本身,我们可以在执行之前操作查询,这样当我们解析结果集时,我们就能将通过网络传输的数据限制在用户真正想要的内容。

现在是时候实际构建将作为我们`where`子句的表达式了。让我们看看它是什么样的:

public Expression<Func<Model.President, Boolean>> ToExpression()
{
    Expression<Func<Model.President, Boolean>> result = null;

    int presidentNumberIntValue = 0;
    if(PresidentNumber.HasValue() && Int32.TryParse(PresidentNumber, out presidentNumberIntValue) && presidentNumberIntValue > 0)
    {
        Expression<Func<Model.President, Boolean>> expr = model => model.PresidentNumber == presidentNumberIntValue;
        result = AppendExpression(result, expr);
    }

    if (FirstName.HasValue())
    {
        Expression<Func<Model.President, Boolean>> expr = model => model.FirstName.Like(FirstName);
        result = AppendExpression(result, expr);
    }

    if (LastName.HasValue())
    {
        Expression<Func<Model.President, Boolean>> expr = model => model.LastName.Like(LastName);
        result = AppendExpression(result, expr);
    }

    if (StartDate.HasValue)
    {
        Expression<Func<Model.President, Boolean>> expr = model => model.TookOffice >= StartDate;
        result = AppendExpression(result, expr);
    }

    if (EndDate.HasValue)
    {
        Expression<Func<Model.President, Boolean>> expr = model => model.LeftOffice <= EndDate;
        result = AppendExpression(result, expr);
    }

    if(Alive.HasValue)
    {
        Expression<Func<Model.President, Boolean>> expr = model => model.IsAlive == Alive;
        result = AppendExpression(result, expr);
    }

    var termCounntIntValue = 0;
    if (TermCount.HasValue() && Int32.TryParse
	(TermCount, out termCounntIntValue) && termCounntIntValue > 0)
    {
        Expression<Func<Model.President, Boolean>> expr = 
		model => model.Terms.Count() == termCounntIntValue;
        result = AppendExpression(result, expr);
    }

    return result;
}

如您所见,对于用户指定的每个搜索条件,我们将构建一个仅搜索该条件的表达式,然后将其附加到我们的总体表达式中。

现在让我们跳转到`PresidentsController`,并构建出API。

public class PresidentsController : ApiController
{
    public IHttpActionResult Get(String firstName ="", 
        String lastName = "", 
        String presidentNumber = "", 
        DateTime? startDate = null, 
        DateTime? endDate = null, 
        String termCount = "",
        Boolean? IsAlive = null,
        String searchOperator = "ANY")
    {
        var searchModel = new SearchModel
        {
            FirstName = firstName,
            LastName = lastName,
            PresidentNumber = presidentNumber,
            StartDate = startDate,
            EndDate = endDate, 
            TermCount = termCount,
            Alive = IsAlive,
            SearchOperator = searchOperator
        };

        var presidents = PresidentRepository.GetAllPresidents();

        if (searchModel.HasCriteria())
        {
            presidents = presidents.Where(searchModel.ToExpression());
        }

        return Ok(presidents);
    }
}

这很简单——接受用户的条件来构建模型,然后如果指定了任何条件,就使用我们构建的表达式来限制结果,否则,就返回所有结果。

您可以通过启动它并搜索来尝试:

 
名字以“George”开头的总统。   1800年至1900年之间的任期为一届的总统。

或者您能想到的任何其他搜索条件的组合。

致谢

历史

  • 2015-04-12:首次发布
© . All rights reserved.