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]”Changed
Section titled “Changed”- The license is now Apache-2.0, previously MIT.
[1.5.0] - 2026-08-15
Section titled “[1.5.0] - 2026-08-15”- Namespace-qualified type names on the right of
isandas(x is System.Int32,x as System.Text.StringBuilder). - Verbatim identifiers (
@value). - Unqualified
EqualsandReferenceEquals. - A standard numeric conversion chained with a user-defined one (
(Money)1goes throughdecimal). - A built-in operator after a user-defined conversion (
p + 1wherePercentageconverts todecimal). - The members of
objecton an interface type (shape.ToString()). - Reference conversions between an interface and a non-sealed class (
shape as Node). +on a nullable enum ((DayOfWeek?)d + 1).- Subtraction with the enum on the right side (
1 - TypeCode.Boolean). - An empty property pattern on a non-nullable value type, which always matches (
5 is { }). - Conversion of the assigned value in an object initializer (
new Circle { R = 1 }).
- A type name in a pattern is a type pattern instead of a constant comparison (
x is not Circle), and it narrows the right side ofand. - An anonymous object member is no longer hidden by a member of its carrier;
new { Count = 7 }.Countreturned1. double.NaN is double.NaNistrue; a pattern constant matches likeEquals, not like==.- An enum constant matches a boxed input (
((object)DayOfWeek.Monday) is DayOfWeek.Mondayreturnedfalse). - Subtracting a number from a nullable enum keeps the enum type; it returned the underlying type.
-2147483648isint; it waslong.- A relational operator and a relational pattern accept the literal zero on an enum (
day > 0,day is > 0). - A null literal compared with an enum lifts the operation instead of being rejected (
day == nullisfalse). - A pattern that can never or always match is rejected (
5 is not { }). - An unreachable switch arm is rejected (
1 switch { { } => 1, _ => 2 }). - A pattern value that is not a constant is rejected (
ts is TimeSpan.Zero). - A constant that overflows outside
uncheckedis rejected (int.MaxValue + 1). is null,?.and??on a non-nullable value type are rejected (5 is null).- A null literal against a non-nullable value type in
?:andswitchis rejected (x > 0 ? 1 : null). - A null literal on both sides of
?:and??is rejected (null ?? null). - A constant negative array size is rejected (
new int[-1]).
Performance
Section titled “Performance”- Conversion operator lookups are cached when options are shared with
UseMemberCache = true, so a call-heavy expression parses up to four times faster. - Method signatures are cached the same way, which helps repeated calls to overloaded methods.
1.4.0 - 2026-07-29
Section titled “1.4.0 - 2026-07-29”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?:,??, switch expression arms and as a lambda body (x > 0 ? x : throw new ArgumentException(),items.Sum(x => throw ...)), opt-in via the newAllowThrowExpressionsoption (defaultfalse).- Constant and relational patterns against an
objectinput, testing the runtime type first ((object)5 is > 3istrue,(object)5L is 5isfalse), and theandpattern narrowing the type for its right side (obj is int and > 5). - Enum constant patterns (
x is DayOfWeek.Monday), relational patterns on enums via the underlying type (x is >= DayOfWeek.Monday),vardeconstruction patterns ((1, 2) is var (a, b)), extended property patterns (x is { B.Length: 1 }) with null checks on intermediate members, and generic or array types on the right side ofis/as(x is List<int>,x as int[]). - Static member access on a generic type name (
Comparer<int>.Default,EqualityComparer<string>.Default). - Type name resolution for nested types (
typeof(List<int>.Enumerator)), CLR names of the built-in types (Int32,Int64,String, …), and namespace-qualified closed generics (new System.Collections.Generic.List<int>()). - Lambdas bind to non-
Func/Actiondelegate parameters (Predicate<T>,Converter<T, R>,Comparison<T>; for examplelist.Find(x => x > 1)), and a lambda body implicitly converts to the delegate’s return type ("ab".Sum(c => c)).
- Numeric promotion and constant conversions follow the C# rules:
byte/ushort/charconvert directly touint/ulong((byte)3 + 5ulisulong,5u & (byte)3isuint), an in-rangeintconstant converts to the smaller or unsigned operand type (5ul + 5is10ul,3u - 1is2u,new byte[] { 1, 2 }), a non-negativelongconstant converts toulong(5L + 5ulis10ul),uintwith a signed operand promotes tolong(also lifted:5u + (int?)4islong?), and unary minus onuintgiveslong(-(5u)is-5L). - The conditional operator and switch expression arms require an implicit conversion to one of the operand types, and
??follows the C# order (the right operand converts to the unwrapped type, else to the nullable type itself, else the left converts to the right type). Mixed cases such astrue ? (int?)4 : 5Lor(int?)4 ?? (uint?)3uare rejected instead of promoting tolong; binary operators still promote ((int?)4 + 5Lislong?). A nullable numeric never converts implicitly to a non-nullable type. - Expressions the C# compiler rejects are now rejected:
==/!=betweenobjectand a value type ((object)5 == 5was a silent reference comparison returningfalse),&&/||onbool?operands (&and|keep the three-valued logic),!on non-bool operands (!5was a bitwise complement), and casts betweenbooland numeric,charor enum types ((int)truereturned1). - A pattern constant must convert to the input type like in C#:
5L is < 3.5,5 is > 3fand'B' is 5are rejected, while2.5m is > 3and66 is 'B'work through constant conversion. - Switch-arm pattern variables are scoped to their own arm, so the same name can appear in several arms.
- The
asoperator with a nullable type whose underlying type does not match a value-type operand givesnulllike C# (200 as long? ?? -1is-1); previously it failed with an internal error. - Casts between
decimaland an enum type work in both directions ((DayOfWeek)2.5m,(decimal)DayOfWeek.Friday) by going through the enum’s underlying type. - The
~operator works on enum types (flags & ~StringSplitOptions.TrimEntries): the complement is computed on the underlying type and converted back. - Members and method calls on a
typeof(...)value resolve againstSystem.Type(typeof(int).Name;typeof(int[]).GetElementType()withAllowReflection); the value was treated as a static type reference (the mechanism behindint.MaxValue). - A generic type added to
IncludedTypesresolves by name; a closed generic (typeof(SortedSet<int>)) is available only with those exact arguments, an open definition (typeof(Queue<>)) with any. - Overload resolution betterness matches the C# compiler: an expanded
paramscandidate compares by element type ("a,b;c".Split(',', ';')gives 3 parts, not 2), a more specific parameter type beats an assignable one (1L.Equals(1)istrue), a closed genericIEnumerable<T>overload beatsparams object[](string.Join(",", new[] { 3, 1, 2 })is"3,1,2"), and a not-yet-bound lambda argument no longer breaks ranking (items.Max(x => x.Value)threw an internal error), with the more specific signature as a tie-break. - Generic arguments infer from array parameters (
Array.Exists(new[] { 1, 2 }, x => x > 1),Array.ConvertAll), through the interfaces of a non-generic type ("racecar".Count()-stringisIEnumerable<char>), from types nested in a lambda parameter type (GroupJoin), andAggregatewith a result selector no longer throws during inference. - Named tuple element names flow everywhere C# preserves them: from an array literal into LINQ lambdas (
new[] { (Name: "x", Val: 1) }.Sum(t => t.Val)), through nested.ItemNaccess, through a conditional where both branches agree, and from method return metadata (items.Index().First().Index,a.Zip(b).First().Second). - Tuples with different element types compare element-wise with implicit conversions (
(1, 2) == (1L, 2L)). - An implicit array infers the C# best common element type (
new[] { 1, 2L }islong[],new[] { (byte)1, 2 }isint[]) and acceptsnullliteral elements (new[] { "a", null }isstring[]). Cast<T>()andOfType<T>()resolve on collections that implement only the non-genericIEnumerable, such as multidimensional arrays.Enumerable.Max/Minover an enum array returns the enum type instead of its underlying integer.- Comparisons with a nullable enum operand are lifted (
(DayOfWeek?)x == DayOfWeek.Monday). - A numeric operand is promoted to a user-defined operator’s parameter type (
TimeSpan.FromHours(1) * 2). - The bare
nullliteral binds to reference and nullable parameters (string.IsNullOrEmpty(null)). - An anonymous object’s
ToString()returns the C# anonymous type format ({ A = 1, B = x }) instead of the internal type name. - The string literal
"default"is no longer parsed as thedefaultkeyword ("default".Lengthworks). - Escape sequences resolve in interpolated string text and format clauses (
$"{ts:hh\\:mm}",$"a\tb{x}").
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.