DotGram.ExpressionLanguage 0.2.0

dotnet add package DotGram.ExpressionLanguage --version 0.2.0
                    
NuGet\Install-Package DotGram.ExpressionLanguage -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DotGram.ExpressionLanguage" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DotGram.ExpressionLanguage" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="DotGram.ExpressionLanguage" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DotGram.ExpressionLanguage --version 0.2.0
                    
#r "nuget: DotGram.ExpressionLanguage, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DotGram.ExpressionLanguage@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DotGram.ExpressionLanguage&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=DotGram.ExpressionLanguage&version=0.2.0
                    
Install as a Cake Tool

DotGram.ExpressionLanguage

A C#-style expression language written in .gram, compiled into System.Linq.Expressions trees.

It is an ordinary C# library. .Gram generated the parser into this assembly when it was compiled, so nothing here carries a parser runtime, and neither does anything that references it.

using System;

using DotGram.ExpressionLanguage;

var square = ExpressionParser.Compile<Func<int, int>>("(int x) => x * x - 1");

square(3); // 8

It is not a sandbox: do not compile text you do not trust. See It is not a sandbox below.

It reads C#'s operators, at C#'s precedence, and its literals down to the digit separator and the verbatim string. Past expressions it has typed locals, blocks, if, while, do, for, switch — the statement and x switch { 1 or 2 => …, _ => … } — try/catch/finally, throw, break, continue and return, and past the keywords it has members, calls, indexers, new with initializers, tuples, generic types, is, as, casts and checked:

var calculate = ExpressionParser.Compile<Func<int, int, int>>(
    """
    (int x, int y) =>
    {
        int sum = x + y;
        return sum * sum;
    }
    """);

calculate(2, 3); // 25

Or keep the expression tree instead of compiling it:

var expression = ExpressionParser.Parse("(double x) => x / 2.0");

Console.WriteLine(expression);   // x => (x / 2)

Or ask, rather than catch: TryParse answers for everything Parse would throw for — text that is not this language, and text that is and means nothing, such as a name nothing declares or an operator its operands do not support.

var match = ExpressionParser.TryParse("(string s) => s - 1");

if (!match.IsSuccess)                    // there is no minus over a string
    Console.WriteLine(match.Error);      // what Expression.Subtract said about String and Int32

Compile, Parse and TryParse may all be called from any number of threads at once — the generated parser's own contract — and the member-resolution caches behind name and overload lookups are shared across every call and thread, kept in their own concurrent tables rather than per call.

A type named rather than spelled as a keyword is found the way C# finds one: written whole, or through a using at the top of the text. Five namespaces are there already — the set a new project gets: System, System.Collections.Generic, System.Linq, System.Text and System.Threading.Tasks — and a text's own usings are its own, so the next text starts with those five again and nothing more. ResolutionScope.DefaultImports names them, and ResolutionScope.WithoutDefaultImports() is a scope that leaves them out. A using may also give a name to one type or one namespace (using L = System.Collections.Generic.List<int>;), or bring a type's static members, nested types and extension methods into reach as bare names (using static System.Math; () => Abs(-2)). A name the text itself declares wins over one a using static gives, as in C#; and static is a keyword here, as it is in C#, so nothing may be named it. What a text can name is what C# written in the calling assembly could: public types, and that assembly's own internal types and members. A full name that two referenced assemblies declare as two different types is refused where it is used, naming both assemblies, as C# refuses it (CS0433). One type that several assemblies answer for, as a facade forwards System.Object, is one type; and the calling assembly's own type wins over a reference's, as the compilation's does in C#.

using System.Collections.Generic;

// `System.Collections.Generic` is one of the five, so the text needs no `using` of its own.
var count = ExpressionParser.Compile<Func<IList<int>, int>>("(IList<int> l) => l.Count");

// One it does not get, it says itself.
var named = ExpressionParser.Compile<Func<string>>(
    """
    using System.Globalization;

    () => CultureInfo.InvariantCulture.Name
    """);

Conversions, operators and overloads follow C#'s rules: x + 1.5 over an int is a double, byte b = 1 fits, and Math.Sqrt(x) finds the double overload. A generic method takes its type arguments from its arguments, an extension method is found through a using, and a lambda that says no types takes them from the overload it is handed to, so a.Where(n => n > 1).Sum() reads as it does in C#. The lambda a text IS takes them the same way from the delegate it is compiled to, so Compile<Func<int, int>>("x => x * x") reads with no types written at all. Where it is not C# — a method is never called with its type arguments written, and a constant is folded only across a minus — is written down, with the reason for each, at the top of the file below.

Where a name is looked for is the caller's to say. By default it is the calling assembly and what it references — what a compilation of that assembly would see — so the same text answers the same whatever else the process has loaded. ResolutionScope.Of(caller, plugin) widens that, WithoutInternals() reads as another assembly would, and what a name MEANS stays C#'s either way. A caller loaded into an AssemblyLoadContext of its own has its references found in that context, as the runtime finds them for its own code.

The grammar calls System.Linq.Expressions factories directly. There is no intermediate AST specific to .Gram that must later be translated into an expression tree — which also means a factory that does not exist, or one handed the wrong type, is a C# error on the line of the grammar that asked for it rather than an exception at run time.

The grammar and the C# it calls are one file, ExpressionParser.cs.

It is not a sandbox

Parse only text you would run as code. A text can name any type the reading can reach and call any member on it — the file system, the process, reflection — and what it compiles to runs with the host's permissions, in the host's process, for as long as the delegate is called. There is no allow-list, no timeout and no quota, and none is planned: this compiles expressions, it does not contain them.

That is the same position System.Linq.Expressions itself takes, and the same one CSharpScript takes. It matters here because the input LOOKS like data — a formula in a configuration file, a rule typed into a form — and a formula from somewhere you do not control is code from somewhere you do not control.

If the text comes from a user, the answer is not to inspect it before parsing. It is to run it where it can do no harm: a process of its own, with the rights you are willing to lose.

Taking it

dotnet add package DotGram.ExpressionLanguage

There is no companion runtime package, and no generator to install alongside it: the parser was generated when this assembly was compiled.

Product 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.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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. 
.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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

  • net10.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on DotGram.ExpressionLanguage:

Package Downloads
DotGram.Finance

FIX 4.2, 4.4 and 5.0 SP2 tag-value parsing and validation: the wire read by a DotGram grammar, and each version's typed messages and checks generated from the FIX repository.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 77 10/1/2026
0.1.0 87 9/15/2026

Since 0.1.0. Lambdas whose parameters say no types, read where the types are known; interpolated strings, verbatim and raw; a resolution scope a host hands in, so where a name is looked for is the host's to decide — and what it means stays C#'s. Names are found in the calling assembly and what it references, as a compilation's are, and no longer in whatever the process happens to have loaded; a caller loaded into a load context of its own, a plugin, has its references found in that context, as the runtime finds them for its code. Five namespaces are there already, the set a new project gets (System, System.Collections.Generic, System.Linq, System.Text, System.Threading.Tasks); ResolutionScope.DefaultImports is the set and WithoutDefaultImports() a scope without them. `using static` brings a type's static methods, members and nested types into reach as bare names and its extension methods into the extension search, with a name the text declared winning over one it gives, as in C#; `static` is a keyword now, as it is in C#, so a text may no longer name anything it — a parameter called `static` was accepted before and is refused now. A `using` may also give a name to one type or one namespace (`using L = System.Collections.Generic.List<int>;`), which beats a name a `using` brings in and may not take one the global namespace has, as in C#. A full name that two referenced assemblies declare as two different types is refused where it is used, naming both assemblies, as C# refuses it (CS0433); it used to mean whichever assembly was found first. The calling assembly's own type still wins over a reference's, as in C#. Parse, TryParse and Compile are what is public, and TryParse now also answers whether without a message. Faster: an expression of a thousand terms no longer costs the square of its length, nor does a text of many blocks each declaring a name; a name that binds a parameter or a local is never looked for as a type, as in C#. The assemblies a scope covers are loaded on the first text that names a type, about 20 ms once per calling assembly and process; a text that names none loads nothing. Faster: an element of a name such as `a[0]` — nested as `a[a[a[0]]]`, a text C# accepts too — no longer reads its operand twice, once to look for a following `=` and again as a postfix operand; nesting cost exponential time before (nine seconds at twenty-four levels) and is linear now. The same double reading was in a nested array initializer's braces (`new object[] { new object[] { … x`, unclosed): allocation grew exponentially with nesting depth before and is flat now. The one outward change on accepted input, or on most refused input, is where an array initializer's own braces go wrong: `new int[] { {1} }` was refused as "'Int32[]' has no constructor taking ()" and is now refused as "Expected '}'", at the point the braces themselves fail to close. Speed against 0.1.0: the language reads more of C# (switch expressions, tuples, using static and aliases, element assignment in linear time), and an ordinary expression parses 10-30% slower than in 0.1.0 while allocating less, deeply nested parentheses about 55% slower, and an untyped lambda about 60% faster. A refused text read through the Match form is read twice to say what was expected; the bool form reads it once.