EvalExpression 1.0.0
dotnet add package EvalExpression --version 1.0.0
NuGet\Install-Package EvalExpression -Version 1.0.0
<PackageReference Include="EvalExpression" Version="1.0.0" />
paket add EvalExpression --version 1.0.0
#r "nuget: EvalExpression, 1.0.0"
// Install EvalExpression as a Cake Addin
#addin nuget:?package=EvalExpression&version=1.0.0
// Install EvalExpression as a Cake Tool
#tool nuget:?package=EvalExpression&version=1.0.0
EvalExpression: C# Eval Expression
EvalExpression is a robust NuGet package that enables .NET developers to evaluate, compile, and execute dynamic C# expressions. By interpreting and processing strings as executable code, this package empowers developers to craft highly adaptable and customizable applications.
It has the capability to handle an extensive range of mathematical operations, string manipulations, boolean logic, and it can even interact with .NET objects and classes.
The principal advantage of EvalExpression lies in its ability to provide enhanced flexibility by facilitating runtime execution and manipulation of C# expressions. This feature is particularly beneficial in scenarios where business rules are subject to frequent changes or need to be configurable.
Supported .NET Versions
EvalExpression exhibits broad compatibility with a variety of .NET versions. Starting from .NET Framework 4.5 and .NET Core 2.0, it extends support to the most recent versions of .NET.
To take full advantage of the enhancements in newer .NET versions, we highly recommend upgrading to the latest package version of EvalExpression.
Main 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.DynamicObject
for get properties and indexes) - 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
Getting Started
EvalExpression is available on [NuGet]. You can install the package using:
PM> Install-Package EvalExpression
Source code and symbols (.pdb files) for debugging are available on [Symbol Source].
To get a feel for EvalExpression, here is a simple example:
using EvalExpression;
public class Program
{
public static void Main()
{
var interpreter = new Interpreter();
var result = interpreter.Eval<int>("X*Y", new Parameter("X", typeof(int), 10), new Parameter("Y", typeof(int), 20));
Console.WriteLine(result); // Outputs: 200
}
}
In the above example, the "X*Y" string expression is evaluated dynamically at runtime with the values of X and Y provided in an anonymous object. The result is then printed to the console.
Advanced Usage
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.
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
You can use the Interpreter.ParseAsExpression<TDelegate>
method to directly parse an expression into a .NET lambda expression (Expression<TDelegate>
).
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.
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:
Operators
Supported operators:
Category | Operators |
---|---|
Primary | x.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).
Some operators, like the assignment operator, can be disabled for security reason.
Literals
Category | Operators |
---|---|
Constants | true false null |
Real literal suffixes | d f m |
Integer literal suffixes | u 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
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)"));
EvalExpression 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)
Lambda expressions
EvalExpression has partial supports of lambda expressions. For example, you can use any Linq method:
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.
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.
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 EvalExpression:
- Programmable applications
- Allow the user to inject customizable rules and logic without recompiling
- Evaluate dynamic functions or commands
- LINQ dynamic query
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.CSharp (>= 4.7.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.0.0 | 100 | 3/4/2024 |