Linq.Expressions.Deconstruct
1.1.0
dotnet add package Linq.Expressions.Deconstruct --version 1.1.0
NuGet\Install-Package Linq.Expressions.Deconstruct -Version 1.1.0
<PackageReference Include="Linq.Expressions.Deconstruct" Version="1.1.0" />
<PackageVersion Include="Linq.Expressions.Deconstruct" Version="1.1.0" />
<PackageReference Include="Linq.Expressions.Deconstruct" />
paket add Linq.Expressions.Deconstruct --version 1.1.0
#r "nuget: Linq.Expressions.Deconstruct, 1.1.0"
#:package Linq.Expressions.Deconstruct@1.1.0
#addin nuget:?package=Linq.Expressions.Deconstruct&version=1.1.0
#tool nuget:?package=Linq.Expressions.Deconstruct&version=1.1.0
Linq.Expressions.Deconstruct
Pattern matching support for System.Linq.Expressions: every expression node can be wrapped into an Expr
object that has a Deconstruct method, so expression trees can be matched with C# recursive patterns.
On top of that the package provides Visit, Find, Transform and a structural EqualsTo.
using Linq.Expressions.Deconstruct;
using static Linq.Expressions.Deconstruct.Expr;
using Ex = System.Linq.Expressions.Expression;
Pattern matching
[Test]
public void MatchTest()
{
Expression<Func<int,int>> f = i => i * 2;
switch (f.ToExpr())
{
case Lambda(Multiply(Parameter("i") p1, Constant(2)), [("i") p2])
when p1 == p2 :
Console.WriteLine("Pattern Matched!");
break;
default:
Console.WriteLine(f);
Assert.Fail();
break;
}
}
Transforming
TransformEx passes wrapped Expr nodes to the function; Transform passes plain Expression nodes.
[Test]
public void ConstantFoldingTest()
{
Expression<Func<int,int>> f = i => i * 0 + 0 + i + 10 * (i * 0 + 2);
var f1 = f.TransformEx(ex => ex switch
{
Multiply(Constant(_, 0) e, _) => e, // 0 * e => 0
Multiply(_, Constant(_, 0) e) => e, // e * 0 => 0
Multiply(Constant(_, 1), var e) => e, // 1 * e => e
Multiply(var e, Constant(_, 1)) => e, // e * 1 => e
Divide (Constant(_, 0) e, _) => e, // 0 / e => 0
Divide (var e, Constant(_, 1)) => e, // e / 1 => e
Add (Constant(_, 0), var e) => e, // 0 + e => e
Add (var e, Constant(_, 0)) => e, // e + 0 => e
Subtract(Constant(_, 0), var e) => Ex.Negate(e), // 0 - e => -e
Subtract(var e, Constant(_, 0)) => e, // e - 0 => e
Multiply(Constant(int x), Constant(int y)) => Ex.Constant(x * y), // x * y => e
Divide (Constant(int x), Constant(int y)) => Ex.Constant(x / y), // x / y => e
Add (Constant(int x), Constant(int y)) => Ex.Constant(x + y), // x + y => e
Subtract(Constant(int x), Constant(int y)) => Ex.Constant(x - y), // x - y => e
_ => ex
});
Assert.That(f1.EqualsTo(i => i + 20), Is.True);
}
Constant deconstructs into (value, intValue) or (value, intValue, type). intValue is not null
for any integer or floating point constant whose value is an integer that fits into int,
so Constant(_, 0) matches 0, 0L, 0.0 and 0m, while Constant(0) matches only a boxed int zero.
API
All methods are extension methods of Expression (Linq.Expressions.Deconstruct.Extensions).
ToExpr
expression.ToExpr() wraps an Expression into the matching Expr.<NodeType> class
(Expr.Add, Expr.Lambda, Expr.Constant, ...) and returns null for null.
Expr converts to Expression and back implicitly. Children of a node are wrapped lazily when they are first accessed
(by Deconstruct or by the property of the same name).
Expression expression = Ex.Add(Ex.Constant(1), Ex.Constant(2));
// Expression => Expr, Expr => Expression.
Expr expr = expression.ToExpr();
Expression back = expr;
Assert.That(back, Is.SameAs(expression));
// Children are wrapped on demand.
if (expr is Add(var left, var right))
Console.WriteLine($"{left} + {right}");
else
Assert.Fail();
// Constant(value, intValue): intValue is set for any numeric constant that fits into int.
Assert.That(Ex.Constant(5L).ToExpr() is Constant(_, 5), Is.True);
Assert.That(Ex.Constant(5.0).ToExpr() is Constant(_, 5), Is.True);
Assert.That(Ex.Constant(0L).ToExpr() is Constant(_, 0), Is.True);
Assert.That(Ex.Constant(0L).ToExpr() is Constant(0), Is.False); // Constant(value) matches the boxed value as is
Visit and Find
Visit(Action<Expression>)andVisitEx(Action<Expr>)call the action for every node, children first (post-order).Visit(Func<Expression, bool>)calls the function for a node before its children (pre-order). Returntrueto visit the children of the node,falseto skip them.Find(Func<Expression, bool>)andFindEx(Func<Expr, bool>)return the first node (in pre-order) for which the function returnstrue, ornull.Find(Expression)searches for a given node.
Expression<Func<int,int>> f = i => i * 2 + 1;
var constants = new List<int>();
// Visit(Action) calls the action for every node, children first.
f.Visit(ex =>
{
if (ex is ConstantExpression { Value: int value })
constants.Add(value);
});
Assert.That(constants, Is.EqualTo(new[] { 2, 1 }));
// Visit(Func) calls the function for a node before its children;
// returning false skips the children of that node.
var multiplies = 0;
f.Visit(ex =>
{
if (ex.NodeType == ExpressionType.Multiply)
multiplies++;
return ex.NodeType != ExpressionType.Multiply;
});
Assert.That(multiplies, Is.EqualTo(1));
// Find returns the first expression for which the function returns true, or null.
var found = f.FindEx(ex => ex is Multiply(_, Constant(2)));
Assert.That(found, Is.Not.Null);
Assert.That(f.Find(ex => ex.NodeType == ExpressionType.Divide), Is.Null);
Transform
Transform and TransformEx rebuild the tree: the function is called for every node and returns the node
to use instead of it (return the argument to keep it). Nodes whose children did not change are not recreated,
so a transformation that changes nothing returns the original instance. Lambdas keep their name and TailCall.
- By default (
forward: false) the tree is processed post-order: the function is called for a node after its children, and sees the already transformed children. - With
forward: truethe function is called for a node before its children. If it returns a different node, that node is used as is and its children are not visited; otherwise the children are transformed.
Expression f = Ex.Multiply(Ex.Add(Ex.Constant(1), Ex.Constant(2)), Ex.Constant(3));
var log = new List<string>();
// By default children are transformed first (post-order):
// the function sees the already transformed children.
f.Transform(ex =>
{
log.Add(ex.NodeType.ToString());
return ex;
});
Assert.That(log, Is.EqualTo(new[] { "Constant", "Constant", "Add", "Constant", "Multiply" }));
// With forward: true the function is called for a node before its children,
// and a replaced node is not descended into.
log.Clear();
f.Transform(true, ex =>
{
log.Add(ex.NodeType.ToString());
return ex;
});
Assert.That(log, Is.EqualTo(new[] { "Multiply", "Add", "Constant", "Constant", "Constant" }));
EqualsTo
expr1.EqualsTo(expr2) compares two expression trees structurally.
- Parameters declared by lambdas, blocks (variables) and catch blocks are matched by position, not by name:
x => x * 2equalsy => y * 2, but(a, b) => adoes not equal(a, b) => b. A parameter that is not declared inside the compared trees is equal only to itself. - Label targets (
goto,label,loop) are paired by their first occurrence in the same way. - Constants of primitive types, strings, enums, etc. are always compared by value; constants that wrap
queries or sequences are compared by their content. Other constants (closure objects, ...) are considered
equal when their types are equal, unless
compareConstantValues: trueis passed, which compares them by reference. - Two
Extensionnodes (custom node types) are equal when they have the same type andEqualsreturns true.DebugInfonodes are compared by document and position.
Expression<Func<int,int>> f1 = x => x * 2;
Expression<Func<int,int>> f2 = y => y * 2;
Expression<Func<int,int>> f3 = y => y * 3;
// Structural comparison; names of lambda parameters and block variables do not matter.
Assert.That(f1.EqualsTo(f2), Is.True);
Assert.That(f1.EqualsTo(f3), Is.False);
// Constants of primitive types, strings, enums etc. are always compared by value.
// compareConstantValues also compares other constants (closures, objects) by reference;
// by default such constants are considered equal if their types are equal.
var o1 = new object();
var o2 = new object();
Assert.That(Ex.Constant(o1).EqualsTo(Ex.Constant(o2)), Is.True);
Assert.That(Ex.Constant(o1).EqualsTo(Ex.Constant(o2), compareConstantValues: true), Is.False);
EqualsToOptions (a [Flags] enum) can be passed instead of the compareConstantValues flag:
CompareConstantValues- the same ascompareConstantValues: true.FreeParametersByName- parameters that are not declared inside the compared trees are equal when their names and types match. Use it to compare fragments of different lambdas, for examplel1.Body.EqualsTo(l2.Body, EqualsToOptions.FreeParametersByName).ParameterNamesMatter- lambda parameters and block/catch variables matched by position must also have the same name.
Expression<Func<int,int>> f1 = a => a + 1;
Expression<Func<int,int>> f2 = a => a + 1;
f1.Body.EqualsTo(f2.Body); // false: the two 'a' are different parameters
f1.Body.EqualsTo(f2.Body, EqualsToOptions.FreeParametersByName); // true
Expression<Func<int,int>> f3 = b => b + 1;
f1.EqualsTo(f3); // true
f1.EqualsTo(f3, EqualsToOptions.ParameterNamesMatter); // false
Supported frameworks
The library targets netstandard2.0. The tests run on .NET 8 and .NET 10; building from source requires the .NET 10 SDK.
The examples above are covered by tests (Tests/ReadmeTests.cs).
| Product | Versions 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 was computed. 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. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.