diff --git a/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs b/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs index 77b41e87a8..7736561627 100644 --- a/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs +++ b/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs @@ -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(); + var dataSource = httpContext.RequestServices.GetRequiredService(); + graphWriter.Write(dataSource, writer); + } + + return Task.CompletedTask; + }, + RoutePatternFactory.Parse("/graph"), + 0, + new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })), + "DFA Graph"), }); services.TryAddEnumerable(ServiceDescriptor.Singleton(endpointDataSource)); diff --git a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index 19f3d0e77a..366f2d30fa 100644 --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -65,6 +65,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddTransient(); + services.TryAddSingleton(); // Link generation related services services.TryAddSingleton, RouteValuesBasedEndpointFinder>(); diff --git a/src/Microsoft.AspNetCore.Routing/Internal/DfaGraphWriter.cs b/src/Microsoft.AspNetCore.Routing/Internal/DfaGraphWriter.cs new file mode 100644 index 0000000000..9a1c8773f3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Internal/DfaGraphWriter.cs @@ -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 +{ + /// + /// + /// 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 + /// + /// + /// You can use http://www.webgraphviz.com/ to visualize the results. + /// + /// + /// This type has no support contract, and may be removed or changed at any time in + /// a future release. + /// + /// + public class DfaGraphWriter + { + private readonly IServiceProvider _services; + + public DfaGraphWriter(IServiceProvider services) + { + _services = services; + } + + public void Write(EndpointDataSource dataSource, TextWriter writer) + { + var builder = _services.GetRequiredService(); + + 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(); + + 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}\"]"); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs index 3157672ea1..b5c3c5e00c 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs @@ -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().ToArray()); diff --git a/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs index 0be502b1a2..4a08df719b 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs @@ -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}"; + } } } }