From cd94cd63b7505e90ebe930c102559e5d5576abb3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 2 Oct 2020 10:10:56 -0700 Subject: [PATCH] Add documentation for Routing surface area (#26513) * Add documentation for Routing surface area * Apply suggestions from code review Co-authored-by: James Newton-King * Fix up some doc strings Co-authored-by: James Newton-King --- src/Http/Http.Abstractions/src/StatusCodes.cs | 2 +- .../Routing.Abstractions/src/IRouteHandler.cs | 2 +- src/Http/Routing.Abstractions/src/IRouter.cs | 2 +- .../MatcherBuilderMultipleEntryBenchmark.cs | 2 +- ...ointRoutingApplicationBuilderExtensions.cs | 3 + .../src/CompositeEndpointDataSource.cs | 12 +++- .../Constraints/OptionalRouteConstraint.cs | 10 +++- .../src/Constraints/RegexRouteConstraint.cs | 15 +++++ src/Http/Routing/src/DataTokensMetadata.cs | 4 ++ src/Http/Routing/src/INamedRouter.cs | 6 ++ src/Http/Routing/src/IRouteCollection.cs | 7 +++ .../Routing/src/InlineRouteParameterParser.cs | 8 +++ .../Routing/src/Internal/DfaGraphWriter.cs | 11 +++- .../src/Matching/EndpointMetadataComparer.cs | 3 + .../Routing/src/Matching/HostMatcherPolicy.cs | 5 ++ .../src/Matching/INodeBuilderPolicy.cs | 19 +++++++ .../Routing/src/Matching/PolicyJumpTable.cs | 7 +++ .../src/Matching/PolicyJumpTableEdge.cs | 15 +++++ .../Routing/src/Matching/PolicyNodeEdge.cs | 15 +++++ .../src/Microsoft.AspNetCore.Routing.csproj | 2 +- .../RequestDelegateRouteBuilderExtensions.cs | 3 + src/Http/Routing/src/Route.cs | 43 +++++++++++++-- src/Http/Routing/src/RouteBase.cs | 49 +++++++++++++++++ src/Http/Routing/src/RouteBuilder.cs | 18 ++++++ src/Http/Routing/src/RouteCollection.cs | 13 +++++ .../Routing/src/RouteConstraintMatcher.cs | 16 ++++++ src/Http/Routing/src/RouteEndpointBuilder.cs | 16 ++++++ src/Http/Routing/src/RouteHandler.cs | 10 ++++ src/Http/Routing/src/RouteOptions.cs | 6 ++ .../Routing/src/RouteValueEqualityComparer.cs | 3 + src/Http/Routing/src/RouterMiddleware.cs | 14 +++++ src/Http/Routing/src/RoutingFeature.cs | 4 ++ .../Routing/src/Template/InlineConstraint.cs | 4 ++ .../Routing/src/Template/RoutePrecedence.cs | 32 +++++++---- .../Routing/src/Template/RouteTemplate.cs | 29 +++++++++- .../Routing/src/Template/TemplateBinder.cs | 27 +++++++-- .../Routing/src/Template/TemplateMatcher.cs | 21 +++++++ .../Routing/src/Template/TemplateParser.cs | 8 +++ src/Http/Routing/src/Template/TemplatePart.cs | 55 +++++++++++++++++++ .../Routing/src/Template/TemplateSegment.cs | 20 +++++++ src/Http/Routing/src/Tree/TreeRouter.cs | 6 +- 41 files changed, 515 insertions(+), 32 deletions(-) diff --git a/src/Http/Http.Abstractions/src/StatusCodes.cs b/src/Http/Http.Abstractions/src/StatusCodes.cs index d3b42b2b10..b61d03efd2 100644 --- a/src/Http/Http.Abstractions/src/StatusCodes.cs +++ b/src/Http/Http.Abstractions/src/StatusCodes.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Http { /// - /// A collection of contants for HTTP status codes. + /// A collection of constants for HTTP status codes. /// /// Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml /// diff --git a/src/Http/Routing.Abstractions/src/IRouteHandler.cs b/src/Http/Routing.Abstractions/src/IRouteHandler.cs index 15aaaeda8b..e00022c6ce 100644 --- a/src/Http/Routing.Abstractions/src/IRouteHandler.cs +++ b/src/Http/Routing.Abstractions/src/IRouteHandler.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing { /// - /// Defines a contract for a handler of a route. + /// Defines a contract for a handler of a route. /// public interface IRouteHandler { diff --git a/src/Http/Routing.Abstractions/src/IRouter.cs b/src/Http/Routing.Abstractions/src/IRouter.cs index ad7bac633c..10fd026c94 100644 --- a/src/Http/Routing.Abstractions/src/IRouter.cs +++ b/src/Http/Routing.Abstractions/src/IRouter.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Microsoft.AspNetCore.Routing { /// - /// + /// Interface for implementing a router. /// public interface IRouter { diff --git a/src/Http/Routing/perf/Matching/MatcherBuilderMultipleEntryBenchmark.cs b/src/Http/Routing/perf/Matching/MatcherBuilderMultipleEntryBenchmark.cs index 7116b43fc1..5229c4f1d6 100644 --- a/src/Http/Routing/perf/Matching/MatcherBuilderMultipleEntryBenchmark.cs +++ b/src/Http/Routing/perf/Matching/MatcherBuilderMultipleEntryBenchmark.cs @@ -205,4 +205,4 @@ namespace Microsoft.AspNetCore.Routing.Matching } } } -} \ No newline at end of file +} diff --git a/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs index dbc3432107..9d87859486 100644 --- a/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs @@ -9,6 +9,9 @@ using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { + /// + /// Constains extensions for configuring routing on an . + /// public static class EndpointRoutingApplicationBuilderExtensions { private const string EndpointRouteBuilder = "__EndpointRouteBuilder"; diff --git a/src/Http/Routing/src/CompositeEndpointDataSource.cs b/src/Http/Routing/src/CompositeEndpointDataSource.cs index 9d0a97c832..916187bef5 100644 --- a/src/Http/Routing/src/CompositeEndpointDataSource.cs +++ b/src/Http/Routing/src/CompositeEndpointDataSource.cs @@ -40,6 +40,11 @@ namespace Microsoft.AspNetCore.Routing _dataSources = dataSources; } + /// + /// Instantiates a object from . + /// + /// An collection of objects. + /// A public CompositeEndpointDataSource(IEnumerable endpointDataSources) : this() { _dataSources = new List(); @@ -62,6 +67,9 @@ namespace Microsoft.AspNetCore.Routing } } + /// + /// Returns the collection of instances associated with the object. + /// public IEnumerable DataSources => _dataSources; /// @@ -123,12 +131,12 @@ namespace Microsoft.AspNetCore.Routing // Refresh the endpoints from datasource so that callbacks can get the latest endpoints _endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray(); - // Prevent consumers from re-registering callback to inflight events as that can + // Prevent consumers from re-registering callback to inflight events as that can // cause a stackoverflow // Example: // 1. B registers A // 2. A fires event causing B's callback to get called - // 3. B executes some code in its callback, but needs to re-register callback + // 3. B executes some code in its callback, but needs to re-register callback // in the same callback var oldTokenSource = _cts; var oldToken = _consumerChangeToken; diff --git a/src/Http/Routing/src/Constraints/OptionalRouteConstraint.cs b/src/Http/Routing/src/Constraints/OptionalRouteConstraint.cs index f66bdd67de..378714f22b 100644 --- a/src/Http/Routing/src/Constraints/OptionalRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/OptionalRouteConstraint.cs @@ -7,10 +7,14 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Constraints { /// - /// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint. + /// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint. /// public class OptionalRouteConstraint : IRouteConstraint { + /// + /// Creates a new instance given the . + /// + /// public OptionalRouteConstraint(IRouteConstraint innerConstraint) { if (innerConstraint == null) @@ -21,8 +25,12 @@ namespace Microsoft.AspNetCore.Routing.Constraints InnerConstraint = innerConstraint; } + /// + /// Gets the associated with the optional parameter. + /// public IRouteConstraint InnerConstraint { get; } + /// public bool Match( HttpContext? httpContext, IRouter? route, diff --git a/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs b/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs index 4d17f5d7ea..53faa5e456 100644 --- a/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs @@ -8,10 +8,17 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Constraints { + /// + /// Constrains a route parameter to match a regular expression. + /// public class RegexRouteConstraint : IRouteConstraint { private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); + /// + /// Constructor for a given a . + /// + /// A instance to use as a constraint. public RegexRouteConstraint(Regex regex) { if (regex == null) @@ -22,6 +29,10 @@ namespace Microsoft.AspNetCore.Routing.Constraints Constraint = regex; } + /// + /// Constructor for a given a . + /// + /// A string containing the regex pattern. public RegexRouteConstraint(string regexPattern) { if (regexPattern == null) @@ -35,8 +46,12 @@ namespace Microsoft.AspNetCore.Routing.Constraints RegexMatchTimeout); } + /// + /// Gets the regular expression used in the route constraint. + /// public Regex Constraint { get; private set; } + /// public bool Match( HttpContext? httpContext, IRouter? route, diff --git a/src/Http/Routing/src/DataTokensMetadata.cs b/src/Http/Routing/src/DataTokensMetadata.cs index 831075af40..9bfa800868 100644 --- a/src/Http/Routing/src/DataTokensMetadata.cs +++ b/src/Http/Routing/src/DataTokensMetadata.cs @@ -14,6 +14,10 @@ namespace Microsoft.AspNetCore.Routing /// public sealed class DataTokensMetadata : IDataTokensMetadata { + /// + /// Constructor for a new given . + /// + /// The data tokens. public DataTokensMetadata(IReadOnlyDictionary dataTokens) { if (dataTokens == null) diff --git a/src/Http/Routing/src/INamedRouter.cs b/src/Http/Routing/src/INamedRouter.cs index 8b3a6c63a3..159b4c660e 100644 --- a/src/Http/Routing/src/INamedRouter.cs +++ b/src/Http/Routing/src/INamedRouter.cs @@ -3,8 +3,14 @@ namespace Microsoft.AspNetCore.Routing { + /// + /// An interface for an with a name. + /// public interface INamedRouter : IRouter { + /// + /// The name of the router. Can be null. + /// string? Name { get; } } } diff --git a/src/Http/Routing/src/IRouteCollection.cs b/src/Http/Routing/src/IRouteCollection.cs index 084f0aef67..6ba36de17e 100644 --- a/src/Http/Routing/src/IRouteCollection.cs +++ b/src/Http/Routing/src/IRouteCollection.cs @@ -3,8 +3,15 @@ namespace Microsoft.AspNetCore.Routing { + /// + /// Interface for a router that supports appending new routes. + /// public interface IRouteCollection : IRouter { + /// + /// Appends the collection of routes defined in . + /// + /// A instance. void Add(IRouter router); } } diff --git a/src/Http/Routing/src/InlineRouteParameterParser.cs b/src/Http/Routing/src/InlineRouteParameterParser.cs index 49dd434d37..2e33a1dd2b 100644 --- a/src/Http/Routing/src/InlineRouteParameterParser.cs +++ b/src/Http/Routing/src/InlineRouteParameterParser.cs @@ -7,8 +7,16 @@ using Microsoft.AspNetCore.Routing.Template; namespace Microsoft.AspNetCore.Routing { + /// + /// Contains methods for parsing processing constraints from a route definition. + /// public static class InlineRouteParameterParser { + /// + /// Parses a string representing the provided into a . + /// + /// A string representation of the route parameter. + /// A instance. public static TemplatePart ParseRouteParameter(string routeParameter) { if (routeParameter == null) diff --git a/src/Http/Routing/src/Internal/DfaGraphWriter.cs b/src/Http/Routing/src/Internal/DfaGraphWriter.cs index 32f1a314f8..62ea3186ce 100644 --- a/src/Http/Routing/src/Internal/DfaGraphWriter.cs +++ b/src/Http/Routing/src/Internal/DfaGraphWriter.cs @@ -26,11 +26,20 @@ namespace Microsoft.AspNetCore.Routing.Internal { private readonly IServiceProvider _services; + /// + /// Constructor for a given . + /// + /// The to add services to. public DfaGraphWriter(IServiceProvider services) { _services = services; } + /// + /// Displays a graph representation of in DOT. + /// + /// The to extract routes from. + /// The to which the content is written. public void Write(EndpointDataSource dataSource, TextWriter writer) { var builder = _services.GetRequiredService(); @@ -46,7 +55,7 @@ namespace Microsoft.AspNetCore.Routing.Internal // Assign each node a sequential index. var visited = new Dictionary(); - + var tree = builder.BuildDfaTree(includeLabel: true); writer.WriteLine("digraph DFA {"); diff --git a/src/Http/Routing/src/Matching/EndpointMetadataComparer.cs b/src/Http/Routing/src/Matching/EndpointMetadataComparer.cs index 823e64f5e2..620e635716 100644 --- a/src/Http/Routing/src/Matching/EndpointMetadataComparer.cs +++ b/src/Http/Routing/src/Matching/EndpointMetadataComparer.cs @@ -87,6 +87,9 @@ namespace Microsoft.AspNetCore.Routing.Matching /// public abstract class EndpointMetadataComparer : IComparer where TMetadata : class { + /// + /// A default instance of the . + /// public static readonly EndpointMetadataComparer Default = new DefaultComparer(); /// diff --git a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs index 2a3047489e..842670f9dd 100644 --- a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs @@ -19,8 +19,10 @@ namespace Microsoft.AspNetCore.Routing.Matching private const string WildcardPrefix = "*."; // Run after HTTP methods, but before 'default'. + /// public override int Order { get; } = -100; + /// public IComparer Comparer { get; } = new HostMetadataEndpointComparer(); bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList endpoints) @@ -70,6 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Matching }); } + /// public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { if (httpContext == null) @@ -189,6 +192,7 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new InvalidOperationException($"Could not parse host: {host}"); } + /// public IReadOnlyList GetEdges(IReadOnlyList endpoints) { if (endpoints == null) @@ -273,6 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Matching .ToArray(); } + /// public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) { if (edges == null) diff --git a/src/Http/Routing/src/Matching/INodeBuilderPolicy.cs b/src/Http/Routing/src/Matching/INodeBuilderPolicy.cs index 0790c5f295..0272bccc61 100644 --- a/src/Http/Routing/src/Matching/INodeBuilderPolicy.cs +++ b/src/Http/Routing/src/Matching/INodeBuilderPolicy.cs @@ -6,12 +6,31 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// Implements an interface for a matcher policy with support for generating graph representations of the endpoints. + /// public interface INodeBuilderPolicy { + /// + /// Evaluates if the policy matches any of the endpoints provided in . + /// + /// A list of . + /// if the policy applies to any of the provided . bool AppliesToEndpoints(IReadOnlyList endpoints); + /// + /// Generates a graph that representations the relationship between endpoints and hosts. + /// + /// A list of . + /// A graph representing the relationship between endpoints and hosts. IReadOnlyList GetEdges(IReadOnlyList endpoints); + /// + /// Constructs a jump table given the a set of . + /// + /// The default destination for lookups. + /// A list of . + /// A instance. PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges); } } diff --git a/src/Http/Routing/src/Matching/PolicyJumpTable.cs b/src/Http/Routing/src/Matching/PolicyJumpTable.cs index e7c5a8f582..ee353ada33 100644 --- a/src/Http/Routing/src/Matching/PolicyJumpTable.cs +++ b/src/Http/Routing/src/Matching/PolicyJumpTable.cs @@ -5,8 +5,15 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// Supports retrieving endpoints that fulfill a certain matcher policy. + /// public abstract class PolicyJumpTable { + /// + /// Returns the destination for a given in the current jump table. + /// + /// The associated with the current request. public abstract int GetDestination(HttpContext httpContext); internal virtual string DebuggerToString() diff --git a/src/Http/Routing/src/Matching/PolicyJumpTableEdge.cs b/src/Http/Routing/src/Matching/PolicyJumpTableEdge.cs index 002ffae583..5d2967e523 100644 --- a/src/Http/Routing/src/Matching/PolicyJumpTableEdge.cs +++ b/src/Http/Routing/src/Matching/PolicyJumpTableEdge.cs @@ -3,16 +3,31 @@ namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// Represents an entry in a . + /// public readonly struct PolicyJumpTableEdge { + /// + /// Constructs a new instance. + /// + /// Represents the match heuristic of the policy. + /// public PolicyJumpTableEdge(object state, int destination) { State = state ?? throw new System.ArgumentNullException(nameof(state)); Destination = destination; } + /// + /// Gets the object used to represent the match heuristic. Can be a host, HTTP method, etc. + /// depending on the matcher policy. + /// public object State { get; } + /// + /// Gets the destination of the current entry. + /// public int Destination { get; } } } diff --git a/src/Http/Routing/src/Matching/PolicyNodeEdge.cs b/src/Http/Routing/src/Matching/PolicyNodeEdge.cs index 419c390e05..e1a0366ad2 100644 --- a/src/Http/Routing/src/Matching/PolicyNodeEdge.cs +++ b/src/Http/Routing/src/Matching/PolicyNodeEdge.cs @@ -6,16 +6,31 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// Represents an edge in a matcher policy graph. + /// public readonly struct PolicyNodeEdge { + /// + /// Constructs a new instance. + /// + /// Represents the match heuristic of the policy. + /// Represents the endpoints that match the policy public PolicyNodeEdge(object state, IReadOnlyList endpoints) { State = state ?? throw new System.ArgumentNullException(nameof(state)); Endpoints = endpoints ?? throw new System.ArgumentNullException(nameof(endpoints)); } + /// + /// Gets the endpoints that match the policy defined by . + /// public IReadOnlyList Endpoints { get; } + /// + /// Gets the object used to represent the match heuristic. Can be a host, HTTP method, etc. + /// depending on the matcher policy. + /// public object State { get; } } } diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 2d65e52b25..b2c6ae9b66 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -7,7 +7,7 @@ Microsoft.AspNetCore.Routing.Route Microsoft.AspNetCore.Routing.RouteCollection $(DefaultNetCoreTargetFramework) true - $(NoWarn);CS1591 + $(NoWarn.Replace('1591', '')) true aspnetcore;routing true diff --git a/src/Http/Routing/src/RequestDelegateRouteBuilderExtensions.cs b/src/Http/Routing/src/RequestDelegateRouteBuilderExtensions.cs index 8a9e228e46..81d4dc6c4a 100644 --- a/src/Http/Routing/src/RequestDelegateRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/RequestDelegateRouteBuilderExtensions.cs @@ -10,6 +10,9 @@ using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Routing { + /// + /// Provides extension methods for adding new handlers to a . + /// public static class RequestDelegateRouteBuilderExtensions { /// diff --git a/src/Http/Routing/src/Route.cs b/src/Http/Routing/src/Route.cs index 555c328127..18406ff372 100644 --- a/src/Http/Routing/src/Route.cs +++ b/src/Http/Routing/src/Route.cs @@ -7,10 +7,19 @@ using System.Threading.Tasks; namespace Microsoft.AspNetCore.Routing { + /// + /// Represents an instance of a route. + /// public class Route : RouteBase { private readonly IRouter _target; + /// + /// Constructs a new instance. + /// + /// An instance associated with the component. + /// A string representation of the route template. + /// An used for resolving inline constraints. public Route( IRouter target, string routeTemplate, @@ -25,6 +34,15 @@ namespace Microsoft.AspNetCore.Routing { } + /// + /// Constructs a new instance. + /// + /// An instance associated with the component. + /// A string representation of the route template. + /// The default values for parameters in the route. + /// The constraints for the route. + /// The data tokens for the route. + /// An used for resolving inline constraints. public Route( IRouter target, string routeTemplate, @@ -36,6 +54,16 @@ namespace Microsoft.AspNetCore.Routing { } + /// + /// Constructs a new instance. + /// + /// An instance associated with the component. + /// The name of the route. + /// A string representation of the route template. + /// The default values for parameters in the route. + /// The constraints for the route. + /// The data tokens for the route. + /// An used for resolving inline constraints. public Route( IRouter target, string? routeName, @@ -45,11 +73,11 @@ namespace Microsoft.AspNetCore.Routing RouteValueDictionary? dataTokens, IInlineConstraintResolver inlineConstraintResolver) : base( - routeTemplate, - routeName, - inlineConstraintResolver, - defaults, - constraints, + routeTemplate, + routeName, + inlineConstraintResolver, + defaults, + constraints, dataTokens) { if (target == null) @@ -60,14 +88,19 @@ namespace Microsoft.AspNetCore.Routing _target = target; } + /// + /// Gets a string representation of the route template. + /// public string? RouteTemplate => ParsedTemplate.TemplateText; + /// protected override Task OnRouteMatched(RouteContext context) { context.RouteData.Routers.Add(_target); return _target.RouteAsync(context); } + /// protected override VirtualPathData? OnVirtualPathGenerated(VirtualPathContext context) { return _target.GetVirtualPath(context); diff --git a/src/Http/Routing/src/RouteBase.cs b/src/Http/Routing/src/RouteBase.cs index 5c9db8f026..94185d08a8 100644 --- a/src/Http/Routing/src/RouteBase.cs +++ b/src/Http/Routing/src/RouteBase.cs @@ -14,6 +14,9 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Routing { + /// + /// Base class implementation of an . + /// public abstract class RouteBase : IRouter, INamedRouter { private readonly object _loggersLock = new object(); @@ -23,6 +26,15 @@ namespace Microsoft.AspNetCore.Routing private ILogger? _logger; private ILogger? _constraintLogger; + /// + /// Creates a new instance. + /// + /// The route template. + /// The name of the route. + /// An used for resolving inline constraints. + /// The default values for parameters in the route. + /// The constraints for the route. + /// The data tokens for the route. public RouteBase( string? template, string? name, @@ -56,20 +68,45 @@ namespace Microsoft.AspNetCore.Routing } } + /// + /// Gets the set of constraints associated with each route. + /// public virtual IDictionary Constraints { get; protected set; } + /// + /// Gets the resolver used for resolving inline constraints. + /// protected virtual IInlineConstraintResolver ConstraintResolver { get; set; } + /// + /// Gets the data tokens associated with the route. + /// public virtual RouteValueDictionary DataTokens { get; protected set; } + /// + /// Gets the default values for each route parameter. + /// public virtual RouteValueDictionary Defaults { get; protected set; } + /// public virtual string? Name { get; protected set; } + /// + /// Gets the associated with the route. + /// public virtual RouteTemplate ParsedTemplate { get; protected set; } + /// + /// Executes asynchronously whenever routing occurs. + /// + /// A instance. protected abstract Task OnRouteMatched(RouteContext context); + /// + /// Executes whenever a virtual path is dervied from a . + /// + /// A instance. + /// A instance. protected abstract VirtualPathData? OnVirtualPathGenerated(VirtualPathContext context); /// @@ -168,6 +205,12 @@ namespace Microsoft.AspNetCore.Routing return pathData; } + /// + /// Extracts constatins from a given . + /// + /// An used for resolving inline constraints. + /// A instance. + /// A collection of constraints on the route template. protected static IDictionary GetConstraints( IInlineConstraintResolver inlineConstraintResolver, RouteTemplate parsedTemplate, @@ -199,6 +242,11 @@ namespace Microsoft.AspNetCore.Routing return constraintBuilder.Build(); } + /// + /// Gets the default values for parameters in a templates. + /// + /// A instance. + /// A collection of defaults for each parameter. protected static RouteValueDictionary GetDefaults( RouteTemplate parsedTemplate, RouteValueDictionary? defaults) @@ -296,6 +344,7 @@ namespace Microsoft.AspNetCore.Routing } } + /// public override string ToString() { return ParsedTemplate.TemplateText!; diff --git a/src/Http/Routing/src/RouteBuilder.cs b/src/Http/Routing/src/RouteBuilder.cs index e6673ac5b7..603504398c 100644 --- a/src/Http/Routing/src/RouteBuilder.cs +++ b/src/Http/Routing/src/RouteBuilder.cs @@ -10,13 +10,26 @@ using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Routing { + /// + /// Provides support for specifying routes in an application. + /// public class RouteBuilder : IRouteBuilder { + /// + /// Constructs a new instance given an . + /// + /// An instance. public RouteBuilder(IApplicationBuilder applicationBuilder) : this(applicationBuilder, defaultHandler: null) { } + /// + /// Constructs a new instance given an + /// and . + /// + /// An instance. + /// The default used if a new route is added without a handler. public RouteBuilder(IApplicationBuilder applicationBuilder, IRouter? defaultHandler) { if (applicationBuilder == null) @@ -39,14 +52,19 @@ namespace Microsoft.AspNetCore.Routing Routes = new List(); } + /// public IApplicationBuilder ApplicationBuilder { get; } + /// public IRouter? DefaultHandler { get; set; } + /// public IServiceProvider ServiceProvider { get; } + /// public IList Routes { get; } + /// public IRouter Build() { var routeCollection = new RouteCollection(); diff --git a/src/Http/Routing/src/RouteCollection.cs b/src/Http/Routing/src/RouteCollection.cs index ac499f4f5e..3e614821fd 100644 --- a/src/Http/Routing/src/RouteCollection.cs +++ b/src/Http/Routing/src/RouteCollection.cs @@ -14,6 +14,9 @@ using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Routing { + /// + /// Supports managing a collection fo multiple routes. + /// public class RouteCollection : IRouteCollection { private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' }; @@ -24,16 +27,24 @@ namespace Microsoft.AspNetCore.Routing private RouteOptions? _options; + /// + /// Gets the route at a given index. + /// + /// The route at the given index. public IRouter this[int index] { get { return _routes[index]; } } + /// + /// Gets the total number of routes registered in the collection. + /// public int Count { get { return _routes.Count; } } + /// public void Add(IRouter router) { if (router == null) @@ -57,6 +68,7 @@ namespace Microsoft.AspNetCore.Routing _routes.Add(router); } + /// public async virtual Task RouteAsync(RouteContext context) { // Perf: We want to avoid allocating a new RouteData for each route we need to process. @@ -88,6 +100,7 @@ namespace Microsoft.AspNetCore.Routing } } + /// public virtual VirtualPathData? GetVirtualPath(VirtualPathContext context) { EnsureOptions(context.HttpContext); diff --git a/src/Http/Routing/src/RouteConstraintMatcher.cs b/src/Http/Routing/src/RouteConstraintMatcher.cs index 268e4110cc..0c66b826fe 100644 --- a/src/Http/Routing/src/RouteConstraintMatcher.cs +++ b/src/Http/Routing/src/RouteConstraintMatcher.cs @@ -9,8 +9,24 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Routing { + /// + /// Use to evaluate if all route parameter values match their constraints. + /// public static class RouteConstraintMatcher { + /// + /// Determines if match the provided . + /// + /// The constraints for the route. + /// The route parameter values extracted from the matched route. + /// The associated with the current request. + /// The router that this constraint belongs to. + /// + /// Indicates whether the constraint check is performed + /// when the incoming request is handled or when a URL is generated. + /// + /// The . + /// if the all route values match their constraints. public static bool Match( IDictionary constraints, RouteValueDictionary routeValues, diff --git a/src/Http/Routing/src/RouteEndpointBuilder.cs b/src/Http/Routing/src/RouteEndpointBuilder.cs index 4397c51cfc..2d55c6b1a4 100644 --- a/src/Http/Routing/src/RouteEndpointBuilder.cs +++ b/src/Http/Routing/src/RouteEndpointBuilder.cs @@ -8,12 +8,27 @@ using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing { + /// + /// Supports building a new . + /// public sealed class RouteEndpointBuilder : EndpointBuilder { + /// + /// Gets or sets the associated with this endpoint. + /// public RoutePattern RoutePattern { get; set; } + /// + /// Gets or sets the order assigned to the endpoint. + /// public int Order { get; set; } + /// + /// Constructs a new instance. + /// + /// The delegate used to process requests for the endpoint. + /// The to use in URL matching. + /// The order assigned to the endpoint. public RouteEndpointBuilder( RequestDelegate requestDelegate, RoutePattern routePattern, @@ -24,6 +39,7 @@ namespace Microsoft.AspNetCore.Routing Order = order; } + /// public override Endpoint Build() { if (RequestDelegate is null) diff --git a/src/Http/Routing/src/RouteHandler.cs b/src/Http/Routing/src/RouteHandler.cs index 197e28794d..b606863080 100644 --- a/src/Http/Routing/src/RouteHandler.cs +++ b/src/Http/Routing/src/RouteHandler.cs @@ -8,26 +8,36 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing { + /// + /// Supports implementing a handler that executes for a given route. + /// public class RouteHandler : IRouteHandler, IRouter { private readonly RequestDelegate _requestDelegate; + /// + /// Constructs a new instance. + /// + /// The delegate used to process requests. public RouteHandler(RequestDelegate requestDelegate) { _requestDelegate = requestDelegate; } + /// public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData) { return _requestDelegate; } + /// public VirtualPathData? GetVirtualPath(VirtualPathContext context) { // Nothing to do. return null; } + /// public Task RouteAsync(RouteContext context) { context.Handler = _requestDelegate; diff --git a/src/Http/Routing/src/RouteOptions.cs b/src/Http/Routing/src/RouteOptions.cs index 85fd37cc35..5ac45a44b1 100644 --- a/src/Http/Routing/src/RouteOptions.cs +++ b/src/Http/Routing/src/RouteOptions.cs @@ -9,6 +9,9 @@ using Microsoft.AspNetCore.Routing.Constraints; namespace Microsoft.AspNetCore.Routing { + /// + /// Represents the configurable options on a route. + /// public class RouteOptions { private IDictionary _constraintTypeMap = GetDefaultConstraintMap(); @@ -64,6 +67,9 @@ namespace Microsoft.AspNetCore.Routing /// public bool SuppressCheckForUnhandledSecurityMetadata { get; set; } + /// + /// Gets or sets a collection of constraints on the current route. + /// public IDictionary ConstraintMap { get diff --git a/src/Http/Routing/src/RouteValueEqualityComparer.cs b/src/Http/Routing/src/RouteValueEqualityComparer.cs index 18ffc3070b..eebf5b49c0 100644 --- a/src/Http/Routing/src/RouteValueEqualityComparer.cs +++ b/src/Http/Routing/src/RouteValueEqualityComparer.cs @@ -20,6 +20,9 @@ namespace Microsoft.AspNetCore.Routing /// public class RouteValueEqualityComparer : IEqualityComparer { + /// + /// A default instance of the . + /// public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer(); /// diff --git a/src/Http/Routing/src/RouterMiddleware.cs b/src/Http/Routing/src/RouterMiddleware.cs index 0190cd468f..3eddbbbaad 100644 --- a/src/Http/Routing/src/RouterMiddleware.cs +++ b/src/Http/Routing/src/RouterMiddleware.cs @@ -9,12 +9,21 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Builder { + /// + /// Middleware responsible for routing. + /// public class RouterMiddleware { private readonly ILogger _logger; private readonly RequestDelegate _next; private readonly IRouter _router; + /// + /// Constructs a new instance with a given . + /// + /// The delegate representing the remaining middleware in the request pipeline. + /// The . + /// The to use for routing requests. public RouterMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, @@ -26,6 +35,11 @@ namespace Microsoft.AspNetCore.Builder _logger = loggerFactory.CreateLogger(); } + /// + /// Evaluates the handler associated with the + /// derived from . + /// + /// A instance. public async Task Invoke(HttpContext httpContext) { var context = new RouteContext(httpContext); diff --git a/src/Http/Routing/src/RoutingFeature.cs b/src/Http/Routing/src/RoutingFeature.cs index f3d9503df5..8895bc7cb1 100644 --- a/src/Http/Routing/src/RoutingFeature.cs +++ b/src/Http/Routing/src/RoutingFeature.cs @@ -3,8 +3,12 @@ namespace Microsoft.AspNetCore.Routing { + /// + /// A feature for routing functionality. + /// public class RoutingFeature : IRoutingFeature { + /// public RouteData? RouteData { get; set; } } } diff --git a/src/Http/Routing/src/Template/InlineConstraint.cs b/src/Http/Routing/src/Template/InlineConstraint.cs index 71be48e247..2bc4c25327 100644 --- a/src/Http/Routing/src/Template/InlineConstraint.cs +++ b/src/Http/Routing/src/Template/InlineConstraint.cs @@ -25,6 +25,10 @@ namespace Microsoft.AspNetCore.Routing.Template Constraint = constraint; } + /// + /// Creates a new instance given a . + /// + /// A instance. public InlineConstraint(RoutePatternParameterPolicyReference other) { if (other == null) diff --git a/src/Http/Routing/src/Template/RoutePrecedence.cs b/src/Http/Routing/src/Template/RoutePrecedence.cs index eea1057285..375b2d02cd 100644 --- a/src/Http/Routing/src/Template/RoutePrecedence.cs +++ b/src/Http/Routing/src/Template/RoutePrecedence.cs @@ -15,11 +15,17 @@ namespace Microsoft.AspNetCore.Routing.Template /// public static class RoutePrecedence { - // Compute the precedence for matching a provided url - // e.g.: /api/template == 1.1 - // /api/template/{id} == 1.13 - // /api/{id:int} == 1.2 - // /api/template/{id:int} == 1.12 + /// + /// Compute the precedence for matching a provided url + /// + /// + /// e.g.: /api/template == 1.1 + /// /api/template/{id} == 1.13 + /// /api/{id:int} == 1.2 + /// /api/template/{id:int} == 1.12 + /// + /// The to compute precendence for. + /// A representing the route's precendence. public static decimal ComputeInbound(RouteTemplate template) { ValidateSegementLength(template.Segments.Count); @@ -61,11 +67,17 @@ namespace Microsoft.AspNetCore.Routing.Template return precedence; } - // Compute the precedence for generating a url - // e.g.: /api/template == 5.5 - // /api/template/{id} == 5.53 - // /api/{id:int} == 5.4 - // /api/template/{id:int} == 5.54 + /// + /// Compute the precedence for generating a url. + /// + /// + /// e.g.: /api/template == 5.5 + /// /api/template/{id} == 5.53 + /// /api/{id:int} == 5.4 + /// /api/template/{id:int} == 5.54 + /// + /// The to compute precendence for. + /// A representing the route's precendence. public static decimal ComputeOutbound(RouteTemplate template) { ValidateSegementLength(template.Segments.Count); diff --git a/src/Http/Routing/src/Template/RouteTemplate.cs b/src/Http/Routing/src/Template/RouteTemplate.cs index 2b699e85cc..9c6f0647af 100644 --- a/src/Http/Routing/src/Template/RouteTemplate.cs +++ b/src/Http/Routing/src/Template/RouteTemplate.cs @@ -11,11 +11,18 @@ using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing.Template { + /// + /// Represents the template for a route. + /// [DebuggerDisplay("{DebuggerToString()}")] public class RouteTemplate { private const string SeparatorString = "/"; + /// + /// Constructs a new instance given . + /// + /// A instance. public RouteTemplate(RoutePattern other) { if (other == null) @@ -42,6 +49,12 @@ namespace Microsoft.AspNetCore.Routing.Template } } + /// + /// Constructs a a new instance given the string + /// and a list of . Computes the parameters in the route template. + /// + /// A string representation of the route template. + /// A list of . public RouteTemplate(string template, List segments) { if (segments == null) @@ -68,12 +81,26 @@ namespace Microsoft.AspNetCore.Routing.Template } } + /// + /// Gets the string representation of the route template. + /// public string? TemplateText { get; } + /// + /// Gets the list of that represent that parameters defined in the route template. + /// public IList Parameters { get; } + /// + /// Gets the list of that compromise the route template. + /// public IList Segments { get; } + /// + /// Gets the at a given index. + /// + /// The index of the element to retrieve. + /// A instance. public TemplateSegment? GetSegment(int index) { if (index < 0) @@ -109,7 +136,7 @@ namespace Microsoft.AspNetCore.Routing.Template } /// - /// Converts the to the equivalent + /// Converts the to the equivalent /// /// /// A . diff --git a/src/Http/Routing/src/Template/TemplateBinder.cs b/src/Http/Routing/src/Template/TemplateBinder.cs index 366a012b58..0012df84da 100644 --- a/src/Http/Routing/src/Template/TemplateBinder.cs +++ b/src/Http/Routing/src/Template/TemplateBinder.cs @@ -15,6 +15,9 @@ using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Routing.Template { + /// + /// Supports processing and binding parameter values in a route template. + /// public class TemplateBinder { private readonly UrlEncoder _urlEncoder; @@ -159,7 +162,12 @@ namespace Microsoft.AspNetCore.Routing.Template _slots = AssignSlots(_pattern, _filters); } - // Step 1: Get the list of values we're going to try to use to match and generate this URI + /// + /// Generates the parameter values in the route. + /// + /// The values associated with the current request. + /// The route values to process. + /// A instance. Can be null. public TemplateValuesResult? GetValues(RouteValueDictionary? ambientValues, RouteValueDictionary values) { // Make a new copy of the slots array, we'll use this as 'scratch' space @@ -424,10 +432,14 @@ namespace Microsoft.AspNetCore.Routing.Template } // Step 1.5: Process constraints - // - // Processes the constraints **if** they were passed in to the TemplateBinder constructor. - // Returns true on success - // Returns false + sets the name/constraint for logging on failure. + /// + /// Processes the constraints **if** they were passed in to the TemplateBinder constructor. + /// + /// The associated with the current request. + /// A dictionary that contains the parameters for the route. + /// The name of the parameter. + /// The constraint object. + /// if constraints were processed succesfully and false otherwise. public bool TryProcessConstraints(HttpContext? httpContext, RouteValueDictionary combinedValues, out string? parameterName, out IRouteConstraint? constraint) { var constraints = _constraints; @@ -447,6 +459,11 @@ namespace Microsoft.AspNetCore.Routing.Template } // Step 2: If the route is a match generate the appropriate URI + /// + /// Returns a string representation of the URI associated with the route. + /// + /// A dictionary that contains the parameters for the route. + /// The string representation of the route. public string? BindValues(RouteValueDictionary acceptedValues) { var context = _pool.Get(); diff --git a/src/Http/Routing/src/Template/TemplateMatcher.cs b/src/Http/Routing/src/Template/TemplateMatcher.cs index 9587525859..e955ca91a8 100644 --- a/src/Http/Routing/src/Template/TemplateMatcher.cs +++ b/src/Http/Routing/src/Template/TemplateMatcher.cs @@ -9,6 +9,9 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Template { + /// + /// Supports matching paths to route templates and extracting parameter values. + /// public class TemplateMatcher { private const string SeparatorString = "/"; @@ -21,6 +24,11 @@ namespace Microsoft.AspNetCore.Routing.Template private static readonly char[] Delimiters = new char[] { SeparatorChar }; private RoutePatternMatcher _routePatternMatcher; + /// + /// Creates a new instance given a and . + /// + /// The to compare against. + /// The default values for parameters in the . public TemplateMatcher( RouteTemplate template, RouteValueDictionary defaults) @@ -62,10 +70,23 @@ namespace Microsoft.AspNetCore.Routing.Template _routePatternMatcher = new RoutePatternMatcher(routePattern, Defaults); } + /// + /// Gets the default values for parameters in the . + /// public RouteValueDictionary Defaults { get; } + /// + /// Gets the to match against. + /// public RouteTemplate Template { get; } + /// + /// Evaluates if the provided matches the . Populates + /// with parameter values. + /// + /// A representing the route to match. + /// A to populate with parameter values. + /// if matches . public bool TryMatch(PathString path, RouteValueDictionary values) { if (values == null) diff --git a/src/Http/Routing/src/Template/TemplateParser.cs b/src/Http/Routing/src/Template/TemplateParser.cs index ad1afe86e6..4318fc78bd 100644 --- a/src/Http/Routing/src/Template/TemplateParser.cs +++ b/src/Http/Routing/src/Template/TemplateParser.cs @@ -6,8 +6,16 @@ using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing.Template { + /// + /// Provides methods for parsing route template strings. + /// public static class TemplateParser { + /// + /// Creates a for a given string. + /// + /// A string representation of the route template. + /// A instance. public static RouteTemplate Parse(string routeTemplate) { if (routeTemplate == null) diff --git a/src/Http/Routing/src/Template/TemplatePart.cs b/src/Http/Routing/src/Template/TemplatePart.cs index c7acbd1b57..06a7426990 100644 --- a/src/Http/Routing/src/Template/TemplatePart.cs +++ b/src/Http/Routing/src/Template/TemplatePart.cs @@ -10,13 +10,23 @@ using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing.Template { + /// + /// Represents a part of a route template segment. + /// [DebuggerDisplay("{DebuggerToString()}")] public class TemplatePart { + /// + /// Constructs a new instance. + /// public TemplatePart() { } + /// + /// Constructs a new instance given a . + /// + /// A instance representing the route part. public TemplatePart(RoutePatternPart other) { IsLiteral = other.IsLiteral || other.IsSeparator; @@ -47,6 +57,11 @@ namespace Microsoft.AspNetCore.Routing.Template } } + /// + /// Create a representing a literal route part. + /// + /// The text of the literate route part. + /// A instance. public static TemplatePart CreateLiteral(string text) { return new TemplatePart() @@ -56,6 +71,15 @@ namespace Microsoft.AspNetCore.Routing.Template }; } + /// + /// Creates a representing a paramter part. + /// + /// The name of the parameter. + /// if the parameter is a catch-all parameter. + /// if the parameter is an optional parameter. + /// The default value of the parameter. + /// A collection of constraints associated with the parameter. + /// A instance. public static TemplatePart CreateParameter( string name, bool isCatchAll, @@ -79,14 +103,41 @@ namespace Microsoft.AspNetCore.Routing.Template }; } + /// + /// if the route part is is a catch-all part (e.g. /*). + /// public bool IsCatchAll { get; private set; } + /// + /// if the route part is represents a literal value. + /// public bool IsLiteral { get; private set; } + /// + /// if the route part represents a parameterized value. + /// public bool IsParameter { get; private set; } + /// + /// if the route part represents an optional part. + /// public bool IsOptional { get; private set; } + /// + /// if the route part represents an optional seperator. + /// public bool IsOptionalSeperator { get; set; } + /// + /// The name of the route parameter. Can be null. + /// public string? Name { get; private set; } + /// + /// The textual representation of the route paramter. Can be null. Used to represent route seperators and literal parts. + /// public string? Text { get; private set; } + /// + /// The default value for route paramters. Can be null. + /// public object? DefaultValue { get; private set; } + /// + /// The constraints associates with a route paramter. + /// public IEnumerable InlineConstraints { get; private set; } = Enumerable.Empty(); internal string? DebuggerToString() @@ -101,6 +152,10 @@ namespace Microsoft.AspNetCore.Routing.Template } } + /// + /// Creates a for the route part designated by the . + /// + /// A instance. public RoutePatternPart ToRoutePatternPart() { if (IsLiteral && IsOptionalSeperator) diff --git a/src/Http/Routing/src/Template/TemplateSegment.cs b/src/Http/Routing/src/Template/TemplateSegment.cs index 304472653e..70a2e5917c 100644 --- a/src/Http/Routing/src/Template/TemplateSegment.cs +++ b/src/Http/Routing/src/Template/TemplateSegment.cs @@ -9,14 +9,24 @@ using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing.Template { + /// + /// Represents a segment of a route template. + /// [DebuggerDisplay("{DebuggerToString()}")] public class TemplateSegment { + /// + /// Constructs a new instance. + /// public TemplateSegment() { Parts = new List(); } + /// + /// Constructs a new instance given another . + /// + /// A instance. public TemplateSegment(RoutePatternPathSegment other) { if (other == null) @@ -32,8 +42,14 @@ namespace Microsoft.AspNetCore.Routing.Template } } + /// + /// if the segment contains a single entry. + /// public bool IsSimple => Parts.Count == 1; + /// + /// Gets the list of individual parts in the template segment. + /// public List Parts { get; } internal string DebuggerToString() @@ -41,6 +57,10 @@ namespace Microsoft.AspNetCore.Routing.Template return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString())); } + /// + /// Returns a for the template segment. + /// + /// A instance. public RoutePatternPathSegment ToRoutePatternPathSegment() { var parts = Parts.Select(p => p.ToRoutePatternPart()); diff --git a/src/Http/Routing/src/Tree/TreeRouter.cs b/src/Http/Routing/src/Tree/TreeRouter.cs index 5a27d237f3..e2a8d52472 100644 --- a/src/Http/Routing/src/Tree/TreeRouter.cs +++ b/src/Http/Routing/src/Tree/TreeRouter.cs @@ -19,8 +19,10 @@ namespace Microsoft.AspNetCore.Routing.Tree /// public class TreeRouter : IRouter { - // Key used by routing and action selection to match an attribute route entry to a - // group of action descriptors. + /// + /// Key used by routing and action selection to match an attribute + /// route entry to agroup of action descriptors. + /// public static readonly string RouteGroupKey = "!__route_group"; private readonly LinkGenerationDecisionTree _linkGenerationTree;