65.9K
CodeProject 正在变化。 阅读更多。
Home

动态表达式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (44投票s)

2013年2月2日

MIT

11分钟阅读

viewsIcon

87205

downloadIcon

966

动态地生成 LambdaExpression 或函数委托,无需编译。

引言

官方 github 仓库

Dynamic Expresso 是一个简单的 C# 语句解释器。Dynamic Expresso 嵌入了自己的解析逻辑,通过将其转换为 .NET lambda 表达式或委托来真正解释 C# 语句。

使用 Dynamic Expresso,开发人员可以创建可脚本化的应用程序,无需编译即可执行 .NET 代码,或创建动态 linq 语句。

语句使用 C# 语言规范的一个子集编写。全局变量或参数可以被注入并在表达式中使用。它不生成程序集,而是动态地创建表达式树。

dynamic expresso workflow

例如,您可以计算数学表达式

var interpreter = new Interpreter();
var result = interpreter.Eval("8 / 2 + 2");

或解析带有变量或参数的表达式并多次调用它

var interpreter = new Interpreter()
                                .SetVariable("service", new ServiceExample());

string expression = "x > 4 ? service.OneMethod() : service.AnotherMethod()";
Lambda parsedExpression = interpreter.Parse(expression, 
                                                new Parameter("x", typeof(int)));

var result = parsedExpression.Invoke(5);

或为 LINQ 查询生成委托和 lambda 表达式

var prices = new [] { 5, 8, 6, 2 };

var whereFunction = new Interpreter()
    .ParseAsDelegate<Func<int, bool>>("arg > 5");

var count = prices.Where(whereFunction).Count();

在线演示

Dynamic Expresso 实时演示: http://dynamic-expresso.azurewebsites.net/

快速开始

Dynamic Expresso 可在 NuGet 上找到。您可以使用以下命令安装该包

PM> Install-Package DynamicExpresso.Core

源代码和符号(.pdb 文件)用于调试,可在 Symbol Source 上获取。

特点

  • 表达式可以使用 C# 语法的子集编写(有关更多信息,请参阅语法部分)
  • 支持变量和参数
  • 可生成委托或 lambda 表达式
  • 完整的单元测试套件
  • 与其他类似项目相比,性能良好
  • 部分支持泛型、params 数组和扩展方法
  • 不区分大小写的表达式(默认区分大小写)
  • 能够发现给定表达式的标识符(变量、类型、参数)
  • 占地面积小,生成的表达式是托管类,可以卸载,并且可以在单个 appdomain 中执行
  • 易于使用和部署,所有内容都包含在单个程序集中,没有其他外部依赖
  • 100% 托管代码,用 C# 4.0 编写
  • 开源(MIT 许可证)

返回值

您可以解析和执行 void 表达式(没有返回值)或者返回任何有效的 .NET 类型。解析表达式时,您可以指定预期的表达式返回类型。例如,您可以编写

var target = new Interpreter();

double result = target.Eval<double>("Math.Pow(x, y) + 5",
                                        new Parameter("x", typeof(double), 10),
                                        new Parameter("y", typeof(double), 2));

内置解析器还可以理解任何给定表达式的返回类型,因此您可以检查表达式是否返回您期望的值。

变量

可以使用 Interpreter.SetVariable 方法在表达式中使用变量

var target = new Interpreter()
                .SetVariable("myVar", 23);
Assert.AreEqual(23, target.Eval("myVar"));

变量可以是原始类型,也可以是自定义的复杂类型(类、结构、委托、数组、集合等)。

可以使用 Interpreter.SetFunction 方法通过委托变量传递自定义函数

Func<double, double, double> pow = (x, y) => Math.Pow(x, y);
var target = new Interpreter()
            .SetFunction("pow", pow);

Assert.AreEqual(9.0, target.Eval("pow(3, 2)"));

可以使用 Interpreter.SetExpression 方法传递自定义 Expression

参数

解析的表达式可以接受一个或多个参数

var interpreter = new Interpreter();

var parameters = new[] {
                new Parameter("x", 23),
                new Parameter("y", 7)
                };

Assert.AreEqual(30, interpreter.Eval("x + y", parameters));

参数可以是原始类型或自定义类型。您可以解析一次表达式,并使用不同的参数值多次调用它

var target = new Interpreter();

var parameters = new[] {
                new Parameter("x", typeof(int)),
                new Parameter("y", typeof(int))
                };

var myFunc = target.Parse("x + y", parameters);

Assert.AreEqual(30, myFunc.Invoke(23, 7));
Assert.AreEqual(30, myFunc.Invoke(32, -2));

内置类型和自定义类型

目前预定义的可用类型是

Object object 
Boolean bool 
Char char
String string
SByte Byte byte
Int16 UInt16 Int32 int UInt32 Int64 long UInt64 
Single Double double Decimal decimal 
DateTime TimeSpan
Guid
Math Convert

您可以通过使用 Interpreter.Reference 方法引用任何其他自定义 .NET 类型

var target = new Interpreter()
                .Reference(typeof(Uri));

Assert.AreEqual(typeof(Uri), target.Eval("typeof(Uri)"));
Assert.AreEqual(Uri.UriSchemeHttp, target.Eval("Uri.UriSchemeHttp"));

生成动态委托

您可以使用 Interpreter.ParseAsDelegate<TDelegate> 方法将表达式直接解析为 .NET 委托类型,该类型可以正常调用。在下面的示例中,我生成了一个 Func<Customer, bool> 委托,该委托可用于 LINQ 的 where 表达式。

class Customer
{
    public string Name { get; set; }
    public int Age { get; set; }
    public char Gender { get; set; }
}

[Test]
public void Linq_Where()
{
    var customers = new List<Customer> { 
                            new Customer() { Name = "David", Age = 31, Gender = 'M' },
                            new Customer() { Name = "Mary", Age = 29, Gender = 'F' },
                            new Customer() { Name = "Jack", Age = 2, Gender = 'M' },
                            new Customer() { Name = "Marta", Age = 1, Gender = 'F' },
                            new Customer() { Name = "Moses", Age = 120, Gender = 'M' },
                            };

    string whereExpression = "customer.Age > 18 && customer.Gender == 'F'";

    var interpreter = new Interpreter();
    Func<Customer, bool> dynamicWhere = interpreter.ParseAsDelegate<Func<Customer, bool>>(whereExpression, "customer");

    Assert.AreEqual(1, customers.Where(dynamicWhere).Count());
}

这是解析在编译时已知其参数和返回值的表达式的首选方法。

生成 lambda 表达式

您可以使用 Interpreter.ParseAsExpression<TDelegate> 方法将表达式直接解析为 .NET lambda 表达式 (Expression<TDelegate>)。

在下面的示例中,我生成了一个 Expression<Func<Customer, bool>> 表达式,该表达式可用于 Queryable LINQ 的 where 表达式或任何其他需要表达式的地方。例如 Entity Framework 或其他类似库。

class Customer
{
    public string Name { get; set; }
    public int Age { get; set; }
    public char Gender { get; set; }
}

[Test]
public void Linq_Queryable_Expression_Where()
{
    IQueryable<Customer> customers = (new List<Customer> { 
        new Customer() { Name = "David", Age = 31, Gender = 'M' },
        new Customer() { Name = "Mary", Age = 29, Gender = 'F' },
        new Customer() { Name = "Jack", Age = 2, Gender = 'M' },
        new Customer() { Name = "Marta", Age = 1, Gender = 'F' },
        new Customer() { Name = "Moses", Age = 120, Gender = 'M' },
    }).AsQueryable();

    string whereExpression = "customer.Age > 18 && customer.Gender == 'F'";

    var interpreter = new Interpreter();
    Expression<Func<Customer, bool>> expression = interpreter.ParseAsExpression<Func<Customer, bool>>(whereExpression, "customer");

    Assert.AreEqual(1, customers.Where(expression).Count());
}

语法和运算符

语句可以使用 C# 语法的子集编写。您可以在此处找到支持的表达式列表

运算符

类别运算符
主(Primary)x.y f(x) a[x] new typeof
一元运算+ - ! (T)x
乘法运算* / %
加法运算+ -
关系运算和类型测试< > <= >= is as
相等== !=
条件与&&
条件或||
Conditional?:
赋值=

运算符的优先级遵循 C# 规则(运算符优先级和关联性)

出于安全原因,某些运算符(如赋值运算符)可以被禁用。

字面量

类别运算符
常量true false null
数值f m
字符串/字符"" ''

字符串或字符字面量中支持以下转义序列

  • \' - 单引号,字符字面量需要
  • \" - 双引号,字符串字面量需要
  • \\ - 反斜杠
  • \0 - Unicode 字符 0
  • \a - 响铃(字符 7)
  • \b - 退格(字符 8)
  • \f - 换页(字符 12)
  • \n - 换行(字符 10)
  • \r - 回车(字符 13)
  • \t - 水平制表符(字符 9)
  • \v - 垂直制表符(字符 11)

类型成员调用

可以调用任何标准的 .NET 方法、字段、属性或构造函数。

var x = new MyTestService();
var target = new Interpreter().SetVariable("x", x);

Assert.AreEqual(x.HelloWorld(), target.Eval("x.HelloWorld()"));
Assert.AreEqual(x.AProperty, target.Eval("x.AProperty"));
Assert.AreEqual(x.AField, target.Eval("x.AField"));

var target = new Interpreter();
Assert.AreEqual(new DateTime(2015, 1, 24), target.Eval("new DateTime(2015, 1, 24)"));

Dynamic Expresso 还支持

  • 扩展方法

    var x = new int[] { 10, 30, 4 }; var target = new Interpreter() .Reference(typeof(System.Linq.Enumerable)) .SetVariable("x", x); Assert.AreEqual(x.Count(), target.Eval("x.Count()"));

  • 索引器方法(如 array[0])

  • 泛型,仅部分支持(仅隐式,您不能调用显式泛型方法)
  • Params 数组(请参阅 C# params 关键字)

区分大小写/不区分大小写

默认情况下,所有表达式都区分大小写(VARXvarx 不同,如同 C# 一样)。有一个选项可以使用不区分大小写的解析器。例如

var target = new Interpreter(InterpreterOptions.DefaultCaseInsensitive);

double x = 2;
var parameters = new[] {
                                        new Parameter("x", x.GetType(), x)
                                        };

Assert.AreEqual(x, target.Eval("x", parameters));
Assert.AreEqual(x, target.Eval("X", parameters));

标识符检测

有时,在解析表达式之前,您需要检查表达式中使用了哪些标识符(变量、类型、参数)。也许是因为您想验证它,或者您想要求用户输入给定表达式的参数值。因为如果您在没有正确参数的情况下解析表达式,将抛出异常。

在这种情况下,您可以使用 Interpreter.DetectIdentifiers 方法来获取已使用标识符的列表,包括已知和未知的。

var target = new Interpreter();

var detectedIdentifiers = target.DetectIdentifiers("x + y");

CollectionAssert.AreEqual(
    new []{ "x", "y"}, 
    detectedIdentifiers.UnknownIdentifiers.ToArray());

限制

并非所有 C# 语法都受支持。以下是一些不支持功能的示例

  • 多行表达式
  • for/foreach/while/do 运算符
  • 数组/列表/字典初始化
  • 显式泛型调用(如 method<type>(arg)
  • Lambda/委托声明(委托和 lambda 仅支持作为变量或参数,或作为表达式的返回类型)

异常

如果在解析过程中出现错误,始终会抛出 ParseException 类型的异常。ParseException 根据错误类型有几个专门的类(UnknownIdentifierException、NoApplicableMethodException 等)。

性能和多线程

Interpreter 类可以被多个线程使用,但不能修改它。本质上,只有 get 属性、ParseEval 方法是线程安全的。其他方法(SetVariableReference 等)必须在初始化阶段调用。LambdaParameter 类完全线程安全。

如果您需要多次运行相同的表达式并使用不同的参数,我建议您解析一次,然后多次调用已解析的表达式。

安全

如果您允许最终用户编写表达式,您必须考虑一些安全问题。

解析的表达式只能访问您使用 Interpreter.Reference 方法引用的 .NET 类型,或您作为变量或参数传递的类型。您必须注意您公开的类型。在任何情况下,生成的委托都像任何其他委托一样执行,并且可以应用标准的 .NET 安全规则(有关更多信息,请参阅 .NET Framework 安全性)。

如果表达式测试可以由用户直接编写,您必须确保只提供某些功能。以下是一些指导原则

例如,您可以禁用赋值运算符,以确保用户无法更改您不期望的一些值。默认情况下,赋值运算符是启用的,但您可以使用以下方式禁用它

var target = new Interpreter()
    .EnableAssignment(AssignmentOperators.None);

从 1.3 版本开始,为了防止恶意用户在表达式中调用意外的类型或程序集,某些反射方法已被阻止。例如,您不能写

var target = new Interpreter();
target.Eval("typeof(double).GetMethods()");
// or
target.Eval("typeof(double).Assembly")

此规则的唯一例外是 Type.Name 属性,出于调试原因允许使用。要启用标准的反射功能,您可以使用 Interpreter.EnableReflection 方法,例如

var target = new Interpreter()
    .EnableReflection();

使用场景

以下是 Dynamic Expresso 的一些可能的使用场景

  • 可编程应用程序
  • 允许用户注入可自定义的规则和逻辑,而无需重新编译
  • 评估动态函数或命令
  • LINQ 动态查询

未来路线图

请参阅 github 开放问题和里程碑

帮助和支持

如果您需要帮助,可以尝试以下方法之一

致谢

本项目基于两项早期工作

  • "使用 FunctionFactory 将字符串表达式转换为 Funcs,作者 Matthew Abbott" (http://www.fidelitydesign.net/?p=333)
  • DynamicQuery - Dynamic LINQ - Visual Studio 2008 示例:http://msdn.microsoft.com/en-us/vstudio/bb894665.aspx, http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

其他资源或类似项目

下面是一些我评估过或值得研究的类似项目。由于种种原因,没有一个项目完全符合我的需求,所以我决定写自己的解释器。

  • Roslyn Project - Compiler as a service - http://msdn.microsoft.com/en-us/vstudio/roslyn.aspx
    • Roslyn 发布后,本项目很可能可以直接使用 Roslyn 编译器/解释器。
  • Mono.CSharp - C# 编译器服务和运行时求值器 - http://docs.go-mono.com/index.aspx?link=N%3AMono.CSharp
  • NCalc - .NET 的数学表达式求值器 - http://ncalc.codeplex.com/
  • David Wynne CSharpEval https://github.com/DavidWynne/CSharpEval
  • CSharp Eval http://csharp-eval.com/
  • C# Expression Evaluator http://csharpeval.codeplex.com/
  • Jint - .NET 的 Javascript 解释器 - http://jint.codeplex.com/
  • Jurassic - .NET 的 Javascript 编译器 - http://jurassic.codeplex.com/
  • Javascrpt.net - javascript V8 引擎 - http://javascriptdotnet.codeplex.com/
  • CS-Script - http://www.csscript.net/
  • IronJS, IronRuby, IronPython
  • paxScript.NET http://eco148-88394.innterhost.net/paxscriptnet/

发行说明

  • 1.3.0

    • 允许禁用赋值运算符(#28)
    • 防止通过反射意外访问类型以提高安全性(#27)。从现在起,调用反射的表达式会抛出 ReflectionNotAllowedException
    • 添加了 Interpreter.EnableReflection 方法以在表达式中启用反射功能。
  • 1.2.0

    • 各种重构
    • 添加了 Interpreter.ParseAsDelegate 以生成委托。
    • 添加了 Interpreter.ParseAsExpression 以生成 Lambda 表达式。
    • 将某些方法标记为已弃用。
    • 修复:现在您可以在使用不区分大小写的委托时,使用不同的大小写指定参数名称。
    • 修复:解决调用 Lambda.Invoke(params object[] args) 时参数的预期顺序的 bug,感谢 Alex141
    • Lambda.Invoke(params object[] args) 现在只接受声明的参数,而在以前的版本中接受使用的参数。基本上,如果您解析一个带有 x 和 y 参数的表达式,但只使用了 x,您仍然需要在任何情况下都传递 y。这是因为此函数不知道参数名称,也无法确保。其他 Invoke 方法未更改。
  • 1.1.0

    • 添加了对等于赋值运算符 (=) 的支持。 #24
  • 1.0.0

    • 添加了 Interpreter.DetectIdentifiers 方法,用于在解析表达式之前发现表达式中使用的标识符(变量、参数、类型)。(#23
    • 添加了 CaseSensitiveReferencedTypesIdentifiers 属性,以了解 Interpreter 对象是如何构造的。
    • 添加了用于注册变量和类型的新方法(请参阅 SetIdentifier 和 Reference)。
    • 添加了 LanguageConstants 类,其中包含默认使用的最常见类型和标识符。
    • 删除了 Interpreter.Analyze 方法,因为它不太有用。要重现此功能,只需捕获异常。
    • 扩展了 Lambda 类,包含已使用的类型、标识符和解析表达式的参数。基本上,您可以了解特定表达式使用了哪些类型、变量和参数。
    • 添加了直接从 Lambda 编译到类型化委托的功能。
    • 内部代码重构
    • 现在解析的 lambda 只包含实际使用的参数,而不是提供的所有参数。
  • 0.11.4

    • 修复:自定义二元运算符不支持,再次!(#21
  • 0.11.3

    • 修复:自定义二元运算符不支持(#21
  • 0.11.2

    • 修复:调用整数文字上的方法(例如:5.ToString())(#20
  • 0.11.1

    • 修复:带转换的表达式解析失败(#19
  • 0.11.0

    • 当存在未知标识符时,改进了异常处理。
    • 添加了 Analyze 方法来检查表达式是否有效。请参阅 Interpreter.Analyze
  • 0.10.0

    • 添加了对不区分大小写表达式的支持。请参阅 InterpreterOptions 枚举。
  • 0.9.2

    修复:支持空字符串字面量(#17

  • 0.9.1

    修复:在接口类型上调用对象方法(#13

  • 0.9.0

    • 表达式的返回类型在需要时会自动转换(#9
    • 求值类型化表达式(#8
    • 隐式转换支持(#7
    • 可空类型支持(#5
    • 扩展方法支持(#2
    • 允许指定默认解释器配置(#12
    • Params 数组支持(#6
    • Enumerable 扩展支持(#3
  • 0.8.1

    修复:API 在错误公式上挂起(#1

  • 0.8.0

    小的 API 改进(不带参数的 Invoke())

  • 0.7.0

    支持字符串或字符字面量中的转义序列(例如 \t

  • 0.6.0

    首次正式 beta 发布

许可证

MIT 许可证

版权所有 (c) 2015 Davide Icardi

特此授予任何人获得本软件及相关文档文件(“软件”)副本的免费许可,不受限制地处理本软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售本软件的副本,并允许接收本软件的人这样做,但须满足以下条件:- 上述版权声明和本许可声明应包含在本软件的所有副本或实质性部分中。- 本软件按“原样”提供,不提供任何形式的保证,明示或暗示,包括但不限于适销性、特定用途的适用性和非侵权性的保证。在任何情况下,作者或版权所有者均不对因软件或使用或交易软件引起的任何索赔、损害或其他责任负责,无论是在合同、侵权行为或其他方面。

© . All rights reserved.