blog post image
Andrew Lock avatar

Andrew Lock

~9 min read

Adding an endpoint graph to your ASP.NET Core application

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

In this post I show how to visualize the endpoint routes in your ASP.NET Core 3.0 application using the DfaGraphWriter service. I show how to generate a directed graph (as shown in my previous post) which can be visualized using GraphVizOnline. Finally, I describe the points in your application's lifetime where you can retrieve the graph data.

In this post I only show how to create the "default" style of graph. In my next post I create a custom writer for generating the customised graphs like the one in my previous post.

Using DfaGraphWriter to visualize your endpoints.

ASP.NET Core comes with a handy class, DfaGraphWriter, that can be used to visualize your routes in an ASP.NET Core 3.x application:

public class DfaGraphWriter
{
    public void Write(EndpointDataSource dataSource, TextWriter writer);
}

This class has a single method, Write. The EndpointDataSource contains the collection of Endpoints describing your application, and the TextWriter is used to write the DOT language graph (as you saw in my previous post).

For now we'll create a middleware that uses the DfaGraphWriter to write the graph as an HTTP response. You can inject both the DfaGraphWriter and the EndpointDataSource it uses into the constructor using DI:

public class GraphEndpointMiddleware
{
    // inject required services using DI
    private readonly DfaGraphWriter _graphWriter;
    private readonly EndpointDataSource _endpointData;

    public GraphEndpointMiddleware(
        RequestDelegate next, 
        DfaGraphWriter graphWriter, 
        EndpointDataSource endpointData)
    {
        _graphWriter = graphWriter;
        _endpointData = endpointData;
    }

    public async Task Invoke(HttpContext context)
    {
        // set the response
        context.Response.StatusCode = 200;
        context.Response.ContentType = "text/plain";

        // Write the response into memory
        await using (var sw = new StringWriter())
        {
            // Write the graph
            _graphWriter.Write(_endpointData, sw);
            var graph = sw.ToString();

            // Write the graph to the response
            await context.Response.WriteAsync(graph);
        }
    }
}

This middleware is pretty simple—we inject the necessary services into the middleware using dependency injection. Writing the graph to the response is a bit more convoluted: you have to write the response in-memory to a StringWriter, convert it to a string, and then write it to the graph.

This is all necessary because the DfaGraphWriter writes to the TextWriter using synchronous Stream API calls, like Write, instead of WriteAsync. Ideally, we would be able to do something like this:

// Create a stream writer that wraps the body
await using (var sw = new StreamWriter(context.Response.Body))
{
    // write asynchronously to the stream
    await _graphWriter.WriteAsync(_endpointData, sw);
}

If DfaGraphWriter used asynchronous APIs, then you could write directly to Response.Body as shown above and avoid the in-memory string. Unfortunately, it's synchronous, and you shouldn't write to Response.Body using synchronous calls for performance reasons. If you try to use pattern above then you may get an InvalidOperationException like the following, depending on the size of the graph being written:

System.InvalidOperationException: Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.

You might not get this exception if the graph is small, but you can see it if you try and map a medium-side application, such as the default Razor Pages app with Identity.

Let's get back on track—we now have a graph-generating middleware, so lets add it to the pipeline. There's two options here:

  • Add it as an endpoint using endpoint routing.
  • Add it as a simple "branch" from your middleware pipeline.

The former approach is the generally recommended method for adding endpoints to ASP.NET Core 3.0 apps, so lets start there.

Adding the graph visualizer as an endpoint

To simplify the endpoint registration code, I'll create a simple extension method for adding the GraphEndpointMiddleware as an endpoint:

public static class GraphEndpointMiddlewareExtensions
{
    public static IEndpointConventionBuilder MapGraphVisualisation(
        this IEndpointRouteBuilder endpoints, string pattern)
    {
        var pipeline = endpoints
            .CreateApplicationBuilder()
            .UseMiddleware<GraphEndpointMiddleware>()
            .Build();

        return endpoints.Map(pattern, pipeline).WithDisplayName("Endpoint Graph");
    }
}

We can then add the graph endpoint to our ASP.NET Core application by calling MapGraphVisualisation("/graph") in the UseEndpoints() method in Startup.Configure():

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHealthChecks("/healthz");
        endpoints.MapControllers();
        // Add the graph endpoint
        endpoints.MapGraphVisualisation("/graph");
    });
}

That's all we need to do. The DfaGraphWriter is already available in DI, so there's no additional configuration required there. Navigating to http://localhost:5000/graph (for example) generates the graph of our endpoints as plain text (shown here for the app from ](/visualizing-asp-net-core-endpoints-using-graphvizonline-and-the-dot-language/):

digraph DFA {
    0 [label="/graph/"]
    1 [label="/healthz/"]
    2 [label="/api/Values/{...}/ HTTP: GET"]
    3 [label="/api/Values/{...}/ HTTP: PUT"]
    4 [label="/api/Values/{...}/ HTTP: DELETE"]
    5 [label="/api/Values/{...}/ HTTP: *"]
    6 -> 2 [label="HTTP: GET"]
    6 -> 3 [label="HTTP: PUT"]
    6 -> 4 [label="HTTP: DELETE"]
    6 -> 5 [label="HTTP: *"]
    6 [label="/api/Values/{...}/"]
    7 [label="/api/Values/ HTTP: GET"]
    8 [label="/api/Values/ HTTP: POST"]
    9 [label="/api/Values/ HTTP: *"]
    10 -> 6 [label="/*"]
    10 -> 7 [label="HTTP: GET"]
    10 -> 8 [label="HTTP: POST"]
    10 -> 9 [label="HTTP: *"]
    10 [label="/api/Values/"]
    11 -> 10 [label="/Values"]
    11 [label="/api/"]
    12 -> 0 [label="/graph"]
    12 -> 1 [label="/healthz"]
    12 -> 11 [label="/api"]
    12 [label="/"]
}

Which can be visualized using GraphVizOnline:

A ValuesController endpoint routing application

Exposing the graph as an endpoint in the endpoint routing system has both pros and cons:

  • You can easily add authorization to the endpoint. You probably don't want just anyone to be able to view this data!
  • The graph endpoint shows up as an endpoint in the system. That's obviously correct, but could be annoying.

If that final point is a deal breaker for you, you could use the old-school way of creating endpoints, using branching middleware.

Adding the graph visualizer as a middleware branch

Adding branches to your middleware pipeline was one of the easiest way to create "endpoints" before we had endpoint routing. It's still available in ASP.NET Core 3.0, it's just far more basic than the endpoint routing system, and doesn't provide the ability to easily add authorization or advanced routing.

To create a middleware branch, use the Map() command. For example, you could add a branch using the following:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // add the graph endpoint as a branch of the pipeline
    app.Map("/graph", branch => 
        branch.UseMiddleware<GraphEndpointMiddleware>());

    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHealthChecks("/healthz");
        endpoints.MapControllers();
    });
}

The pros and cons of using this approach are essentially the opposite of the endpoint-routing version: there's no /graph endpoint in your graph, but you can't easily apply authorization to the endpoint!

A ValuesController endpoint routing application without the graph endpoint

For me, it doesn't make a lot of sense to expose the graph of your application like this. In the next section, I show how you can generate the graph from a small integration test instead.

Generating an endpoint graph from an integration test

ASP.NET Core has a good story for running in-memory integration tests, exercising your full middleware pipeline and API controllers/Razor Pages, without having to make network calls.

As well as the traditional "end-to-end" integration tests that you can use to confirm the overall correct operation of your app, I sometimes like to write "sanity-check" tests, that confirm that an application is configured correctly. You can achieve this by using the WebApplicationFactory<> facility, in Microsoft.AspNetCore.Mvc.Testing which exposes the underlying DI container. This allows you to run code in the DI context of the application, but from a unit test.

To try it out

  • Create a new xUnit project (my testing framework of choice) 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 the test project to <Project Sdk="Microsoft.NET.Sdk.Web">
  • Reference your ASP.NET Core project from the test project

We can now create a simple test that generates the endpoint graph, and writes it to the test output. In the example below, I'm using the default WebApplicationFactory<> as a class fixture; if you need to customise the factory, see the docs or my previous post for details.

In addition to WebApplicationFactory<>, I'm also injecting ITestOutputHelper. You need to use this class to record test output with xUnit. Writing directly to Console won't work..

public class GenerateGraphTest
    : IClassFixture<WebApplicationFactory<ApiRoutes.Startup>>
{

    // Inject the factory and the output helper
    private readonly WebApplicationFactory<ApiRoutes.Startup> _factory;
    private readonly ITestOutputHelper _output;

    public GenerateGraphTest(
        WebApplicationFactory<Startup> factory, ITestOutputHelper output)
    {
        _factory = factory;
        _output = output;
    }

    [Fact]
    public void GenerateGraph()
    {
        // fetch the required services from the root container of the app
        var graphWriter = _factory.Services.GetRequiredService<DfaGraphWriter>();
        var endpointData = _factory.Services.GetRequiredService<EndpointDataSource>();

        // build the graph as before
        using (var sw = new StringWriter())
        {
            graphWriter.Write(endpointData, sw);
            var graph = sw.ToString();
            
            // write the graph to the test output
            _output.WriteLine(graph);
        }
    }
}

The bulk of the test is the same as for the middleware, but instead of writing to the response, we write to xUnit's ITestOutputHelper. This records the output against the test. In Visual Studio, you can view this output by opening Test Explorer, navigating to the GenerateGraph test, and clicking "Open additional output for this result", which opens the result as a tab:

Viewing endpoint data from a test in Visual Studio

I find a simple test like this is often sufficient for my purposes. There's lots of advantages in my eyes:

  • It doesn't expose this data as an endpoint
  • Has no impact on your application
  • Can be easily generated

Nevertheless, maybe you want to generate this graph from your application, but you don't want to include it using either of the middleware approaches shown so far. If so, just be careful exactly where you do it.

You can't generate the graph in an IHostedService

Generally speaking, you can access the DfaGraphWriter and EndpointDataSource services from anywhere in your app that uses dependency injection, or that has access to an IServiceProvider instance. That means generating the graph in the context of a request, for example from an MVC controller or Razor Page is easy, and identical to the approach you've seen so far.

Where you must be careful is if you're trying to generate the graph early in your application's lifecycle. This applies particularly to IHostedServices.

In ASP.NET Core 3.0, the web infrastructure was rebuilt on top of the generic host, which means your server (Kestrel) runs as an IHostedService in your application. In most cases, this shouldn't have a big impact, but it changes the order that your application is built, compared to ASP.NET Core 2.x.

In ASP.NET Core 2.x, the following things would happen:

  • The middleware pipeline is built.
  • The server (Kestrel) starts listening for requests.
  • The IHostedService implementations are started.

Instead, on ASP.NET Core 3.x, you have the following:

  • The IHostedService implementations are started.
  • The GenericWebHostService is started:
    • The middleware pipeline is built
    • The server (Kestrel) starts listening for requests.

The important thing to note is that the middleware pipeline isn't built until after your IHostedServices are executed. As UseEndpoints() has not yet been called, EndpointDataSource won't contain any data!

The EndpointDataSource will be empty if you try and generate your graph using DfaGraphWriter from an IHostedService.

The same goes if you try and use another standard mechanisms for injecting early behaviour, like IStartupFilter—these execute before Startup.Configure() is called, so the EndpointDataSource will be empty.

Similarly, you can't just build a Host by calling IHostBuilder.Build() in Program.Main, and access the services using IHost.Services: until you call IHost.Run, and the server has started, your endpoint list will be empty!

These limitations may or may not be an issue depending on what you're trying to achieve. For me, the unit test approach solves most of my issues.

Whichever approach you use, you're stuck only being able to generate the "default" endpoint graphs shown in this post. As I mentioned in my previous post, this hides a lot of really useful information, like which nodes generate an endpoint. In the next post, I show how to create a custom graph writer, so you can generate your own graphs.

Summary

In this post I showed how to use the DfaGraphWriter and EndpointDataSource to create a graph of all the endpoints in your application. I showed how to create a middleware endpoint to expose this data, and how to use this middleware both with a branching middleware strategy and as an endpoint route.

I also showed how to use a simple integration test to generate the graph data without having to run your application. This avoids exposing (the potentially sensitive) endpoint graph publicly, while still allowing easy access to the data.

Finally, I discussed when you can generate the graph in your application's lifecycle. The EndpointDataSource is not populated until after the Server (Kestrel) has started, so you're primarily limited to accessing the data in a Request context. IHostedService and IStartupFilter execute too early to access the data, and IHostBuilder.Build() only builds the DI container, not the middleware pipeline.

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