在.NET Core中使用Irony實現本身的查詢語言語法解析器

在以前《在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數據服務(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\""

你將獲得下面的結果:數據庫

image

能夠看到,數據服務返回了全部Name字段以「fr」開頭的客戶信息。固然,還支持排序操做。好比執行下面的命令:編程

curl -G "http://localhost:58928/api/customers" --data-urlencode "sort=name d"

將獲得下面的結果:api

image

此時返回結果已經按Name字段倒序排列。bash

在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("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還能根據語法定義,自動產生語法高亮:

image

點擊右邊的語法樹中的節點,便可定位到輸入字符串的相應部分。比較有趣的一點是,在Irony Grammar Explorer的Github Repo裏,還包含了一個語法定義的案例庫:IronyExplorer.Samples,它包含了不少流行編程語言的語法定義。好比,下面是C# 3.5語言的語法測試效果:

image

有關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表達式正確產生,測試順利經過:

image

總結

本文介紹了Apworks中自定義查詢語句在Apworks數據服務中的應用,並介紹了查詢語句和排序語句的實現方式,與此同時對Irony Grammar Parser進行了介紹。Apworks中查詢語句的實現仍是相對簡單的,目前不支持內嵌對象的屬性查詢,好比Customer.Address.Country EQ 「China」 這樣的查詢是不支持的。爲了保證明現過程相對簡單快速,從此也不打算支持。若是須要用到這種內嵌對象屬性的查詢,請擴展DataServiceController以實現本身的特定API來完成。

接下來我會介紹Entity Framework Core在Apworks數據服務中的使用(雖然已經預告了好幾回了-_-!!)。

相關文章
相關標籤/搜索