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# 構建動態查詢的資料請關注我們其它相關文章!