-
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) => (int)p.DOB.DayOfWeek == 2
*/
Console.WriteLine(expr.ToString("Visual Basic"));
// prints:
/*
Function(p As Person) CInt(p.DOB.DayOfWeek) = 2
*/
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; see below).
There are currently five available formatters:
-
C#
-- C#-style pseudocodeConsole.WriteLine(expr.ToString("C#")); /* (Person p) => (int)p.DOB.DayOfWeek == 2 */
-
Visual Basic
-- Visual Basic -style pseudocodeConsole.WriteLine(expr.ToString("Visual Basic")); /* Function(p As Person) CInt(p.DOB.DayOfWeek) = 2 */
-
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" ) ) */
-
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.Left (int)p.DOB.DayOfWeek
Body.Right 2
Body (int)p.DOB.DayOfWeek == 2
(Person p) => (int)p.DOB.DayOfWeek == 2
This tells us that the object corresponding to the expression p.DOB.DayOfWeek
is at expr.Body.Left.Operand
.
When using the factory methods formatter, 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"),
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.TimeSpan |
#1:1:1# (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 |
1-dimensional array | new[] { 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:
Type / value | "C#" |
"Visual Basic" |
---|---|---|
Type | typeof(string) |
GetType(String) |
ByRef type | typeof(string).MakeByRef() |
GetType(String).MakeByRef() |
ConstructorInfo | typeof(Timer).GetConstructor() |
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") |
(Note that there is no attempt to find the appropriate overload for methods or constructors. This is being tracked at #7.)
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
.