-
Notifications
You must be signed in to change notification settings - Fork 11
Home
Welcome to the wiki for the ExpressionTreeToString library. This library generates string representations of expression trees and related types:
Expression<Func<Person, bool>> expr = p => p.DOB.DayOfWeek == DayOfWeek.Tuesday;
Console.WriteLine(expr.ToString("C#"));
// prints:
/*
(Person p) => p.DOB.DayOfWeek == DayOfWeek.Tuesday
*/
Console.WriteLine(expr.ToString("Visual Basic"));
// prints:
/*
Function(p As Person) p.DOB.DayOfWeek = DayOfWeek.Tuesday
*/
There are two built-in string representations in .NET. One is the Expression.ToString method:
Console.WriteLine(expr.ToString());
/*
p => (Convert(p.DOB.DayOfWeek) == 2)
*/
However, the generated string has a number of limitations:
- conversions (
Convert(...)
vs(int)...
), - type names use the
Type.Name
property (List`1
vsList<string>
), - closed-over variables are rendered as members of a hidden class: (
value(sampleCode.Program+<>c__DisplayClass0_0).i
vsi
). - only C#-style
There is also the DebugView
property, which uses a special syntax to represent more of the expression tree's information:
.Lambda #Lambda1<System.Func`2[sampleCode.Person,System.Boolean]>(sampleCode.Person $p) {
(System.Int32)($p.DOB).DayOfWeek == 2
}
but the additional information is not always necessary, and can make it even harder to read. Also, unless you want to jump through some reflection hoops, you can only read this property while in a debugging session.
The string rendering library provides a set of .ToString
extension methods on the various types used in expression trees -- Expression
, CatchBlock
etc.. You pass in the formatter name to use when rendering the string (and optionally the language for rendering literals and type names).
There are currently five available formatters:
-
C#
-- C#-style pseudocodeConsole.WriteLine(expr.ToString("C#")); /* (Person p) => p.DOB.DayOfWeek == DayOfWeek.Tuesday */
-
Visual Basic
-- Visual Basic -style pseudocodeConsole.WriteLine(expr.ToString("Visual Basic")); /* Function(p As Person) p.DOB.DayOfWeek = DayOfWeek.Tuesday */
-
Factory methods
-- factory method calls used to generate the expression:Console.WriteLine(expr.ToString("Factory methods")); /* // using static System.Linq.Expressions.Expression Lambda( Equal( Convert( MakeMemberAccess( MakeMemberAccess(p, typeof(Person).GetProperty("DOB") ), typeof(DateTime).GetProperty("DayOfWeek") ), typeof(int) ), Constant(2) ), var p = Parameter( typeof(Person), "p" ) ) */
Note that you need to reuse the same parameter object across the entire expression tree, so we use a non-syntactical inline variable declaration (
var p = ...
). Ideally this declaration should be before the first function call; this is tracked in https://github.com/zspitz/ExpressionTreeToString/issues/8. -
Object notation
-- describes objects and collections using initializer syntax:Console.WriteLine(expr.ToString("Object notation")); /* new Expression<Func<Person, bool>> { NodeType = ExpressionType.Lambda, Type = typeof(Func<Person, bool>), Parameters = new ReadOnlyCollection<ParameterExpression> { new ParameterExpression { Type = typeof(Person), IsByRef = false, Name = "p" } }, Body = new BinaryExpression { NodeType = ExpressionType.Equal, Type = typeof(bool), Left = new UnaryExpression { NodeType = ExpressionType.Convert, Type = typeof(int), Operand = new MemberExpression { Type = typeof(DayOfWeek), Expression = new MemberExpression { Type = typeof(DateTime), Expression = new ParameterExpression { Type = typeof(Person), IsByRef = false, Name = "p" }, Member = typeof(Person).GetProperty("DOB") }, Member = typeof(DateTime).GetProperty("DayOfWeek") } }, Right = new ConstantExpression { Type = typeof(int), Value = 2 } }, ReturnType = typeof(bool) } */
-
Textual tree
-- describes the structure of the expression tree: node type, reflection type, name and value, as appropriateConsole.WriteLine(expr.ToString("Textual tree")); /* Lambda (Func<Person, bool>) Parameters[0] - Parameter (Person) p Body - Equal (bool) Left - Convert (int) Operand - MemberAccess (DayOfWeek) DayOfWeek Expression - MemberAccess (DateTime) DOB Expression - Parameter (Person) p Right - Constant (int) = 2 */
Let's say you want to know which part of the expression tree corresponds to p.DOB.DayOfWeek
in the C# or Visual Basic formatters.
The .ToString
extension methods have an additional overload that takes an out Dictionary<string, (int start, int length)>
. Each key is the property path to an expression tree node from the root of the expression tree; the value is a tuple of the start and length of the corresponding substring.
For example, you could write the following:
string s = expr.ToString("C#", out Dictionary<string, (int start, int length)> pathSpans);
const int firstColumnAlignment = -45;
Console.WriteLine($"{"Path",firstColumnAlignment}Substring");
Console.WriteLine(new string('-', 85));
foreach (var kvp in pathSpans) {
var path = kvp.Key;
var (start, length) = kvp.Value;
Console.WriteLine(
$"{path,firstColumnAlignment}{new string(' ', start)}{s.Substring(start, length)}"
);
}
which would print:
Path Substring
-----------------------------------------------------------------------------------------------
Parameters[0] Person p
Body.Left.Operand.Expression.Expression p
Body.Left.Operand.Expression p.DOB
Body.Left.Operand p.DOB.DayOfWeek
Body.Right DayOfWeek.Tuesday
Body p.DOB.DayOfWeek == DayOfWeek.Tuesday
(Person p) => p.DOB.DayOfWeek == DayOfWeek.Tuesday
This tells us that the object corresponding to the text p.DOB.DayOfWeek
is at expr.Body.Left.Operand
.
When using the non-language formatters (i.e. factory methods, object notation, or textual tree formatters) you can also specify the language for rendering literals, type names and other code constructs as C#
(the default) or Visual Basic
:
Console.WriteLine(expr.ToString("Factory methods", "Visual Basic"));
/*
' Imports System.Linq.Expressions.Expression
Lambda(
Call(
MakeMemberAccess(p,
GetType(Person).GetProperty("LastName")
),
GetType(String).GetMethod("StartsWith", { GetType(String) }),
Constant("A")
),
Dim p = Parameter(
GetType(Person),
"p"
)
)
*/
Literals are rendered as follows, depending on the value passed into the language
parameter:
Type / value | "C#" |
"Visual Basic" |
Anything else |
---|---|---|---|
null |
null |
Nothing |
␀ |
numeric types | ToString | ToString | ToString |
System.Boolean |
true / false
|
True / False
|
ToString |
System.Char | 'a' |
"a"C |
|
System.DateTime |
#1-1-1980# (using ToString) |
||
System.String (no control characters) | "abcd" |
"abcd" |
"abcd" |
System.String (with control characters) | "ab\ncd" |
"ab cd"
|
|
Enum values | DayOfWeek.Tuesday |
DayOfWeek.Tuesday |
DayOfWeek.Tuesday |
Flag enum values | `BindingFlags.Static | BindingFlags.Public` | BindingFlags.Static Or BindingFlags.Public |
1-dimensional array | new object[] { 1, "2" } |
{ 1, "2" } |
|
System.Tuple, System.ValueTuple | (1, "2") |
(1, "2") |
(1, "2") |
If the object/value doesn't have a matching rendering it will display as #<TypeName>
-- e.g. #String
, #ArrayList
or #Random
.
Some instances of types in the System.Reflection
namespace have special handling, using the language-specific type operator and reflection methods. For example, a MethodInfo
will be rendered as a call to GetMethod
; a PropertyInfo, as a call to GetProperty
.
If the relevant reflection method requires additional parameters in order to resolve the member -- e.g. multiple methods with the same name, or a non-public or static property -- those parameters will also be rendered:
Type / value | "C#" |
"Visual Basic" |
---|---|---|
Type | typeof(string) |
GetType(String) |
ByRef type | typeof(string).MakeByRef() |
GetType(String).MakeByRef() |
ConstructorInfo | typeof(Timer).GetConstructor(new Type[] { }) |
GetType(Timer).GetConstructor({ }) |
EventInfo | typeof(Timer).GetEvent("Elapsed") |
GetType(String).GetEvent("Elapsed") |
FieldInfo | typeof(string).GetField("m_stringLength") |
GetType(String).GetField("m_stringLength") |
MethodInfo | typeof(string).GetMethod("Clone") |
GetType(String).GetMethod("Clone") |
PropertyInfo | typeof(string).GetProperty("Length") |
GetType(String).GetProperty("Length") |
Type names are rendered using language-specific keywords: e.g. int
instead of Int32
with C#
passed as the language, or Date
instead of DateTime
when Visual Basic
is passed in.
Generic types use the language-specific syntax -- List<string>
or List(Of Date)
instead of List`1
.
Note that if you try to pass in the language
parameter together with one of the language formatters (C#
or Visual Basic
) it will be ignored.