Skip to content

Commit 39622b1

Browse files
Ryan Nowakrynowak
Ryan Nowak
authored andcommitted
Fixes: #4597 Parse URI path with an endpoint
Adds functionality to LinkGenerator 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. I added this to LinkGenerator because I think it feels like the right thing do, despite the naming conflict. I don't really want to create a new top-level service for this.
1 parent 0eb1fd7 commit 39622b1

8 files changed

+651
-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: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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, endpoints);
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+
89+
private MatcherState CreateRoutePatternMatcher(RouteEndpoint endpoint)
90+
{
91+
var constraints = new Dictionary<string, List<IRouteConstraint>>(StringComparer.OrdinalIgnoreCase);
92+
93+
var policies = endpoint.RoutePattern.ParameterPolicies;
94+
foreach (var kvp in policies)
95+
{
96+
var constraintsForParameter = new List<IRouteConstraint>();
97+
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key);
98+
for (var i = 0; i < kvp.Value.Count; i++)
99+
{
100+
var policy = _parameterPolicyFactory.Create(parameter, kvp.Value[i]);
101+
if (policy is IRouteConstraint constraint)
102+
{
103+
constraintsForParameter.Add(constraint);
104+
}
105+
}
106+
107+
if (constraintsForParameter.Count > 0)
108+
{
109+
constraints.Add(kvp.Key, constraintsForParameter);
110+
}
111+
}
112+
113+
var matcher = new RoutePatternMatcher(endpoint.RoutePattern, new RouteValueDictionary(endpoint.RoutePattern.Defaults));
114+
return new MatcherState(matcher, constraints);
115+
}
116+
117+
// Internal for testing
118+
internal MatcherState GetMatcherState(RouteEndpoint endpoint) => _matcherCache.EnsureInitialized().GetOrAdd(endpoint, _createMatcher);
119+
120+
// Internal for testing
121+
internal bool TryParse(RouteEndpoint endpoint, PathString path, out RouteValueDictionary values)
122+
{
123+
var (matcher, constraints) = GetMatcherState(endpoint);
124+
125+
values = new RouteValueDictionary();
126+
if (!matcher.TryMatch(path, values))
127+
{
128+
values = null;
129+
return false;
130+
}
131+
132+
foreach (var kvp in constraints)
133+
{
134+
for (var i = 0; i < kvp.Value.Count; i++)
135+
{
136+
var constraint = kvp.Value[i];
137+
if (!constraint.Match(httpContext: null, NullRouter.Instance, kvp.Key, values, RouteDirection.IncomingRequest))
138+
{
139+
values = null;
140+
return false;
141+
}
142+
}
143+
}
144+
145+
return true;
146+
}
147+
148+
public void Dispose()
149+
{
150+
_matcherCache.Dispose();
151+
}
152+
153+
// internal for testing
154+
internal readonly struct MatcherState
155+
{
156+
public readonly RoutePatternMatcher Matcher;
157+
public readonly Dictionary<string, List<IRouteConstraint>> Constraints;
158+
159+
public MatcherState(RoutePatternMatcher matcher, Dictionary<string, List<IRouteConstraint>> constraints)
160+
{
161+
Matcher = matcher;
162+
Constraints = constraints;
163+
}
164+
165+
public void Deconstruct(out RoutePatternMatcher matcher, out Dictionary<string, List<IRouteConstraint>> constraints)
166+
{
167+
matcher = Matcher;
168+
constraints = Constraints;
169+
}
170+
}
171+
172+
private static class Log
173+
{
174+
public static class EventIds
175+
{
176+
public static readonly EventId EndpointsFound = new EventId(100, "EndpointsFound");
177+
public static readonly EventId EndpointsNotFound = new EventId(101, "EndpointsNotFound");
178+
179+
public static readonly EventId PathParsingSucceeded = new EventId(102, "PathParsingSucceeded");
180+
public static readonly EventId PathParsingFailed = new EventId(103, "PathParsingFailed");
181+
}
182+
183+
private static readonly Action<ILogger, IEnumerable<string>, object, Exception> _endpointsFound = LoggerMessage.Define<IEnumerable<string>, object>(
184+
LogLevel.Debug,
185+
EventIds.EndpointsFound,
186+
"Found the endpoints {Endpoints} for address {Address}");
187+
188+
private static readonly Action<ILogger, object, Exception> _endpointsNotFound = LoggerMessage.Define<object>(
189+
LogLevel.Debug,
190+
EventIds.EndpointsNotFound,
191+
"No endpoints found for address {Address}");
192+
193+
private static readonly Action<ILogger, IEnumerable<string>, string, Exception> _pathParsingSucceeded = LoggerMessage.Define<IEnumerable<string>, string>(
194+
LogLevel.Debug,
195+
EventIds.PathParsingSucceeded,
196+
"Path parsing succeeded for endpoints {Endpoints} and URI path {URI}");
197+
198+
private static readonly Action<ILogger, IEnumerable<string>, string, Exception> _pathParsingFailed = LoggerMessage.Define<IEnumerable<string>, string>(
199+
LogLevel.Debug,
200+
EventIds.PathParsingFailed,
201+
"Path parsing failed for endpoints {Endpoints} and URI path {URI}");
202+
203+
public static void EndpointsFound(ILogger logger, object address, IEnumerable<Endpoint> endpoints)
204+
{
205+
// Checking level again to avoid allocation on the common path
206+
if (logger.IsEnabled(LogLevel.Debug))
207+
{
208+
_endpointsFound(logger, endpoints.Select(e => e.DisplayName), address, null);
209+
}
210+
}
211+
212+
public static void EndpointsNotFound(ILogger logger, object address)
213+
{
214+
_endpointsNotFound(logger, address, null);
215+
}
216+
217+
public static void PathParsingSucceeded(ILogger logger, PathString path, IEnumerable<Endpoint> endpoints)
218+
{
219+
// Checking level again to avoid allocation on the common path
220+
if (logger.IsEnabled(LogLevel.Debug))
221+
{
222+
_pathParsingSucceeded(logger, endpoints.Select(e => e.DisplayName), path.Value, null);
223+
}
224+
}
225+
226+
public static void PathParsingFailed(ILogger logger, PathString path, IEnumerable<Endpoint> endpoints)
227+
{
228+
// Checking level again to avoid allocation on the common path
229+
if (logger.IsEnabled(LogLevel.Debug))
230+
{
231+
_pathParsingFailed(logger, endpoints.Select(e => e.DisplayName), path.Value, null);
232+
}
233+
}
234+
}
235+
}
236+
}

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="LinkGenerator"/>.</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)