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 Endpoint
s 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:
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!
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:
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 IHostedService
s.
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 IHostedService
s 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 usingDfaGraphWriter
from anIHostedService
.
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.