ToString on Expression Trees produces badly format

2019-06-25 04:31发布

问题:

When I use Expression.ToString() to convert an Expression Tree into human readable form, the result is something like this:

x => ((x.ID > 2) OrElse (x.ID != 6))
x => ((x.ID > 2) AndAlso (x.ID != 6))

Ideally, I would want the output to show the operators instead of "OrElse" and "AndAlso":

x => ((x.ID > 2) || (x.ID != 6))
x => ((x.ID > 2) && (x.ID != 6))

As a workaround, I could use the string.Replace() method..

.Replace("AndAlso", "&&")
.Replace("OrElse", "||")

but that has obvious weaknesses and seems awkward. Also I do not want to create a large 'Replace'-section or huge regex-tree simply to get the formatting right.

Is there a simple way to get a code-like human-readable form of expression trees?

回答1:

Unfortunately the simplest way to do this correctly would be to write your own ExpressionVisitor class which produces the C#-formatted output code.

The simplest way to do this is to use the ExpressionStringBuilder from the reference sources as a starting point and to tweak it until you are happy with the output result.



回答2:

When I'm interested in the semantics of the code represented by the expression, rather than the exact syntax tree, I've found it very useful to compile it to an Assembly and view that in ILSpy. Convenience method:

// Code is probably adapted from some other answer, don't remember
public static void CompileToAssemblyFile(
  this LambdaExpression expression,
  string outputFilePath = null,
  string assemblyAndModuleName = null,
  string typeName = "TheType",
  string methodName = "TheMethod",
  // Adjust this
  string ilSpyPath = @"C:\path\to\ILSpy.exe")
{
  assemblyAndModuleName = assemblyAndModuleName ?? nameof(CompileToAssemblyFile);

  outputFilePath = outputFilePath ??
                   Path.Combine(
                     Path.GetTempPath(),
                     $"{assemblyAndModuleName}_{DateTime.Now:yyyy-MM-dd_HH_mm_ss}_{Guid.NewGuid()}.dll");

  var domain = AppDomain.CurrentDomain;
  var asmName = new AssemblyName {Name = assemblyAndModuleName};

  var asmBuilder = domain.DefineDynamicAssembly(
    asmName,
    AssemblyBuilderAccess.RunAndSave,
    Path.GetDirectoryName(outputFilePath));

  string outputFileName = Path.GetFileName(outputFilePath);

  var module = asmBuilder.DefineDynamicModule(
    assemblyAndModuleName,
    outputFileName,
    true);

  var typeBuilder = module.DefineType(typeName, TypeAttributes.Public);

  var methodBuilder = typeBuilder.DefineMethod(
    methodName,
    MethodAttributes.Public | MethodAttributes.Static,
    expression.ReturnType,
    expression.Parameters.Select(p => p.Type).ToArray());

  var pdbGenerator = DebugInfoGenerator.CreatePdbGenerator();

  expression.CompileToMethod(methodBuilder, pdbGenerator);

  typeBuilder.CreateType();

  asmBuilder.Save(outputFileName);

  Process.Start(ilSpyPath, outputFilePath);
}

(This is not very faithful to the syntax tree because it goes through both the Expression -> IL translation done by the LambdaCompiler, and the IL -> C# decompilation by ILSpy. OTOH, it can improve readability by converting some gotos into loops, and by producing actual C#.)

This will fail if the Expression contains "non-trivial constants" (live objects); but for that one could write a visitor that replaces constants by new variables and then lambda-abstracts these variables at the toplevel.