NET Core中使用Irony实现自己的查询语言语法解析器
在之前《在ASP.NET Core中使用Apworks快速開發數據服務》一文的評論部分,.NET大神張善友為我提了個建議,可以使用Compile As a Service的Roslyn為語法解析提供支持。在此非常感激友哥給我的建議,也讓我了解了一些Roslyn的知識。使用Roslyn的一個很大的好處是,框架無需依賴第三方的組件,并且Roslyn也是.NET Foundation的一個開源項目,為.NET語言提供編譯服務,社區支持做的也非常出色。然而,經過一段時間的思考,我還是選擇了一個折中的方案:在Apworks中使用Irony作為查詢語言的語法解析器,與此同時,為查詢語言語法解析提供可擴展的框架級支持。
那么問題來了:為什么我需要在Apworks中設計查詢語言?Irony是什么?如何使用Irony實現自己的查詢語言語法解析器?下面我就一一為大家介紹。
Apworks中的查詢語言
很多體驗過Apworks數據服務(Apworks Data Services)案例:TaskList的讀者肯定有這樣的感受:為什么每次我新建的任務項目(Task Item)都是出現在列表中不確定的位置?難道新建的任務就不應該放在最前面嗎?是的,你的疑問沒有錯,在之前的TaskList中,的確存在這樣的問題,因為那時候Apworks數據服務在返回任務列表時,還不支持查詢和排序,也就是說,它只能默認以Id作為升序進行分頁,返回所有的數據。當然,在最近一版的Apworks數據服務中,通過基于Irony的語法解析器,已經能夠成功地支持查詢和排序了。
如果你之前有仔細閱讀《在ASP.NET Core中使用Apworks快速開發數據服務》一文,并按照文中的演練步驟實現過一個簡單的RESTful服務的話,那么,請你重新在Visual Studio 2017中打開你的解決方案,將Apworks相關庫更新到最新版本,然后不要修改任何代碼,直接運行你的應用。等應用程序運行后,執行一次GET請求,URL中你就可以使用query作為查詢條件輸入了。比如,使用curl執行下面的命令:
| 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" |
將得到下面的結果:
此時返回結果已經按Name字段倒序排列。
在Apworks中,查詢語言支持以下操作和運算:
邏輯運算:AND OR NOT
關系運算:EQ(相等),NE(不等),LT(小于),LE(小于等于),GT(大于),GE(大于等于)
字符串運算:SW(以某字符串開頭)、EW(以某字符串結尾)、CT(包含某字符串)
括號優先級
日期類型的比對
排序語言支持升序(用字母a表示)以及降序(用字母d表示),多個排序條件使用AND關鍵字連接。例如:name a AND email d,表示使用name字段做升序排序,并以email做降序排序。
以上就給大家大概介紹了一下Apworks數據服務對查詢和排序的支持功能。設計這部分功能的需求是顯而易見的:開發人員無需為一般的查詢和排序功能自定義額外的接口。或許你會問,為何不使用已有的框架,比如OData。不錯,OData的確可以提供統一的查詢界面,做系統集成也會相對容易,但一方面我還是覺得OData太重,Apworks數據服務我希望能夠提供更加簡單便捷的功能;另一方面,看上去目前OData還不支持.NET Core(應該是不支持,我不太確定,有知道的朋友也歡迎留言指正)。
實現這套查詢和排序語法,我使用的是一個.NET下開源的語法解析器生成工具集,它的名字叫做Irony。
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( "xpression 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的案例代碼中,我們的目的是能夠解析一個四則運算表達式,并計算出結果,于是,我們定義了下面的對象模型:
frameborder="0" scrolling="no" style="border-width: medium; width: 650px; height: 494px;">
因此,只需要將解析的語法樹轉換成上面的對象模型,也就能夠通過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數據服務中的使用(雖然已經預告了好幾次了-_-!!)。
原文地址:http://www.cnblogs.com/daxnet/p/6953418.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的NET Core中使用Irony实现自己的查询语言语法解析器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .net Core 生产环境 Kestr
- 下一篇: 6月Unity技术路演华东站报名启动!