blog post image
Andrew Lock avatar

Andrew Lock

~6 min read

Creating a custom DfaGraphWriter using ImpromptuInterface for easier reflection

Visualizing ASP.NET Core 3.0 endpoints using GraphvizOnline - Part 3

In this post I lay the groundwork for creating a custom implementation of DfaGraphWriter. DfaGraphWriter is public, so you can use it in your application as I showed in my previous post, but all the classes it uses are marked internal. That makes creating your own version problematic. To work around that, I use an open source reflection library, ImpromptuInterface, to make creating a custom DfaGraphWriter implementation easier.

We'll start by looking at the existing DfaGraphWriter, to understand the internal classes it uses and the issues that causes us. Then we'll look at using some custom interfaces and the ImpromptuInterface library to allow us to call those classes. In the next post, we'll look at how to use our custom interfaces to create a custom version of the DfaGraphWriter.

Exploring the existing DfaGraphWriter

The DfaGraphWriter class lives inside one of the "pubternal" folders in ASP.NET Core. It's registered as a singleton and uses an injected IServiceProvider to retrieve the helper service, DfaMatcherBuilder:

 public class DfaGraphWriter
{
    private readonly IServiceProvider _services;
    public DfaGraphWriter(IServiceProvider services)
    {
        _services = services;
    }

    public void Write(EndpointDataSource dataSource, TextWriter writer)
    {
        // retrieve the required DfaMatcherBuilder
        var builder = _services.GetRequiredService<DfaMatcherBuilder>();

        // loop through the endpoints in the dataSource, and add them to the builder
        var endpoints = dataSource.Endpoints;
        for (var i = 0; i < endpoints.Count; i++)
        {
            if (endpoints[i] is RouteEndpoint endpoint && (endpoint.Metadata.GetMetadata<ISuppressMatchingMetadata>()?.SuppressMatching ?? false) == false)
            {
                builder.AddEndpoint(endpoint);
            }
        }

        // Build the DfaTree. 
        // This is what we use to create the endpoint graph
        var tree = builder.BuildDfaTree(includeLabel: true);

        // Add the header
        writer.WriteLine("digraph DFA {");
        
        // Visit each node in the graph to create the output
        tree.Visit(WriteNode);

        //Close the graph
        writer.WriteLine("}");

        // Recursively walks the tree, writing it to the TextWriter
        void WriteNode(DfaNode node)
        {
            // Removed for brevity - we'll explore it in the next post
        }
    }
}

The code above shows everything the graph writer's Write method does, but in summary:

  • Fetches a DfaMatcherBuilder
  • Writes all of the endpoints in the EndpointDataSource to the DfaMatcherBuilder.
  • Calls BuildDfaTree on the DfaMatcherBuilder. This creates a graph of DfaNodes.
  • Visit each DfaNode in the tree, and write it to the TextWriter output. We'll explore this method in the next post.

The goal of creating our own custom writer is to customise that last step, by controlling how different nodes are written to the output, so we can create more descriptive graphs, as I showed previously:

Using different styling for an endpoint graph

Our problem is that two key classes, DfaMatcherBuilder and DfaNode, are internal so we can't easily instantiate them, or write methods that use them. That gives one of two options:

  • Reimplement the internal classes, including any further internal classes they depend on.
  • Use reflection to create and invoke methods on the existing classes.

Neither of those are great options, but given that the endpoint graph isn't a performance-critical thing, I decided using reflection would be the easiest. To make things even easier, I used the open source library, ImpromptuInterface.

Making reflection easier with ImpromptuInterface

ImpromptuInterface is a library that makes it easier to call dynamic objects, or to invoke methods on the underlying object stored in an object reference. It essentially adds easy duck/structural typing, by allowing you to use a stronlgy-typed interface for the object. It achieves that using the Dynamic Language Runtime and Reflection.Emit.

For example, lets take the existing DfaMatcherBuilder class that we want to use. Even though we can't reference it directly, we can still get an instance of this class from the DI container as shown below:

// get the DfaMatcherBuilder type - internal, so needs reflection :(
Type matcherBuilder = typeof(IEndpointSelectorPolicy).Assembly
    .GetType("Microsoft.AspNetCore.Routing.Matching.DfaMatcherBuilder");

object rawBuilder = _services.GetRequiredService(matcherBuilder);

The rawBuilder is an object reference, but it contains an instance of the DfaMatcherBuilder. We can't directly call methods on it, but we can invoke them using reflection by building MethodInfo and calling Invoke directly.

ImpromptuInterface makes that process a bit easier, by providing a static interface that you can directly call methods on. For example, for the DfaMatcherBuilder, we only need to call two methods, AddEndpoint and BuildDfaTree. The original class looks something like this:

internal class DfaMatcherBuilder : MatcherBuilder
{
    public override void AddEndpoint(RouteEndpoint endpoint) { /* body */ }
    public DfaNode BuildDfaTree(bool includeLabel = false)
}

We can create an interface that exposes these methods:

public interface IDfaMatcherBuilder
{
    void AddEndpoint(RouteEndpoint endpoint);
    object BuildDfaTree(bool includeLabel = false);
}

We can then use the ImpromptuInterface ActLike<> method to create a proxy object that implements the IDfaMatcherBuilder. This proxy wraps the rawbuilder object, so that when you invoke a method on the interface, it calls the equivalent method on the underlying DfaMatcherBuilder:

Using ImpromptuInterface to add a wrapper proxy

In code, that looks like:

// An instance of DfaMatcherBuilder in an object reference
object rawBuilder = _services.GetRequiredService(matcherBuilder);

// wrap the instance in the ImpromptuInterface interface
IDfaMatcherBuilder builder = rawBuilder.ActLike<IDfaMatcherBuilder>();

// we can now call methods on the builder directly, e.g. 
object rawTree =  builder.BuildDfaTree();

There's an important difference between the original DfaMatcherBuilder.BuildDfaTree() method and the interface version: the original returns a DfaNode, but that's another internal class, so we can't reference it in our interface.

Instead we create another ImpromptuInterface for the DfaNode class, and expose the properties we're going to need (you'll see why we need them in the next post):

public interface IDfaNode
{
    public string Label { get; set; }
    public List<Endpoint> Matches { get; }
    public IDictionary Literals { get; } // actually a Dictionary<string, DfaNode>
    public object Parameters { get; } // actually a DfaNode
    public object CatchAll { get; } // actually a DfaNode
    public IDictionary PolicyEdges { get; } // actually a Dictionary<object, DfaNode>
}

We'll use these properties in the WriteNode method in the next post, but there's some complexities. In the original DfaNode class, the Parameters and CatchAll properties return DfaNode objects. In our IDfaNode version of the properties we have to return object instead. We can't reference a DfaNode (because it's internal) and we can't return an IDfaNode, because DfaNode doesn't implement IDfaNode, so you can't you can't implicitly cast the object reference to an IDfaNode. You have to use ImpromptuInterface to explicitly add a proxy that implements the interface.

For example:

// Wrap the instance in the ImpromptuInterface interface
IDfaMatcherBuilder builder = rawBuilder.ActLike<IDfaMatcherBuilder>();

// We can now call methods on the builder directly, e.g. 
object rawTree =  builder.BuildDfaTree();
// Use ImpromptuInterface to add an IDfaNode wrapper
IDfaNode tree = rawTree.ActLike<IDfaNode>();

// We can now call methods and properties on the node...
object rawParameters = tree.Parameters;
// ...but they need to be wrapped using ImpromptuInterface too
IDfaNode parameters = rawParameters.ActLike<IDfaNode>();

We have another problem with the properties that return Dictionary types: Literals and PolicyEdges. The actual types returned are Dictionary<string, DfaNode> and Dictionary<object, DfaNode> respectively, but we need to use a type that doesn't contain the DfaNode type. Unfortunately, that means we have to fall back to the .NET 1.1 IDictionary interface!

You can't cast a Dictionary<string, DfaNode> to an IDictionary<string, object> is because doing so would be an unsafe form of covariance.

IDictionary is a non-generic interface, so the key and value are only exposed as objects. For the string key you can cast directly, and for the DfaNode we can use ImpromptuInterface to create the proxy wrapper for us:

// Enumerate the key-value pairs as DictinoaryEntrys
foreach (DictionaryEntry dictEntry in node.Literals)
{
    // Cast the key value to a string directly
    var key = (string)dictEntry.Key;
    // Use ImpromptuInterface to add a wrapper
    IDfaNode value = dictEntry.Value.ActLike<IDfaNode>();
}

We now have everything we need to create a custom DfaWriter implementation by implementing WriteNode, but this post is already a bit long, so we'll explore how to do that in the next post!

Summary

In this post I explored the DfaWriter implementation in ASP.NET Core, and the two internal classes it uses: DfaMatcherBuilder and DfaNode. The fact these class are internal makes it tricky to create our own implementation of the DfaWriter. To implement it cleanly we would have to reimplement both of these types and all the classes they depend on.

Instead, I used the ImpromptuInterface library to create a wrapper proxy that implements similar methods to the object being wrapped. This uses reflection to invoke methods on the wrapped property, but allows us to work with a strongly typed interface. In the next post I'll show how to use these wrappers to create a custom DfaWriter for customising your endpoint graphs.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?