Expreszo 0.4.3
dotnet add package Expreszo --version 0.4.3
NuGet\Install-Package Expreszo -Version 0.4.3
<PackageReference Include="Expreszo" Version="0.4.3" />
<PackageVersion Include="Expreszo" Version="0.4.3" />
<PackageReference Include="Expreszo" />
paket add Expreszo --version 0.4.3
#r "nuget: Expreszo, 0.4.3"
#:package Expreszo@0.4.3
#addin nuget:?package=Expreszo&version=0.4.3
#tool nuget:?package=Expreszo&version=0.4.3
<picture> <source media="(prefers-color-scheme: dark)" srcset="docs/logo_dark.png"> <img src="docs/logo.png" alt="ExpresZo" width="420"> </picture>
ExpresZo .NET
A safe, extensible expression evaluator for .NET - a configurable alternative to eval(). Parses and evaluates the same expression language as expreszo-typescript.
Highlights
- Pratt parser, immutable AST, 27 operators, 82 built-in functions.
- JsonDocument-based I/O - variables in, results out, using
System.Text.Jsonprimitives. - Single async-capable evaluator with a synchronous fast path (
ValueTask<Value>under the hood). - Native AOT and trim compatible - zero reflection, zero runtime code generation. CI verifies on every PR.
- Thread-safe by design - one
Parserinstance can serve many threads; parsedExpressions are immutable and safe to share. net10.0target.
Install
dotnet add package Expreszo
Targets net10.0. Packages are published to NuGet.org automatically on every v*.*.* tag - see the releases page for changelogs.
Quick start
using System.Text.Json;
using Expreszo;
using Expreszo.Json;
var parser = new Parser();
// Simple evaluation.
var result = parser.Evaluate("1 + 2 * 3");
Console.WriteLine(result); // 7
// Variables from a JsonDocument.
using var scope = JsonDocument.Parse("""{"x":10,"y":32}""");
Console.WriteLine(parser.Evaluate("x + y", scope)); // 42
// Parse once, evaluate many times.
var expr = parser.Parse("base * qty * (1 - discount)");
using var v = JsonDocument.Parse("""{"base":25,"qty":4,"discount":0.1}""");
Console.WriteLine(expr.Evaluate(v)); // 90
// Higher-order: lambdas and array callbacks.
using var data = JsonDocument.Parse("""{"items":[10,20,30,40]}""");
Console.WriteLine(parser.Evaluate("sum(filter(items, x => x > 15))", data)); // 90
// JSON in → JSON out. The result is a Value, which JsonBridge serialises
// back to System.Text.Json primitives with no reflection.
using var order = JsonDocument.Parse("""{"price":25,"qty":4,"tax":0.21}""");
var totals = parser.Evaluate(
"{ subtotal: price * qty, total: price * qty * (1 + tax) }",
order);
Console.WriteLine(JsonBridge.ToJsonString(totals));
// {"subtotal":100,"total":121}
// Dynamic variable resolver - called per reference, only for names the
// scope didn't already bind. Return NotResolved to fall through.
var env = new Dictionary<string, string>
{
["REGION"] = "eu-west-1",
["USER"] = "alice",
};
VariableResolver resolveFromEnv = name => env.TryGetValue(name, out var value)
? new VariableResolveResult.Bound(new Value.String(value))
: VariableResolveResult.NotResolved;
Console.WriteLine(parser.Evaluate(
"USER | \" @ \" | REGION",
resolver: resolveFromEnv));
// alice @ eu-west-1
Expression language - cheat sheet
| Category | Examples |
|---|---|
| Literals | 42, 3.14, 1e10, 0xFF, 0b1010, "hello", true, false, null, undefined |
| Arithmetic | +, -, *, /, %, ^ (power), \| (concat) |
| Comparison | ==, !=, <, <=, >, >=, in, not in |
| Logical | and / &&, or / \|\|, not / ! |
| Ternary / coalesce | cond ? a : b, x ?? fallback |
| Type cast | x as "number", x as "int", x as "boolean" |
| Member access | obj.name, xs[0] |
| Arrays & spread | [1, 2, ...rest] |
| Objects & spread | { a: 1, "b-key": 2, ...base } |
| Arrow functions | x => x * 2, (a, b) => a + b |
| Function defs | f(x, y) = x + y; f(1, 2) |
| Assignment | x = 5; x + 1 |
| Case | case x when 1 then "one" when 2 then "two" else "other" end |
| Sequences | a = 1; b = 2; a + b |
Built-in functions
Math (17): atan2, clamp, fac, gamma, hypot, max, min, pow, random, roundTo, sum, mean, median, mostFrequent, variance, stddev, percentile
Array (20): count, filter, fold, reduce, find, some, every, unique, distinct, indexOf, join, map, range, chunk, union, intersect, groupBy, countBy, sort, flatten
String (28): length, isEmpty, contains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight, padBoth, slice, urlEncode, base64Encode, base64Decode, coalesce
Object (7): merge, keys, values, mapValues, pick, omit, flattenObject
Utility (2): if (lazy), json
Type-check (8): isArray, isObject, isNumber, isString, isBoolean, isNull, isUndefined, isFunction
Design notes
Three behaviours to know about:
Undefinedis distinct fromNull. Missing members (obj.nope) and lambda parameters you didn't pass returnValue.Undefined; explicit JSON nulls returnValue.Null. Reading a completely unknown identifier is a hard error - it throwsVariableExceptionrather than returningUndefined, so typos don't silently evaluate to nothing. The??operator,isUndefined/isNull, and short-circuit semantics all depend on the Null/Undefined distinction.- Assignments don't propagate back to your input
JsonDocument. The evaluator copies the document into an internal scope on entry. Assignments mutate that scope only; the caller's document is untouched. (JsonDocumentis immutable in System.Text.Json.) - Numbers are IEEE 754
double- same semantics as JavaScript. Very large integers in your JSON lose precision; serialise them as strings if you need exact round-tripping. There is nodecimalmode.
Security
The library validates every access point before evaluating it:
- Properties named
__proto__,prototype, orconstructorare always rejected - both via dot access and via bracket-string access. The same names are filtered out of JSON I/O and scope bindings for defence-in-depth. - Array indices must be finite non-negative integers.
- Only registered functions (built-ins + any you add via a future
OperatorTableBuilderAPI) can be invoked. Raw CLR delegates smuggled in through a customVariableResolverorScopebinding are rejected at the call site via an identity-based allow-list. - Recursion is capped at 256 levels of nested calls and 256 levels of parse depth - runaway recursion (
f(x) = f(x); f(1)) throws anEvaluationExceptioninstead of aStackOverflowException. - Resource-heavy built-ins (
repeat,padLeft/Right/Both,range,fac, postfix!) enforce output-size and input-range budgets published asExpreszo.EvaluationLimitsconstants. EvaluateAsync'sCancellationTokenis observed on every call boundary and inside every looping built-in, so timeouts are honoured in bounded time.
AOT-safe
The library assembly is built with IsAotCompatible=true and every trim / AOT analyser enabled. The CI matrix includes a dedicated job that runs:
dotnet publish samples/AotCheck --configuration Release -r linux-x64 --self-contained -p:PublishAot=true
and executes the resulting native binary. Any code path introducing reflection or dynamic code generation fails the build.
Your app can enable <PublishAot>true</PublishAot> without any warnings from this library.
Benchmarks
BenchmarkDotNet-based micro-benchmarks live in bench/Expreszo.Benchmarks/ and cover parsing, evaluation, simplification, and end-to-end parse+evaluate cycles:
dotnet run --project bench/Expreszo.Benchmarks -c Release -- --list flat # list available benchmarks
dotnet run --project bench/Expreszo.Benchmarks -c Release # run the full matrix (~minutes)
dotnet run --project bench/Expreszo.Benchmarks -c Release -- --filter '*Eval*' # run a subset
Tooling
A Language Server Protocol (LSP) implementation lives in src/Expreszo.LanguageServer and src/Expreszo.LanguageServer.Host. The host is a stdio server consumable from any LSP-aware editor (VS Code, Neovim, Zed, JetBrains, Emacs). Current coverage: diagnostics (syntax + statically-detectable type issues), hover, completion, signature help, document symbols, goto-definition, find-references, rename, and semantic tokens. A VS Code extension and published host binaries are the next things on the roadmap.
Error-recovering parsing and the type-tag enum are exposed on the library API — Parser.TryParse(string) returns a ParseResult with a best-effort expression plus an ImmutableArray<ExpressionException>, and Expreszo.ValueKind is the coarse type tag used by the language server's literal-driven validator. Library callers that want the same diagnostics flow are free to wire those pieces together without the server.
Out of scope
- MCP server.
- Legacy-mode semantics.
- Runtime disabling of specific operators.
- Customising the built-in operator set (adding custom named operators). Adding custom functions will be supported via
OperatorTableBuilderin a future release.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Expreszo:
| Package | Downloads |
|---|---|
|
Expreszo.Analysis
Static analysis primitives for the ExpresZo expression language: literal-driven type inference, type validation, symbol indexing, AST cursor lookup, line / offset conversion, and the built-in function catalogue. Targets tooling authors who want the analyser without pulling in the OmniSharp Language Server runtime. |
|
|
Expreszo.LanguageServer
OmniSharp-based Language Server Protocol implementation for the ExpresZo expression language. Wraps Expreszo.Analysis into diagnostics / hover / completion / signature help / document symbols / definition / references / rename / semantic-tokens handlers and a stdio host entry point. Not AOT-compatible — consumers who only want the analyser should reference Expreszo.Analysis instead. |
GitHub repositories
This package is not used by any popular GitHub repositories.