Add support for GraphViz

Adds **internal** support for dumping a route table to GraphViz DOT
notation. This allows us to dump the DFA graph for a route table and
visualize it.

Example:
https://gist.github.com/rynowak/2b24e4a6a602ca6f9c4de3ec227d621b
This commit is contained in:
Ryan Nowak 2018-08-06 20:52:08 -07:00
parent dce72c9553
commit 12cb35894e
5 changed files with 122 additions and 1 deletions

View File

@ -2,10 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
@ -77,6 +80,21 @@ namespace RoutingSample.Web
0,
EndpointMetadataCollection.Empty,
"withoptionalconstraints"),
new MatcherEndpoint((next) => (httpContext) =>
{
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
{
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
graphWriter.Write(dataSource, writer);
}
return Task.CompletedTask;
},
RoutePatternFactory.Parse("/graph"),
0,
new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })),
"DFA Graph"),
});
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));

View File

@ -65,6 +65,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<MatchProcessorFactory, DefaultMatchProcessorFactory>();
services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();
services.TryAddTransient<DfaMatcherBuilder>();
services.TryAddSingleton<DfaGraphWriter>();
// Link generation related services
services.TryAddSingleton<IEndpointFinder<RouteValuesAddress>, RouteValuesBasedEndpointFinder>();

View File

@ -0,0 +1,92 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Routing.Internal
{
/// <summary>
/// <para>
/// A singleton service that can be used to write the route table as a state machine
/// in GraphViz DOT language https://www.graphviz.org/doc/info/lang.html
/// </para>
/// <para>
/// You can use http://www.webgraphviz.com/ to visualize the results.
/// </para>
/// <para>
/// This type has no support contract, and may be removed or changed at any time in
/// a future release.
/// </para>
/// </summary>
public class DfaGraphWriter
{
private readonly IServiceProvider _services;
public DfaGraphWriter(IServiceProvider services)
{
_services = services;
}
public void Write(EndpointDataSource dataSource, TextWriter writer)
{
var builder = _services.GetRequiredService<DfaMatcherBuilder>();
var endpoints = dataSource.Endpoints;
for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i] as MatcherEndpoint;
if (endpoint != null)
{
builder.AddEndpoint(endpoint);
}
}
// Assign each node a sequential index.
var visited = new Dictionary<DfaNode, int>();
var tree = builder.BuildDfaTree();
writer.WriteLine("digraph DFA {");
tree.Visit(WriteNode);
writer.WriteLine("}");
void WriteNode(DfaNode 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.
foreach (var literal in node.Literals)
{
writer.WriteLine($"{label} -> {visited[literal.Value]} [label=\"/{literal.Key}\"]");
}
if (node.Parameters != null)
{
writer.WriteLine($"{label} -> {visited[node.Parameters]} [label=\"/*\"]");
}
if (node.CatchAll != null && node.Parameters != node.CatchAll)
{
writer.WriteLine($"{label} -> {visited[node.CatchAll]} [label=\"/**\"]");
}
foreach (var policy in node.PolicyEdges)
{
writer.WriteLine($"{label} -> {visited[policy.Value]} [label=\"{policy.Key}\"]");
}
writer.WriteLine($"{label} [label=\"{node.Label}\"]");
}
}
}
}

View File

@ -534,7 +534,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
var edge = edges[k];
var next = new DfaNode();
var next = new DfaNode()
{
Label = parent.Label + " " + edge.State.ToString(),
};
// TODO: https://github.com/aspnet/Routing/issues/648
next.Matches.AddRange(edge.Endpoints.Cast<MatcherEndpoint>().ToArray());

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -367,6 +368,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
hash.Add(HttpMethod, StringComparer.Ordinal);
return hash;
}
// Used in GraphViz output.
public override string ToString()
{
return IsCorsPreflightRequest ? $"CORS: {HttpMethod}" : $"HTTP: {HttpMethod}";
}
}
}
}