前文介绍了C#中表达式树的基本知识,在实际中,表达式树有很多用法,这里举几个例子,说明如何使用表达式树构建动态查询,从而扩展LINQ的查询方法。

  在LINQ中,只要数据源实现了IQuerable<T>接口,表达式树就可以用来表示结构化查询。比如,LINQ提供了用来查询关系数据源的IQueryable<T>接口的实现,C#编译器在执行这类数据源查询时,会在运行时生成表达式树,然后,查询会遍历表达式树的数据结构,然后将其转换成针对特定数据源的合适的查询语言。

   下面的几个例子演示了如何使用表达式树动态生成查询。

Example 1:动态生成Where和OrderBy


    这个例子是MSDN上的例子,平常在C#中我们一般直接写LINQ代码,比如下面这个:

companies.Where(company => (company.ToLower() == "coho winery" || company.Length > 16))
         .OrderBy(company => company)

   对集合进行查询,然后排序等。这个时固定化了数据源,在有些时候,我们需要把这个查询“泛型化”,这就需要能够动态构造查询的能力,恰好我们可以使用表达式树,动态构建查询。

   在System.Linq.Expression命名空间下有一些工厂方法能够用来表示各种查询,从而来组合出上述的查询条件。首先构造出一个表达式树,然后将表达式树传给要查询数据的CreateQuery<IElement>(Expression)方法。

//数据源
string[] companies = { "Consolidated Messenger", "Alpine Ski House", "Southridge Video", "City Power & Light",
                   "Coho Winery", "Wide World Importers", "Graphic Design Institute", "Adventure Works",
                   "Humongous Insurance", "Woodgrove Bank", "Margie's Travel", "Northwind Traders",
                   "Blue Yonder Airlines", "Trey Research", "The Phone Company",
                   "Wingtip Toys", "Lucerne Publishing", "Fourth Coffee" };

IQueryable<string> queryableData = companies.AsQueryable();

//company 参数
ParameterExpression pe = Expression.Parameter(typeof(string), "company");

// ***** 开始构造 Where(company => (company.ToLower() == "coho winery" || company.Length > 16)) *****  

//构造表达式 company.ToLower() == "coho winery" 
Expression left = Expression.Call(pe, typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
Expression right = Expression.Constant("coho winery");
Expression e1 = Expression.Equal(left, right);

//构造表达式 company.Length > 16
left = Expression.Property(pe, typeof(string).GetProperty("Length"));
right = Expression.Constant(16, typeof(int));
Expression e2 = Expression.GreaterThan(left, right);

//构造上述两个表达式的或
// '(company.ToLower() == "coho winery" || company.Length > 16)'.
Expression predictBody = Expression.OrElse(e1, e2);

//构造where表达式  'queryableData.Where(company => (company.ToLower() == "coho winery" || company.Length > 16))'  
MethodCallExpression whereCallExpression = Expression.Call(
    typeof(Queryable),
    "Where",
    new Type[] { queryableData.ElementType },
    queryableData.Expression,
    Expression.Lambda<Func<string, bool>>(predictBody, new ParameterExpression[] { pe }));
// ***** Where 部分构造完成 ***** 

// *****开始构造 OrderBy(company => company) *****  
// 构造表达式树,表示  'whereCallExpression.OrderBy(company => company)'  
MethodCallExpression orderByCallExpression = Expression.Call(
    typeof(Queryable),
    "OrderBy",
    new Type[] { queryableData.ElementType, queryableData.ElementType },
    whereCallExpression,
    Expression.Lambda<Func<string, string>>(pe, new ParameterExpression[] { pe }));
//***** OrderBy 构造完成 *****

//创建查询
IQueryable<string> results = queryableData.Provider.CreateQuery<string>(orderByCallExpression);

foreach (string company in results)
{
    Console.WriteLine(company);
}

  上面代码中,在传递到Queryable.Where方法中,使用了固定数量的表达式,在实际中,可以动态根据用户输入,来合并各种查询操作。

Example 2: 扩展in查询


   LINQ中,并没有提供In的操作,比如要查询xx 是否在["xx1","xx2"...]中时,需要手动编写SQL使用in,并将array拼成分割的字符串。如果使用LINQ,则可以变成xx==xx1,或者xx==xx2,实现如下:

public static Expression<Func<TElement, bool>> BuildWhereInExpression<TElement, TValue>(
 Expression<Func<TElement, TValue>> propertySelector, IEnumerable<TValue> values)
{
    ParameterExpression p = propertySelector.Parameters.Single();
    if (!values.Any())
        return e => false;

    var equals = values.Select(value => (Expression)Expression.Equal(propertySelector.Body,
        Expression.Constant(value, typeof(TValue))));
    var body = equals.Aggregate<Expression>((accumulate, equal) => Expression.Or(accumulate, equal));

    return Expression.Lambda<Func<TElement, bool>>(body, p);
}

public static IQueryable<TElement> WhereIn<TElement, TValue>(this IQueryable<TElement> source,
    Expression<Func<TElement, TValue>> propertySelector, params TValue[] values)
{
    return source.Where(BuildWhereInExpression(propertySelector, values));
}

    比如上例中,我们要判断在集合中是否存在以下数据,则可以直接使用where in

string[] companies = { "Consolidated Messenger", "Alpine Ski House", "Southridge Video", "City Power & Light",
                   "Coho Winery", "Wide World Importers", "Graphic Design Institute", "Adventure Works",
                   "Humongous Insurance", "Woodgrove Bank", "Margie's Travel", "Northwind Traders",
                   "Blue Yonder Airlines", "Trey Research", "The Phone Company",
                   "Wingtip Toys", "Lucerne Publishing", "Fourth Coffee" };

string[] t = new string[] { "Consolidated Messenger", "Alpine Ski House 1" };
 
IQueryable<string> results = companies.AsQueryable().WhereIn(x => x, t);
foreach (string company in results)
{
    Console.WriteLine(company);
}

Example 3:封装分页逻辑


   在很多地方都会用到分页,所以我们可以把查询排序跟分页封装到一起,这样用的时候就很方便,这里针对查询和排序,新建一个实体对象。

public class QueryOptions
{
    public int CurrentPage { get; set; } = 1;
    public int PageSize { get; set; } = 10;
    public string OrderPropertyName { get; set; }
    public bool DescendingOrder { get; set; }

    public string SearchPropertyName { get; set; }
    public string SearchTerm { get; set; }
}

   上面这个对象,包含了分页相关设置,以及查询和排序字段及属性。

    将排序逻辑封装在了PageList对象中,该对象继承自List,每次分页就返回分页后的数据放在PageList中。

public class PagedList<T> : List<T>
{
    public int CurrentPage { get; set; }
    public int PageSize { get; set; }
    public int TotalPages { get; set; }
    public QueryOptions Options { get; set; }
    public bool HasPreviousPage => CurrentPage > 1;
    public bool HasNextPage => CurrentPage < TotalPages;

    public PagedList(IQueryable<T> query, QueryOptions o)
    {
        CurrentPage = o.CurrentPage;
        PageSize = o.PageSize;
        Options = o;
        if (o != null)
        {
            if (!string.IsNullOrEmpty(o.OrderPropertyName))
            {
                query = Order(query, o.OrderPropertyName, o.DescendingOrder);
            }

            if (!string.IsNullOrEmpty(o.SearchPropertyName) && !string.IsNullOrEmpty(o.SearchTerm))
            {
                query = Search(query, o.SearchPropertyName, o.SearchTerm);
            }
        }
        TotalPages = query.Count() / PageSize;
        AddRange(query.Skip((CurrentPage - 1) * PageSize).Take(PageSize));
    }

    private IQueryable<T> Search(IQueryable<T> query, string searchPropertyName, string searchItem)
    {
        var parameter = Expression.Parameter(typeof(T), "x");
        var source = searchPropertyName.Split(".").Aggregate((Expression)parameter, Expression.Property);
        var body = Expression.Call(source, "Contains", Type.EmptyTypes, Expression.Constant(searchItem, typeof(string)));
        var lambda = Expression.Lambda<Func<T, bool>>(body, parameter);
        return query.Where(lambda);
    }

    private IQueryable<T> Order(IQueryable<T> query, string orderPropertyName, bool descendingOrder)
    {
        var parameter = Expression.Parameter(typeof(T), "x");
        var source = orderPropertyName.Split(".").Aggregate((Expression)parameter, Expression.Property);
        var lambda = Expression.Lambda(typeof(Func<,>).MakeGenericType(typeof(T), source.Type), source, parameter);
        return typeof(Queryable).GetMethods().Single(method =>
                method.Name == (descendingOrder ? "OrderByDescending" : "OrderBy")
                && method.IsGenericMethodDefinition
                && method.GetGenericArguments().Length == 2
                && method.GetParameters().Length == 2)
            .MakeGenericMethod(typeof(T), source.Type)
            .Invoke(null, new object[] { query, lambda }) as IQueryable<T>;
    }
}

   可以看到,在where和order部分,我们通过查询表达式,动态构造了这两部分的逻辑。使用的时候,非常方便:

public PagedList<Category> GetCategories(QueryOptions options)
{
   return new PagedList<Category>(context.Categories, options);
}

    这里context.Categories时DbSet集合,QueryOptions是用户传入的分页选项。每次输入查询排序及分页选项,就把分好的放在PageList里面返回。

Example 4 简化数据绑定


   在Windows Form中,所有的控件都继承自Control基类,它有一个DataBindings属性,可以用来绑定对象,这样就能自动更新。通常,在需要给一些对象赋值,或者保存空间里设置的值到配置文件时,我们可以使用数据绑定,而不是手动的去一个一个的赋值。但DataBindings的原型方法Add为如下,比如我要将某个空间的值绑定到配置对象里,代码如下:

textBoxCustomerName.DataBindings.Add("Text", bindingSource, "CustomerName");

  上述代码将TextBox控件的Text属性跟bingSource对象的CustomerName属性进行了绑定,可以看到以上代码有很多地方需要手写字符串:TextBox控件的属性“Text”,bingSource对象的“CustomerName”属性,非常不友好,而且容易出错,一看这种需要手打字符串的地方,就是"Bad Smell",需要优化。。

    可以感觉到,如果我们直接能像在Visual Studio里面那样,直接使用对象.属性的方式来赋值,比如这里直接用textBoxCustomerName.Text,和bindingSource.CustomerName,来替代两个手动输入的地方,整个体验就要好很多,比如,如果将上述代码写为下面的形式:

dataSource.CreateBinding(textBoxCustomerName, ctl => ctl.Text, data => data.Name);

  就要好很多。其实现方式为,我们先定义一个绑定的类,如下:

public class FlexBindingSource<TDataSource>
{
    private BindingSource _winFormsBindingSource;

    /// <summary>
    /// 
    /// </summary>
    /// <param name="source">object entity binding to the control</param>
    public FlexBindingSource(TDataSource source)
    {
        _winFormsBindingSource = new BindingSource();
        _winFormsBindingSource.DataSource = source;
    }

    /// <summary>
    /// Creates a DataBinding between a control property and a datasource property
    /// </summary>
    /// <typeparam name="TControl">The control type, must derive from System.Winforms.Control</typeparam>
    /// <param name="controlInstance">The control instance on wich the databinding will be added</param>
    /// <param name="controlPropertyAccessor">A lambda expression which specifies the control property to be databounded (something like textboxCtl => textboxCtl.Text)</param>
    /// <param name="datasourceMemberAccesor">A lambda expression which specifies the datasource property (something like datasource => datasource.Property) </param>
    /// <param name="dataSourceUpdateMode">See control.DataBindings.Add method </param>
    public void CreateBinding<TControl>(TControl controlInstance, Expression<Func<TControl, object>> controlPropertyAccessor, Expression<Func<TDataSource, object>> datasourceMemberAccesor, DataSourceUpdateMode dataSourceUpdateMode = DataSourceUpdateMode.OnValidation)
        where TControl : Control
    {
        string controlPropertyName = FlexReflector.GetPropertyName(controlPropertyAccessor);
        string sourcePropertyName = FlexReflector.GetPropertyName(datasourceMemberAccesor);
        controlInstance.DataBindings.Add(controlPropertyName, _winFormsBindingSource, sourcePropertyName, true, dataSourceUpdateMode);
    }
}

    这里面,构造函数里面的source,就是需要绑定的实体对象;在CreateBinding方法中,我们通过在FlexReflector在表达式中获取待绑定的字符串,然后调用传进去的Control对象的绑定方法来实现绑定。FlexReflector方法内容如下:

private static MemberExpression GetMemberExpression(Expression selector)
{
    LambdaExpression lambdaExpression = selector as LambdaExpression;
    if (lambdaExpression == null)
    {
        throw new ArgumentNullException("The selector is not a valid lambda expression.");
    }
    MemberExpression memberExpression = null;
    if (lambdaExpression.Body.NodeType == ExpressionType.Convert)
    {
        memberExpression = ((UnaryExpression)lambdaExpression.Body).Operand as MemberExpression;
    }
    else if (lambdaExpression.Body.NodeType == ExpressionType.MemberAccess)
    {
        memberExpression = lambdaExpression.Body as MemberExpression;
    }
    if (memberExpression == null)
    {
        throw new ArgumentException("The property is not a valid property to be extracted by a lambda expression.");
    }
    return memberExpression;
}

    使用方法很简单,首先定义一个绑定源的实例。

private FlexBindingSource<CustomerInfo> dataSource;
dataSource = new FlexBindingSource<CustomerInfo>(customerInstance);

然后就可以使用CreateBinding方法直接绑定了。

dataSource.CreateBinding(textBoxCustomerName, ctl => ctl.Text, data => data.Name);

 

参考资料

  1. https://www.codeproject.com/articles/38772/safe-winforms-databinding
  2. https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.control.databindings?view=netcore-3.1
  3. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/how-to-use-expression-trees-to-build-dynamic-queries
  4. https://www.cnblogs.com/JimmyZheng/archive/2012/02/23/2364154.html