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.
This commit is contained in:
parent
0748d18a0c
commit
127bc7ddd3
|
|
@ -193,6 +193,15 @@ namespace Microsoft.AspNetCore.Routing
|
|||
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() { }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -85,6 +85,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
|
||||
services.TryAddSingleton<IEndpointAddressScheme<string>, EndpointNameAddressScheme>();
|
||||
services.TryAddSingleton<IEndpointAddressScheme<RouteValuesAddress>, RouteValuesAddressScheme>();
|
||||
services.TryAddSingleton<LinkParser, DefaultLinkParser>();
|
||||
|
||||
//
|
||||
// Endpoint Selection
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
// Tests LinkParser functionality using ParsePathByAddress - see tests for the extension
|
||||
// methods for more E2E tests.
|
||||
//
|
||||
// Does not cover template processing in detail, those scenarios are validated by other tests.
|
||||
public class DefaultLinkParserTest : LinkParserTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ParsePathByAddresss_NoMatchingEndpoint_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", displayName: "Test1", metadata: new object[] { new IntMetadata(1), });
|
||||
|
||||
var sink = new TestSink();
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var parser = CreateLinkParser(services => { services.AddSingleton<ILoggerFactory>(loggerFactory); }, endpoint);
|
||||
|
||||
// Act
|
||||
var values = parser.ParsePathByAddress(0, "/Home/Index/17");
|
||||
|
||||
// Assert
|
||||
Assert.Null(values);
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
w => Assert.Equal("No endpoints found for address 0", w.Message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePathByAddresss_HasMatches_ReturnsNullWhenParsingFails()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", displayName: "Test1", metadata: new object[] { new IntMetadata(1), });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", displayName: "Test2", metadata: new object[] { new IntMetadata(0), });
|
||||
|
||||
var sink = new TestSink();
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var parser = CreateLinkParser(services => { services.AddSingleton<ILoggerFactory>(loggerFactory); }, endpoint1, endpoint2);
|
||||
|
||||
// Act
|
||||
var values = parser.ParsePathByAddress(0, "/");
|
||||
|
||||
// Assert
|
||||
Assert.Null(values);
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
w => Assert.Equal("Found the endpoints Test2 for address 0", w.Message),
|
||||
w => Assert.Equal("Path parsing failed for endpoints Test2 and URI path /", w.Message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePathByAddresss_HasMatches_ReturnsFirstSuccessfulParse()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint0 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}", displayName: "Test1",metadata: new object[] { new IntMetadata(0), });
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", displayName: "Test2", metadata: new object[] { new IntMetadata(0), });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", displayName: "Test3", metadata: new object[] { new IntMetadata(0), });
|
||||
|
||||
var sink = new TestSink();
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var parser = CreateLinkParser(services => { services.AddSingleton<ILoggerFactory>(loggerFactory); }, endpoint0, endpoint1, endpoint2);
|
||||
|
||||
// Act
|
||||
var values = parser.ParsePathByAddress(0, "/Home/Index/17");
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertRouteValuesEqual(new { controller= "Home", action = "Index", id = "17" }, values);
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
w => Assert.Equal("Found the endpoints Test1, Test2, Test3 for address 0", w.Message),
|
||||
w => Assert.Equal("Path parsing succeeded for endpoint Test2 and URI path /Home/Index/17", w.Message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePathByAddresss_HasMatches_IncludesDefaults()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller=Home}/{action=Index}/{id?}", metadata: new object[] { new IntMetadata(0), });
|
||||
|
||||
var parser = CreateLinkParser(endpoint);
|
||||
|
||||
// Act
|
||||
var values = parser.ParsePathByAddress(0, "/");
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", }, values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePathByAddresss_HasMatches_RunsConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint0 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id:int}", metadata: new object[] { new IntMetadata(0), });
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2:alpha}", metadata: new object[] { new IntMetadata(0), });
|
||||
|
||||
var parser = CreateLinkParser(endpoint0, endpoint1);
|
||||
|
||||
// Act
|
||||
var values = parser.ParsePathByAddress(0, "/Home/Index/abc");
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", id2 = "abc" }, values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRoutePatternMatcher_CanCache()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), });
|
||||
var dataSource = new DynamicEndpointDataSource(endpoint1);
|
||||
|
||||
var parser = CreateLinkParser(dataSources: new[] { dataSource });
|
||||
|
||||
var expected = parser.GetMatcherState(endpoint1);
|
||||
|
||||
// Act
|
||||
var actual = parser.GetMatcherState(endpoint1);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expected.Matcher, actual.Matcher);
|
||||
Assert.Same(expected.Constraints, actual.Constraints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRoutePatternMatcherr_CanClearCache()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), });
|
||||
var dataSource = new DynamicEndpointDataSource(endpoint1);
|
||||
|
||||
var parser = CreateLinkParser(dataSources: new[] { dataSource });
|
||||
var original = parser.GetMatcherState(endpoint1);
|
||||
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), });
|
||||
dataSource.AddEndpoint(endpoint2);
|
||||
|
||||
// Act
|
||||
var actual = parser.GetMatcherState(endpoint1);
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(original.Matcher, actual.Matcher);
|
||||
Assert.NotSame(original.Constraints, actual.Constraints);
|
||||
}
|
||||
|
||||
protected override void AddAdditionalServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IEndpointAddressScheme<int>, IntAddressScheme>();
|
||||
}
|
||||
|
||||
private class IntAddressScheme : IEndpointAddressScheme<int>
|
||||
{
|
||||
private readonly EndpointDataSource _dataSource;
|
||||
|
||||
public IntAddressScheme(EndpointDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
}
|
||||
|
||||
public IEnumerable<Endpoint> FindEndpoints(int address)
|
||||
{
|
||||
return _dataSource.Endpoints.Where(e => e.Metadata.GetMetadata<IntMetadata>().Value == address);
|
||||
}
|
||||
}
|
||||
|
||||
private class IntMetadata
|
||||
{
|
||||
public IntMetadata(int value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public int Value { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// 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.Routing.Matching;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class LinkParserEndpointNameExtensionsTest : LinkParserTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ParsePathByAddresss_NoMatchingEndpoint_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new EndpointNameMetadata("Test2"), });
|
||||
|
||||
var parser = CreateLinkParser(endpoint);
|
||||
|
||||
// Act
|
||||
var values = parser.ParsePathByEndpointName("Test", "/Home/Index/17");
|
||||
|
||||
// Assert
|
||||
Assert.Null(values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePathByAddresss_HasMatches_ReturnsNullWhenParsingFails()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new EndpointNameMetadata("Test2"), });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", metadata: new object[] { new EndpointNameMetadata("Test"), });
|
||||
|
||||
var parser = CreateLinkParser(endpoint1, endpoint2);
|
||||
|
||||
// Act
|
||||
var values = parser.ParsePathByEndpointName("Test", "/");
|
||||
|
||||
// Assert
|
||||
Assert.Null(values);
|
||||
}
|
||||
|
||||
[Fact] // Endpoint name does not support multiple matches
|
||||
public void ParsePathByAddresss_HasMatches_ReturnsFirstSuccessfulParse()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new EndpointNameMetadata("Test"), });
|
||||
|
||||
var parser = CreateLinkParser(endpoint);
|
||||
|
||||
// Act
|
||||
var values = parser.ParsePathByEndpointName("Test", "/Home/Index/17");
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", id = "17" }, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// 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;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public abstract class LinkParserTestBase
|
||||
{
|
||||
protected ServiceCollection GetBasicServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddOptions();
|
||||
services.AddRouting();
|
||||
services.AddLogging();
|
||||
return services;
|
||||
}
|
||||
|
||||
protected virtual void AddAdditionalServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
private protected DefaultLinkParser CreateLinkParser(params Endpoint[] endpoints)
|
||||
{
|
||||
return CreateLinkParser(configureServices: null, endpoints);
|
||||
}
|
||||
|
||||
private protected DefaultLinkParser CreateLinkParser(
|
||||
Action<IServiceCollection> configureServices,
|
||||
params Endpoint[] endpoints)
|
||||
{
|
||||
return CreateLinkParser(configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty<Endpoint>()) });
|
||||
}
|
||||
|
||||
private protected DefaultLinkParser CreateLinkParser(EndpointDataSource[] dataSources)
|
||||
{
|
||||
return CreateLinkParser(configureServices: null, dataSources);
|
||||
}
|
||||
|
||||
private protected DefaultLinkParser CreateLinkParser(
|
||||
Action<IServiceCollection> configureServices,
|
||||
EndpointDataSource[] dataSources)
|
||||
{
|
||||
var services = GetBasicServices();
|
||||
AddAdditionalServices(services);
|
||||
configureServices?.Invoke(services);
|
||||
|
||||
services.Configure<RouteOptions>(o =>
|
||||
{
|
||||
if (dataSources != null)
|
||||
{
|
||||
foreach (var dataSource in dataSources)
|
||||
{
|
||||
o.EndpointDataSources.Add(dataSource);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
var routeOptions = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
|
||||
|
||||
return new DefaultLinkParser(
|
||||
new DefaultParameterPolicyFactory(routeOptions, serviceProvider),
|
||||
new CompositeEndpointDataSource(routeOptions.Value.EndpointDataSources),
|
||||
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<DefaultLinkParser>(),
|
||||
serviceProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,21 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
internal static class MatcherAssert
|
||||
{
|
||||
public static void AssertRouteValuesEqual(object expectedValues, RouteValueDictionary actualValues)
|
||||
{
|
||||
AssertRouteValuesEqual(new RouteValueDictionary(expectedValues), actualValues);
|
||||
}
|
||||
|
||||
public static void AssertRouteValuesEqual(RouteValueDictionary expectedValues, RouteValueDictionary actualValues)
|
||||
{
|
||||
if (expectedValues.Count != actualValues.Count ||
|
||||
!expectedValues.OrderBy(kvp => kvp.Key).SequenceEqual(actualValues.OrderBy(kvp => kvp.Key)))
|
||||
{
|
||||
throw new XunitException(
|
||||
$"Expected values:{FormatRouteValues(expectedValues)} Actual values: {FormatRouteValues(actualValues)}.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void AssertMatch(EndpointSelectorContext context, HttpContext httpContext, Endpoint expected)
|
||||
{
|
||||
AssertMatch(context, httpContext, expected, new RouteValueDictionary());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
// 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.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
// Functional tests for MVC's scenarios with LinkParser
|
||||
public class LinkParserTest : IClassFixture<MvcTestFixture<RoutingWebSite.StartupForLinkGenerator>>
|
||||
{
|
||||
public LinkParserTest(MvcTestFixture<RoutingWebSite.StartupForLinkGenerator> fixture)
|
||||
{
|
||||
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
|
||||
Client = factory.CreateDefaultClient();
|
||||
}
|
||||
|
||||
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
|
||||
builder.UseStartup<RoutingWebSite.StartupForLinkGenerator>();
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
[Fact]
|
||||
public async Task ParsePathByEndpoint_CanParsedWithDefaultRoute()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync("LinkParser/Index/18");
|
||||
var values = await response.Content.ReadAsAsync<JObject>();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Collection(
|
||||
values.Properties().OrderBy(p => p.Name),
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("action", p.Name);
|
||||
Assert.Equal("Index", p.Value.Value<string>());
|
||||
},
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("controller", p.Name);
|
||||
Assert.Equal("LinkParser", p.Value.Value<string>());
|
||||
},
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("id", p.Name);
|
||||
Assert.Equal("18", p.Value.Value<string>());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsePathByEndpoint_CanParsedWithNamedAttributeRoute()
|
||||
{
|
||||
// Act
|
||||
//
|
||||
// %2F => /
|
||||
var response = await Client.GetAsync("LinkParser/Another?path=%2Fsome-path%2Fa%2Fb%2Fc");
|
||||
var values = await response.Content.ReadAsAsync<JObject>();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Collection(
|
||||
values.Properties().OrderBy(p => p.Name),
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("action", p.Name);
|
||||
Assert.Equal("AnotherRoute", p.Value.Value<string>());
|
||||
},
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("controller", p.Name);
|
||||
Assert.Equal("LinkParser", p.Value.Value<string>());
|
||||
},
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("x", p.Name);
|
||||
Assert.Equal("a", p.Value.Value<string>());
|
||||
},
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("y", p.Name);
|
||||
Assert.Equal("b", p.Value.Value<string>());
|
||||
},
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("z", p.Name);
|
||||
Assert.Equal("c", p.Value.Value<string>());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace RoutingWebSite.Controllers
|
||||
{
|
||||
public class LinkParserController : Controller
|
||||
{
|
||||
private readonly LinkParser _linkParser;
|
||||
|
||||
public LinkParserController(LinkParser linkParser)
|
||||
{
|
||||
_linkParser = linkParser;
|
||||
}
|
||||
|
||||
public JObject Index()
|
||||
{
|
||||
var parsed = _linkParser.ParsePathByEndpointName("default", HttpContext.Request.Path);
|
||||
if (parsed == null)
|
||||
{
|
||||
throw new Exception("Parsing failed.");
|
||||
}
|
||||
|
||||
return ToJObject(parsed);
|
||||
}
|
||||
|
||||
public JObject Another(string path)
|
||||
{
|
||||
var parsed = _linkParser.ParsePathByEndpointName("AnotherRoute", path);
|
||||
if (parsed == null)
|
||||
{
|
||||
throw new Exception("Parsing failed.");
|
||||
}
|
||||
|
||||
return ToJObject(parsed);
|
||||
}
|
||||
|
||||
[Route("some-path/{x}/{y}/{z?}", Name = "AnotherRoute")]
|
||||
public void AnotherRoute()
|
||||
{
|
||||
throw null;
|
||||
}
|
||||
|
||||
private static JObject ToJObject(RouteValueDictionary values)
|
||||
{
|
||||
var obj = new JObject();
|
||||
foreach (var kvp in values)
|
||||
{
|
||||
obj.Add(kvp.Key, new JValue(kvp.Value.ToString()));
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue