Creating a custom endpoint visualization graph: Visualizing ASP.NET Core 3.0 endpoints using GraphvizOnline - Part 4
Tags: aspnetcore, routing
Published: 2020-08-02 17:52:29

---
title: Creating a custom endpoint visualization graph: Visualizing ASP.NET Core 3.0 endpoints using GraphvizOnline - Part 4
published: true
date: 2020-07-21 10:00:00 UTC
tags: ASPNETCore,Routing
canonical_url: https://andrewlock.net/creating-a-custom-endpoint-visualization-graph/
---

 ![Creating a custom endpoint visualization graph](https://andrewlock.net/content/images/2020/customgraph_banner.png)

In [this series](https://andrewlock.net/visualizing-asp-net-core-endpoints-using-graphvizonline-and-the-dot-language/), I've been laying the groundwork for building a custom endpoint visualization graph, as I showed [in my first post](https://andrewlock.net/visualizing-asp-net-core-endpoints-using-graphvizonline-and-the-dot-language/). This graph shows the different parts of the endpoint routes: literal values, parameters, verb constraints, and endpoints that generate a result:

![A ValuesController endpoint routing application with different styling](https://andrewlock.net/content/images/2020/graph_07.png)

In this post I show how you can create an endpoint graph like this for your own application, by creating a custom `DfaGraphWriter`.

> This post uses techniques and classes from the previous posts in the series, so I strongly suggest reading those before continuing.

## Adding configuration for the endpoint graph

The first thing we'll look at is how to configure what the final endpoint graph will look like. We'll add configuration for two types of node and four types of edge. The edges are:

- Literal edges: Literal matches for route sections, such as `api` and `values` in the route `api/values/{id}`.
- Parameters edges: Parameterised sections for routes, such as `{id}` in the route `api/values/{id}`.
- Catch all edges: Edges that correspond to the catch-all route parameter, such as `{**slug}`.
- Policy edges: Edges that correspond to a constraint other than the URL. For example, the HTTP verb-based edges in the graph, such as `HTTP: GET`.

and the nodes are:

- Matching node: A node that is associated with an endpoint match, so will generate a response.
- Default node: A node that is _not_ associated with an endpoint match.

Each of these nodes and edges can have [any number of Graphviz attributes](https://www.graphviz.org/doc/info/attrs.html) to control their display. The `GraphDisplayOptions` below show the default values I used to generate the graph at the start of this post:

```csharp
public class GraphDisplayOptions
{
    /// <summary>
    /// Additional display options for literal edges
    /// </summary>
    public string LiteralEdge { get; set; } = string.Empty;

    /// <summary>
    /// Additional display options for parameter edges
    /// </summary>
    public string ParametersEdge { get; set; } = "arrowhead=diamond color=\"blue\"";

    /// <summary>
    /// Additional display options for catchall parameter edges
    /// </summary>
    public string CatchAllEdge { get; set; } = "arrowhead=odot color=\"green\"";

    /// <summary>
    /// Additional display options for policy edges
    /// </summary>
    public string PolicyEdge { get; set; } = "color=\"red\" style=dashed arrowhead=open";

    /// <summary>
    /// Additional display options for node which contains a match
    /// </summary>
    public string MatchingNode { get; set; } = "shape=box style=filled color=\"brown\" fontcolor=\"white\"";

    /// <summary>
    /// Additional display options for node without matches
    /// </summary>
    public string DefaultNode { get; set; } = string.Empty;
}
```

We can now create our custom graph writer using this object to control the display, and using the _ImpromptuInterface_ "proxy" technique shown in [the previous post](https://andrewlock.net/creating-a-custom-dfagraphwriter-using-impromptuinterface-and-reflection/).

## Creating a custom DfaGraphWriter

Our custom graph writer (cunningly called `CustomDfaGraphWriter`) is heavily based on [the `DfaGraphWriter` included in ASP.NET Core](https://github.com/dotnet/aspnetcore/blob/0889a62250e56066055d45a6fce99413d3f48d56/src/Http/Routing/src/Internal/DfaGraphWriter.cs). The bulk of this class is the same as the original, with the following changes:

- Inject the `GraphDisplayOptions` into the class to customise the display.
- Use the _ImpromptuInterface_ library to work with the internal `DfaMatcherBuilder` and `DfaNode` classes, [as shown in the previous post](https://andrewlock.net/creating-a-custom-dfagraphwriter-using-impromptuinterface-and-reflection/).
- Customise the `WriteNode` function to use our custom styles.
- Add a `Visit` function to work with the `IDfaNode` interface, instead of using the `Visit()` method on the internal `DfaNode` class.

The whole `CustomDfaGraphWriter` is shown below, focusing on the main `Write()` function. I've kept the implementation almost identical to [the original](https://github.com/dotnet/aspnetcore/blob/0889a62250e56066055d45a6fce99413d3f48d56/src/Http/Routing/src/Internal/DfaGraphWriter.cs), only updating the parts we have to.

```csharp
public class CustomDfaGraphWriter
{
    // Inject the GraphDisplayOptions 
    private readonly IServiceProvider _services;
    private readonly GraphDisplayOptions _options;
    public CustomDfaGraphWriter(IServiceProvider services, GraphDisplayOptions options)
    {
        _services = services;
        _options = options;
    }

    public void Write(EndpointDataSource dataSource, TextWriter writer)
    {
        // Use ImpromptuInterface to create the required dependencies as shown in previous post
        Type matcherBuilder = typeof(IEndpointSelectorPolicy).Assembly
            .GetType("Microsoft.AspNetCore.Routing.Matching.DfaMatcherBuilder");

        // Build the list of endpoints used to build the graph
        var rawBuilder = _services.GetRequiredService(matcherBuilder);
        IDfaMatcherBuilder builder = rawBuilder.ActLike<IDfaMatcherBuilder>();

        // This is the same logic as the original graph writer
        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 raw tree from the registered routes
        var rawTree = builder.BuildDfaTree(includeLabel: true);
        IDfaNode tree = rawTree.ActLike<IDfaNode>();

        // Store a list of nodes that have already been visited 
        var visited = new Dictionary<IDfaNode, int>();

        // Build the graph by visiting each node, and calling WriteNode on each
        writer.WriteLine("digraph DFA {");
        Visit(tree, WriteNode);
        writer.WriteLine("}");

        void WriteNode(IDfaNode node)
        {
            /* Write the node to the TextWriter */
            /* Details shown later in this post*/
        }
    }

    static void Visit(IDfaNode node, Action<IDfaNode> visitor)
    {
        /* Recursively visit each node in the tree. */
        /* Details shown later in this post*/
    }
}
```

I've elided the `Visit` and `WriteNode` functions here for brevity, but we'll look into them soon. We'll start with the `Visit` function, as that flies closest to the original.

## Updating the Visit function to work with IDfaNode

As I discussed in [my previous post](https://andrewlock.net/creating-a-custom-dfagraphwriter-using-impromptuinterface-and-reflection/), one of the biggest problems creating a custom `DfaGraphWriter` is its use of `internal` classes. To work around that I used _ImpromptuInterface_ to create proxy objects that wrap the original:

![Using ImpromptuInterface to add a wrapper proxy](https://andrewlock.net/content/images/2020/impromptuinterface.png)

The original `Visit()` method is [a method on the `DfaNode`](https://github.com/dotnet/aspnetcore/blob/0889a62250e56066055d45a6fce99413d3f48d56/src/Http/Routing/src/Matching/DfaNode.cs#L80) class. It recursively visits every node in the endpoint tree, calling a provided `Action<>` function for each node.

> As `DfaNode` is `internal`, I implemented the `Visit` function as a static method on `CustomDfaGraphWriter` instead.

Our custom implementation is broadly the same as [the original](https://github.com/dotnet/aspnetcore/blob/0889a62250e56066055d45a6fce99413d3f48d56/src/Http/Routing/src/Matching/DfaNode.cs#L80), but we have to do some somewhat arduous conversions between the "raw" `DfaNode`s and our `IDfaNode` proxies. The updated method is shown below. The method takes two parameters—the node being checked, and an `Action<>` to run on each.

```csharp
static void Visit(IDfaNode node, Action<IDfaNode> visitor)
{
    // Does the node of interest have any nodes connected by literal edges?
    if (node.Literals?.Values != null)
    {
        // node.Literals is actually a Dictionary<string, DfaNode>
        foreach (var dictValue in node.Literals.Values)
        {
            // Create a proxy for the child DfaNode node and visit it
            IDfaNode value = dictValue.ActLike<IDfaNode>();
            Visit(value, visitor);
        }
    }

    // Does the node have a node connected by a parameter edge?
    // The reference check breaks any cycles in the graph
    if (node.Parameters != null && !ReferenceEquals(node, node.Parameters))
    {
        // Create a proxy for the DfaNode node and visit it
        IDfaNode parameters = node.Parameters.ActLike<IDfaNode>();
        Visit(parameters, visitor);
    }

    // Does the node have a node connected by a catch-all edge?
    // The refernece check breaks any cycles in the graph
    if (node.CatchAll != null && !ReferenceEquals(node, node.CatchAll))
    {
        // Create a proxy for the DfaNode node and visit it
        IDfaNode catchAll = node.CatchAll.ActLike<IDfaNode>();
        Visit(catchAll, visitor);
    }

    // Does the node have a node connected by a policy edges?
    if (node.PolicyEdges?.Values != null)
    {
        // node.PolicyEdges is actually a Dictionary<object, DfaNode>
        foreach (var dictValue in node.PolicyEdges.Values)
        {
            IDfaNode value = dictValue.ActLike<IDfaNode>();
            Visit(value, visitor);
        }
    }

    // Write the node using the provided Action<>
    visitor(node);
}
```

The `Visit` function uses a post-order traversal, so it traverses "deep" into a node's child nodes first before writing the node using the `visitor` function. This is the same as [the original `DfaNode.Visit()` function](https://github.com/dotnet/aspnetcore/blob/0889a62250e56066055d45a6fce99413d3f48d56/src/Http/Routing/src/Matching/DfaNode.cs#L80).

We're almost there now. We have a class that builds the endpoint node tree, traverses all the nodes in the tree, and runs a function for each. All that remains is to define the visitor function, `WriteNode()`.

## Defining a custom WriteNode function

We've _finally_ got to the meaty part, controlling how the endpoint graph is displayed. All of the customisation and effort so far has been to enable us to customise the `WriteNode` function.

`WriteNode()` is [a local function](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions) that writes a node to the `TextWriter` output, along with any connected edges, using the [DOT graph description language](https://en.wikipedia.org/wiki/DOT_(graph_description_language)).

Our custom `WriteNode()` function is, again, almost the same as [the original](https://github.com/dotnet/aspnetcore/blob/0889a62250e56066055d45a6fce99413d3f48d56/src/Http/Routing/src/Internal/DfaGraphWriter.cs#L56-L94). There are two main differences:

- The original graph writer works with `DfaNode`s, we have to convert to using the `IDfaNode` proxy.
- The original graph writer uses the same styling for all nodes and edges. We customise the display of nodes and edges based on the configured `GraphDisplayOptions`.

> As `WriteNode` is a local function, it can access variables from the enclosing function. This includes the `writer` parameter, used to write the graph to output and the `visited` dictionary of previously written nodes.

The following shows our (heavily commented) custom version of the `WriteNode()` method.

```csharp
void WriteNode(IDfaNode node)
{
    // add the node to the visited node dictionary if it isn't already
    // generate a zero-based integer label for the node
    if (!visited.TryGetValue(node, out var label))
    {
        label = visited.Count;
        visited.Add(node, label);
    }

    // We can safely index into visited because this is a post-order traversal,
    // all of the children of this node are already in the dictionary.

    // If this node is linked to any nodes by a literal edge
    if (node.Literals != null)
    {
        foreach (DictionaryEntry dictEntry in node.Literals)
        {
            // Foreach linked node, get the label for the edge and the linked node
            var edgeLabel = (string)dictEntry.Key;
            IDfaNode value = dictEntry.Value.ActLike<IDfaNode>();
            int nodeLabel = visited[value];

            // Write an edge, including our custom styling for literal edges
            writer.WriteLine($"{label} -> {nodeLabel} [label=\"/{edgeLabel}\" {_options.LiteralEdge}]");
        }
    }

    // If this node is linked to a nodes by a parameter edge
    if (node.Parameters != null)
    {
        IDfaNode parameters = node.Parameters.ActLike<IDfaNode>();
        int nodeLabel = visited[catchAll];

        // Write an edge labelled as /* using our custom styling for parameter edges
        writer.WriteLine($"{label} -> {nodeLabel} [label=\"/**\" {_options.CatchAllEdge}]");
    }

    // If this node is linked to a catch-all edge
    if (node.CatchAll != null && node.Parameters != node.CatchAll)
    {
        IDfaNode catchAll = node.CatchAll.ActLike<IDfaNode>();
        int nodeLabel = visited[catchAll];

        // Write an edge labelled as /** using our custom styling for catch-all edges
        writer.WriteLine($"{label} -> {nodelLabel} [label=\"/**\" {_options.CatchAllEdge}]");
    }

    // If this node is linked to any Policy Edges
    if (node.PolicyEdges != null)
    {
        foreach (DictionaryEntry dictEntry in node.PolicyEdges)
        {
            // Foreach linked node, get the label for the edge and the linked node
            var edgeLabel = (object)dictEntry.Key;
            IDfaNode value = dictEntry.Value.ActLike<IDfaNode>();
            int nodeLabel = visited[value];

            // Write an edge, including our custom styling for policy edges
            writer.WriteLine($"{label} -> {nodeLabel} [label=\"{key}\" {_options.PolicyEdge}]");
        }
    }

    // Does this node have any associated matches, indicating it generates a response?
    var matchCount = node?.Matches?.Count ?? 0;

    var extras = matchCount > 0 
        ? _options.MatchingNode // If we have matches, use the styling for response-generating nodes...
        : _options.DefaultNode; // ...otherwise use the default style

    // Write the node to the graph output
    writer.WriteLine($"{label} [label=\"{node.Label}\" {extras}]");
}
```

Tracing the flow of these interactions can be a little confusing, because of the way we write the nodes from the "leaf" nodes back to the root of the tree. For example if we look at the output for the basic app shown at the start of this post, you can see the "leaf" endpoints are all written first: the `healthz` health check endpoint and the terminal match generating endpoints with the longest route:

```javaScript
digraph DFA {
  1 [label="/healthz/" shape=box style=filled color="brown" fontcolor="white"]
  2 [label="/api/Values/{...}/ HTTP: GET" shape=box style=filled color="brown" fontcolor="white"]
  3 [label="/api/Values/{...}/ HTTP: PUT" shape=box style=filled color="brown" fontcolor="white"]
  4 [label="/api/Values/{...}/ HTTP: DELETE" shape=box style=filled color="brown" fontcolor="white"]
  5 [label="/api/Values/{...}/ HTTP: *" shape=box style=filled color="brown" fontcolor="white"]
  6 -> 2 [label="HTTP: GET" color="red" style=dashed arrowhead=open]
  6 -> 3 [label="HTTP: PUT" color="red" style=dashed arrowhead=open]
  6 -> 4 [label="HTTP: DELETE" color="red" style=dashed arrowhead=open]
  6 -> 5 [label="HTTP: *" color="red" style=dashed arrowhead=open]
  6 [label="/api/Values/{...}/"]
  7 [label="/api/Values/ HTTP: GET" shape=box style=filled color="brown" fontcolor="white"]
  8 [label="/api/Values/ HTTP: POST" shape=box style=filled color="brown" fontcolor="white"]
  9 [label="/api/Values/ HTTP: *" shape=box style=filled color="brown" fontcolor="white"]
  10 -> 6 [label="/*" arrowhead=diamond color="blue"]
  10 -> 7 [label="HTTP: GET" color="red" style=dashed arrowhead=open]
  10 -> 8 [label="HTTP: POST" color="red" style=dashed arrowhead=open]
  10 -> 9 [label="HTTP: *" color="red" style=dashed arrowhead=open]
  10 [label="/api/Values/"]
  11 -> 10 [label="/Values"]
  11 [label="/api/"]
  12 -> 1 [label="/healthz"]
  12 -> 11 [label="/api"]
  12 [label="/"]
}
```

Even though the leaf nodes are written to the graph output first, the Graphviz visualizer will generally draw the graph with the leaf nodes at the _bottom_, and the edges pointing down. You can visualize the graph online at [https://dreampuf.github.io/GraphvizOnline/](https://dreampuf.github.io/GraphvizOnline/):

![A ValuesController endpoint routing application with different styling](https://andrewlock.net/content/images/2020/graph_07.png)

If you want to change how the graph is rendered you can customize the `GraphDisplayOptions`. If you use the "test" approach I described in a previous post, you can pass these options in directly when generating the graph. If you're using the "middleware" approach, you can register the `GraphDisplayOptions` using the `IOptions<>` system instead, and control the display using the configuration system.

## Summary

In this post I showed how to create a custom `DfaGraphWriter` to control how an application's endpoint graph is generated. To interoperate with the `internal` classes we used _ImpromptuInterface_, as described in [the previous post](https://andrewlock.net/creating-a-custom-dfagraphwriter-using-impromptuinterface-and-reflection/), to create proxies we can interact with. We then had to write a custom `Visit()` function to work with the `IDfaNode` proxies. Finally, we created a custom `WriteNode` function that uses custom settings defined in a `GraphDisplayOptions` object to display each type of node and edge differently.