在.NET Framework 3.5中提供了LINQ 支持后,LINQ就以其強大而優雅的編程方式贏得了開發人員的喜愛,而各種LINQ Provider更是滿天飛,如LINQ to NHibernate、LINQ to Google等.
隨著Linq的普及,大家都對實現自己的Linq provider很感興趣.VB Team的 Kevin 寫了兩篇相關的文章,我覺得很有幫助.現在嘗試翻譯了第一篇,希望大家能夠從中有所收穫.
原文是How to implement IQueryable (Part 1)
隨著Orcas(Visual Studio 2008的code name)的推出,微軟計劃為我們經常用到的一些數據應用的場合提供Linq的支持.比如:DLinq用於SQL Server,Xlinq則用於XML相關的處理.事實上還有其他數不清的數據讀取的場合,用戶也希望能用上Linq這個方便快捷的工具.多數情況下,我們只需要把數據放到CLR Collection里,用Linq基本的支持足以.舉個例子來說:要是你想找到「我的文檔」里的所有新放進來的.exe文件,可以用下邊這句:
Dim newExe = From fileName In Directory.GetFiles( _
My.Computer.FileSystem.SpecialDirectories.MyDocuments, _
"*.exe", SearchOption.AllDirectories) _
Where (New FileInfo(fileName)).CreationTime > 6/30/2007 _
Select fileName
很簡單,是吧?
這樣看起來很好,唯一的缺點是沒有涉及到我們這篇文章的主題「實現IQueryable」.下邊還是看看怎麼通過實現IQueryable達到我們的目的,也就是為Linq提供我們自已的Linq Provider.
在當今的開發領域,已經存在很多的程序介面和對象模型提供對各式各樣的數據的讀取和操作.比如:Windows Desktop Search (關於它的詳細內容參見http://www.microsoft.com/windows/desktopsearch/default.mspx)
它就提供了一個OLE DB Provider讓你能查詢系統已經建立好的各種文件信息的索引.那麼,我們可不可以不用寫SQL語句而是使用Linq來訪問這些內容呢?答案是:能,但我們要實現Linq Provider.對相關背景知識不太了解而又有興趣的朋友來說,本文末尾的參考資源很有必要,不妨一看.
寫一個自己的Linq provider,首先要做的就是實現IQueryable和IQueryProvider這兩個介面.
我們要操作的是文件對象(FileInfo),
要實現IQueryable(Of FileInfo),代碼如下:
Imports System.IO
PublicClass WDSQueryObject
Implements IQueryable(Of FileInfo), IQueryProvider
EndClass
當你在Visual Studio里敲出(估計沒有人真的會敲)或是Paste出上面的這些代碼,IDE會提示你要實現這兩個介面,有幾個方法你必須要實現.下面我一一介紹:
注意:
完整的代碼在末尾的給出的鏈接里能找到,下載下來並調試運行一下,收穫更大.
我的代碼基於Orcas Beta2.IQueryable介面在Beta1版本後有過重構.
CreateQuery
IQueryProvider介面裡面定義了兩個CreateQuery方法. 一個返回泛型的 IQueryable(Of TElement).另外一個返回非泛型的 Iqueryable.大多數情況下你可能只需要在非泛型的裡面調用泛型的那個.就象下面這樣:
Public Function CreateQuery(ByVal expression As Expression) As IQueryable Implements IQueryProvider.CreateQuery
Return CreateQuery1(Of FileInfo)(expression)
End Function
對一個簡單查詢來說,每一個「Where」子句中的過濾條件都會調用一次CreatQuery,每一個「Select」會調用一次CreatQuery.像下面這一句:
Dim r = From file In index _
Where file.Name Like"%.exe" _
Select file.FullName
表達式「file.Name Like"%.exe"」和「file.FullName」分別會調用一次CreateQuery方法.下面是我實現處理這兩個語句的代碼的框架:
Public Function CreateQuery1(Of TElement)(ByVal expression As Expression) As IQueryable(Of TElement) Implements IQueryProvider.CreateQuery
Dim querySource As IQueryable(Of TElement) = Nothing
Dim nodeType = expression.NodeType
Select Case nodeType
Case ExpressionType.Call
Dim m As MethodCallExpression = expression
Dim methodName = m.Method.Name
Select Case methodName
Case "Select"
' insert Select processing code
Case "Where"
' insert Where processing code
CaseElse
Throw New NotSupportedException("Queries using '" & methodName & "' are not supported for this collection.")
End Select
Case Else
Throw New NotSupportedException("Creating a query from an expression of type '" & nodeType & "' is supported.")
End Select
Return querySource
End Function
你可能注意到了我們在CreateQuery里得到的expression已經包括了調用者的信息(比如:Select,Where等等),我們將用到這些信息來處理剩下的內容.在這個文件系統的例子里,「Where」子句是比較有意思的,我們先來談談它.Where是Queryable中定義的一個擴展方法,它的簽名如下:
PublicSharedFunction Where(Of TSource)( _
ByVal source As IQueryable(Of TSource), _
ByVal predicate As Expression(Of System.Func(Of TSource, Boolean)) _
) As System.Linq.IQueryable(Of TSource)
你要是察看它的expression tree里的詳細信息(下圖),你會發現關於上面簽名的信息被處理過了.按層次展開下面的expression tree,就能得到上面簽名的對應結構.第一層:方法名是Where
,返回類型是IQueryable.第二層是兩個參數:一個是作為source的WDSQueryObject,還有一個是作為predicate的lambda表達式.
下面是上面代碼中「' insert Where processing code」部分的內容,我來詳細介紹一下:
m_query = New StringBuilder()
m_funclets = New List(Of KeyValuePair(OfString, Func(OfString)))()
Dim lambda As LambdaExpression = CType(m.Arguments(1), UnaryExpression).Operand
ExpandExpression(lambda.Body)
m_query.Insert(0, "SELECT System.ItemPathDisplay FROM SystemIndex WHERE NOT CONTAINS(System.ItemType, 'folder') AND (")
m_query.Append(")")
querySource = Me
想必大家都了解,上面的「SELECT」那一句是用來拼接SQL字元串用的.關於如何寫用於Windows Desktop Search的SQL語句,請參考後面給出的關於WDS的鏈接.這裡要說明的是,在第二次調用CreateQuery時才會處理Linq中的Select部分.從上面的Expression tree中我們可以看出,Where的第一個參數(是個常量表達式)是指向我們的WDSQueryObject的一個引用.這個參數也就是實現IQueryable.Expression的返回值.第二個參數是需要被我們轉換成SQL語句的lambda表達式,這個也就是我們實現Iqueryable的核心.我們要做的就是,把Linq表達式轉換成一組指令用於從制定的數據原理取得數據.在我的代碼里,是由方法ExpandExpression具體負責這一轉換過程的.它遍歷整個expression tree並把它展開轉換成相對應的SQL語句.方法ExpandExpression返回的時候,m_query里就包含了與Linq中where條件相對應的SQL語句.然後調用
m_query.Insert(0, "SELECT System.ItemPathDisplay FROM SystemIndex WHERE NOT CONTAINS(System.ItemType, 'folder') AND (")
m_query.Append(")")
就構造出了完整的SQL句子.
下面是ExpandExpression的代碼:
Private Sub ExpandExpression(ByVal e As Expression)
Select Case e.NodeType
Case ExpressionType.And
ExpandBinary(e, "AND")
Case ExpressionType.Equal
ExpandBinary(e, "=")
Case ExpressionType.GreaterThan
ExpandBinary(e, ">")
Case ExpressionType.GreaterThanOrEqual
ExpandBinary(e, ">=")
Case ExpressionType.LessThan
ExpandBinary(e, "<")
Case ExpressionType.LessThanOrEqual
ExpandBinary(e, "<=")
Case ExpressionType.NotEqual
ExpandBinary(e, "!=")
Case ExpressionType.Not
ExpandUnary(e, "NOT")
Case ExpressionType.Or
ExpandBinary(e, "OR")
Case ExpressionType.Call
ExpandCall(e)
Case ExpressionType.MemberAccess
ExpandMemberAccess(e)
Case ExpressionType.Constant
ExpandConstant(e)
Case Else
Throw New NotSupportedException("Expressions of type '" & e.NodeType.ToString() & "' are not supported.")
End Select
End Sub
我們通過判斷expression tree的節點類型,對希望能夠支持的操作,調用了相應的處理方法.雖然在這兒我們沒有支持所有的表達式類型,可最常用的基本都包括了(湊合夠用了).下面我們看看一個簡單的Linq 語句是怎樣得到處理的.如下:
Dim index AsNew WDSQueryObject
Dim cutoffDate = 6/28/2007
Dim r = From file In index _
Where file.CreationTime > cutoffDate And _
file.Name Like"%.exe" _
Select file.FullName
在處理Where的expression tree的過程中,第一個被調用的方法是ExpandBinary.ExpandBinary又會調用ConcatBinary,ConcatBinary通過合適的操作符來把左右兩邊連到一塊兒(在這兒是「AND」).
Private Sub ExpandBinary(ByVal b As BinaryExpression, ByVal op AsString)
ConcatBinary(b.Left, b.Right, op)
End Sub
Private Sub ConcatBinary(ByVal left As Expression, ByVal right As Expression, ByVal op AsString)
ExpandExpression(left)
m_query.Append(" ")
m_query.Append(op)
m_query.Append(" ")
ExpandExpression(right)
End Sub
處理「And」語句左邊部分會再次調用ConcatBinary(這次是處理「>」),接著會調用ExpandMemberAccess,如下:
Private Sub ExpandMemberAccess(ByVal m As MemberExpression)
Dim member = m.Member
Dim e = m.Expression
Select Case e.NodeType
Case ExpressionType.Parameter
' Parameter processing code
Case ExpressionType.Constant
' Constant processing code
Case Else
Throw New NotSupportedException("Accessing member '" & member.Name & "' is not supported in this context.")
End Select
End Sub
先來看看代碼中的 『Parameter processing code』.此處,『Parameter『就是整個查詢中用到的迭代器,也就是指「From file In index」中的「file」.我們要做的就是把FileInfo 類型的屬性的名稱(如:file.CreationTime)轉換成.net中對應的windows文件系統的屬性名稱.如下:
PrivateFunction GetAttributeName(ByVal m As MemberInfo) AsString
Dim name AsString
Dim memberName = m.Name
Select Case memberName
Case "CreationTime"
name = "System.DateCreated"
Case "Name"
name = "System.FileName"
Case Else
Throw New NotSupportedException("Using the property '" & memberName & "' in filter expressions is not supported.")
End Select
Return name
End Function
跟前面一樣,目前我們對屬性的支持並不完整,但這並不妨礙我們的理解和簡單的使用.完整的支持請參考本文末尾的鏈接.
面介紹一下 『Constant processing code』. 在這兒我們把對變數cutoffDate的訪問「翻譯」 成SQL語言. 如下:
Dim valueName = "[value" & m_funclets.Count & "]"
Dim valueFunc As Func(OfString) = Nothing
Dim memberType = member.MemberType
If m.Type IsGetType(String) OrElse m.Type IsGetType(Date) Then
m_query.Append("'")
m_query.Append(valueName)
m_query.Append("'")
Else
m_query.Append(valueName)
EndIf
Dim funclet As Func(OfString) = Nothing
SelectCase memberType
Case MemberTypes.Field
Dim f As FieldInfo = member
Dim c As ConstantExpression = e
If m.Type IsGetType(Date) Then
funclet = Function() CDate(f.GetValue(c.Value)).ToString("yyyy-MM-dd")
Else
funclet = Function() CStr(f.GetValue(c.Value))
EndIf
CaseElse
Throw New NotSupportedException("Accessing member of type'" & memberType & "' is not supported.")
EndSelect
m_funclets.Add(New KeyValuePair(OfString, Func(OfString))(valueName, funclet))
看到上面的代碼,很多朋友會問裡面那個「funclet」是什麼東東?用來做啥?這個就涉及到了Linq架構中一個很重要的特點」延遲執行」.換句話說,在我們建立一個query時,我們只是定義了它,並沒有運行它(廢話).而有很多信息只有運行時才能被獲知,比如說cutoffDate的值6/28/2007.這就是說我們沒有辦法在執行query前驗證這個query(拿到6/28/2007,並查詢底層的數據源).因此,我們想存儲關於如何得到cutoffDate的值的信息,而不是它的具體的值.我在這所做的就是,在查詢字元串中放了一個佔位符([value*]),並建了一個函數,讓這個函數在可以拿到查詢結果的時候返回cutoffDate的值.
我在這兒用到了lambda表達式,當你想創建一個inline函數或匿名代理的時候,用lambda表達式很方便.它同時會自動創建一個closure類來保存我在當前block里讀到的所有變數的信息.比如上面:當代碼進入「Case」 后,就會生成一個新的closure類,變數 『f』 和『c』的值都會存到裡面.編譯器會自動把對這些局部變數的讀取轉換成對相應的closure類的欄位的讀取.執行的query時候,就會執行上面的「funclet」來替換佔位符[value*],這要就能在運行query時拿到變數的值(而不是在query被創建的時候).你可能會注意到cutoffDate的MemberAccessExpression,同樣是一個已被提升過的局部變數.這也就是為什麼它的成員類型是 「Field」,正是在query中用到cutoffDate ,他的值其實是存到了closure 類中的一個欄位里.
關於Closures類的相關內容請參考:http://blogs.msdn.com/vbteam/search.aspx?q=closure &p=1
接下來談談「file.Name Like"%.exe"」.你可能會奇怪為什麼我們在ExpandCall里處理這一部分,而不是ExpandBinary.事實上,VB編譯器把一些的二元操作符直接轉換成對VB運行庫中相應方法的調用.這給VB添加了一些CLR沒有的功能.比如:LikeString (由VB中的「Like」操作生成) 和CompareString (由VB中的字元串比較的表達式例如「「a」 = 「A」 」生成).下面是我實現的ExpandCall中處理LikeString的一段:
Private Sub ExpandCall(ByVal m As MethodCallExpression, OptionalByVal op AsString = "")
Dim methodName = m.Method.Name
Select Case methodName
Case "LikeString"
ConcatBinary(m.Arguments(0), m.Arguments(1), "LIKE")
Case Else
Throw New NotSupportedException("Using method '" & methodName & "' in a filter expression is not supported.")
End Select
End Sub
處理「Where」語句所做的一件事就是處理常量字元串值"%.exe".這一步很簡單,值得在此一提的是,一些數據類型默認的轉換操作不一定適用於你的數據源.比如下面:WDS就要求日期必須是指定的格式.
Private Sub ExpandConstant(ByVal c As ConstantExpression)
Dim value = c.Value
If value.GetType() IsGetType(String) Then
m_query.Append("'")
m_query.Append(CStr(value))
m_query.Append("'")
ElseIf value.GetType() IsGetType(Date) Then
m_query.Append("'")
m_query.Append(CDate(value).ToString("yyyy-MM-dd"))
m_query.Append("'")
Else
m_query.Append(value.ToString())
End If
End Sub
處理完「Where」語句后,最終我們得到的傳給WDS的字元串如下:
"SELECT System.ItemPathDisplay FROM SystemIndex WHERE NOT CONTAINS(System.ItemType, 'folder') AND (System.DateCreated > '[value0]' AND System.FileName LIKE '%.exe')"
在我的下一篇博客里,會講到GetEnumerator和Select.
Resources
Full source code for this project:
http://hresult.members.winisp.net/FileSystemQuery.zip
Bart De Smet』s excellent blog on Implementing IQueryable for Linq to LDAP:
http://community.bartdesmet.net/blogs/bart/archive/2007/04/05/the-iqueryable-tales-linq-to-ldap-part-0.aspx
Fabrice Marguerie』s blog in implementing Linq to Amazon:
http://weblogs.asp.net/fmarguerie/archive/2006/06/26/Introducing-Linq-to-Amazon.aspx
Catherine Heller』s blog on Windows Desktop (Vista) Search:
http://blogs.msdn.com/cheller/archive/2006/06/21/642220.aspx
List of query attributes supported by the Windows filesystem
http://msdn2.microsoft.com/en-us/library/aa830600.aspx
另外大家也可以參考我們team 的blog里的一些資源 http://blog.joycode.com/vbcti/category/1465.aspx