1. 程式人生 > 程式設計 >C#用表示式樹構建動態查詢的方法

C#用表示式樹構建動態查詢的方法

前文介紹了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,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,params TValue[] values)
{
  return source.Where(BuildWhereInExpression(propertySelector,values));
}

比如上例中,我們要判斷在集合中是否存在以下資料,則可以直接使用where in

string[] companies = { "Consolidated Messenger","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,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,data => data.Name);

以上就是C#用表示式樹構建動態查詢的方法的詳細內容,更多關於c# 構建動態查詢的資料請關注我們其它相關文章!