TagBites.Expressions
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.
var options = new ExpressionParserOptions { Parameters = { (typeof(int), "a"), (typeof(int), "b") } };var func = ExpressionParser.Compile<Func<int, int, int>>("(a + b) / 2", options);int r = 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.
Install
Section titled “Install”dotnet add package TagBites.ExpressionsTargets 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:
var options = new ExpressionParserOptions { Parameters = { (typeof(double), "x"), (typeof(double), "y") } };var func = ExpressionParser.Compile<Func<double, double, double>>("Math.Pow(x, y) + 5", options);func(2, 10); // 1029func(2, 2); // 9Bind an object as this:
var options = new ExpressionParserOptions{ Parameters = { (typeof(TestModel), "this") }, UseFirstParameterAsThis = true};
ExpressionParser.Invoke("X + Y", options, new TestModel { X = 1, Y = 2 }); // 3Expose named values and delegates with GlobalMembers:
var options = new ExpressionParserOptions{ Parameters = { (typeof(int), "a") }, GlobalMembers = { { "b", (null, 2) } }};
var func = 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:
var options = new ExpressionParserOptions { 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:
var script = "new[] { 1, 2, 3 }.Select(v => new { Value = v, Doubled = v * 2 }).Sum(v => v.Value + v.Doubled)";dynamic result = ExpressionParser.Invoke(script);Console.WriteLine(result); // 18Get the expression tree, or parse without throwing:
LambdaExpression lambda = ExpressionParser.Parse("x * 2 + 1", options);
if (!ExpressionParser.TryParse("a + ", options, out var expr, out var error)) Console.WriteLine(error);Use cases
Section titled “Use cases”Typical uses: business rules and predicates defined at runtime, user-defined formulas and calculations, configurable filters and scoring logic.
Supported C# expression syntax
Section titled “Supported C# expression syntax”- 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)).
Supported expressions examples
Section titled “Supported expressions examples”// Switch expression1 switch { 1 => 10, 2 => 20, _ => 0 }
// Switch expression with a `when` guard5 switch { 5 when 1 > 2 => 1, 5 => 2, _ => 0 }
// Relational and logical patterns5 is > 0 and < 10
// List pattern with a slicenew[] { 1, 2, 3 } is [1, .., 3]
// Tuple deconstruction pattern(1, 2) is (int a, int b) && a < b
// Property pattern"ab" is { Length: 2 }
// Target-typed new() in nested collection initializersnew List<List<int>> { new() { 1, 2 }, new() { 3, 4 } }[1][0]
// Jagged arraynew int[][] { new[] { 1 }, new[] { 2, 3 } }[1][1] // 3
// Implicitly typed multidimensional arraynew[,] { { 1, 2 }, { 3, 4 } }[1, 0] // 3
// Dictionary index initializernew Dictionary<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, reorderedMath.Round(digits: 2, value: 2.567)
// Bare default literal, target-typed from the other argumentMath.Max(default, 5)
// Anonymous object carrying a named tuple, combined with named args and lambdasnew[] { 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() // 14Configuration
Section titled “Configuration”ExpressionParserOptions controls what an expression may reference: parameters, global members, allowed types, static imports, member cache and more.
Guide: Configuration.
Advanced usage
Section titled “Advanced usage”FastExpressionCompiler
Section titled “FastExpressionCompiler”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.
Dynamic / Runtime-defined types
Section titled “Dynamic / Runtime-defined types”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.
Alternatives
Section titled “Alternatives”| 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 |
Comparison
Section titled “Comparison”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.
Benchmark
Section titled “Benchmark”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.