Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Section titled “[Unreleased]”ExpressionParserOptions.Fork(resultType, resultCastType, parameters, useFirstParameterAsThis): creates a read-only variant that reuses the prepared, shared settings of the source instance (global members, included types, static imports, the reflection member cache and the resolution flags) while overriding the result type, result cast type, parameters orthishandling. The shared lookups are prepared once and reused by every fork; only the parameters andthishandling rebuild the parameter-specific part of the context. Passnullfor any argument to inherit the source value.- Jagged array creation (
new int[][] { ... },new int[2][], and deeper forms such asint[][][]): previously rejected with an error. Every rank specifier after the first becomes part of the element type, soint[][]creates an array whose elements areint[]. Only the outermost dimension may carry a size, sonew int[3][1]is still rejected. Example:new int[][] { new[] { 1 }, new[] { 2, 3 } }[1][1]returns3. - Implicitly typed multidimensional arrays (
new[,] { { 1, 2 }, { 3, 4 } },new[,,] { ... }): only the explicitly typed form (new int[,] { ... }) worked before. The element type is inferred from the leaf elements, which must all have the same type. Example:new[,] { { 1, 2 }, { 3, 4 } }[1, 0]returns3. - Index initializers in object initializers (
new Dictionary<string, int> { ["a"] = 1, ["b"] = 2 }): each entry assigns through the indexer setter. Works with any type that has an accessible indexer setter, and can be mixed with member assignments. The collection form ({ { "a", 1 } }) still works. Example:new Dictionary<string, int> { ["a"] = 1 }["a"]returns1. - Unbound generic types in
typeof(typeof(List<>),typeof(Dictionary<,>)). - Tuples with more than seven elements (
(1, 2, 3, 4, 5, 6, 7, 8, 9)): built with a nestedRestand accessible by.ItemNor element name. - Target-typed
new()as a method, constructor or indexer argument (obj.Method(new()),obj.Method(new() { X = 1 })): the type comes from the resolved parameter. throwexpressions in?:and??(x > 0 ? x : throw new ArgumentException()), opt-in via the newAllowThrowExpressionsoption (defaultfalse).
- Implicit arrays now infer the C# best common element type instead of requiring identical element types.
new[] { 1, 2L }islong[]andnew[] { (byte)1, 2 }isint[]; previously both failed with a type-not-found error. Enumerable.MaxandEnumerable.Minwith a selector lambda (for exampleitems.Max(x => x.Value)) threw an internalExtension node must override the property Expression.Typeerror during overload resolution. The not-yet-bound lambda argument is now skipped when ranking overloads, and a more-specific-signature tie-break selects the correct member (a concreteFunc<TSource, int>result over the fully genericFunc<TSource, TResult>).SumandAveragewere not affected.- Overload resolution now prefers a generic
IEnumerable<T>overload over aparams object[]one.string.Join(",", new[] { 3, 1, 2 })bound the array toparams object[]and returned"System.Int32[]"; it now binds the genericJoin<T>(string, IEnumerable<T>)overload and returns"3,1,2". The betterness comparison previously read the open generic parameters (IEnumerable<T>) instead of the closed ones (IEnumerable<int>). - LINQ and other
IEnumerable<T>extension methods now resolve onstring, which isIEnumerable<char>."racecar".Count(),"abc".Reverse()and similar calls previously failed with a method-not-found error, because generic-argument inference did not look through the interfaces of a non-generic type. - Members of a
typeof(...)value now resolve againstSystem.Type.typeof(int).Namepreviously failed withUnknown member Namebecause the value was treated as a static type reference (the mechanism behindint.MaxValue).NameandIsValueTypework without reflection; otherTypemembers requireAllowReflection. Enumerable.Aggregatewith a result selector (the three-argument overload) no longer throws an index-out-of-range error during generic inference.Enumerable.GroupJoinand similar calls now infer generic arguments nested in a lambda parameter type (for exampleFunc<T, IEnumerable<TInner>, TResult>).- Switch-arm pattern variables are scoped to their own arm, so the same name can appear in several arms.
- Named-tuple element names flow from an array literal into LINQ lambdas (for example
new[] { (Name: "x", Val: 1) }.Sum(t => t.Val)). - A numeric operand is promoted to a user-defined operator’s parameter type (for example
TimeSpan.FromHours(1) * 2). - The bare
nullliteral binds to reference and nullable parameters (for examplestring.IsNullOrEmpty(null)). - Enum constant patterns (
x is DayOfWeek.Monday) andvardeconstruction patterns ((1, 2) is var (a, b)) inisexpressions. - Namespace-qualified closed generic types (for example
new System.Collections.Generic.List<int>()). - Nested tuple element names now flow through direct
.ItemNaccess (for example(Name: "x", (Inner: 5, 6)).Item2.Inner). Enumerable.Max/Minover an enum array now returns the enum type instead of its underlying integer.uintcombined with a signedsbyte/short/intpromotes both operands tolong(for example1u + 1yields2L).- Relational patterns compare enum operands via the underlying type (for example
x is >= DayOfWeek.Monday). - The string literal
"default"is no longer parsed as thedefaultkeyword ("default".Lengthandx ?? "default"now work). - Lambdas bind to non-
Func/Actiondelegate parameters such asPredicate<T>,Converter<T, R>andComparison<T>(for examplelist.Find(x => x > 1),list.ConvertAll(x => x * 2)). - Generic and array types resolve on the right side of
is/as(for examplex is List<int>,x as int[]). - Comparisons with a nullable enum operand are lifted (for example
(DayOfWeek?)x == DayOfWeek.Monday). - C# constant conversions apply to
intliterals in range of a smaller integral type (for examplenew byte[] { 1, 2 },new List<byte> { 1 },new sbyte[] { -5 }). - Tuple element names survive a conditional where both branches agree (for example
(c ? (a: 1, b: 2) : (a: 3, b: 4)).a). - Tuples with different element types compare element-wise with implicit conversions (for example
(1, 2) == (1L, 2L)). - A lambda body implicitly converts to the delegate’s return type (for example
"ab".Sum(c => c),bytes.Sum(b => b)). - Tuple element names declared by method return metadata are visible (for example
items.Index().First().Index,a.Zip(b).First().Second). - Escape sequences resolve in interpolated string text and format clauses (for example
$"{ts:hh\\:mm}",$"a\tb{x}"). - Extended property patterns (
x is { B.Length: 1 }), with null checks on the intermediate members like C#. - An implicit array infers its element type when some elements are
nullliterals (for examplenew[] { "a", null }isstring[]).
Performance
Section titled “Performance”- Indexer property lookups are cached in the shared member cache.
- Instance member, user-defined conversion operator and
ValueTuple.Createlookups are cached; constructed generic methods are cached per options. All caches live on the options instance, so collectible assemblies can unload. - A lambda body is resolved once and reused across overload candidates with the same parameter types (for example the
Enumerable.Max/Min/Sumoverload sets).
[1.3.2] - 2026-07-24
Section titled “[1.3.2] - 2026-07-24”- Namespace-qualified types now resolve inside expression bodies, not only in
new/cast/typeofpositions. Static access such asSystem.Math.Pow(2, 3),System.Math.PIandSystem.TimeSpan.FromMinutes(2)previously failed with an unknown-identifier error. TheTypeResolverfallback is consulted with the full dotted name (for exampleSystem.Math) for these accesses, and a qualifier that does not match a resolved type’s actual namespace (for exampleSystem.Text.Math) is rejected.
[1.3.1] - 2026-07-24
Section titled “[1.3.1] - 2026-07-24”IgnoreBuiltInTypesoption (defaultfalse): hides the fixed set of common framework types otherwise available by short name regardless ofIncludedTypes(DateTime,TimeSpan,Guid,Math,Convert,Enumerable,List<>,Dictionary<,>, the common collection interfaces, and others - see the README). Set it totrueto accept only explicitly allowed types; the C# primitive keywords (int,string,bool, …) remain always available.TypeResolveroption (Func<string, Type?>): a fallback consulted afterResultType,Parameters,IncludedTypesand the built-in types, letting you resolve a type from its name. The name may be namespace-qualified (for exampleSystem.Text.StringBuilder) or a short name; generic type names carry an apostrophe + arity suffix (List'1,Dictionary'2) and expect the open generic definition. Returnnullwhen the name is not recognized.
Performance
Section titled “Performance”- Faster parsing: option state prepared once and reused across parses, cached reflection lookups, fewer hot-path allocations.
- Allocation-free candidate handling.
- Early lambda-arity rejection.
- Element-type-info propagation skipped when no
CustomPropertyResolveris set. - Lock-free member cache reads (
ConcurrentDictionary).
[1.3.0] - 2026-07-23
Section titled “[1.3.0] - 2026-07-23”- Named tuple element names (
(Name: "Bob", Age: 30).Name): supports names declared explicitly, names inferred from identifiers and member accesses ((a, x.B)), and names that flow through generic and LINQ chains (people.Select(p => (Name: p.Name, Age: p.Age)).First().Name), matching C# - including the same rules for reserved (ItemN,Rest, …), duplicate and conflicting names. StaticImportsoption: a collection of static classes whose public static methods, fields, properties and constants can be used unqualified, as ifusing staticwas applied (for example, importingMathmakesSqrt(x),Max(a, b),PIandEavailable). Non-static classes are rejected, and instance members, global members and instance types always take precedence.IgnoreCaseoption: resolve parameters, variables, global members, type members andIncludedTypescase-insensitively.- Full support for the bare
defaultliteral: it is now target-typed wherever C# can infer the type (method arguments, comparisons,??, ternary, casts), not only whenResultTypeis set. It still fails, like C#, where there is no target type (a baredefault,default == default, an overloaded-method argument). - Named arguments on method, constructors, indexers and extension-method calls (
obj.Sum(b: 2, a: 1)) now can bind by name instead of being passed positionally. Supports reordering, mixing positional and named arguments, and skipping optional parameters (obj.Concat3("x", c: "z")), and participates in overload and generic-method resolution. Honors theIgnoreCaseoption.
Changed
Section titled “Changed”ExpressionParserOptionsis now read-only after it is first used for parsing: property setters throwInvalidOperationExceptionand theParameters,GlobalMembers,IncludedTypesandStaticImportscollections throwNotSupportedExceptionon mutation.
- Null-conditional access (
?.) evaluated its receiver twice (once for the null check, once for the access) instead of once; a receiver with a side effect, like a method call, was invoked twice.
Deprecated
Section titled “Deprecated”UseReducedExpressionsis obsolete, the parser always produces standard expression nodes, so there is nothing left to reduce.
1.2.1 - 2026-07-17
Section titled “1.2.1 - 2026-07-17”- Delegate-typed members can be invoked with method-call syntax (
DelegateField(args)), including delegates returned fromCustomPropertyResolver, matching C#. UseMemberCacheoption (defaultfalse): when enabled, reflected members (methods, indexers, extension methods) are memoized perExpressionParserOptionsinstance. Turn it on when reusing the same options across many parses to skip repeated reflection.
CustomPropertyResolver: element type info now propagates through method and indexer chains over dynamic collections, so chains likepeople.Where(p => p.Age > 18).FirstOrDefault()?.Nameresolve members correctly (previously only a bare lambda parameter inherited it).
Performance
Section titled “Performance”- Overload resolution rejects candidates with unfilled required parameters before running generic inference and lambda binding - a large speedup and allocation drop for LINQ-heavy expressions with many overloads (for example
Sum). - Conversion-operator lookups are short-circuited for primitive types, so numeric conversions (
int->double) no longer scan reflection. - Fewer allocations across the parse hot path: eliminated enumerator boxing and several intermediate lists/arrays, and index-based loops replace LINQ in hot spots.
1.2.0 - 2026-07-13
Section titled “1.2.0 - 2026-07-13”- Anonymous objects (
new { X = 1, Y = 2 }), internally mapped toDynamicObjectwith parse-time member validation and value equality (Equals/GetHashCode), without generating a new type. - Recursive/tuple deconstruction patterns (
x is (int a, int b),x is Point(int x, int y)), includingDeconstructmethods. - List patterns (
arr is [1, 2, 3],arr is [1, .., 3]). - Target-typed
new(), including inside object/collection initializers (new List<Point> { new() { X = 1 } }). - Collection initializers for
Add-based collections (new List<int> { 1, 2, 3 },new Dictionary<string, int> { { "a", 1 } }), not just arrays. AllowStringRelationalOperatorsoption to opt into</<=/>/>=on strings (ordinal, viastring.Compare) - disabled by default, matching real C#.
</<=/>/>=on strings are rejected by default, matching real C# (previously always allowed viastring.Compare).- A discard (
_) used as a nested sub-pattern (for example(1, 2) is (1, _)) returned the matched value instead oftrue. - Reflection-based member lookups are now trim/AOT-compatible (annotated for the trimmer, so publishing with trimming enabled no longer strips members the parser depends on).
Known limitations
Section titled “Known limitations”- Target-typed
new()is not yet inferred as a method call argument (obj.Method(new())); use an explicit type there for now.
1.1.2 - 2026-07-08
Section titled “1.1.2 - 2026-07-08”- Index-from-end operator in indexers:
x[^1]for arrays, strings andIList/IReadOnlyList(lowered tox[length - n], so no dependency onSystem.Index).
1.1.1 - 2026-07-08
Section titled “1.1.1 - 2026-07-08”- Alignment in interpolated strings (
$"{x,6}") was ignored; it is now honored together with format specifiers.
1.1.0 - 2026-07-08
Section titled “1.1.0 - 2026-07-08”A large expansion of the supported C# expression grammar, plus several correctness and performance improvements.
switchexpressions with full pattern support: type, constant, relational,and/or/not, property, positional andvarpatterns,whenguards, declaration patterns and exhaustive (no-discard) switches.- Tuple equality (
==/!=, compared element-wise). - Array creation with explicit sizes and multidimensional arrays (
new int[2, 3],new int[,] { { 1, 2 }, { 3, 4 } }). typeof,default(T),nameof,sizeof,checkedandunchecked.- Null-forgiving operator (
x!) and bitwise complement (~). paramsmethod arguments.- Custom-named indexers (for example indexing a
string). - Enum arithmetic (
E + U,E - E, bitwise and comparison operators, following the C# rules). - User-defined
implicitconversion operators and nullable conversions. - More built-in types usable by name (
Dictionary<,>,HashSet<>,IReadOnlyList<>,Guid,Convert, and others). ResultTypenow applies an implicit conversion when one exists (for exampleint->long).
- Small integers (
byte,sbyte,short,ushort,char) are now promoted tointfor arithmetic, bitwise and unary operators, matching C#. - Shift operators no longer coerce both operands to a common type (
1L << 40). - Error when resolving array types (
typeof(int[]),default(int[]), casts).
Performance
Section titled “Performance”- Parse only the expression via
SyntaxFactory.ParseExpressioninstead of a full script compilation unit - several times faster with fewer allocations. - Reflection detection is folded into the build pass, removing a separate expression-tree walk.
1.0.8 - 2025-04-23
Section titled “1.0.8 - 2025-04-23”- Duplicate methods coming from
Enumerablewhen resolving extension methods.
1.0.7 - 2025-03-23
Section titled “1.0.7 - 2025-03-23”- Extension methods from types listed in
IncludedTypes.
1.0.6 - 2025-03-05
Section titled “1.0.6 - 2025-03-05”ExpressionParser.CompileandInvokehelpers.GlobalMembers- named values and delegates usable by name in an expression.
Changed
Section titled “Changed”- Swapped the argument order of the
typeis/typeas/typecastruntime-cast keywords.
1.0.5 - 2025-03-04
Section titled “1.0.5 - 2025-03-04”- Void expressions with conditional (
?.) calls.
1.0.4 - 2025-03-04
Section titled “1.0.4 - 2025-03-04”- A void method could be invoked in a value context.
1.0.3 - 2025-03-03
Section titled “1.0.3 - 2025-03-03”- A delegate passed as a parameter could not be invoked directly.
- Corrections to member full path preservation.
1.0.2 - 2024-10-24
Section titled “1.0.2 - 2024-10-24”- The full member path is preserved and exposed to
CustomPropertyResolver.
1.0.1 - 2024-01-18
Section titled “1.0.1 - 2024-01-18”netstandard2.0target.
1.0.0 - 2024-01-18
Section titled “1.0.0 - 2024-01-18”- Initial release. Converts C# text expressions into
System.Linq.Expressionsusing Roslyn.