Nothing can be more frustrating than going into a situation “thinking” you know how a framework works, only to spend the next several hours pulling your hair out and stewing in a pot of unhealthy feelings. I like to consider myself an ASP.NET routing expert with my experience dating back to MVC 1.0. Recently, I’ve started using ASP.NET Core Razor Pages mixed in with MVC and API approaches. I find the combination of all this technology to be a winning one, but it can also add complexity when building views. In this post, I’ll show you a simple one page Razor Page that can help diagnose route resolution issues quickly. Quickly see what your ASP.NET Core application sees and what it requires to resolve routes.

Endpoint Routing

With .NET Core 3, the responsibility of routing is on the shoulders of the EndpointMiddlware component. Registrations of the approaches described above, MVC, Razor Pages, and API occur in the Configure method of the Startup class. Below, you’ll see the registration of all Razor Pages endpoints.

app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); });

The method scans the project for all Razor Pages and registers them as individual endpoints. Cool right?

What you may not realize is that each registration goes into a global bank of endpoints. This data source of endpoints is called the EndpointDataSource. Luckily, this data source is also registered (on the down-low) with your dependency injection container. The endpoint data source will be critical for our solution.

Solution

When generating links within your application, you’ll end up using helper methods from Url and Html.

<a class="dropdown-item" href="@Url.Page("/Admin/Conferences/Index")">Conferences</a>
<!-- or -->
<a href="@Url.Page("/Admin/Conferences/Edit", new { @conference.Id })" role="button">
    Edit
</a>   

If you’re like me, you may forget to add a critical route value, which causes ASP.NET Core to give up on route generation. I’ve found the best way to debug these situations, is to see what routes are registered. To do that, I’ve built a Razor page that you can drop into your project right now and get more information. I named my page _routes.cshtml, and the code-behind is unnecessary.

@page
@using Microsoft.AspNetCore.Mvc.ApplicationModels
@using Microsoft.AspNetCore.Mvc.RazorPages
@using Microsoft.AspNetCore.Routing
@inject EndpointDataSource EndpointsDataSource

@{
    var endpoints = EndpointsDataSource.Endpoints.ToList();
}

<table class="table">
    <thead class="thead-dark">
    <tr>
        <th scope="col">Order</th>
        <th scope="col">Display Name</th>
        <th scope="col">Route Pattern</th>
        <th scope="col">Metadata</th>
    </tr>
    </thead>
    <tbody>
    @foreach (var endpoint in endpoints)
    {
        var routeEndpoint = endpoint as RouteEndpoint;
        <tr>
            <td>@routeEndpoint?.Order</td>
            <td>@endpoint.DisplayName</td>
            <td>@routeEndpoint?.RoutePattern.RawText</td>
            <td>
                <ul>
                    @foreach (var md in endpoint.Metadata)
                    {
                        switch (md)
                        {
                            case PageRouteMetadata prm:
                                <li>
                                    <p>@nameof(PageRouteMetadata)</p>
                                    <ul>
                                        <li>Page Route: @prm.PageRoute</li>
                                        <li>Route Template: @prm.RouteTemplate</li>
                                    </ul>
                                </li>
                                break;
                            case PageActionDescriptor pad:
                                <li>
                                    <p>@nameof(PageActionDescriptor)</p>
                                    <ul>
                                        <li>Id: @pad.Id</li>
                                        <li>Area: @pad.AreaName</li>
                                        <li>Display Name: @pad.DisplayName</li>
                                        <li>View Engine Path: @pad.ViewEnginePath</li>
                                        <li>Relative Path: @pad.RelativePath</li>
                                    </ul>
                                </li>
                                break;
                            case RouteNameMetadata rnm:
                                <li>
                                    Route Name Metadata: @rnm.RouteName
                                </li>
                                break;
                            case SuppressLinkGenerationMetadata slg:
                                <li>
                                    suppress link: @slg.SuppressLinkGeneration;
                                </li>
                                break;
                            default:
                                <li>@md.ToString()</li>
                                break;
                        }
                    }
                </ul>
            </td>
        </tr>
    }
    </tbody>
</table>

And here is what it looks like when you visit the page.

Awesome right?!

More Endpoint Information

The exciting part of debugging endpoints is that there is a treasure trove of information for each route. You can see what required parameters exist for each route, the expected name for resolution, the view engine path, and much more. The debugger in this post was written to diagnose Razor Page issues, but will also work with any library that registers an endpoint.

Conclusion

ASP.NET Core routing is powerful but can be a bit of a black box. Understanding where routes are registered and how you can debug them can save you hours of frustration. Understanding that all endpoints are registered globally and can be accessed via an EndpointDataSource makes the task of debugging much more straightforward. I hope this post has helped, and please feel free to leave a comment.