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.
This commit is contained in:
Ryan Nowak 2019-05-02 21:11:59 -07:00 committed by GitHub
parent 0748d18a0c
commit 127bc7ddd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 830 additions and 0 deletions

View File

@ -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() { }

View File

@ -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);
}
}
}
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());

View File

@ -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>());
});
}
}
}

View File

@ -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;
}
}
}