在以前《在ASP.NET Core中使用Apworks快速開發數據服務》一文的評論部分,.NET大神張善友爲我提了個建議,可使用Compile As a Service的Roslyn爲語法解析提供支持。在此很是感激友哥給個人建議,也讓我瞭解了一些Roslyn的知識。使用Roslyn的一個很大的好處是,框架無需依賴第三方的組件,而且Roslyn也是.NET Foundation的一個開源項目,爲.NET語言提供編譯服務,社區支持作的也很是出色。然而,通過一段時間的思考,我仍是選擇了一個折中的方案:在Apworks中使用Irony做爲查詢語言的語法解析器,與此同時,爲查詢語言語法解析提供可擴展的框架級支持。html
那麼問題來了:爲何我須要在Apworks中設計查詢語言?Irony是什麼?如何使用Irony實現本身的查詢語言語法解析器?下面我就一一爲你們介紹。node
不少體驗過Apworks數據服務(Apworks Data Services)案例:TaskList的讀者確定有這樣的感覺:爲何每次我新建的任務項目(Task Item)都是出如今列表中不肯定的位置?難道新建的任務就不該該放在最前面嗎?是的,你的疑問沒有錯,在以前的TaskList中,的確存在這樣的問題,由於那時候Apworks數據服務在返回任務列表時,還不支持查詢和排序,也就是說,它只能默認以Id做爲升序進行分頁,返回全部的數據。固然,在最近一版的Apworks數據服務中,經過基於Irony的語法解析器,已經可以成功地支持查詢和排序了。git
若是你以前有仔細閱讀《在ASP.NET Core中使用Apworks快速開發數據服務》一文,並按照文中的演練步驟實現過一個簡單的RESTful服務的話,那麼,請你從新在Visual Studio 2017中打開你的解決方案,將Apworks相關庫更新到最新版本,而後不要修改任何代碼,直接運行你的應用。等應用程序運行後,執行一次GET請求,URL中你就可使用query做爲查詢條件輸入了。好比,使用curl執行下面的命令:github
curl -G "http://localhost:58928/api/customers" --data-urlencode "query=name sw \"fr\""
你將獲得下面的結果:數據庫
能夠看到,數據服務返回了全部Name字段以「fr」開頭的客戶信息。固然,還支持排序操做。好比執行下面的命令:編程
curl -G "http://localhost:58928/api/customers" --data-urlencode "sort=name d"
將獲得下面的結果:api
此時返回結果已經按Name字段倒序排列。bash
在Apworks中,查詢語言支持如下操做和運算:服務器
排序語言支持升序(用字母a表示)以及降序(用字母d表示),多個排序條件使用AND關鍵字鏈接。例如:name a AND email d,表示使用name字段作升序排序,並以email作降序排序。框架
以上就給你們大概介紹了一下Apworks數據服務對查詢和排序的支持功能。設計這部分功能的需求是顯而易見的:開發人員無需爲通常的查詢和排序功能自定義額外的接口。或許你會問,爲什麼不使用已有的框架,好比OData。不錯,OData的確能夠提供統一的查詢界面,作系統集成也會相對容易,但一方面我仍是以爲OData過重,Apworks數據服務我但願可以提供更加簡單便捷的功能;另外一方面,看上去目前OData還不支持.NET Core(應該是不支持,我不太肯定,有知道的朋友也歡迎留言指正)。
實現這套查詢和排序語法,我使用的是一個.NET下開源的語法解析器生成工具集,它的名字叫作Irony。
Irony項目最開始是發佈在微軟的Codeplex代碼託管服務上的,地址是:http://irony.codeplex.com/。在Codeplex上的好評數有51顆星,也已經很不錯了。惋惜的是,最近一次更新是在2013年12月,看起來已經中止維護了,不過以前使用了一下,感受這個項目確實不錯,不只提供了開發庫,並且還有一個圖形化的語法解析器的測試工具,在寫完本身的自定義語言的語法以後,還能夠經過這個工具進行測試。因而,我把它遷移到了Github,成爲個人一個公共repo,地址是:https://github.com/daxnet/irony。固然,我沿用了原有的MIT許可協議,並在首頁的README.md中提供了原始地址(很惋惜Codeplex將在年末關閉),並保留了開發者的名字。不只如此,在一番踩坑以後,我把它遷移到了.NET Core平臺。
在個人Irony Github Repo裏,提供了一個很是簡單的案例,就是實現四則混合運算的字符串解析,並計算最終結果。固然,這個案例也被包含在了這個項目的源代碼裏。你們能夠本身下載查看。
Irony的一個特點就是運用了C#的運算符重載,使得語法定義借用了C#的編譯功能(語法、類型檢查等),簡單直觀,又不容易出錯。好比,在以下案例中的語法定義類型中:
[Language("Expression Grammar", "1.0", "abc")] public class ExpressionGrammar : Grammar { /// <summary> /// Initializes a new instance of the <see cref="ExpressionGrammar"/> class. /// </summary> public ExpressionGrammar() : base(false) { var number = new NumberLiteral("Number"); number.DefaultIntTypes = new TypeCode[] { TypeCode.Int16, TypeCode.Int32, TypeCode.Int64 }; number.DefaultFloatType = TypeCode.Single; var identifier = new IdentifierTerminal("Identifier"); var comma = ToTerm(","); var BinOp = new NonTerminal("BinaryOperator", "operator"); var ParExpr = new NonTerminal("ParenthesisExpression"); var BinExpr = new NonTerminal("BinaryExpression", typeof(BinaryOperationNode)); var Expr = new NonTerminal("Expression"); var Term = new NonTerminal("Term"); var Program = new NonTerminal("Program", typeof(StatementListNode)); Expr.Rule = Term | ParExpr | BinExpr; Term.Rule = number | identifier; ParExpr.Rule = "(" + Expr + ")"; BinExpr.Rule = Expr + BinOp + Expr; BinOp.Rule = ToTerm("+") | "-" | "*" | "/"; RegisterOperators(10, "+", "-"); RegisterOperators(20, "*", "/"); MarkPunctuation("(", ")"); RegisterBracePair("(", ")"); MarkTransient(Expr, Term, BinOp, ParExpr); this.Root = Expr; } }
從中能夠很容易理解:運算符(BinOp)包含+、-、*和/,而一個二元運算的表達式(BinExpr)由兩個表達式(Expr)和一個運算符(BinOp)組成,而二元運算的表達式又是表達式(Expr)的一種。經過這樣的語法定義,就可使用Irony的Parser產生語法樹了:
var language = new LanguageData(new ExpressionGrammar()); var parser = new Parser(language); var syntaxTree = parser.Parse(input);
怎麼樣,是否是很是方便?
在遷移Irony項目的同時,我還將Irony的測試工具Irony Grammar Explorer分離出來成爲了一個單獨的Github Repo。在你定義了上面的ExpressionGrammar類以後,編譯你的程序集,而後就可使用Irony Grammar Explorer進行測試了。好比,使用Irony Grammar Explorer打開Apworks.Querying.Parsers.Irony程序集,它將自動掃描程序集中全部的Grammar定義,而後讓用戶對各類Grammar進行測試。值得一提的是,在測試界面,Irony Grammar Explorer還能根據語法定義,自動產生語法高亮:
點擊右邊的語法樹中的節點,便可定位到輸入字符串的相應部分。比較有趣的一點是,在Irony Grammar Explorer的Github Repo裏,還包含了一個語法定義的案例庫:IronyExplorer.Samples,它包含了不少流行編程語言的語法定義。好比,下面是C# 3.5語言的語法測試效果:
有關Irony Grammar Explorer的其它功能,我就不一一介紹了,你們能夠本身實踐一下。總的來講,Irony能夠幫助你們快速方便地實現語法解析器,並且功能也可以知足絕大多數需求,針對.NET Core的支持,也使得Irony可以直接被應用在跨平臺的.NET應用程序中,並支持Docker部署。接下來的問題就更有趣了:我已經定義了本身的語法,並使用Irony Grammar Explorer經過了測試,接下來,我如何在個人應用程序中運用這個語法?換個方式問:我拿到了語法樹後,該怎麼辦呢?
雖然咱們可以將字符串文本解析成一棵語法樹,可以經過語法樹來體現一個字符串中各個部分的含義,以及它們之間的關係,可是如何可以讓計算機來讀懂這棵樹,並執行相應的任務呢?這就涉及到語法樹的處理問題。參考編譯原理,詞法分析和語法分析已經由Irony完成,接下來的語義分析,就須要咱們本身寫代碼了。
在Irony Repo的案例代碼中,咱們的目的是可以解析一個四則運算表達式,並計算出結果,因而,咱們定義了下面的對象模型:
所以,只須要將解析的語法樹轉換成上面的對象模型,也就可以經過Evaluation.Value屬性,獲得計算的最終結果。從代碼上看,向對象模型的轉換,是經過遞歸的方式遍歷語法樹實現的:
private Evaluation PerformEvaluate(ParseTreeNode node) { switch (node.Term.Name) { case "BinaryExpression": var leftNode = node.ChildNodes[0]; var opNode = node.ChildNodes[1]; var rightNode = node.ChildNodes[2]; Evaluation left = PerformEvaluate(leftNode); Evaluation right = PerformEvaluate(rightNode); BinaryOperation op = BinaryOperation.Add; switch (opNode.Term.Name) { case "+": op = BinaryOperation.Add; break; case "-": op = BinaryOperation.Sub; break; case "*": op = BinaryOperation.Mul; break; case "/": op = BinaryOperation.Div; break; } return new BinaryEvaluation(left, right, op); case "Number": var value = Convert.ToSingle(node.Token.Text); return new ConstantEvaluation(value); } throw new InvalidOperationException($"Unrecognizable term {node.Term.Name}."); }
以上完整代碼請參考Evaluator的實現。整個案例及使用方式能夠點擊https://github.com/daxnet/irony#example查看。能夠看到,使用Irony來實現一個四則混合運算的計算器仍是很是方便的。
在Apworks中,咱們須要的是可以將一個表達查詢語義的語法樹,轉換成Lambda表達式,以便於後臺數據庫引擎可以直接執行Lambda表達式完成查詢。經過數據庫引擎執行Lambda表達式的優點是很是明顯的,好比Entity Framework Core能夠經過Lambda表達式生成高效的SQL語句並在數據庫服務器上執行,性能方面也能兼顧得很是好。
相似的,咱們使用.NET Expression的對象模型,經過遍歷查詢語句的語法樹來生成表達式模型,最後轉換成Lambda表達式便可。具體過程就再也不贅述了,請參考Apworks的源代碼。如今咱們來看看實際效果。
假設咱們的測試數據以下:
Customers.Add(new Customer { Id = 1, Email = "jim@example.com", Name = "jim", DateRegistered = DateTime.Now.AddDays(-1) }); Customers.Add(new Customer { Id = 2, Email = "tom@example.com", Name = "tom", DateRegistered = DateTime.Now.AddDays(-2) }); Customers.Add(new Customer { Id = 3, Email = "alex@example.com", Name = "alex", DateRegistered = DateTime.Now.AddDays(-3) }); Customers.Add(new Customer { Id = 4, Email = "carol@example.com", Name = "carol", DateRegistered = DateTime.Now.AddDays(-4) }); Customers.Add(new Customer { Id = 5, Email = "david@example.com", Name = "david", DateRegistered = DateTime.Now.AddDays(-5) }); Customers.Add(new Customer { Id = 6, Email = "frank@example.com", Name = "frank", DateRegistered = DateTime.Now.AddDays(-6) }); Customers.Add(new Customer { Id = 7, Email = "peter@example.com", Name = "peter", DateRegistered = DateTime.Now.AddDays(-7) }); Customers.Add(new Customer { Id = 8, Email = "paul@example.com", Name = "paul", DateRegistered = DateTime.Now.AddDays(1) }); Customers.Add(new Customer { Id = 9, Email = "winter@example.com", Name = "winter", DateRegistered = DateTime.Now.AddDays(2) }); Customers.Add(new Customer { Id = 10, Email = "julie@example.com", Name = "julie", DateRegistered = DateTime.Now.AddDays(3) }); Customers.Add(new Customer { Id = 11, Email = "jim@example.com", Name = "jim", DateRegistered = DateTime.Now.AddDays(4) }); Customers.Add(new Customer { Id = 12, Email = "brian@example.com", Name = "brian", DateRegistered = DateTime.Now.AddDays(5) }); Customers.Add(new Customer { Id = 13, Email = "david@example.com", Name = "david", DateRegistered = DateTime.Now.AddDays(6) }); Customers.Add(new Customer { Id = 14, Email = "daniel@example.com", Name = "daniel", DateRegistered = DateTime.Now.AddDays(7) }); Customers.Add(new Customer { Id = 15, Email = "jill@example.com", Name = "jill", DateRegistered = DateTime.Now.AddDays(8) });
下面調試單元測試,並查看所產生的Lambda表達式,能夠看到,Lambda表達式正確產生,測試順利經過:
本文介紹了Apworks中自定義查詢語句在Apworks數據服務中的應用,並介紹了查詢語句和排序語句的實現方式,與此同時對Irony Grammar Parser進行了介紹。Apworks中查詢語句的實現仍是相對簡單的,目前不支持內嵌對象的屬性查詢,好比Customer.Address.Country EQ 「China」 這樣的查詢是不支持的。爲了保證明現過程相對簡單快速,從此也不打算支持。若是須要用到這種內嵌對象屬性的查詢,請擴展DataServiceController以實現本身的特定API來完成。
接下來我會介紹Entity Framework Core在Apworks數據服務中的使用(雖然已經預告了好幾回了-_-!!)。