在.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数据服务中的使用(虽然已经预告了好几回了-_-!!)。

相关文章
相关标签/搜索