Skip to content

Extensibility and plugin mechanism

Zev Spitz edited this page Aug 19, 2020 · 6 revisions

The library has two goals:

  • Return a string rendering of an expression tree (or other related type)

  • Optionally, identify which part of the string corresponds to which part of the expression tree. (This powers the selection sync of ExpressionTreeVisualizer):

    Selection sync

The library comes with a number of built-in renderers. However, you can register additional renderers, identified with a string key, by calling the ExpressionTreeToString.Renderers.Register method, and passing in:

  1. the renderer key, and

  2. the renderer itself, matching the following delegate type:

    public delegate (string result, Dictionary<string, (int start, int length)>? pathSpans) Writer(
        object o, OneOf<string, Language?> languageArg, bool usePathSpans
    );

Like so:

// using ExpressionTreeToString
Renderers.Register(
    "DummyRenderer",
    (o, language, usePathSpans) => "Hello world!", null)
);

Calling the ToString extension method with the renderer key will return the generated string:

// using ExpressionTreeToString
Expression<Func<bool>> expr = () => true;
var s = expr.ToString("DummyRenderer");
/*
      Hello world!
*/

You can generate the string representation however you like -- in the above (contrived) example, it's always returning the same string. However, the library also provides a set of base classes, to ease the implementation.

Renderer parameters and return type

The Renderer delegate takes three parameters:

and should return a value tuple with two members:

  1. the representation
  2. if usePathSpans is true, the property path / spans Dictionary<string, (int start, int length)>
    if usePathSpans is false, then null

For example:

// using ExpressionTreeToString
Renderers.Register(
    "Factory methods",
    (o, language, usePathSpans) =>
        usePathSpans ?
            (new FactoryMethodsWriterVisitor(o, languageArg, out var pathSpans).ToString(), pathSpans) :
            (new FactoryMethodsWriterVisitor(o, languageArg).ToString(), null)
);

NB. I haven't decided if it should be possible to override existing renderers with the same key. This behavior is currently unsupported.

Class hierarchy

Although the library doesn't dictate to you how to produce the string representation or the path spans, it exposes additional classes for visiting the nodes of the expression tree and writing the representation, at various levels of abstraction.

Class Base class Description
WriterVisitorBase Low-level writing methods, and WriteNode. Each call to WriteNode adds an entry to the pathSpans.
BuiltinsWriterVisitor WriterVisitorBase Provides abstract methods that force an implementation for each type in the System.Linq.Expressions namespace
CodeWriterVisitor BuiltinsWriterVisitor Common functionality for rendering an expression tree as code

You can also inherit from one of the visitor-writer classes used by the built-in renderers:

  • CSharpWriterVisitor
  • VBWriterVisitor
  • FactoryMethodsWriterVisitor
  • ObjectNotationWriterVisitor
  • TextualTreeWriterVisitor

Which class you should inherit from, depends on what you're trying to do:

  • If you don't care about the built-in expression types at all, inherit from WriterBase. When a given node is passed to WriteNode, the implementation would forward the child expressions of the node to recursively call WriteNode.
  • If you want to handle the built-in expression types in a new way unrelated to existing renderers -- say XML or JSON data extracted from the expression tree -- inherit from BuiltinsWriterVisitor.
  • If you want to extend an existing writer-visitor, you can do so, optionally overriding the base methods as needed; for example, in order to handle various extension expression types.

Plugin mechanism

The API for loading renderers from third-party DLLs hasn't yet been implemented, and will probably look something like this.