Skip to content

Fixes: #4597 Parse URI path with an endpoint #9728

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,15 @@ public static partial class LinkGeneratorRouteValuesAddressExtensions
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; }
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; }
}
public abstract partial class LinkParser
{
protected LinkParser() { }
public abstract Microsoft.AspNetCore.Routing.RouteValueDictionary ParsePathByAddress<TAddress>(TAddress address, Microsoft.AspNetCore.Http.PathString path);
}
public static partial class LinkParserEndpointNameAddressExtensions
{
public static Microsoft.AspNetCore.Routing.RouteValueDictionary ParsePathByEndpointName(this Microsoft.AspNetCore.Routing.LinkParser parser, string endpointName, Microsoft.AspNetCore.Http.PathString path) { throw null; }
}
public abstract partial class MatcherPolicy
{
protected MatcherPolicy() { }
Expand Down
235 changes: 235 additions & 0 deletions src/Http/Routing/src/DefaultLinkParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// 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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Routing
{
internal class DefaultLinkParser : LinkParser, IDisposable
{
private readonly ParameterPolicyFactory _parameterPolicyFactory;
private readonly ILogger<DefaultLinkParser> _logger;
private readonly IServiceProvider _serviceProvider;

// Caches RoutePatternMatcher instances
private readonly DataSourceDependentCache<ConcurrentDictionary<RouteEndpoint, MatcherState>> _matcherCache;

// Used to initialize RoutePatternMatcher and constraint instances
private readonly Func<RouteEndpoint, MatcherState> _createMatcher;

public DefaultLinkParser(
ParameterPolicyFactory parameterPolicyFactory,
EndpointDataSource dataSource,
ILogger<DefaultLinkParser> logger,
IServiceProvider serviceProvider)
{
_parameterPolicyFactory = parameterPolicyFactory;
_logger = logger;
_serviceProvider = serviceProvider;

// We cache RoutePatternMatcher instances per-Endpoint for performance, but we want to wipe out
// that cache is the endpoints change so that we don't allow unbounded memory growth.
_matcherCache = new DataSourceDependentCache<ConcurrentDictionary<RouteEndpoint, MatcherState>>(dataSource, (_) =>
{
// We don't eagerly fill this cache because there's no real reason to. Unlike URL matching, we don't
// need to build a big data structure up front to be correct.
return new ConcurrentDictionary<RouteEndpoint, MatcherState>();
});

// Cached to avoid per-call allocation of a delegate on lookup.
_createMatcher = CreateRoutePatternMatcher;
}

public override RouteValueDictionary ParsePathByAddress<TAddress>(TAddress address, PathString path)
{
var endpoints = GetEndpoints(address);
if (endpoints.Count == 0)
{
return null;
}

for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
if (TryParse(endpoint, path, out var values))
{
Log.PathParsingSucceeded(_logger, path, endpoint);
return values;
}
}

Log.PathParsingFailed(_logger, path, endpoints);
return null;
}

private List<RouteEndpoint> GetEndpoints<TAddress>(TAddress address)
{
var addressingScheme = _serviceProvider.GetRequiredService<IEndpointAddressScheme<TAddress>>();
var endpoints = addressingScheme.FindEndpoints(address).OfType<RouteEndpoint>().ToList();

if (endpoints.Count == 0)
{
Log.EndpointsNotFound(_logger, address);
}
else
{
Log.EndpointsFound(_logger, address, endpoints);
}

return endpoints;
}

private MatcherState CreateRoutePatternMatcher(RouteEndpoint endpoint)
{
var constraints = new Dictionary<string, List<IRouteConstraint>>(StringComparer.OrdinalIgnoreCase);

var policies = endpoint.RoutePattern.ParameterPolicies;
foreach (var kvp in policies)
{
var constraintsForParameter = new List<IRouteConstraint>();
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key);
for (var i = 0; i < kvp.Value.Count; i++)
{
var policy = _parameterPolicyFactory.Create(parameter, kvp.Value[i]);
if (policy is IRouteConstraint constraint)
{
constraintsForParameter.Add(constraint);
}
}

if (constraintsForParameter.Count > 0)
{
constraints.Add(kvp.Key, constraintsForParameter);
}
}

var matcher = new RoutePatternMatcher(endpoint.RoutePattern, new RouteValueDictionary(endpoint.RoutePattern.Defaults));
return new MatcherState(matcher, constraints);
}

// Internal for testing
internal MatcherState GetMatcherState(RouteEndpoint endpoint) => _matcherCache.EnsureInitialized().GetOrAdd(endpoint, _createMatcher);

// Internal for testing
internal bool TryParse(RouteEndpoint endpoint, PathString path, out RouteValueDictionary values)
{
var (matcher, constraints) = GetMatcherState(endpoint);

values = new RouteValueDictionary();
if (!matcher.TryMatch(path, values))
{
values = null;
return false;
}

foreach (var kvp in constraints)
{
for (var i = 0; i < kvp.Value.Count; i++)
{
var constraint = kvp.Value[i];
if (!constraint.Match(httpContext: null, NullRouter.Instance, kvp.Key, values, RouteDirection.IncomingRequest))
{
values = null;
return false;
}
}
}

return true;
}

public void Dispose()
{
_matcherCache.Dispose();
}

// internal for testing
internal readonly struct MatcherState
{
public readonly RoutePatternMatcher Matcher;
public readonly Dictionary<string, List<IRouteConstraint>> Constraints;

public MatcherState(RoutePatternMatcher matcher, Dictionary<string, List<IRouteConstraint>> constraints)
{
Matcher = matcher;
Constraints = constraints;
}

public void Deconstruct(out RoutePatternMatcher matcher, out Dictionary<string, List<IRouteConstraint>> constraints)
{
matcher = Matcher;
constraints = Constraints;
}
}

private static class Log
{
public static class EventIds
{
public static readonly EventId EndpointsFound = new EventId(100, "EndpointsFound");
public static readonly EventId EndpointsNotFound = new EventId(101, "EndpointsNotFound");

public static readonly EventId PathParsingSucceeded = new EventId(102, "PathParsingSucceeded");
public static readonly EventId PathParsingFailed = new EventId(103, "PathParsingFailed");
}

private static readonly Action<ILogger, IEnumerable<string>, object, Exception> _endpointsFound = LoggerMessage.Define<IEnumerable<string>, object>(
LogLevel.Debug,
EventIds.EndpointsFound,
"Found the endpoints {Endpoints} for address {Address}");

private static readonly Action<ILogger, object, Exception> _endpointsNotFound = LoggerMessage.Define<object>(
LogLevel.Debug,
EventIds.EndpointsNotFound,
"No endpoints found for address {Address}");

private static readonly Action<ILogger, string, string, Exception> _pathParsingSucceeded = LoggerMessage.Define<string, string>(
LogLevel.Debug,
EventIds.PathParsingSucceeded,
"Path parsing succeeded for endpoint {Endpoint} and URI path {URI}");

private static readonly Action<ILogger, IEnumerable<string>, string, Exception> _pathParsingFailed = LoggerMessage.Define<IEnumerable<string>, string>(
LogLevel.Debug,
EventIds.PathParsingFailed,
"Path parsing failed for endpoints {Endpoints} and URI path {URI}");

public static void EndpointsFound(ILogger logger, object address, IEnumerable<Endpoint> endpoints)
{
// Checking level again to avoid allocation on the common path
if (logger.IsEnabled(LogLevel.Debug))
{
_endpointsFound(logger, endpoints.Select(e => e.DisplayName), address, null);
}
}

public static void EndpointsNotFound(ILogger logger, object address)
{
_endpointsNotFound(logger, address, null);
}

public static void PathParsingSucceeded(ILogger logger, PathString path, Endpoint endpoint)
{
// Checking level again to avoid allocation on the common path
if (logger.IsEnabled(LogLevel.Debug))
{
_pathParsingSucceeded(logger, endpoint.DisplayName, path.Value, null);
}
}

public static void PathParsingFailed(ILogger logger, PathString path, IEnumerable<Endpoint> endpoints)
{
// Checking level again to avoid allocation on the common path
if (logger.IsEnabled(LogLevel.Debug))
{
_pathParsingFailed(logger, endpoints.Select(e => e.DisplayName), path.Value, null);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public static IServiceCollection AddRouting(this IServiceCollection services)
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
services.TryAddSingleton<IEndpointAddressScheme<string>, EndpointNameAddressScheme>();
services.TryAddSingleton<IEndpointAddressScheme<RouteValuesAddress>, RouteValuesAddressScheme>();
services.TryAddSingleton<LinkParser, DefaultLinkParser>();

//
// Endpoint Selection
Expand Down
37 changes: 37 additions & 0 deletions src/Http/Routing/src/LinkParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Defines a contract to parse URIs using information from routing.
/// </summary>
public abstract class LinkParser
{
/// <summary>
/// Attempts to parse the provided <paramref name="path"/> using the route pattern
/// specified by the <see cref="Endpoint"/> matching <paramref name="address"/>.
/// </summary>
/// <typeparam name="TAddress">The address type.</typeparam>
/// <param name="address">The address value. Used to resolve endpoints.</param>
/// <param name="path">The URI path to parse.</param>
/// <returns>
/// A <see cref="RouteValueDictionary"/> with the parsed values if parsing is successful;
/// otherwise <c>null</c>.
/// </returns>
/// <remarks>
/// <para>
/// <see cref="ParsePathByAddress{TAddress}(TAddress, PathString)"/> will attempt to first resolve
/// <see cref="Endpoint"/> instances that match <paramref name="address"/> and then use the route
/// pattern associated with each endpoint to parse the URL path.
/// </para>
/// <para>
/// The parsing operation will fail and return <c>null</c> if either no endpoints are found or none
/// of the route patterns match the provided URI path.
/// </para>
/// </remarks>
public abstract RouteValueDictionary ParsePathByAddress<TAddress>(TAddress address, PathString path);
}
}
54 changes: 54 additions & 0 deletions src/Http/Routing/src/LinkParserEndpointNameAddressExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Extension methods for using <see cref="LinkParser"/> with an endpoint name.
/// </summary>
public static class LinkParserEndpointNameAddressExtensions
{
/// <summary>
/// Attempts to parse the provided <paramref name="path"/> using the route pattern
/// specified by the <see cref="Endpoint"/> matching <paramref name="endpointName"/>.
/// </summary>
/// <param name="parser">The <see cref="LinkParser"/>.</param>
/// <param name="endpointName">The endpoint name. Used to resolve endpoints.</param>
/// <param name="path">The URI path to parse.</param>
/// <returns>
/// A <see cref="RouteValueDictionary"/> with the parsed values if parsing is successful;
/// otherwise <c>null</c>.
/// </returns>
/// <remarks>
/// <para>
/// <see cref="ParsePathByEndpointName(LinkParser, string, PathString)"/> will attempt to first resolve
/// <see cref="Endpoint"/> instances that match <paramref name="endpointName"/> and then use the route
/// pattern associated with each endpoint to parse the URL path.
/// </para>
/// <para>
/// The parsing operation will fail and return <c>null</c> if either no endpoints are found or none
/// of the route patterns match the provided URI path.
/// </para>
/// </remarks>
public static RouteValueDictionary ParsePathByEndpointName(
this LinkParser parser,
string endpointName,
PathString path)
{
if (parser == null)
{
throw new ArgumentNullException(nameof(parser));
}

if (endpointName == null)
{
throw new ArgumentNullException(nameof(endpointName));
}

return parser.ParsePathByAddress<string>(endpointName, path);
}
}
}
Loading