Skip to content
Zev Spitz edited this page Aug 5, 2020 · 16 revisions

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
*/

Why not the built-in .NET representations -- .ToString() and DebugView?

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 vs List<string>),
  • closed-over variables are rendered as members of a hidden class: (value(sampleCode.Program+<>c__DisplayClass0_0).i vs i).
  • 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.

.ToString extension methods

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 pseudocode

    Console.WriteLine(expr.ToString("C#"));
    /*
      (Person p) => p.DOB.DayOfWeek == DayOfWeek.Tuesday
    */
  • Visual Basic -- Visual Basic -style pseudocode

    Console.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 appropriate

    Console.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
    */

Individual nodes represented in the final string

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.

The language parameter

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.

Clone this wiki locally