由淺入深表達式樹(三)
由淺入深表達式樹(完結篇)重磅打造
2018-03-142018-03-14 13:23:06閱讀 6140一個多月之後,由淺入深表達式系列的最後一篇終於要問世了。想對所有關注的朋友說聲:“對不起,我來晚了!” 希望最後一篇的內容對得起這一個月時間的等待。在學習完表示式樹的建立和遍歷之後,我們要利用它的特性來寫一個我們自己的Linq Provider。人家都有Linq to Amazon為什麼我們不能有Linq to cnblogs呢?今天我們就來一步一步的打造自己的Linq Provider,文章未尾已附上原始碼下載地址。如果對於表示式樹的建立和遍歷還是熟悉的話,建議先看前面兩篇:
建立表示式樹 http://www.cnblogs.com/jesse2013/p/expressiontree-part1.html
遍歷表示式樹 http://www.cnblogs.com/jesse2013/p/expressiontree-part2.html
更新:之前沒有描述清楚本篇部落格的意圖,導致很多朋友的誤解表示抱歉。本系列重在理解表示式目錄樹,以及Linq Provider。最後一篇是Linq Provider的實現,之所有會寫這麼多的程式碼去做一件簡單的事(拉取部落格園首頁文章列表)完全是為了有一個生動的例子去展示如何實現自己的Linq Provider。和我們專案中的三層架構,或者直接序列化到本地是沒有可比性的。
當然,表示式目錄樹以及Linq Provider的強大也遠非這個小小的Demo能體現得了的,如果你真正知道Linq Provider和表示式樹目錄樹是什麼,用來幹什麼的,也許你就能明白本篇部落格的意圖了。如果不瞭解的,建議讀完前面兩篇之後再做評論。因為你在自己不理解的情況下就直接去評論其它的領域,你就失去了一個瞭解它的機會。:)
實現目標
我們實現的目標就像Linq to SQL一樣,可以用Linq查詢語句來查詢資料,我們這裡面的資料用到了部落格園官方的Service去查詢到最新的釋出到首頁的部落格資訊。看下面的程式碼:
var provider = new CnblogsQueryProvider();
var queryable = new Query<Post>(provider);
var query =
from p in queryable
where p.Diggs >= 10 &&
p.Comments > 10 &&
p.Views > 10 &&
p.Comments < 20
select p;
var list = query.ToList();
作為實時訪問遠端的Service,我們還應該具體以下幾點要求:
- 延遲載入,即到最後使用的時候才真正的去請求資料
- 只返回需要的資料,不能把所有的資料全下載過來再到本地過濾,那就沒有意義了
最後實現的結果:
資料準備
根據部落格園公開的API顯示,獲取首頁文章列表非常容易,大家可以點下面的URL來體檢一把。我們最後給的引數是100000,當然真實返回肯定是沒有那麼多的,我們是希望把能夠取回來的都取回來。
http://wcf.open.cnblogs.com/blog/sitehome/recent/100000
點選完上面的URL之後呢,問題就來了,它只有一個引數。我並不能傳給它查詢條件,比如說根據標題來搜尋,或者根據評論數,瀏覽量來過濾。難道我的計劃就此要泡湯了麼,剛開始我很不開心,為什麼部落格園就不能提供靈活一點的Service呢?但是事實就是這樣,咋是程式設計師呀,需求擺在這,怎麼著還得實現是不?沒有辦法,我給它封裝了一層。在它的基礎上做了一個自己的Service。
封裝部落格園Service
我們如何在部落格園公開Service的基礎上加一層實現條件查詢呢?主要思路是這樣的:
- 為文章建立實體類(Post)
- 將部落格園Service返回的資料解析成Post的集合,我們可以加上自己的快取機制,可以採用1分鐘才到部落格園取一次資料
- 把我們上面建立的post集合當作資料庫,建立查詢Service
我們首先要做的就是為部落格園的部落格建立實體類。
public class Post
{
// Id
public int Id { get; set; }
// 標題
public string Title { get; set; }
// 釋出時間
public DateTime Published { get; set; }
// 推薦資料
public int Diggs { get; set; }
// 訪問人數
public int Views { get; set; }
// 評論資料
public int Comments { get; set; }
// 作者
public string Author { get; set; }
// 部落格連結
public string Href { get; set; }
// 摘要
public string Summary { get; set; }
}
接著,我們要將部落格園返回給我們的XML資料轉換成我們要的post集合,所以我們要用到Linq to XML。
_list = new List<CnblogsLinqProvider.Post>();
var document = XDocument.Load(
"http://wcf.open.cnblogs.com/blog/sitehome/recent/100000"
);
var elements = document.Root.Elements();
var result = from entry in elements
where entry.HasElements == true
select new CnblogsLinqProvider.Post
{
Id = Convert.ToInt32(entry.Elements()
.SingleOrDefault(x => x.Name.LocalName == "id").Value),
Title = entry.Elements()
.SingleOrDefault(x => x.Name.LocalName == "title").Value,
Published = Convert.ToDateTime(entry.Elements()
.SingleOrDefault(x => x.Name.LocalName == "published").Value),
Diggs = Convert.ToInt32(entry.Elements()
.SingleOrDefault(x => x.Name.LocalName == "diggs").Value),
Views = Convert.ToInt32(entry.Elements()
.SingleOrDefault(x => x.Name.LocalName == "views").Value),
Comments = Convert.ToInt32(entry.Elements()
.SingleOrDefault(x => x.Name.LocalName == "comments").Value),
Summary = entry.Elements()
.SingleOrDefault(x=>x.Name.LocalName=="summary").Value,
Href = entry.Elements()
.SingleOrDefault(x => x.Name.LocalName == "link")
.Attribute("href").Value,
Author = entry.Elements()
.SingleOrDefault(x => x.Name.LocalName == "author")
.Elements()
.SingleOrDefault(x => x.Name.LocalName == "name").Value
};
_list.AddRange(result);
然後就到了我們的查詢了,實際上我們有了IEnumrable<Post>的資料就可以直接在本地用Linq去查詢它了。但是這不是我們想要的,因為我們上面的步驟是把所有的資料一次性全部下載下來了,而不是根據我們的需求返回資料。另外我們這裡面是在部落格園Service的基礎上做一層封裝,實現通過Url直接查詢首頁的文章。為什麼要通過Url來查詢?因為我們最後會通過我們自己的LinqProvider將Linq查詢語句直接翻譯成Url這樣就能夠實現遠端的返回資料了。來看看我們對Url引數的定義:
- 標題中包括模式的文章:http://linqtocnblogs.cloudapp.net?Title=模式
- 訪問人數大於5並且評論大於10的文章 http://linqtocnblogs.cloudapp.net?MinViews=5&MinComments=10
利用JsonResult 返回json資料來建立我們的Service
作為Service,我們返回Json或者XML格式的資料都是可以的。當然實現這個需求的方法有很多種,我們這裡面有選了一種最簡單方便又比較適合我們需求方式。不需要WCF Service也不需要Web API,直接用MVC裡面的Action返回JsonResult就可以了。
[HttpGet]
public JsonResult Index(SearchCriteria criteria = null)
{
var result = PostManager.Posts;
if (criteria != null)
{
if (!string.IsNullOrEmpty(criteria.Title))
result = result.Where(
p => p.Title.IndexOf(criteria.Title, StringComparison.OrdinalIgnoreCase) >= 0);
if (!string.IsNullOrEmpty(criteria.Author))
result = result.Where(p => p.Author.IndexOf(criteria.Author, StringComparison.OrdinalIgnoreCase) >= 0);
if (criteria.Start.HasValue)
result = result.Where(p => p.Published >= criteria.Start.Value);
if (criteria.End.HasValue)
result = result.Where(p => p.Published <= criteria.End.Value);
if (criteria.MinComments > 0)
result = result.Where(p => p.Comments >= criteria.MinComments);
if (criteria.MinDiggs > 0)
result = result.Where(p => p.Diggs >= criteria.MinDiggs);
if (criteria.MinViews > 0)
result = result.Where(p => p.Diggs >= criteria.MinViews);
if (criteria.MaxComments > 0)
result = result.Where(p => p.Comments <= criteria.MaxComments);
if (criteria.MaxDiggs > 0)
result = result.Where(p => p.Diggs <= criteria.MaxDiggs);
if (criteria.MaxViews > 0)
result = result.Where(p => p.Diggs <= criteria.MaxViews);
}
return Json(result, JsonRequestBehavior.AllowGet);
}
利用Action來做這種Service還有一個好處就是我們不需要一個一個的宣告查詢引數,只需要把所有的引數放到一個model中就可以了。剩下的事就交給Model Binder吧。
public class SearchCriteria
{
public string Title { get; set; }
public string Author { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
public int MinDiggs { get; set; }
public int MaxDiggs { get; set; }
public int MinViews { get; set; }
public int MaxViews { get; set; }
public int MinComments { get; set; }
public int MaxComments { get; set; }
}
如果大家想更熟悉這個Service的功能,可以參考上面的引數自己去體驗一下(用IE會直接下載.json的檔案,用Chrom是可以直接在瀏覽器裡面看資料的)。但是我沒有做任何安全性的措施,希望大俠高抬貴手,別把網站整掛了就行。
認識IQueryable和IQueryProvider介面
有了上面的Service之後,我們要做的事情就簡單多了,但是在我們真正開始動手寫自己的Linq Provider之前,先來看看IQueryable和IQueryProvider這兩個重要的介面。
IQueryable
IQueryable本身並沒有包含多少的東西,它只有三個屬性:
- ElementType 代表當然這個Query所對應的型別
- Expression 包含了我們當然Query所執行的所有查詢或者是其它的操作
- IQueryProvider則是負責處理上面的Expression的實現
更為重要的是,在IQueryable這個介面之上,.net為我們提供了很多的擴充套件方法:
我們平常用到的Where,Select,Max,Any都包括在其中,具體的方法大家可以到System.Linq.Queryable這個靜態類下去看。大家注意一下,傳給Where方法的正是我們現在學習的Expression。
在另外一個很重要的介面IEnumrable下,也有著同樣的擴充套件方法:
這些擴充套件方法來自System.Linq.Enumrable這個靜態類下。我們可以看到兩組擴充套件方法的不同之處在於IQueryable下傳入的Expression型別,而IEnumrable下傳入的是委託。這樣做的用意是什麼呢?您請接著往下看。
IQueryProvider
我們上面講到了Enumrable和Queryable這兩個靜態類下的擴充套件方法,對於Enumrable下的擴充套件方法來說他們傳入的是委託,對於委託而言直接執行就可以了。
public static IEnumerable<T> Where<T>(this IEnumerable<T> list, Func<T, bool> predicate)
{
var result = new List<T>();
foreach (var element in list)
{
// 呼叫委託是驗證這個元素是否符合條件
if(predicate(element))
result.Add(element);
}
return result;
}
上面的程式碼給大家作一個參考,相信不難理解,所以Enumrable下的靜態方法都是操作本地資料的。而對於Queryable下的靜態方法而言,他們接收的是表示式,還記得表示式的最大特徵嗎?可以在執行時去遍歷解釋然後執行,那麼這樣就可以將表示式轉換成各種其它的方式去獲取資料,偉大的Linq to SQL就是這麼實現的。而這背後的大功臣就是我們的Linq Provider了,而IQueryProvider就是LinqProvider的介面。
IQueryProvider只有兩個操作,CreateQuery和Execute分別有泛型版本和非泛型版本。 CreatQuery用於構造一個IQueryable<T>的物件,這個類其實沒有任何實現,只是繼承了IQueryable和IEnumrable介面。主要用於計算指定表示式目錄樹所表示的查詢,返回的結果是一個可列舉的型別。而Execute會執行指定表示式目錄樹所表示的查詢,返回指定的結果。所有的內幕就在這個Execute方法裡面,拿我們要進行的Linq to cnblogs方法來舉例,我們將把傳入的表示式目錄樹翻譯成一個URL就是指向我們封裝好的Service的URL,通過發起web request到這個URL,拿到response進行解析,最終得到我們所要的資料,這就是我們Linq to cnblogs的思路。
Linq to cnblogs的實現
有了前面的資料準備和一些實現的大致思路以後,我們就可以著手開始實現我們的CnblogsQueryProvider了。我們的思路大致是這樣的:
- 實現自己的ExpressionVisitor類去訪問表示式目錄數,將其翻譯成可以訪問Service的Url
- 呼叫WebRequest去訪問這個Url
- 將上面返回的Response解析成我們要的物件
實現PostExpressionVisitor
關於表示式樹的訪問,我們在第二篇中已經有了比較詳細的介紹。如果對於表示式樹的遍歷不清楚的,可以去第二篇《遍歷表示式》中查閱。在這裡,我們建立一個我們自己的ExpressionVisitor類,去遍歷表示式樹。我們暫時只需要生成一個SearchCriteria(我們上面已經定義好了,對於查詢條件建的模)物件即可。
1 public class PostExpressionVisitor
2 {
3 private SearchCriteria _criteria;
4
5 // 入口方法
6 public SearchCriteria ProcessExpression(Expression expression)
7 {
8 _criteria = new SearchCriteria();
9 VisitExpression(expression);
10 return _criteria;
11 }
12
13 private void VisitExpression(Expression expression)
14 {
15 switch (expression.NodeType)
16 {
17 // 訪問 &&
18 case ExpressionType.AndAlso:
19 VisitAndAlso((BinaryExpression)expression);
20 break;
21 // 訪問 等於
22 case ExpressionType.Equal:
23 VisitEqual((BinaryExpression)expression);
24 break;
25 // 訪問 小於和小於等於
26 case ExpressionType.LessThan:
27 case ExpressionType.LessThanOrEqual:
28 VisitLessThanOrEqual((BinaryExpression)expression);
29 break;
30 // 訪問大於和大於等於
31 case ExpressionType.GreaterThan:
32 case ExpressionType.GreaterThanOrEqual:
33 GreaterThanOrEqual((BinaryExpression)expression);
34 break;
35 // 訪問呼叫方法,主要有於解析Contains方法,我們的Title會用到
36 case ExpressionType.Call:
37 VisitMethodCall((MethodCallExpression)expression);
38 break;
39 // 訪問Lambda表示式
40 case ExpressionType.Lambda:
41 VisitExpression(((LambdaExpression)expression).Body);
42 break;
43 }
44 }
45
46 // 訪問 &&
47 private void VisitAndAlso(BinaryExpression andAlso)
48 {
49 VisitExpression(andAlso.Left);
50 VisitExpression(andAlso.Right);
51 }
52
53 // 訪問 等於
54 private void VisitEqual(BinaryExpression expression)
55 {
56 // 我們這裡面只處理在Author上的等於操作
57 // Views, Comments, 和 Diggs 我們都是用的大於等於,或者小於等於
58 if ((expression.Left.NodeType == ExpressionType.MemberAccess) &&
59 (((MemberExpression)expression.Left).Member.Name == "Author"))
60 {
61 if (expression.Right.NodeType == ExpressionType.Constant)
62 _criteria.Author =
63 (String)((ConstantExpression)expression.Right