OData – Query to Expression

前言

EF Core 可以把 expression 轉換成 string, 但沒辦法轉回來. 

想把 string 轉成 expression, 目前最合適的工具是 OData. 雖然 Dynamic LINQ 也有人用, 但畢竟 OData 是微軟的, 而且有規範文檔.

可惜, 就目前的 OData 要想做到 string to expression 還是很麻煩的. 這也是 OData.NxT 想達到的其中一個目標. 但以 OData Team 的實力, 估計還要等好多年呢. 我們還是實際一點, 來看看目前該怎麼弄吧.

 

主要參考:

OData Nxt 004: Query to Expression (IQueryable)

Using Uri Parser

 

分析源碼之路

如果你只是想看結果, 可以跳過這個 part, 如果想學習怎樣通過源碼找方案可以看看.

OData 肯定是有辦法做到的, 在 ODataController 裏, 可以通過 ODataQueryOptions.ApplyTo 來把 query params apply 進去 queryable.

猜測它內部就是把 string 轉成 expression 然後調用 queryable.where(expression), 大概是這樣.

但我們要怎樣從 0 搞起呢? 怎麼弄 OdataQueryOptions 出來呢?

接着查下去.

打開 ODataQueryOptions.cs

發現要初始化它就需要有一個 HttpRequest...這個接口太上層了吧, 和 HttpRequest 綁的這麼死...

接着查下去

在 ApplyTo 方法裏看到了 handle $filter 的 apply. 我這裏只 focus $filter 的處理 (因爲我這一次也只需要到這個).

底層調用的是 FilterQueryOption.ApplyTo

FilterQueryOption.cs

初始化它需要 content 和 parser. 嗯...感覺這個接口就比較底層了, 是我們要的了. 先不管 context, parser 怎麼搞, 我們去看看它的 apply 做些什麼.

ApplyTo

和我們猜測的一樣, 裏面搞了一個 queryable.where(expression). 它是利用了 FilterBinder.Bind 方法來把 string 轉成 expression.

如果你有看上面的參考視頻, 這個就是作者通過反射調用的方法. 因爲這個 Bind 是 internal 方法來的, 所以他要反射.

FilterBinder.cs

好, 既然確定找對了路, 那麼我們回去看看怎樣從 0 做出來, context 和 parser 怎樣弄.

ODataQueryContext.cs

初始化需要 EdmModel, ClrType, ODataPath.

EdmModel 就是我們每次 startup.cs 要搞的, 它和 EF Core 的 model 是同一個概念.

var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<Product>("products");
odataBuilder.EnableLowerCamelCase();
var edmModel = odataBuilder.GetEdmModel();

ClrType 指的就是這一次的 $filter 的起頭. OData 是對着 resource (entity) 的, 一定會有一個開始的 entity.

上面例子就是 typeof(Product).

ODataPath 我們參考它怎麼做.

EnableQueryAttribute.cs

在 request 階段就已經生產的, 全場找一下後它是這樣做的

var entitySet = edmModel.FindDeclaredEntitySet("products");
var entitySetSegment = new EntitySetSegment(entitySet);
var odataPath = new ODataPath(new[] { entitySetSegment });

context 搞定, 下一個要做的是 parser.

ODataQueryOptionParser.cs (源碼是在 odata.net library)

初始化有幾個重載, 先挑一個最簡單實現的唄.

model 有了, odataPath 有了,

queryOptions

Using Uri Parser 學的, 但這裏只教到怎樣 parser 出 filter clause 但是沒有教怎樣轉成 expression.

搞定. 到這裏我們就掌握了所有的資料了.

 

寫 Demo

var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<Product>("products");
odataBuilder.EnableLowerCamelCase();
var edmModel = odataBuilder.GetEdmModel(); // 做 edm model
var entitySet = edmModel.FindDeclaredEntitySet("products");
var entitySetSegment = new EntitySetSegment(entitySet);
var odataPath = new ODataPath(new[] { entitySetSegment }); // 做 odata path
var queryOptions = new Dictionary<string, string> // 定義 $filter query
{
    ["$filter"] = "id eq 2",
};
var queryOptionParser = new ODataQueryOptionParser(edmModel, odataPath, queryOptions); // 做 parser
var context = new ODataQueryContext(edmModel, typeof(Product), odataPath); // 做 context
var filterQueryOption = new FilterQueryOption($"id eq 2", context, queryOptionParser); // 做 filter options
var productQuery = new List<Product> { new Product { Id = 1 }, new Product { Id = 2 } }.AsQueryable(); // 定義 queryable
var newQuery = filterQueryOption.ApplyTo(productQuery, new ODataQuerySettings()); // apply to
var result = newQuery.OfType<Product>().ToList(); // 等到結果

這樣就實現了一個通過 OData 把 query string apply to queryable 的過程了.

那如果我不想 apply 只想獲取到 expression 行不行? 

不行, 除非像參考視頻裏那樣, 通過反射去調用 internal 的 Bind 方法. 然後做 apply to 所有的動作, 除了最後一個 where.

 

總結

OData 確實很不錯, 但想把 query string 轉換成 expression 目前還沒有一個 build-in 的 solution. 

也有一些 library 可以做到類似的效果

Is there a ODATA query to linq where expression (ODATA to Linq )

本篇嘗試探索如果利用 OData 現有的接口來儘量做到 query to expression. 同時期待 OData.NxT 帶給開發者們更好的使用體驗.

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章