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 theDfaMatcherBuilder
. - Calls
BuildDfaTree
on theDfaMatcherBuilder
. This creates a graph ofDfaNode
s. - Visit each
DfaNode
in the tree, and write it to theTextWriter
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:
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 furtherinternal
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
:
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 anIDictionary<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 object
s. 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.