Dynamic Expresso的应用

Dynamic Expresso

Supported platforms: .NET Core 3.1, .NET Core 5.0 and above, .NET 4.6.2

  • Dynamic Expresso is an interpreter for simple C# statements written in .NET Standard 2.0.
    Dynamic Expresso 是用 .NET Standard 2.0 编写的简单 C# 语句的解释器。

  • Dynamic Expresso embeds its own parsing logic, really interprets C# statements by converting it to .NET lambda expressions or delegates.
    Dynamic Expresso 嵌入了自己的解析逻辑,通过将 C# 语句转换为 .NET lambda 表达式或委托来真正解释 C# 语句。

  • Using Dynamic Expresso developers can create scriptable applications, execute .NET code without compilation or create dynamic linq
    statements.
    使用 Dynamic Expresso 开发人员可以创建可编写脚本的应用程序,无需编译即可执行 .NET 代码或创建动态 linq 语句。

  • Statements are written using a subset of C# language specifications. Global variables or parameters can be injected and used inside
    expressions. It doesn’t generate assembly but it creates an expression tree on the fly.
    语句是使用 C# 语言规范的子集编写的。全局变量或参数可以在表达式中注入和使用。它不会生成程序集,但会动态创建表达式树。
    在这里插入图片描述
    例如,您可以评估数学表达式:
    For example you can evaluate math expressions:

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

或解析带有变量或参数的表达式并多次调用它:
or parse an expression with variables or parameters and invoke it multiple times:

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 表达式:
or generate delegates and lambda expressions for LINQ queries:

var prices = new [] { 5, 8, 6, 2 };
var whereFunction = new Interpreter().ParseAsDelegate<Func<int, bool>>("arg > 5");
var count = prices.Where(whereFunction).Count();

Live demo

Dynamic Expresso live demo: http://dynamic-expresso.azurewebsites.net/

Quick start

Dynamic Expresso is available on [NuGet]. You can install the package using:

PM> Install-Package DynamicExpresso.Core

Source code and symbols (.pdb files) for debugging are available on [Symbol Source].

Features

  • Expressions can be written using a subset of C# syntax (see Syntax section for more information)
  • Support for variables and parameters
  • Can generate delegates or lambda expression
  • Full suite of unit tests
  • Good performance compared to other similar projects
  • Partial support of generic, params array and extension methods (only with implicit generic arguments detection)
  • Partial support of dynamic (ExpandoObject for get properties, method invocation and indexes(#142), see #72. DynamicObject for get properties and indexes, see #142)
  • Partial support of lambda expressions (disabled by default, because it has a slight performance penalty)
  • Case insensitive expressions (default is case sensitive)
  • Ability to discover identifiers (variables, types, parameters) of a given expression
  • Small footprint, generated expressions are managed classes, can be unloaded and can be executed in a single appdomain
  • Easy to use and deploy, it is all contained in a single assembly without other external dependencies
  • Written in .NET Standard 2.0
    • Build available for .NET 4.6.1 and .NET Core 2.0
  • Open source (MIT license)
  • 可以使用 C# 语法的子集编写表达式(有关更多信息,请参阅语法部分)
  • 支持变量和参数
  • 可以生成委托或 lambda 表达式
  • 全套单元测试
  • 与其他同类项目相比表现良好
  • 部分支持泛型、参数数组和扩展方法(仅用于隐式泛型参数检测)
  • 部分支持dynamic(ExpandoObject获取属性、方法调用和索引(#142),参见#72。DynamicObject获取属性和索引,参见#142)
  • 部分支持 lambda 表达式(默认禁用,因为它有轻微的性能损失)
  • 不区分大小写的表达式(默认区分大小写)
  • 发现给定表达式的标识符(变量、类型、参数)的能力
  • 占用空间小,生成的表达式是托管类,可以卸载并且可以在单个 appdomain 中执行
  • 易于使用和部署,全部包含在单个程序集中,没有其他外部依赖项
  • 用 .NET 标准 2.0 编写
  • 可用于 .NET 4.6.1 和 .NET Core 2.0 的构建

Return value

You can parse and execute void expression (without a return value) or you can return any valid .NET type.
When parsing an expression you can specify the expected expression return type. For example you can write:

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));

The built-in parser can also understand the return type of any given expression so you can check if the expression returns what you expect.

Variables

Variables can be used inside expressions with Interpreter.SetVariable method:

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

Assert.AreEqual(23, target.Eval("myVar"));

Variables can be primitive types or custom complex types (classes, structures, delegates, arrays, collections, …).

Custom functions can be passed with delegate variables using Interpreter.SetFunction method:

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)"));

Custom Expression can be passed by using Interpreter.SetExpression method.

Parameters

Parsed expressions can accept one or more parameters:

var interpreter = new Interpreter();

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

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

Parameters can be primitive types or custom types. You can parse an expression once and invoke it multiple times with different parameter values:

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));

Special identifiers

Either a variable or a parameter with name this can be referenced implicitly.

class Customer { public string Name { get; set; } }

var target = new Interpreter();

// 'this' is treated as a special identifier and can be accessed implicitly 
target.SetVariable("this", new Customer { Name = "John" });

// explicit context reference via 'this' variable
Assert.AreEqual("John", target.Eval("this.Name"));

// 'this' variable is referenced implicitly
Assert.AreEqual("John", target.Eval("Name"));

Built-in types and custom types

Currently predefined types available are:

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

You can reference any other custom .NET type by using Interpreter.Reference method:

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

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

Generate dynamic delegates 生成动态委托

You can use the Interpreter.ParseAsDelegate<TDelegate> method to directly parse an expression into a .NET delegate type that can be normally invoked.
In the example below I generate a Func<Customer, bool> delegate that can be used in a LINQ where expression.

您可以使用该Interpreter.ParseAsDelegate方法将表达式直接解析为可以正常调用的 .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());
}

This is the preferred way to parse an expression that you known at compile time what parameters can accept and what value must return.

Generate lambda expressions 生成 lambda 表达式

You can use the Interpreter.ParseAsExpression<TDelegate> method to directly parse an expression into a .NET lambda expression (Expression<TDelegate>).
您可以使用该Interpreter.ParseAsExpression方法直接将表达式解析为 .NET lambda 表达式 ( Expression)。

In the example below I generate a Expression<Func<Customer, bool>> expression that can be used in a Queryable LINQ where expression or in any other place where an expression is required. Like Entity Framework or other similar libraries.

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

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());
}

Syntax and operators 语法和运算符

Statements can be written using a subset of the C# syntax. Here you can find a list of the supported expressions:
可以使用 C# 语法的子集编写语句。在这里您可以找到支持的表达式列表:

Operators

Supported operators:
项目 | Value

CategoryOperators
Primaryx.y f(x) a[x] new typeof
Unary+ - ! (T)x
Multiplicative* / %
Additive+ -
Relational and type testing< > <= >= is as
Equality== !=
Logical AND&
Logical OR|
Logical XOR^
Conditional AND&&
Conditional OR||
Conditional?:
Assignment=
Null coalescing??

Operators precedence is respected following C# rules (Operator precedence and associativity).
运算符优先级遵循C# 规则(运算符优先级和关联性)。
Some operators, like the assignment operator, can be disabled for security reason.

Literals

CategoryOperators
Constantstrue false null
Real literal suffixesd f m
Integer literal suffixesu l ul lu
String/char"" ''

The following character escape sequences are supported inside string or char literals:

  • \' - single quote, needed for character literals
  • \" - double quote, needed for string literals
  • \\ - backslash
  • \0 - Unicode character 0
  • \a - Alert (character 7)
  • \b - Backspace (character 8)
  • \f - Form feed (character 12)
  • \n - New line (character 10)
  • \r - Carriage return (character 13)
  • \t - Horizontal tab (character 9)
  • \v - Vertical quote (character 11)

Type’s members invocation 类型的成员调用

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

Any standard .NET method, field, property or constructor can be invoked.

var service = new MyTestService();
var context = new MyTestContext();

var target = new Interpreter()
  .SetVariable("x", service)
  .SetVariable("this", context);

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

// implicit context reference
Assert.AreEqual(context.GetContextId(), target.Eval("GetContextId()"));
Assert.AreEqual(context.ContextName, target.Eval("ContextName"));
Assert.AreEqual(context.ContextField, target.Eval("ContextField"));
var target = new Interpreter();
Assert.AreEqual(new DateTime(2015, 1, 24), target.Eval("new DateTime(2015, 1, 24)"));

Dynamic Expresso also supports:

  • Extension methods 扩展方法
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()"));
  • Indexer methods (like array[0])
  • Generics, only partially supported (only implicit, you cannot invoke an explicit generic method)
  • Params array (see C# params keyword)
  • 索引器方法(如array[0])
  • 泛型,仅部分支持(仅隐式,不能调用显式泛型方法)
  • 参数数组(参见 C#params关键字)

Lambda expressions

Dynamic Expresso has partial supports of lambda expressions. For example, you can use any Linq method:
Dynamic Expresso 部分支持 lambda 表达式。例如,您可以使用任何 Linq 方法:

var x = new string[] { "this", "is", "awesome" };
var options = InterpreterOptions.Default | InterpreterOptions.LambdaExpressions; // enable lambda expressions
var target = new Interpreter(options)
	.SetVariable("x", x);

var results = target.Eval<IEnumerable<string>>("x.Where(str => str.Length > 5).Select(str => str.ToUpper())");
Assert.AreEqual(new[] { "AWESOME" }, results);

Note that parsing lambda expressions is disabled by default, because it has a slight performance cost.
To enable them, you must set the InterpreterOptions.LambdaExpressions flag.

It’s also possible to create a delegate directly from a lambda expression:

var options = InterpreterOptions.Default | InterpreterOptions.LambdaExpressions; // enable lambda expressions
var target = new Interpreter(options)
	.SetVariable("increment", 3); // access a variable from the lambda expression

var myFunc = target.Eval<Func<int, string, string>>("(i, str) => str.ToUpper() + (i + increment)");
Assert.AreEqual("TEST8", lambda.Invoke(5, "test"));

Case sensitive/insensitive

By default all expressions are considered case sensitive (VARX is different than varx, as in C#).
There is an option to use a case insensitive parser. For example:

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));

Identifiers detection

Sometimes you need to check which identifiers (variables, types, parameters) are used in expression before parsing it.
Maybe because you want to validate it or you want to ask the user to enter parameters value of a given expression.
Because if you parse an expression without the right parameter an exception is throwed.

In these cases you can use Interpreter.DetectIdentifiers method to obtain a list of used identifiers, both known and unknown.

var target = new Interpreter();

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

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

Default number type

In C #, numbers are usually interpreted as integers or doubles if they have decimal places.
在 C# 中,如果数字有小数位,通常会被解释为整数或双精度数。

In some cases it may be useful to be able to configure the default type of numbers if no particular suffix is ​​specified: for example in financial calculations, where usually numbers are interpreted as decimal type.
在某些情况下,如果没有指定特定的后缀,那么配置默认数字类型可能会很有用:例如在金融计算中,通常将数字解释为十进制类型。

In these cases you can set the default number type using Interpreter.SetDefaultNumberType method.
在这些情况下,您可以使用方法设置默认号码类型Interpreter.SetDefaultNumberType 。

var target = new Interpreter();

target.SetDefaultNumberType(DefaultNumberType.Decimal);

Assert.IsInstanceOf(typeof(System.Decimal), target.Eval("45"));
Assert.AreEqual(10M/3M, target.Eval("10/3")); // 3.33333333333 instead of 3

Limitations

Not every C# syntaxes are supported. Here some examples of NOT supported features:

  • Multiline expressions
  • for/foreach/while/do operators
  • Array/list/dictionary initialization
  • Explicit generic invocation (like method<type>(arg))
  • Lambda/delegate declaration (delegate and lamda are only supported as variables or parameters or as a return type of the expression)
  • Array/list/dictionary element assignment (set indexer operator)
  • Other operations on dynamic objects (only property, method invocation and index now are supported)

Exceptions

If there is an error during the parsing always an exception of type ParseException is throwed.
ParseException has several specialization classes based on the type of error (UnknownIdentifierException, NoApplicableMethodException. …).

Performance and multithreading

The Interpreter class can be used by multiple threads but without modify it.
In essence only get properties, Parse and Eval methods are thread safe. Other methods (SetVariable, Reference, …) must be called in an initialization phase.
Lambda and Parameter classes are completely thread safe.

If you need to run the same expression multiple times with different parameters I suggest to parse it one time and then invoke the parsed expression multiple times.

Security

If you allow an end user to write expression you must consider some security implications.

Parsed expressions can access only the .NET types that you have referenced using the Interpreter.Reference method or types that you pass as a variable or parameter.
You must pay attention of what types you expose.
In any case generated delegates are executed as any other delegate and standard security .NET rules can be applied (for more info see Security in the .NET Framework).

If expressions test can be written directly by users you must ensure that only certain features are available. Here some guidelines:

For example you can disable assignment operators, to ensure that the user cannot change some values that you don’t expect.
By default assignment operators are enables, by you can disable it using:

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

From version 1.3 to prevent malicious users to call unexpected types or assemblies within an expression,
some reflection methods are blocked. For example you cannot write:

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

The only exception to this rule is the Type.Name property that is permitted for debugging reasons.
To enable standard reflection features you can use Interpreter.EnableReflection method, like:

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

Usage scenarios

Here are some possible usage scenarios of Dynamic Expresso:

  • Programmable applications
  • Allow the user to inject customizable rules and logic without recompiling
  • Evaluate dynamic functions or commands
  • LINQ dynamic query
  • 可编程应用
  • 允许用户在不重新编译的情况下注入可定制的规则和逻辑
  • 评估动态函数或命令
  • LINQ 动态查询

Future roadmap

See github open issues and milestones.

Help and support

If you need help you can try one of the following:

Maintainers

Currently Dynamic Expresso is maintained by @davideicardi and @metoule.

Credits

This project is based on two old works:

  • “Converting String expressions to Funcs with FunctionFactory by Matthew Abbott” (link not more available)
  • DynamicQuery - Dynamic LINQ - Visual Studio 2008 sample: http://msdn.microsoft.com/en-us/vstudio/bb894665.aspx

Thanks to all contributors!

Other resources or similar projects

Below you can find a list of some similar projects that I have evaluated or that can be interesting to study.
For one reason or another none of these projects exactly fit my needs so I decided to write my own interpreter.

  • Roslyn Project - Scripting API - https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples
    • This is the new Microsoft Official Compiler as a service library. I suggest to consider using Roslyin instead of DynamicExpresso for complex scenarios.
  • Mono.CSharp - C# Compiler Service and Runtime Evaulator - http://docs.go-mono.com/index.aspx?link=N%3AMono.CSharp
  • NCalc - Mathematical Expressions Evaluator for .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 - Javascript interpreter for .NET - http://jint.codeplex.com/
  • Jurassic - Javascript compiler for .NET - http://jurassic.codeplex.com/
  • Javascrpt.net - javascript V8 engine - http://javascriptdotnet.codeplex.com/
  • CS-Script - http://www.csscript.net/
  • IronJS, IronRuby, IronPython
  • paxScript.NET http://eco148-88394.innterhost.net/paxscriptnet/

Continuous Integration

A continuous integration pipeline is configured using Github Actions, see .github/workflows folder.

Whenever a new Release is created, Nuget packages are published. For snapshot releases packages are published only to Github.
For official releases packages are published to both GitHub and Nuget.

Compiling and run tests

To compile the solution you can run:

dotnet build DynamicExpresso.sln -c Release

To create nuget packages:

dotnet pack DynamicExpresso.sln -c Release

To run unit tests:

dotnet test DynamicExpresso.sln -c Release

or run unit tests for a specific project with a specific framework:

dotnet test DynamicExpresso.sln --no-restore -c Release --verbosity normal -f netcoreapp3.1

Add --logger:trx to generate test results for VSTS.

Release notes

See releases page.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是刘彦宏吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值