Skip to content
Zev Spitz edited this page Aug 16, 2024 · 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
*/

Limitations of 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

This is by design, per the docs, section 2.17:

Expression.ToString is for light weight debugging purposes only. ... ToString does not try to return semantically accurate C# or VB code in particular. We try to return terse strings loosely suggesting what an ET node contains for quick inspection only.

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 a string or enum value identifying which renderer you want to use (and optionally the language for rendering literals and type names).

For example:

Console.WriteLine(expr.ToString("Textual tree", Language.CSharp));
// prints:
/*
    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 rendering corresponds to the node at Body.Left.Operand.

The .ToString extension methods have an additional overload that takes an out Dictionary<string, (int start, int length)>. Each key in the dictionary 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 text corresponding to the object at Body.Left.Operand is p.DOB.DayOfWeek.

The language parameter

When using the non-language renderers (i.e. factory methods, object notation, or textual tree renderers) 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 depending on the value passed into the language parameter, as described here.

Type names are rendered as described here.

Note that if you try to pass in the language parameter together with one of the language renderers (C# or Visual Basic) it will be ignored.