TagBites.Expressions is a Roslyn-based C# expression parser and evaluator for .NET.
It compiles runtime string expressions into strongly typed Func<> delegates or LambdaExpression expression trees, without creating a new assembly.
varoptions=newExpressionParserOptions{Parameters={(typeof(int),"a"),(typeof(int),"b")}};varfunc=ExpressionParser.Compile<Func<int,int,int>>("(a + b) / 2",options);intr=func(2,4);// 3Roslyn does the parsing, so expressions use real C# syntax with the compiler's semantics.
Try it online - type an expression and evaluate it in the browser.
dotnet add package TagBites.Expressions
Targets netstandard2.0. Only dependency is Microsoft.CodeAnalysis.CSharp.
Evaluate once:
ExpressionParser.Invoke("5 / 2.5");// 2dExpressionParser.Invoke<int>("new [] { 1, 2, 3 }.Sum()");// 6ExpressionParser.Invoke<int>("(a + b) / 2",("a",2),("b",4));// 3Compile once, run many times:
varoptions=newExpressionParserOptions{Parameters={(typeof(double),"x"),(typeof(double),"y")}};varfunc=ExpressionParser.Compile<Func<double,double,double>>("Math.Pow(x, y) + 5",options);func(2,10);// 1029func(2,2);// 9Bind an object as this:
varoptions=newExpressionParserOptions{Parameters={(typeof(TestModel),"this")},UseFirstParameterAsThis=true};ExpressionParser.Invoke("X + Y",options,newTestModel{X=1,Y=2});// 3Expose named values and delegates with GlobalMembers:
varoptions=newExpressionParserOptions{Parameters={(typeof(int),"a")},GlobalMembers={{"b",(null,2)}}};varfunc=ExpressionParser.Compile<Func<int,int>>("a switch { 1 => b, 2 => b * 2, _ => b + a }",options);func(3);// 5Import static classes, as if using static was applied:
varoptions=newExpressionParserOptions{StaticImports={typeof(Math)}};ExpressionParser.Invoke<double>("Sqrt(Max(9, 16)) + PI",options);// 7.14159...String interpolation, including alignment and format specifiers (formatting follows the current culture):
ExpressionParser.Invoke(@"$""sum = {1 + 2}""");// sum = 3ExpressionParser.Invoke(@"$""{5,-4}|""");// "5 |" (left aligned)ExpressionParser.Invoke(@"$""{5,6:000}""");// " 005" (alignment + format)ExpressionParser.Invoke(@"$""{255:X}""");// FFExpressionParser.Invoke(@"$""{new DateTime(2021, 8, 14):yyyy-MM-dd}""");// 2021-08-14ExpressionParser.Invoke(@"$""{(1 < 2 ? ""yes"" : ""no"")}""");// yesAnonymous objects (new { ... }) behave like real anonymous types without generating one - internally they map to DynamicObject:
varscript="new[] { 1, 2, 3 }.Select(v => new { Value = v, Doubled = v * 2 }).Sum(v => v.Value + v.Doubled)";dynamicresult=ExpressionParser.Invoke(script);Console.WriteLine(result);// 18Get the expression tree, or parse without throwing:
LambdaExpressionlambda=ExpressionParser.Parse("x * 2 + 1",options);if(!ExpressionParser.TryParse("a + ",options,outvarexpr,outvarerror))Console.WriteLine(error);Typical uses: business rules and predicates defined at runtime, user-defined formulas and calculations, configurable filters and scoring logic.
- Operators: arithmetic, bitwise, shifts, comparison,
&& || !,?:,??,?./?[],is/as,x!. - User-defined operator overloads and user-defined implicit/explicit conversions.
- Literals: all numeric types,
char,string, verbatim, raw and interpolated strings, hex, digit separators. - Members and calls: properties, fields, indexers (including index-from-end
x[^1]), generic and extension methods,params. - Named arguments (
Method(digits: 2, value: 1)), including reordering, mixing positional and named arguments, and skipping optional parameters. new: constructors, object initializers (including index initializers["key"] = value), collection initializers, arrays (jagged, multidimensional including implicitly typednew[,], and sized), target-typednew()(including as a method or constructor argument).- Anonymous objects (
new { X = 1, Y = 2 }- see Usage above). - Lambdas and LINQ (
Select,Where,GroupBy, ...), including nested and multi-argument lambdas. - Tuples, including named elements (
(Name: "Bob", Age: 30).Name) and element-wise equality. typeof,default(T), the baredefaultliteral (target-typed),nameof,sizeof,checked,unchecked.- Pattern matching in
isandswitch: type, constant, relational,and/or/not, property (including extended{ A.B: 1 }), positional,varand list patterns,whenguards. throwexpressions in?:,??, switch arms and as a lambda body (x > 0 ? x : throw new ArgumentException()), opt-in via theAllowThrowExpressionsoption.
Not currently supported:
- LINQ query syntax (
from x in items where x > 1 select x) - use the method syntax (items.Where(x => x > 1)). - The range operator (
1..2,arr[1..^1]). - Method group conversion as an argument (
items.Select(int.Parse)) - use a lambda (items.Select(x => int.Parse(x))). - Tuple types in a type position (
default((int, int)),new (int A, int B)[] { ... }) - name the elements through values instead. - The unsigned right-shift operator
>>>- depends on theMicrosoft.CodeAnalysis.CSharpversion the parser is built against. - Invoking a delegate value (
((Func<int, int>)(x => x * 2))(5),new Func<int, int>(x => x + 1)(4)) - call a method instead. - Wrapping a lambda in a delegate creation inside another lambda (
items.Select(x => new Func<int, int>(y => x + y))).
Not supported:
- Statements (like
if),async/await, and declarations (methods, types) are out of scope - this is an expression parser. - Block-bodied lambdas (
items.Select(x => { ...; return x; })) - a block is a statement. - Compound assignment and increment/decrement (
x += 1,x++,--x,??=) - this is an expression parser, expressions don't mutate variables. ref/outarguments, includingout vardeclarations (e.g.int.TryParse(s, out var n)).
// Switch expression1switch{1=>10,2=>20, _ =>0}// Switch expression with a `when` guard5switch{5when1>2=>1,5=>2, _ =>0}// Relational and logical patterns5is>0 and <10// List pattern with a slicenew[]{1,2,3}is[1, ..,3]// Tuple deconstruction pattern(1,2)is(inta,intb)&&a< b
// Property pattern"ab"is{Length:2}// Target-typed new() in nested collection initializers
new List<List<int>>{new(){1,2},new(){3,4}}[1][0]// Jagged arraynewint[][]{new[]{1},new[]{2,3}}[1][1]// 3// Implicitly typed multidimensional arraynew[,]{{1,2},{3,4}}[1,0]// 3// Dictionary index initializernewDictionary<string,int>{["a"]=1,["b"]=2}["b"]// 2// Raw string literal"""hello world""".Length// Digit separators1_000_000// Index from endnew[]{1,2,3}[^1]// Null-forgiving operator"a"!.Length
// Unchecked integer overflow, same wraparound as C#unchecked(2147483647+1)// Generic method call with an explicit type argumentnew[]{1,2,3}.OfType<int>().Count()// User-defined operator overload (DateTime.op_Addition / op_GreaterThan)
DateTime.Now+TimeSpan.FromDays(1)>DateTime.Now// Tuple equality(1,2)==(1,2)// Tuple with named elements(Name:"Bob",Age:30).Name// Named arguments, reordered
Math.Round(digits:2,value:2.567)// Bare default literal, target-typed from the other argument
Math.Max(default,5)// Anonymous object carrying a named tuple, combined with named args and lambdas
new[]{1,2,3}.Select(n =>new{N=n,Stats=(Sum:n+n,Label:$"#{n}")}).Where(x =>x.Stats.Sum>=4).Select(x =>Math.Round(digits:0,value:(double)x.Stats.Sum)+x.Stats.Label.Length).Sum()// 14ExpressionParserOptions controls what an expression may reference: parameters, global members, allowed types, static imports, member cache and more.
Guide: Configuration.
ExpressionParser.Parse() returns a plain LambdaExpression, so any compiler can turn it into a delegate. FastExpressionCompiler is a drop-in replacement for the built-in Compile() that produces the same delegate 12 to 30 times faster.
Guide: FastExpressionCompiler.
CustomPropertyResolver lets an expression navigate types whose shape only exists at runtime - a database row, a CMS content type, a value that lives in another process. LINQ over such a collection needs no extra code, because the parser propagates the element type info through method chains.
Guide: Dynamic / Runtime-defined types.
| TagBites.Expressions | DynamicExpresso | System.Linq.Dynamic.Core | Roslyn scripting (CSharpScript) | |
|---|---|---|---|---|
| Language | C# expressions (Roslyn) | C#-like (own parser) | Dynamic LINQ dialect | Full C# (official) |
| Output | Delegate / Expression | Delegate / Expression | Expression tree | Compiled assembly |
| Startup / memory | Low | Low | Low | High |
| Dependency | Roslyn | None | None | Roslyn |
The table below is generated by LibraryFeatureComparer.cs (run the benchmarks project with the feature-comparer argument).
Rows are ordered by how many of the three libraries support each feature, most first:
| C# syntax | TagBites.Expressions v. 1.4.0 | DynamicExpresso v. 2.19.3 | System.Linq.Dynamic.Core v. 1.7.3 |
|---|---|---|---|
| Arithmetic and logical operators | ✅ | ✅ | ✅ |
| Ternary | ✅ | ✅ | ✅ |
| Member access and method calls | ✅ | ✅ | ✅ |
params method call (string.Format("{0}{1}", 1, 2)) | ✅ | ✅ | ✅ |
| Lambdas and LINQ | ✅ | ✅ | ✅ |
is / as | ✅ | ✅ | ❌ |
typeof, default(T) | ✅ | ✅ | ❌ |
Null-coalescing ?? / null-conditional ?. | ✅ | ✅ | ❌ |
| Object and collection initializers | ✅ | ✅ | ❌ |
Static members on a generic type (Comparer<int>.Default) | ✅ | ✅ | ❌ |
User-defined operator overloads (DateTime.Now + TimeSpan.FromDays(1)) | ✅ | ❌ | ✅ |
| User-defined implicit/explicit conversion operators | ✅ | ❌ | ✅ |
Named arguments, reordered (Substring(length: 2, startIndex: 1)) | ✅ | ❌ | ❌ |
Indexers and index-from-end (xs[^1]) | ✅ | ❌ | ❌ |
Bare default literal (target-typed) | ✅ | ❌ | ❌ |
Index initializers (new Dictionary<string, int> { ["a"] = 1 }) | ✅ | ❌ | ❌ |
Verbatim strings @"..." | ✅ | ❌ | ❌ |
Digit separators 1_000 | ✅ | ❌ | ❌ |
String interpolation $"{x,6:0.00}" (alignment + format) | ✅ | ❌ | ❌ |
Raw string literals """...""" | ✅ | ❌ | ❌ |
| Tuples and tuple equality | ✅ | ❌ | ❌ |
| Tuples with named elements | ✅ | ❌ | ❌ |
Anonymous objects (new { X = 1 }) | ✅ | ❌ | ❌ |
Null-forgiving x! | ✅ | ❌ | ❌ |
checked / unchecked | ✅ | ❌ | ❌ |
nameof, sizeof | ✅ | ❌ | ❌ |
| Array creation: sized and multidimensional | ✅ | ❌ | ❌ |
Jagged arrays (new int[][] { ... }) | ✅ | ❌ | ❌ |
Implicit arrays with the best common type (new[] { 1, 2L }) | ✅ | ❌ | ❌ |
Target-typed new() | ✅ | ❌ | ❌ |
Lambdas for Predicate<T>/Comparison<T> delegates (list.Find(x => x > 1)) | ✅ | ❌ | ❌ |
Nested types (typeof(List<int>.Enumerator)) | ✅ | ❌ | ❌ |
throw expressions (x > 0 ? x : throw ...) - opt-in | ✅ | ❌ | ❌ |
Generic method call with explicit type argument (xs.OfType<int>()) | ✅ | ❌ | ❌ |
Static imports (using static, unqualified Sqrt(16)) | ✅ | ❌ | ❌ |
| Switch expressions | ✅ | ❌ | ❌ |
Pattern matching: relational, and/or/not, property | ✅ | ❌ | ❌ |
Patterns against an object input ((object)x is > 3) | ✅ | ❌ | ❌ |
List patterns (arr is [1, 2, 3]) | ✅ | ❌ | ❌ |
Tuple/recursive deconstruction patterns (x is (int a, int b)) | ✅ | ❌ | ❌ |
✅/❌ is based on parsing and evaluating each expression to the expected result, not just on whether parsing throws.
The table below is generated by Program.cs.
| TestCase | TagBites.Expressions v. 1.5.0 | DynamicExpresso v. 2.19.3 | System.Linq.Dynamic.Core v. 1.7.3 |
|---|---|---|---|
| Parse | 12,57 us (1,00x)5,99 KB (1,00x) | 43,65 us (3,47x)30,88 KB (5,16x) | 4020,53 us (319,8x)281,82 KB (47,09x) |
| Parse_SharedEnv | 8,10 us (1,00x)3,00 KB (1,00x) | 25,42 us (3,14x)12,39 KB (4,13x) | 111,47 us (13,75x)102,71 KB (34,24x) |
| ParseCalls | 57,40 us (1,00x)29,86 KB (1,00x) | 87,20 us (1,52x)49,65 KB (1,66x) | 4397,08 us (76,61x)302,09 KB (10,12x) |
| ParseCalls_SharedEnv | 22,72 us (1,00x)9,78 KB (1,00x) | 74,94 us (3,30x)31,73 KB (3,24x) | 166,61 us (7,33x)123,37 KB (12,62x) |
| ParseLambda | 105,05 us (1,00x)36,56 KB (1,00x) | 435,01 us (4,14x)122,43 KB (3,35x) | 4041,08 us (38,47x)211,07 KB (5,77x) |
| ParseLambda_SharedEnv | 37,33 us (1,00x)10,97 KB (1,00x) | 365,05 us (9,78x)103,99 KB (9,48x) | 64,66 us (1,73x)36,08 KB (3,29x) |
SharedEnv = shared options/interptreter/config.
SharedOptions for TagBites.Expressions usesUseMemberCache = true.
"Parse" expression:Math.Pow(x, y) + 5
"ParseCalls" expression:name.Trim().ToUpper().Length + Math.Round(total, 2)
"ParseLambda" expression:list.Where(x => x > limit).Select(x => Math.Pow(x, y)).Sum()
Benchmark source: ParseToExpression.cs.