Skip to content

Commit 127bc7d

Browse files
authored
Fixes: #4597 Parse URI path with an endpoint (#9728)
Adds functionality to parse a URI path given a way to find an endpoint. This is the replacement for various machinications using the global route collection and `RouteData.Routers` in earlier versions. For now I'm just adding a way to do this using Endpoint Name since it's a pretty low level feature. Endpoint Name is also very direct, so it feels good for something like this.
1 parent 0748d18 commit 127bc7d

File tree

11 files changed

+830
-0
lines changed

11 files changed

+830
-0
lines changed

src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ public static partial class LinkGeneratorRouteValuesAddressExtensions
193193
public static string GetUriByRouteValues(this Microsoft.AspNetCore.Routing.LinkGenerator generator, Microsoft.AspNetCore.Http.HttpContext httpContext, string routeName, object values, string scheme = null, Microsoft.AspNetCore.Http.HostString? host = default(Microsoft.AspNetCore.Http.HostString?), Microsoft.AspNetCore.Http.PathString? pathBase = default(Microsoft.AspNetCore.Http.PathString?), Microsoft.AspNetCore.Http.FragmentString fragment = default(Microsoft.AspNetCore.Http.FragmentString), Microsoft.AspNetCore.Routing.LinkOptions options = null) { throw null; }
194194
public static string GetUriByRouteValues(this Microsoft.AspNetCore.Routing.LinkGenerator generator, string routeName, object values, string scheme, Microsoft.AspNetCore.Http.HostString host, Microsoft.AspNetCore.Http.PathString pathBase = default(Microsoft.AspNetCore.Http.PathString), Microsoft.AspNetCore.Http.FragmentString fragment = default(Microsoft.AspNetCore.Http.FragmentString), Microsoft.AspNetCore.Routing.LinkOptions options = null) { throw null; }
195195
}
196+
public abstract partial class LinkParser
197+
{
198+
protected LinkParser() { }
199+
public abstract Microsoft.AspNetCore.Routing.RouteValueDictionary ParsePathByAddress<TAddress>(TAddress address, Microsoft.AspNetCore.Http.PathString path);
200+
}
201+
public static partial class LinkParserEndpointNameAddressExtensions
202+
{
203+
public static Microsoft.AspNetCore.Routing.RouteValueDictionary ParsePathByEndpointName(this Microsoft.AspNetCore.Routing.LinkParser parser, string endpointName, Microsoft.AspNetCore.Http.PathString path) { throw null; }
204+
}
196205
public abstract partial class MatcherPolicy
197206
{
198207
protected MatcherPolicy() { }
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Logging;
11+
12+
namespace Microsoft.AspNetCore.Routing
13+
{
14+
internal class DefaultLinkParser : LinkParser, IDisposable
15+
{
16+
private readonly ParameterPolicyFactory _parameterPolicyFactory;
17+
private readonly ILogger<DefaultLinkParser> _logger;
18+
private readonly IServiceProvider _serviceProvider;
19+
20+
// Caches RoutePatternMatcher instances
21+
private readonly DataSourceDependentCache<ConcurrentDictionary<RouteEndpoint, MatcherState>> _matcherCache;
22+
23+
// Used to initialize RoutePatternMatcher and constraint instances
24+
private readonly Func<RouteEndpoint, MatcherState> _createMatcher;
25+
26+
public DefaultLinkParser(
27+
ParameterPolicyFactory parameterPolicyFactory,
28+
EndpointDataSource dataSource,
29+
ILogger<DefaultLinkParser> logger,
30+
IServiceProvider serviceProvider)
31+
{
32+
_parameterPolicyFactory = parameterPolicyFactory;
33+
_logger = logger;
34+
_serviceProvider = serviceProvider;
35+
36+
// We cache RoutePatternMatcher instances per-Endpoint for performance, but we want to wipe out
37+
// that cache is the endpoints change so that we don't allow unbounded memory growth.
38+
_matcherCache = new DataSourceDependentCache<ConcurrentDictionary<RouteEndpoint, MatcherState>>(dataSource, (_) =>
39+
{
40+
// We don't eagerly fill this cache because there's no real reason to. Unlike URL matching, we don't
41+
// need to build a big data structure up front to be correct.
42+
return new ConcurrentDictionary<RouteEndpoint, MatcherState>();
43+
});
44+
45+
// Cached to avoid per-call allocation of a delegate on lookup.
46+
_createMatcher = CreateRoutePatternMatcher;
47+
}
48+
49+
public override RouteValueDictionary ParsePathByAddress<TAddress>(TAddress address, PathString path)
50+
{
51+
var endpoints = GetEndpoints(address);
52+
if (endpoints.Count == 0)
53+
{
54+
return null;
55+
}
56+
57+
for (var i = 0; i < endpoints.Count; i++)
58+
{
59+
var endpoint = endpoints[i];
60+
if (TryParse(endpoint, path, out var values))
61+
{
62+
Log.PathParsingSucceeded(_logger, path, endpoint);
63+
return values;
64+
}
65+
}
66+
67+
Log.PathParsingFailed(_logger, path, endpoints);
68+
return null;
69+
}
70+
71+
private List<RouteEndpoint> GetEndpoints<TAddress>(TAddress address)
72+
{
73+
var addressingScheme = _serviceProvider.GetRequiredService<IEndpointAddressScheme<TAddress>>();
74+
var endpoints = addressingScheme.FindEndpoints(address).OfType<RouteEndpoint>().ToList();
75+
76+
if (endpoints.Count == 0)
77+
{
78+
Log.EndpointsNotFound(_logger, address);
79+
}
80+
else
81+
{
82+
Log.EndpointsFound(_logger, address, endpoints);
83+
}
84+
85+
return endpoints;
86+
}
87+
88+
private MatcherState CreateRoutePatternMatcher(RouteEndpoint endpoint)
89+
{
90+
var constraints = new Dictionary<string, List<IRouteConstraint>>(StringComparer.OrdinalIgnoreCase);
91+
92+
var policies = endpoint.RoutePattern.ParameterPolicies;
93+
foreach (var kvp in policies)
94+
{
95+
var constraintsForParameter = new List<IRouteConstraint>();
96+
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key);
97+
for (var i = 0; i < kvp.Value.Count; i++)
98+
{
99+
var policy = _parameterPolicyFactory.Create(parameter, kvp.Value[i]);
100+
if (policy is IRouteConstraint constraint)
101+
{
102+
constraintsForParameter.Add(constraint);
103+
}
104+
}
105+
106+
if (constraintsForParameter.Count > 0)
107+
{
108+
constraints.Add(kvp.Key, constraintsForParameter);
109+
}
110+
}
111+
112+
var matcher = new RoutePatternMatcher(endpoint.RoutePattern, new RouteValueDictionary(endpoint.RoutePattern.Defaults));
113+
return new MatcherState(matcher, constraints);
114+
}
115+
116+
// Internal for testing
117+
internal MatcherState GetMatcherState(RouteEndpoint endpoint) => _matcherCache.EnsureInitialized().GetOrAdd(endpoint, _createMatcher);
118+
119+
// Internal for testing
120+
internal bool TryParse(RouteEndpoint endpoint, PathString path, out RouteValueDictionary values)
121+
{
122+
var (matcher, constraints) = GetMatcherState(endpoint);
123+
124+
values = new RouteValueDictionary();
125+
if (!matcher.TryMatch(path, values))
126+
{
127+
values = null;
128+
return false;
129+
}
130+
131+
foreach (var kvp in constraints)
132+
{
133+
for (var i = 0; i < kvp.Value.Count; i++)
134+
{
135+
var constraint = kvp.Value[i];
136+
if (!constraint.Match(httpContext: null, NullRouter.Instance, kvp.Key, values, RouteDirection.IncomingRequest))
137+
{
138+
values = null;
139+
return false;
140+
}
141+
}
142+
}
143+
144+
return true;
145+
}
146+
147+
public void Dispose()
148+
{
149+
_matcherCache.Dispose();
150+
}
151+
152+
// internal for testing
153+
internal readonly struct MatcherState
154+
{
155+
public readonly RoutePatternMatcher Matcher;
156+
public readonly Dictionary<string, List<IRouteConstraint>> Constraints;
157+
158+
public MatcherState(RoutePatternMatcher matcher, Dictionary<string, List<IRouteConstraint>> constraints)
159+
{
160+
Matcher = matcher;
161+
Constraints = constraints;
162+
}
163+
164+
public void Deconstruct(out RoutePatternMatcher matcher, out Dictionary<string, List<IRouteConstraint>> constraints)
165+
{
166+
matcher = Matcher;
167+
constraints = Constraints;
168+
}
169+
}
170+
171+
private static class Log
172+
{
173+
public static class EventIds
174+
{
175+
public static readonly EventId EndpointsFound = new EventId(100, "EndpointsFound");
176+
public static readonly EventId EndpointsNotFound = new EventId(101, "EndpointsNotFound");
177+
178+
public static readonly EventId PathParsingSucceeded = new EventId(102, "PathParsingSucceeded");
179+
public static readonly EventId PathParsingFailed = new EventId(103, "PathParsingFailed");
180+
}
181+
182+
private static readonly Action<ILogger, IEnumerable<string>, object, Exception> _endpointsFound = LoggerMessage.Define<IEnumerable<string>, object>(
183+
LogLevel.Debug,
184+
EventIds.EndpointsFound,
185+
"Found the endpoints {Endpoints} for address {Address}");
186+
187+
private static readonly Action<ILogger, object, Exception> _endpointsNotFound = LoggerMessage.Define<object>(
188+
LogLevel.Debug,
189+
EventIds.EndpointsNotFound,
190+
"No endpoints found for address {Address}");
191+
192+
private static readonly Action<ILogger, string, string, Exception> _pathParsingSucceeded = LoggerMessage.Define<string, string>(
193+
LogLevel.Debug,
194+
EventIds.PathParsingSucceeded,
195+
"Path parsing succeeded for endpoint {Endpoint} and URI path {URI}");
196+
197+
private static readonly Action<ILogger, IEnumerable<string>, string, Exception> _pathParsingFailed = LoggerMessage.Define<IEnumerable<string>, string>(
198+
LogLevel.Debug,
199+
EventIds.PathParsingFailed,
200+
"Path parsing failed for endpoints {Endpoints} and URI path {URI}");
201+
202+
public static void EndpointsFound(ILogger logger, object address, IEnumerable<Endpoint> endpoints)
203+
{
204+
// Checking level again to avoid allocation on the common path
205+
if (logger.IsEnabled(LogLevel.Debug))
206+
{
207+
_endpointsFound(logger, endpoints.Select(e => e.DisplayName), address, null);
208+
}
209+
}
210+
211+
public static void EndpointsNotFound(ILogger logger, object address)
212+
{
213+
_endpointsNotFound(logger, address, null);
214+
}
215+
216+
public static void PathParsingSucceeded(ILogger logger, PathString path, Endpoint endpoint)
217+
{
218+
// Checking level again to avoid allocation on the common path
219+
if (logger.IsEnabled(LogLevel.Debug))
220+
{
221+
_pathParsingSucceeded(logger, endpoint.DisplayName, path.Value, null);
222+
}
223+
}
224+
225+
public static void PathParsingFailed(ILogger logger, PathString path, IEnumerable<Endpoint> endpoints)
226+
{
227+
// Checking level again to avoid allocation on the common path
228+
if (logger.IsEnabled(LogLevel.Debug))
229+
{
230+
_pathParsingFailed(logger, endpoints.Select(e => e.DisplayName), path.Value, null);
231+
}
232+
}
233+
}
234+
}
235+
}

src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public static IServiceCollection AddRouting(this IServiceCollection services)
8585
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
8686
services.TryAddSingleton<IEndpointAddressScheme<string>, EndpointNameAddressScheme>();
8787
services.TryAddSingleton<IEndpointAddressScheme<RouteValuesAddress>, RouteValuesAddressScheme>();
88+
services.TryAddSingleton<LinkParser, DefaultLinkParser>();
8889

8990
//
9091
// Endpoint Selection

src/Http/Routing/src/LinkParser.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Routing
7+
{
8+
/// <summary>
9+
/// Defines a contract to parse URIs using information from routing.
10+
/// </summary>
11+
public abstract class LinkParser
12+
{
13+
/// <summary>
14+
/// Attempts to parse the provided <paramref name="path"/> using the route pattern
15+
/// specified by the <see cref="Endpoint"/> matching <paramref name="address"/>.
16+
/// </summary>
17+
/// <typeparam name="TAddress">The address type.</typeparam>
18+
/// <param name="address">The address value. Used to resolve endpoints.</param>
19+
/// <param name="path">The URI path to parse.</param>
20+
/// <returns>
21+
/// A <see cref="RouteValueDictionary"/> with the parsed values if parsing is successful;
22+
/// otherwise <c>null</c>.
23+
/// </returns>
24+
/// <remarks>
25+
/// <para>
26+
/// <see cref="ParsePathByAddress{TAddress}(TAddress, PathString)"/> will attempt to first resolve
27+
/// <see cref="Endpoint"/> instances that match <paramref name="address"/> and then use the route
28+
/// pattern associated with each endpoint to parse the URL path.
29+
/// </para>
30+
/// <para>
31+
/// The parsing operation will fail and return <c>null</c> if either no endpoints are found or none
32+
/// of the route patterns match the provided URI path.
33+
/// </para>
34+
/// </remarks>
35+
public abstract RouteValueDictionary ParsePathByAddress<TAddress>(TAddress address, PathString path);
36+
}
37+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Routing
8+
{
9+
/// <summary>
10+
/// Extension methods for using <see cref="LinkParser"/> with an endpoint name.
11+
/// </summary>
12+
public static class LinkParserEndpointNameAddressExtensions
13+
{
14+
/// <summary>
15+
/// Attempts to parse the provided <paramref name="path"/> using the route pattern
16+
/// specified by the <see cref="Endpoint"/> matching <paramref name="endpointName"/>.
17+
/// </summary>
18+
/// <param name="parser">The <see cref="LinkParser"/>.</param>
19+
/// <param name="endpointName">The endpoint name. Used to resolve endpoints.</param>
20+
/// <param name="path">The URI path to parse.</param>
21+
/// <returns>
22+
/// A <see cref="RouteValueDictionary"/> with the parsed values if parsing is successful;
23+
/// otherwise <c>null</c>.
24+
/// </returns>
25+
/// <remarks>
26+
/// <para>
27+
/// <see cref="ParsePathByEndpointName(LinkParser, string, PathString)"/> will attempt to first resolve
28+
/// <see cref="Endpoint"/> instances that match <paramref name="endpointName"/> and then use the route
29+
/// pattern associated with each endpoint to parse the URL path.
30+
/// </para>
31+
/// <para>
32+
/// The parsing operation will fail and return <c>null</c> if either no endpoints are found or none
33+
/// of the route patterns match the provided URI path.
34+
/// </para>
35+
/// </remarks>
36+
public static RouteValueDictionary ParsePathByEndpointName(
37+
this LinkParser parser,
38+
string endpointName,
39+
PathString path)
40+
{
41+
if (parser == null)
42+
{
43+
throw new ArgumentNullException(nameof(parser));
44+
}
45+
46+
if (endpointName == null)
47+
{
48+
throw new ArgumentNullException(nameof(endpointName));
49+
}
50+
51+
return parser.ParsePathByAddress<string>(endpointName, path);
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)