Detecting duplicate routes in ASP.NET Core: Visualizing ASP.NET Core 3.0 endpoints using GraphvizOnline - Part 5
Tags: aspnetcore, routing
Published: 2020-08-09 16:12:53

---
title: Detecting duplicate routes in ASP.NET Core: Visualizing ASP.NET Core 3.0 endpoints using GraphvizOnline - Part 5
published: true
date: 2020-07-28 10:00:00 UTC
tags: ASPNETCore,Routing
canonical_url: https://andrewlock.net/detecting-duplicate-routes-in-aspnetcore/
---

 ![Detecting duplicate routes in ASP.NET Core](https://andrewlock.net/content/images/2020/duplicate_detector2.png)

In [this series](https://andrewlock.net/series/visualizing-asp-net-core-3-endpoints-using-graphvizonline/), I have shown how to view all the routes in your application using an endpoint graph. In this post I show how to do something more useful—detecting duplicate routes that would throw an exception at runtime. I show how to use the same `DfaGraphWriter` primitives as in previous posts to create a unit test that fails if you have any duplicate routes in your application that would cause a failure at runtime.

## Duplicate routes and runtime exceptions

ASP.NET Core 3.x uses endpoint routing for all your MVC/API controllers and Razor Pages, as well as dedicated endpoints like a health check endpoint. As you've already seen in this series, those endpoints are built into a directed graph, which is used to route incoming requests to a handler:

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

Unfortunately, it's perfectly possible to structure your controllers/Razor Pages in such as way as to have multiple endpoints be a match for a given route. For example, imagine you have a "values" controller in your project:

```csharp
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "value1", "value2" };
    }
}
```

Meanwhile, some joker has also created the following controller too:

```csharp
[ApiController]
public class DuplicateController
{
    [HttpGet("api/values")]
    public string Get()
    {
        return "oops";
    }
}
```

As routing is only handled at runtime, everything will appear to be fine when you build and deploy your application. Health checks will pass, and all other routes will behave normally. But when someone hits the URL `/api/values`, you'll get this beauty:

![AmbiguousMatchException: The request matched multiple endpoints](https://andrewlock.net/content/images/2020/duplicate_detector2.png)

The good thing here at least is that the error message tells you where the problem is. But wouldn't it be nice to know that _before_ you've deployed to production?

Trying to detect this error ahead of time was what started me down the graph-drawing rabbit-hole of this series. My goal was to create a simple unit test that can run as part of the build, to identify errors like these.

## Creating a duplicate endpoint detector

If you've read the rest of [this series](https://andrewlock.net/series/visualizing-asp-net-core-3-endpoints-using-graphvizonline/), then you may have already figured out how this is going to work. The `DfaGraphWriter` that is built-in to the framework, which can be used to visualize the endpoint graph, knows how to visit each node (route) in the graph. We'll adapt this somewhat: instead of outputting a graph, we'll return a list of routes which match multiple endpoints.

The detector class we'll create, `DuplicateEndpointDetector` uses [the same ImpromptuInterface technique as previous posts](https://andrewlock.net/creating-a-custom-dfagraphwriter-using-impromptuinterface-and-reflection/), reusing the `IDfaNode` and `IDfaMatcherBuilder` interfaces, shown below for completeness:

```csharp
public interface IDfaNode
{
    public string Label { get; set; }
    public List<Endpoint> Matches { get; }
    public IDictionary Literals { get; }
    public object Parameters { get; }
    public object CatchAll { get; }
    public IDictionary PolicyEdges { get; }
}

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

These interfaces give us a slightly nicer way to work with [the internal classes used by the default `DfaGraphWriter`](https://github.com/dotnet/aspnetcore/blob/083f81d7604352822820f6313c3d4eb219c044a8/src/Http/Routing/src/Internal/DfaGraphWriter.cs).

The bulk of the `DuplicateEndpointDetector` is identical to the `CustomDfaGraphWriter` [from my previous post](https://andrewlock.net/creating-a-custom-endpoint-visualization-graph/). The only difference is that we a `GetDuplicateEndpoints()` method instead of a `Write()` method. We also don't use a `TextWriter` (as we're not trying to build a graph for display) and we return a dictionary of duplicated endpoints (keyed on the route).

I've shown almost the complete code below, with the exception of the `LogDuplicates` method which we'll get to shortly.

```csharp
public class DuplicateEndpointDetector
{
    private readonly IServiceProvider _services;
    public DuplicateEndpointDetector(IServiceProvider services)
    {
        _services = services;
    }

    public Dictionary<string, List<string>> GetDuplicateEndpoints(EndpointDataSource dataSource)
    {
        // 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>();

        // Store a dictionary of duplicates
        var duplicates = new Dictionary<string, List<string>>();

        // Build the graph by visiting each node, and calling LogDuplicates on each
        Visit(tree, LogDuplicates);

        // done
        return duplicates;

        void LogDuplicates(IDfaNode node)
        {
            /* Details shown later in this post*/
        }
    }

    // Identical to the version shown in the previous post
    static void Visit(IDfaNode node, Action<IDfaNode> visitor)
    {
        if (node.Literals?.Values != null)
        {
            foreach (var dictValue in node.Literals.Values)
            {
                IDfaNode value = dictValue.ActLike<IDfaNode>();
                Visit(value, visitor);
            }
        }

        // Break cycles
        if (node.Parameters != null && !ReferenceEquals(node, node.Parameters))
        {
            IDfaNode parameters = node.Parameters.ActLike<IDfaNode>();
            Visit(parameters, visitor);
        }

        // Break cycles
        if (node.CatchAll != null && !ReferenceEquals(node, node.CatchAll))
        {
            IDfaNode catchAll = node.CatchAll.ActLike<IDfaNode>();
            Visit(catchAll, visitor);
        }

        if (node.PolicyEdges?.Values != null)
        {
            foreach (var dictValue in node.PolicyEdges.Values)
            {
                IDfaNode value = dictValue.ActLike<IDfaNode>();
                Visit(value, visitor);
            }
        }

        visitor(node);
    }
}
```

This class follows the same approach as the graph writer. It uses _ImpromptuInterface_ to create an `IDfaMatcherBuilder` proxy, which it uses to build a graph of all the endpoints in the system. It then visits each of the `IDfaNode`s in the graph and calls the `LogDuplicates()` method, passing in each node in turn.

`LogDuplicates()` is a lot simpler than the `WriteNode()` implementation [from the previous post](https://andrewlock.net/creating-a-custom-endpoint-visualization-graph/). All we're concerned about here is whether a node has multiple entries in its `Matches` property. If it does, we have an ambiguous route, so we add it to the dictionary. I'm using the `DisplayName` to identify the ambiguous endpoints, just as the exception message shown earlier does.

```csharp
// LogDuplicates is a local method. visited and duplicates are defined in the calling function
var visited = new Dictionary<IDfaNode, int>();
var duplicates = new Dictionary<string, List<string>>();

void LogDuplicates(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.

    // Does this node have multiple matches?
    var matchCount = node?.Matches?.Count ?? 0;
    if(matchCount > 1)
    {
        // Add the node to the dictionary!
        duplicates[node.Label] = node.Matches.Select(x => x.DisplayName).ToList();
    }
}
```

After executing `GetDuplicateEndpoints()` you'll have a dictionary containing all the duplicate routes in your application. The final thing to do is create a unit test, so that we can fail the build if we detect a problem.

## Creating a unit test to detect duplicate endpoints

I like to put this check in a unit test, because performance is much less of an issue. You _could_ check for duplicate routes on app startup or using a health check But given that we use reflection heavily this will add to startup time, and also given that the routes are fixed once you deploy your application, a unit test seems like the best place for it.

Just as [in a previous post where I _draw_ the graph in a unit test](https://andrewlock.net/adding-an-endpoint-graph-to-your-aspnetcore-application/), in this post I'll use an "integration test" to check for duplicate endpoints in my main app.

- Create a new xUnit project using VS or by running `dotnet new xunit`.
- Install _Microsoft.AspNetCore.Mvc.Testing_ by running `dotnet add package Microsoft.AspNetCore.Mvc.Testing`
- Update the `<Project>` element of your test project to `<Project Sdk="Microsoft.NET.Sdk.Web">`
- Reference your ASP.NET Core project from the test project

You can now create a simple integration test using [the `WebApplicationFactory<>` as a class fixture](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1#basic-tests-with-the-default-webapplicationfactory). I also inject `ITestOutputHelper` so that we can list out the duplicate endpoints for the test:

```csharp
public class DuplicateDetectorTest
    : IClassFixture<WebApplicationFactory<ApiRoutes.Startup>>
{
    // Inject the factory and the output helper
    private readonly WebApplicationFactory<ApiRoutes.Startup> _factory;
    private readonly ITestOutputHelper _output;
    public DuplicateDetectorTest(WebApplicationFactory<Startup> factory, ITestOutputHelper output)
    {
        _factory = factory;
        _output = output;
    }

    [Fact]
    public void ShouldNotHaveDuplicateEndpoints()
    {
        // Create an instance of the detector using the IServiceProvider from the app
        // and get an instance of the endpoint data
        var detector = new DuplicateEndpointDetector(_factory.Services);
        var endpointData = _factory.Services.GetRequiredService<EndpointDataSource>();

        // Find all the duplicates
        var duplicates = detector.GetDuplicateEndpoints(endpointData);

        // Print the duplicates to the output
        foreach (var keyValuePair in duplicates)
        {
            var allMatches = string.Join(", ", keyValuePair.Value);
            _output.WriteLine($"Duplicate route: '{keyValuePair.Key}'. Matches: {allMatches}");
        }

        // If we have some duplicates, then fail the CI build!
        Assert.Empty(duplicates);
    }
}
```

When you run the test, the assertion will fail, and if you view the output, you'll see a list of the routes with issues, as well as the endpoints that caused the problem:

![Using a unit test to detect duplicate endpoints](https://andrewlock.net/content/images/2020/duplicate_detector_1.png)

If you're running your unit tests as part of your CI build, then you'll catch the issue _before_ it gets to production. Pretty handy!

## Summary

In this post I show how you can build a duplicate endpoint detector using the endpoint graph building facilities included in ASP.NET Core. The end result requires quite a few tricks, as I've shown in the previous posts of this series, such as using _ImpromptuInterface_ to create proxies for interacting with `internal` types.

The end result is that you can detect when multiple endpoints (e.g. API controllers) are using the same route in your application. Given that those throw routes would throw an `AmbiguousMatchException` at runtime, being able to detect issues using a unit test is pretty handy!