Merge pull request #790 from dotnet-maestro-bot/merge/release/2.2-to-master
[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
commit
087e6d05ee
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace RoutingSample.Web
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
|
@ -19,7 +20,6 @@ namespace Microsoft.AspNetCore.Routing
|
|||
{
|
||||
internal sealed class DefaultLinkGenerator : LinkGenerator
|
||||
{
|
||||
private static readonly char[] UrlQueryDelimiters = new char[] { '?', '#' };
|
||||
private readonly ParameterPolicyFactory _parameterPolicyFactory;
|
||||
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
|
||||
private readonly ILogger<DefaultLinkGenerator> _logger;
|
||||
|
|
@ -172,7 +172,18 @@ namespace Microsoft.AspNetCore.Routing
|
|||
private List<RouteEndpoint> GetEndpoints<TAddress>(TAddress address)
|
||||
{
|
||||
var addressingScheme = _serviceProvider.GetRequiredService<IEndpointFinder<TAddress>>();
|
||||
return addressingScheme.FindEndpoints(address).OfType<RouteEndpoint>().ToList();
|
||||
var endpoints = addressingScheme.FindEndpoints(address).OfType<RouteEndpoint>().ToList();
|
||||
|
||||
if (endpoints.Count == 0)
|
||||
{
|
||||
Log.EndpointsNotFound(_logger, address);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.EndpointsFound(_logger, address, endpoints);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
// Also called from DefaultLinkGenerationTemplate
|
||||
|
|
@ -195,15 +206,17 @@ namespace Microsoft.AspNetCore.Routing
|
|||
options,
|
||||
out var result))
|
||||
{
|
||||
|
||||
return UriHelper.BuildRelative(
|
||||
var uri = UriHelper.BuildRelative(
|
||||
pathBase,
|
||||
result.path,
|
||||
result.query,
|
||||
fragment);
|
||||
Log.LinkGenerationSucceeded(_logger, endpoints, uri);
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
Log.LinkGenerationFailed(_logger, endpoints);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -229,16 +242,19 @@ namespace Microsoft.AspNetCore.Routing
|
|||
options,
|
||||
out var result))
|
||||
{
|
||||
return UriHelper.BuildAbsolute(
|
||||
var uri = UriHelper.BuildAbsolute(
|
||||
scheme,
|
||||
host,
|
||||
pathBase,
|
||||
result.path,
|
||||
result.query,
|
||||
fragment);
|
||||
Log.LinkGenerationSucceeded(_logger, endpoints, uri);
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
Log.LinkGenerationFailed(_logger, endpoints);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -266,20 +282,25 @@ namespace Microsoft.AspNetCore.Routing
|
|||
{
|
||||
// We're missing one of the required values for this route.
|
||||
result = default;
|
||||
Log.TemplateFailedRequiredValues(_logger, endpoint, ambientValues, explicitValues);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues))
|
||||
{
|
||||
result = default;
|
||||
|
||||
// MatchesConstraints does its own logging, so we're not logging here.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!templateBinder.TryBindValues(templateValuesResult.AcceptedValues, options, _globalLinkOptions, out result))
|
||||
{
|
||||
Log.TemplateFailedExpansion(_logger, endpoint, templateValuesResult.AcceptedValues);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.TemplateSucceeded(_logger, endpoint, result.path, result.query);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -304,6 +325,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
if (parameterPolicy is IRouteConstraint routeConstraint
|
||||
&& !routeConstraint.Match(httpContext, NullRouter.Instance, kvp.Key, routeValues, RouteDirection.UrlGeneration))
|
||||
{
|
||||
Log.TemplateFailedConstraint(_logger, endpoint, kvp.Key, routeConstraint, routeValues);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -317,5 +339,163 @@ namespace Microsoft.AspNetCore.Routing
|
|||
{
|
||||
return httpContext?.Features.Get<IRouteValuesFeature>()?.RouteValues;
|
||||
}
|
||||
|
||||
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 TemplateSucceeded = new EventId(102, "TemplateSucceeded");
|
||||
public static readonly EventId TemplateFailedRequiredValues = new EventId(103, "TemplateFailedRequiredValues");
|
||||
public static readonly EventId TemplateFailedConstraint = new EventId(103, "TemplateFailedConstraint");
|
||||
public static readonly EventId TemplateFailedExpansion = new EventId(104, "TemplateFailedExpansion");
|
||||
|
||||
public static readonly EventId LinkGenerationSucceeded = new EventId(105, "LinkGenerationSucceeded");
|
||||
public static readonly EventId LinkGenerationFailed = new EventId(106, "LinkGenerationFailed");
|
||||
}
|
||||
|
||||
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, string, string, Exception> _templateSucceeded = LoggerMessage.Define<string, string, string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.TemplateSucceeded,
|
||||
"Successfully processed template {Template} for {Endpoint} resulting in {Path} and {Query}");
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, string, string, Exception> _templateFailedRequiredValues = LoggerMessage.Define<string, string, string, string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.TemplateFailedRequiredValues,
|
||||
"Failed to process the template {Template} for {Endpoint}. " +
|
||||
"A required route value is missing, or has a different value from the required default values." +
|
||||
"Supplied ambient values {AmbientValues} and {Values} with default values {Defaults}");
|
||||
|
||||
private static readonly Action<ILogger, string, string, IRouteConstraint, string, string, Exception> _templateFailedConstraint = LoggerMessage.Define<string, string, IRouteConstraint, string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.TemplateFailedConstraint,
|
||||
"Failed to process the template {Template} for {Endpoint}. " +
|
||||
"The constraint {Constraint} for parameter {ParameterName} failed with values {Values}");
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, Exception> _templateFailedExpansion = LoggerMessage.Define<string, string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.TemplateFailedExpansion,
|
||||
"Failed to process the template {Template} for {Endpoint}. " +
|
||||
"The failure occured while expanding the template with values {Values}. " +
|
||||
"This is usually due to a missing or empty value in a complex segment.");
|
||||
|
||||
private static readonly Action<ILogger, IEnumerable<string>, string, Exception> _linkGenerationSucceeded = LoggerMessage.Define<IEnumerable<string>, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.LinkGenerationSucceeded,
|
||||
"Link generation succeeded for endpoints {Endpoints} with result {URI}");
|
||||
|
||||
private static readonly Action<ILogger, IEnumerable<string>, Exception> _linkGenerationFailed = LoggerMessage.Define<IEnumerable<string>>(
|
||||
LogLevel.Debug,
|
||||
EventIds.LinkGenerationFailed,
|
||||
"Link generation failed for endpoints {Endpoints}");
|
||||
|
||||
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 TemplateSucceeded(ILogger logger, RouteEndpoint endpoint, PathString path, QueryString query)
|
||||
{
|
||||
_templateSucceeded(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, path.Value, query.Value, null);
|
||||
}
|
||||
|
||||
public static void TemplateFailedRequiredValues(ILogger logger, RouteEndpoint endpoint, RouteValueDictionary ambientValues, RouteValueDictionary values)
|
||||
{
|
||||
// Checking level again to avoid allocation on the common path
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_templateFailedRequiredValues(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, FormatRouteValues(ambientValues), FormatRouteValues(values), FormatRouteValues(endpoint.RoutePattern.Defaults), null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TemplateFailedConstraint(ILogger logger, RouteEndpoint endpoint, string parameterName, IRouteConstraint constraint, RouteValueDictionary values)
|
||||
{
|
||||
// Checking level again to avoid allocation on the common path
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_templateFailedConstraint(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, constraint, parameterName, FormatRouteValues(values), null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TemplateFailedExpansion(ILogger logger, RouteEndpoint endpoint, RouteValueDictionary values)
|
||||
{
|
||||
// Checking level again to avoid allocation on the common path
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_templateFailedExpansion(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, FormatRouteValues(values), null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LinkGenerationSucceeded(ILogger logger, IEnumerable<Endpoint> endpoints, string uri)
|
||||
{
|
||||
// Checking level again to avoid allocation on the common path
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_linkGenerationSucceeded(logger, endpoints.Select(e => e.DisplayName), uri, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LinkGenerationFailed(ILogger logger, IEnumerable<Endpoint> endpoints)
|
||||
{
|
||||
// Checking level again to avoid allocation on the common path
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_linkGenerationFailed(logger, endpoints.Select(e => e.DisplayName), null);
|
||||
}
|
||||
}
|
||||
|
||||
// EXPENSIVE: should only be used at Debug and higher levels of logging.
|
||||
private static string FormatRouteValues(IReadOnlyDictionary<string, object> values)
|
||||
{
|
||||
if (values == null || values.Count == 0)
|
||||
{
|
||||
return "{ }";
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("{ ");
|
||||
|
||||
foreach (var kvp in values.OrderBy(kvp => kvp.Key))
|
||||
{
|
||||
builder.Append("\"");
|
||||
builder.Append(kvp.Key);
|
||||
builder.Append("\"");
|
||||
builder.Append(":");
|
||||
builder.Append(" ");
|
||||
builder.Append("\"");
|
||||
builder.Append(kvp.Value);
|
||||
builder.Append("\"");
|
||||
builder.Append(", ");
|
||||
}
|
||||
|
||||
// Trim trailing ", "
|
||||
builder.Remove(builder.Length - 2, 2);
|
||||
|
||||
builder.Append(" }");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,8 +74,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<DfaGraphWriter>();
|
||||
|
||||
// Link generation related services
|
||||
services.TryAddSingleton<IEndpointFinder<RouteValuesAddress>, RouteValuesBasedEndpointFinder>();
|
||||
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
|
||||
services.TryAddSingleton<IEndpointFinder<string>, EndpointNameEndpointFinder>();
|
||||
services.TryAddSingleton<IEndpointFinder<RouteValuesAddress>, RouteValuesBasedEndpointFinder>();
|
||||
|
||||
//
|
||||
// Endpoint Selection
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
// 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 System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal class EndpointNameEndpointFinder : IEndpointFinder<string>
|
||||
{
|
||||
private readonly DataSourceDependentCache<Dictionary<string, Endpoint[]>> _cache;
|
||||
|
||||
public EndpointNameEndpointFinder(CompositeEndpointDataSource dataSource)
|
||||
{
|
||||
_cache = new DataSourceDependentCache<Dictionary<string, Endpoint[]>>(dataSource, Initialize);
|
||||
}
|
||||
|
||||
// Internal for tests
|
||||
internal Dictionary<string, Endpoint[]> Entries => _cache.EnsureInitialized();
|
||||
|
||||
public IEnumerable<Endpoint> FindEndpoints(string address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
// Capture the current value of the cache
|
||||
var entries = Entries;
|
||||
|
||||
entries.TryGetValue(address, out var result);
|
||||
return result ?? Array.Empty<Endpoint>();
|
||||
}
|
||||
|
||||
private static Dictionary<string, Endpoint[]> Initialize(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
// Collect duplicates as we go, blow up on startup if we find any.
|
||||
var hasDuplicates = false;
|
||||
|
||||
var entries = new Dictionary<string, Endpoint[]>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var endpoint = endpoints[i];
|
||||
|
||||
var endpointName = GetEndpointName(endpoint);
|
||||
if (endpointName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entries.TryGetValue(endpointName, out var existing))
|
||||
{
|
||||
// This isn't a duplicate (so far)
|
||||
entries[endpointName] = new[] { endpoint };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ok this is a duplicate, because we have two endpoints with the same name. Bail out, because we
|
||||
// are just going to throw, we don't need to finish collecting data.
|
||||
hasDuplicates = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasDuplicates)
|
||||
{
|
||||
// No duplicates, success!
|
||||
return entries;
|
||||
}
|
||||
|
||||
// OK we need to report some duplicates.
|
||||
var duplicates = endpoints
|
||||
.GroupBy(e => GetEndpointName(e))
|
||||
.Where(g => g.Key != null)
|
||||
.Where(g => g.Count() > 1);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine(Resources.DuplicateEndpointNameHeader);
|
||||
|
||||
foreach (var group in duplicates)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(Resources.FormatDuplicateEndpointNameEntry(group.Key));
|
||||
|
||||
foreach (var endpoint in group)
|
||||
{
|
||||
builder.AppendLine(endpoint.DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(builder.ToString());
|
||||
|
||||
string GetEndpointName(Endpoint endpoint)
|
||||
{
|
||||
if (endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>() != null)
|
||||
{
|
||||
// Skip anything that's suppressed for linking.
|
||||
return null;
|
||||
}
|
||||
|
||||
return endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies an endpoint name in <see cref="Endpoint.Metadata"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Endpoint names must be unique within an application, and can be used to unambiguously
|
||||
/// identify a desired endpoint for URI generation using <see cref="LinkGenerator"/>.
|
||||
/// </remarks>
|
||||
public class EndpointNameMetadata : IEndpointNameMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="EndpointNameMetadata"/> with the provided endpoint name.
|
||||
/// </summary>
|
||||
/// <param name="endpointName">The endpoint name.</param>
|
||||
public EndpointNameMetadata(string endpointName)
|
||||
{
|
||||
if (endpointName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointName));
|
||||
}
|
||||
|
||||
EndpointName = endpointName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the endpoint name.
|
||||
/// </summary>
|
||||
public string EndpointName { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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 use to specify an endpoint name in <see cref="Endpoint.Metadata"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Endpoint names must be unique within an application, and can be used to unambiguously
|
||||
/// identify a desired endpoint for URI generation using <see cref="LinkGenerator"/>.
|
||||
/// </remarks>
|
||||
public interface IEndpointNameMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the endpoint name.
|
||||
/// </summary>
|
||||
string EndpointName { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
// 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;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for using <see cref="LinkGenerator"/> with and endpoint name.
|
||||
/// </summary>
|
||||
public static class LinkGeneratorEndpointNameAddressExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a URI with an absolute path based on the provided values.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
|
||||
/// <param name="endpointName">The endpoint name. Used to resolve endpoints.</param>
|
||||
/// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
|
||||
/// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
|
||||
/// <param name="options">
|
||||
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
|
||||
/// names from <c>RouteOptions</c>.
|
||||
/// </param>
|
||||
/// <returns>A URI with an absolute path, or <c>null</c>.</returns>
|
||||
public static string GetPathByName(
|
||||
this LinkGenerator generator,
|
||||
HttpContext httpContext,
|
||||
string endpointName,
|
||||
object values,
|
||||
FragmentString fragment = default,
|
||||
LinkOptions options = default)
|
||||
{
|
||||
if (generator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(generator));
|
||||
}
|
||||
|
||||
if (endpointName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointName));
|
||||
}
|
||||
|
||||
return generator.GetPathByAddress<string>(httpContext, endpointName, new RouteValueDictionary(values), fragment, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URI with an absolute path based on the provided values.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="endpointName">The endpoint name. Used to resolve endpoints.</param>
|
||||
/// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
|
||||
/// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
|
||||
/// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
|
||||
/// <param name="options">
|
||||
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
|
||||
/// names from <c>RouteOptions</c>.
|
||||
/// </param>
|
||||
/// <returns>A URI with an absolute path, or <c>null</c>.</returns>
|
||||
public static string GetPathByName(
|
||||
this LinkGenerator generator,
|
||||
string endpointName,
|
||||
object values,
|
||||
PathString pathBase = default,
|
||||
FragmentString fragment = default,
|
||||
LinkOptions options = default)
|
||||
{
|
||||
if (generator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(generator));
|
||||
}
|
||||
|
||||
if (endpointName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointName));
|
||||
}
|
||||
|
||||
return generator.GetPathByAddress<string>(endpointName, new RouteValueDictionary(values), pathBase, fragment, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an absolute URI based on the provided values.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
|
||||
/// <param name="endpointName">The endpoint name. Used to resolve endpoints.</param>
|
||||
/// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
|
||||
/// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
|
||||
/// <param name="options">
|
||||
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
|
||||
/// names from <c>RouteOptions</c>.
|
||||
/// </param>
|
||||
/// <returns>A URI with an absolute path, or <c>null</c>.</returns>
|
||||
public static string GetUriByName(
|
||||
this LinkGenerator generator,
|
||||
HttpContext httpContext,
|
||||
string endpointName,
|
||||
object values,
|
||||
FragmentString fragment = default,
|
||||
LinkOptions options = default)
|
||||
{
|
||||
if (generator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(generator));
|
||||
}
|
||||
|
||||
if (endpointName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointName));
|
||||
}
|
||||
|
||||
return generator.GetUriByAddress<string>(httpContext, endpointName, new RouteValueDictionary(values), fragment, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an absolute URI based on the provided values.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="endpointName">The endpoint name. Used to resolve endpoints.</param>
|
||||
/// <param name="values">The route values. Used to expand parameters in the route template. Optional.</param>
|
||||
/// <param name="scheme">The URI scheme, applied to the resulting URI.</param>
|
||||
/// <param name="host">The URI host/authority, applied to the resulting URI.</param>
|
||||
/// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
|
||||
/// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
|
||||
/// <param name="options">
|
||||
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
|
||||
/// names from <c>RouteOptions</c>.
|
||||
/// </param>
|
||||
/// <returns>An absolute URI, or <c>null</c>.</returns>
|
||||
public static string GetUriByName(
|
||||
this LinkGenerator generator,
|
||||
string endpointName,
|
||||
object values,
|
||||
string scheme,
|
||||
HostString host,
|
||||
PathString pathBase = default,
|
||||
FragmentString fragment = default,
|
||||
LinkOptions options = default)
|
||||
{
|
||||
if (generator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(generator));
|
||||
}
|
||||
|
||||
if (endpointName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointName));
|
||||
}
|
||||
|
||||
return generator.GetUriByAddress<string>(endpointName, new RouteValueDictionary(values), scheme, host, pathBase, fragment, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="LinkGenerationTemplate"/> based on the provided <paramref name="endpointName"/>.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="endpointName">The endpoint name. Used to resolve endpoints. Optional.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="LinkGenerationTemplate"/> if one or more endpoints matching the address can be found, otherwise <c>null</c>.
|
||||
/// </returns>
|
||||
public static LinkGenerationTemplate GetTemplateByName(this LinkGenerator generator, string endpointName)
|
||||
{
|
||||
if (generator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(generator));
|
||||
}
|
||||
|
||||
if (endpointName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointName));
|
||||
}
|
||||
|
||||
return generator.GetTemplateByAddress<string>(endpointName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,19 @@ namespace Microsoft.AspNetCore.Routing
|
|||
/// </summary>
|
||||
public static class LinkGeneratorRouteValuesAddressExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a URI with an absolute path based on the provided values.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
|
||||
/// <param name="routeName">The route name. Used to resolve endpoints. Optional.</param>
|
||||
/// <param name="values">The route values. Used to resolve endpoints and expand parameters in the route template. Optional.</param>
|
||||
/// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
|
||||
/// <param name="options">
|
||||
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
|
||||
/// names from <c>RouteOptions</c>.
|
||||
/// </param>
|
||||
/// <returns>A URI with an absolute path, or <c>null</c>.</returns>
|
||||
public static string GetPathByRouteValues(
|
||||
this LinkGenerator generator,
|
||||
HttpContext httpContext,
|
||||
|
|
@ -28,6 +41,19 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return generator.GetPathByAddress<RouteValuesAddress>(httpContext, address, address.ExplicitValues, fragment, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URI with an absolute path based on the provided values.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="routeName">The route name. Used to resolve endpoints. Optional.</param>
|
||||
/// <param name="values">The route values. Used to resolve endpoints and expand parameters in the route template. Optional.</param>
|
||||
/// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
|
||||
/// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
|
||||
/// <param name="options">
|
||||
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
|
||||
/// names from <c>RouteOptions</c>.
|
||||
/// </param>
|
||||
/// <returns>A URI with an absolute path, or <c>null</c>.</returns>
|
||||
public static string GetPathByRouteValues(
|
||||
this LinkGenerator generator,
|
||||
string routeName,
|
||||
|
|
@ -45,6 +71,19 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return generator.GetPathByAddress<RouteValuesAddress>(address, address.ExplicitValues, pathBase, fragment, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an absolute URI based on the provided values.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
|
||||
/// <param name="routeName">The route name. Used to resolve endpoints. Optional.</param>
|
||||
/// <param name="values">The route values. Used to resolve endpoints and expand parameters in the route template. Optional.</param>
|
||||
/// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
|
||||
/// <param name="options">
|
||||
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
|
||||
/// names from <c>RouteOptions</c>.
|
||||
/// </param>
|
||||
/// <returns>A URI with an absolute path, or <c>null</c>.</returns>
|
||||
public static string GetUriByRouteValues(
|
||||
this LinkGenerator generator,
|
||||
HttpContext httpContext,
|
||||
|
|
@ -62,6 +101,21 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return generator.GetUriByAddress<RouteValuesAddress>(httpContext, address, address.ExplicitValues, fragment, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an absolute URI based on the provided values.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="routeName">The route name. Used to resolve endpoints. Optional.</param>
|
||||
/// <param name="values">The route values. Used to resolve endpoints and expand parameters in the route template. Optional.</param>
|
||||
/// <param name="scheme">The URI scheme, applied to the resulting URI.</param>
|
||||
/// <param name="host">The URI host/authority, applied to the resulting URI.</param>
|
||||
/// <param name="pathBase">An optional URI path base. Prepended to the path in the resulting URI.</param>
|
||||
/// <param name="fragment">An optional URI fragment. Appended to the resulting URI.</param>
|
||||
/// <param name="options">
|
||||
/// An optional <see cref="LinkOptions"/>. Settings on provided object override the settings with matching
|
||||
/// names from <c>RouteOptions</c>.
|
||||
/// </param>
|
||||
/// <returns>An absolute URI, or <c>null</c>.</returns>
|
||||
public static string GetUriByRouteValues(
|
||||
this LinkGenerator generator,
|
||||
string routeName,
|
||||
|
|
@ -81,7 +135,29 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return generator.GetUriByAddress<RouteValuesAddress>(address, address.ExplicitValues, scheme, host, pathBase, fragment, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="LinkGenerationTemplate"/> based on the provided <paramref name="routeName"/> and <paramref name="values"/>.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="LinkGenerator"/>.</param>
|
||||
/// <param name="routeName">The route name. Used to resolve endpoints. Optional.</param>
|
||||
/// <param name="values">The route values. Used to resolve endpoints and expand parameters in the route template. Optional.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="LinkGenerationTemplate"/> if one or more endpoints matching the address can be found, otherwise <c>null</c>.
|
||||
/// </returns>
|
||||
public static LinkGenerationTemplate GetTemplateByRouteValues(
|
||||
this LinkGenerator generator,
|
||||
string routeName,
|
||||
object values)
|
||||
{
|
||||
if (generator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(generator));
|
||||
}
|
||||
|
||||
var address = CreateAddress(httpContext: null, routeName, values);
|
||||
return generator.GetTemplateByAddress<RouteValuesAddress>(address);
|
||||
}
|
||||
|
||||
private static RouteValuesAddress CreateAddress(HttpContext httpContext, string routeName, object values)
|
||||
{
|
||||
return new RouteValuesAddress()
|
||||
|
|
|
|||
|
|
@ -459,7 +459,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
=> string.Format(CultureInfo.CurrentCulture, GetString("ConstraintMustBeStringOrConstraint"), p0, p1, p2);
|
||||
|
||||
/// <summary>
|
||||
/// Invalid constraint '{0}'. A constraint must be of type 'string', '{1}', or '{2}'.
|
||||
/// Invalid constraint '{0}'. A constraint must be of type 'string' or '{1}'.
|
||||
/// </summary>
|
||||
internal static string RoutePattern_InvalidConstraintReference
|
||||
{
|
||||
|
|
@ -514,6 +514,34 @@ namespace Microsoft.AspNetCore.Routing
|
|||
internal static string FormatRoutePattern_InvalidStringConstraintReference(object p0, object p1, object p2, object p3)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_InvalidStringConstraintReference"), p0, p1, p2, p3);
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints with endpoint name '{0}':
|
||||
/// </summary>
|
||||
internal static string DuplicateEndpointNameEntry
|
||||
{
|
||||
get => GetString("DuplicateEndpointNameEntry");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints with endpoint name '{0}':
|
||||
/// </summary>
|
||||
internal static string FormatDuplicateEndpointNameEntry(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("DuplicateEndpointNameEntry"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The following endpoints with a duplicate endpoint name were found.
|
||||
/// </summary>
|
||||
internal static string DuplicateEndpointNameHeader
|
||||
{
|
||||
get => GetString("DuplicateEndpointNameHeader");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The following endpoints with a duplicate endpoint name were found.
|
||||
/// </summary>
|
||||
internal static string FormatDuplicateEndpointNameHeader()
|
||||
=> GetString("DuplicateEndpointNameHeader");
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -225,4 +225,10 @@
|
|||
<data name="RoutePattern_InvalidStringConstraintReference" xml:space="preserve">
|
||||
<value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</value>
|
||||
</data>
|
||||
<data name="DuplicateEndpointNameEntry" xml:space="preserve">
|
||||
<value>Endpoints with endpoint name '{0}':</value>
|
||||
</data>
|
||||
<data name="DuplicateEndpointNameHeader" xml:space="preserve">
|
||||
<value>The following endpoints with a duplicate endpoint name were found.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -6,33 +6,27 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal class RouteValuesBasedEndpointFinder : IEndpointFinder<RouteValuesAddress>
|
||||
{
|
||||
private readonly CompositeEndpointDataSource _endpointDataSource;
|
||||
private readonly ObjectPool<UriBuildingContext> _objectPool;
|
||||
private readonly CompositeEndpointDataSource _dataSource;
|
||||
private LinkGenerationDecisionTree _allMatchesLinkGenerationTree;
|
||||
private IDictionary<string, List<OutboundMatchResult>> _namedMatchResults;
|
||||
|
||||
public RouteValuesBasedEndpointFinder(
|
||||
CompositeEndpointDataSource endpointDataSource,
|
||||
ObjectPool<UriBuildingContext> objectPool)
|
||||
public RouteValuesBasedEndpointFinder(CompositeEndpointDataSource dataSource)
|
||||
{
|
||||
_endpointDataSource = endpointDataSource;
|
||||
_objectPool = objectPool;
|
||||
_dataSource = dataSource;
|
||||
|
||||
// Build initial matches
|
||||
BuildOutboundMatches();
|
||||
|
||||
// Register for changes in endpoints
|
||||
Extensions.Primitives.ChangeToken.OnChange(
|
||||
_endpointDataSource.GetChangeToken,
|
||||
_dataSource.GetChangeToken,
|
||||
HandleChange);
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +62,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
// re-register the callback as the change token is one time use only and a new change token
|
||||
// is produced every time
|
||||
Extensions.Primitives.ChangeToken.OnChange(
|
||||
_endpointDataSource.GetChangeToken,
|
||||
_dataSource.GetChangeToken,
|
||||
HandleChange);
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +98,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var namedOutboundMatchResults = new Dictionary<string, List<OutboundMatchResult>>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var endpoints = _endpointDataSource.Endpoints.OfType<RouteEndpoint>();
|
||||
var endpoints = _dataSource.Endpoints.OfType<RouteEndpoint>();
|
||||
foreach (var endpoint in endpoints)
|
||||
{
|
||||
// Do not consider an endpoint for link generation if the following marker metadata is on it
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
// 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.Http;
|
||||
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class EndpointNameEndpointFinderTest
|
||||
{
|
||||
[Fact]
|
||||
public void EndpointFinder_Match_ReturnsMatchingEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||
"/a",
|
||||
metadata: new object[] { new EndpointNameMetadata("name1"), });
|
||||
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||
"/b",
|
||||
metadata: new object[] { new EndpointNameMetadata("name2"), });
|
||||
|
||||
var finder = CreateEndpointFinder(endpoint1, endpoint2);
|
||||
|
||||
// Act
|
||||
var endpoints = finder.FindEndpoints("name2");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
endpoints,
|
||||
e => Assert.Same(endpoint2, e));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointFinder_NoMatch_ReturnsEmptyCollection()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = EndpointFactory.CreateRouteEndpoint(
|
||||
"/a",
|
||||
metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), });
|
||||
|
||||
var finder = CreateEndpointFinder(endpoint);
|
||||
|
||||
// Act
|
||||
var endpoints = finder.FindEndpoints("name2");
|
||||
|
||||
// Assert
|
||||
Assert.Empty(endpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointFinder_NoMatch_CaseSensitive()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = EndpointFactory.CreateRouteEndpoint(
|
||||
"/a",
|
||||
metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), });
|
||||
|
||||
var finder = CreateEndpointFinder(endpoint);
|
||||
|
||||
// Act
|
||||
var endpoints = finder.FindEndpoints("NAME1");
|
||||
|
||||
// Assert
|
||||
Assert.Empty(endpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointFinder_UpdatesWhenDataSourceChanges()
|
||||
{
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||
"/a",
|
||||
metadata: new object[] { new EndpointNameMetadata("name1"), });
|
||||
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
|
||||
|
||||
// Act 1
|
||||
var finder = CreateEndpointFinder(dynamicDataSource);
|
||||
|
||||
// Assert 1
|
||||
var match = Assert.Single(finder.Entries);
|
||||
Assert.Same(endpoint1, match.Value.Single());
|
||||
|
||||
// Arrange 2
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||
"/b",
|
||||
metadata: new object[] { new EndpointNameMetadata("name2"), });
|
||||
|
||||
// Act 2
|
||||
// Trigger change
|
||||
dynamicDataSource.AddEndpoint(endpoint2);
|
||||
|
||||
// Assert 2
|
||||
Assert.Collection(
|
||||
finder.Entries.OrderBy(kvp => kvp.Key),
|
||||
(m) =>
|
||||
{
|
||||
Assert.Same(endpoint1, m.Value.Single());
|
||||
},
|
||||
(m) =>
|
||||
{
|
||||
Assert.Same(endpoint2, m.Value.Single());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointFinder_IgnoresEndpointsWithSuppressLinkGeneration()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = EndpointFactory.CreateRouteEndpoint(
|
||||
"/a",
|
||||
metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), });
|
||||
|
||||
// Act
|
||||
var finder = CreateEndpointFinder(endpoint);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(finder.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointFinder_IgnoresEndpointsWithoutEndpointName()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = EndpointFactory.CreateRouteEndpoint(
|
||||
"/a",
|
||||
metadata: new object[] { });
|
||||
|
||||
// Act
|
||||
var finder = CreateEndpointFinder(endpoint);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(finder.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointFinder_ThrowsExceptionForDuplicateEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
var endpoints = new Endpoint[]
|
||||
{
|
||||
EndpointFactory.CreateRouteEndpoint("/a", displayName: "a", metadata: new object[] { new EndpointNameMetadata("name1"), }),
|
||||
EndpointFactory.CreateRouteEndpoint("/b", displayName: "b", metadata: new object[] { new EndpointNameMetadata("name1"), }),
|
||||
EndpointFactory.CreateRouteEndpoint("/c", displayName: "c", metadata: new object[] { new EndpointNameMetadata("name1"), }),
|
||||
|
||||
//// Not a duplicate
|
||||
EndpointFactory.CreateRouteEndpoint("/d", displayName: "d", metadata: new object[] { new EndpointNameMetadata("NAME1"), }),
|
||||
|
||||
EndpointFactory.CreateRouteEndpoint("/e", displayName: "e", metadata: new object[] { new EndpointNameMetadata("name2"), }),
|
||||
EndpointFactory.CreateRouteEndpoint("/f", displayName: "f", metadata: new object[] { new EndpointNameMetadata("name2"), }),
|
||||
};
|
||||
|
||||
var finder = CreateEndpointFinder(endpoints);
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => finder.FindEndpoints("any name"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(@"The following endpoints with a duplicate endpoint name were found.
|
||||
|
||||
Endpoints with endpoint name 'name1':
|
||||
a
|
||||
b
|
||||
c
|
||||
|
||||
Endpoints with endpoint name 'name2':
|
||||
e
|
||||
f
|
||||
", ex.Message);
|
||||
}
|
||||
|
||||
private EndpointNameEndpointFinder CreateEndpointFinder(params Endpoint[] endpoints)
|
||||
{
|
||||
return CreateEndpointFinder(new DefaultEndpointDataSource(endpoints));
|
||||
}
|
||||
|
||||
private EndpointNameEndpointFinder CreateEndpointFinder(params EndpointDataSource[] dataSources)
|
||||
{
|
||||
return new EndpointNameEndpointFinder(new CompositeEndpointDataSource(dataSources));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
// 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 Microsoft.AspNetCore.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
// Integration tests for GetXyzByName. These are basic because important behavioral details
|
||||
// are covered elsewhere.
|
||||
//
|
||||
// Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests
|
||||
// and DefaultLinkGeneratorProcessTemplateTest
|
||||
//
|
||||
// Does not cover the EndpointNameEndpointFinder in detail. see EndpointNameEndpointFinderTest
|
||||
public class LinkGeneratorEndpointNameExtensionsTest : LinkGeneratorTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void GetPathByName_WithoutHttpContext_WithPathBaseAndFragment()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), });
|
||||
|
||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||
|
||||
var values = new { p = "In?dex", query = "some?query", };
|
||||
|
||||
// Act
|
||||
var path = linkGenerator.GetPathByName(
|
||||
endpointName: "name2",
|
||||
values,
|
||||
new PathString("/Foo/Bar?encodeme?"),
|
||||
new FragmentString("#Fragment?"),
|
||||
new LinkOptions() { AppendTrailingSlash = true, });
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByName_WithHttpContext_WithPathBaseAndFragment()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), });
|
||||
|
||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||
|
||||
var httpContext = CreateHttpContext();
|
||||
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
|
||||
|
||||
var values = new { p = "In?dex", query = "some?query", };
|
||||
|
||||
// Act
|
||||
var path = linkGenerator.GetPathByName(
|
||||
httpContext,
|
||||
endpointName: "name2",
|
||||
values,
|
||||
new FragmentString("#Fragment?"),
|
||||
new LinkOptions() { AppendTrailingSlash = true, });
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUriByRouteValues_WithoutHttpContext_WithPathBaseAndFragment()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), });
|
||||
|
||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||
|
||||
var values = new { p = "In?dex", query = "some?query", };
|
||||
|
||||
// Act
|
||||
var path = linkGenerator.GetUriByName(
|
||||
endpointName: "name2",
|
||||
values,
|
||||
"http",
|
||||
new HostString("example.com"),
|
||||
new PathString("/Foo/Bar?encodeme?"),
|
||||
new FragmentString("#Fragment?"),
|
||||
new LinkOptions() { AppendTrailingSlash = true, });
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUriByName_WithHttpContext_WithPathBaseAndFragment()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), });
|
||||
|
||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||
|
||||
var httpContext = CreateHttpContext();
|
||||
httpContext.Request.Scheme = "http";
|
||||
httpContext.Request.Host = new HostString("example.com");
|
||||
httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?");
|
||||
|
||||
var values = new { p = "In?dex", query = "some?query", };
|
||||
|
||||
// Act
|
||||
var uri = linkGenerator.GetUriByName(
|
||||
httpContext,
|
||||
endpointName: "name2",
|
||||
values,
|
||||
new FragmentString("#Fragment?"),
|
||||
new LinkOptions() { AppendTrailingSlash = true, });
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTemplateByName_CreatesTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), });
|
||||
|
||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||
|
||||
// Act
|
||||
var template = linkGenerator.GetTemplateByName(endpointName: "name2");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(template);
|
||||
Assert.Collection(
|
||||
Assert.IsType<DefaultLinkGenerationTemplate>(template).Endpoints.Cast<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText),
|
||||
e => Assert.Same(endpoint2, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// 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 Microsoft.AspNetCore.Http;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -96,7 +97,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUri_WithHttpContext_WithPathBaseAndFragment()
|
||||
public void GetUriByRouteValues_WithHttpContext_WithPathBaseAndFragment()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||
|
|
@ -124,5 +125,31 @@ namespace Microsoft.AspNetCore.Routing
|
|||
// Assert
|
||||
Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex/?query=some%3Fquery#Fragment?", uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTemplateByRouteValues_CreatesTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||
"{controller}/{action}/{id}",
|
||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
|
||||
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||
"{controller}/{action}/{id?}",
|
||||
metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) });
|
||||
|
||||
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||
|
||||
// Act
|
||||
var template = linkGenerator.GetTemplateByRouteValues(
|
||||
routeName: null,
|
||||
values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(template);
|
||||
Assert.Collection(
|
||||
Assert.IsType<DefaultLinkGenerationTemplate>(template).Endpoints.Cast<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText),
|
||||
e => Assert.Same(endpoint2, e),
|
||||
e => Assert.Same(endpoint1, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Routing.Internal;
|
|||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
|
|
@ -83,13 +82,9 @@ namespace Microsoft.AspNetCore.Routing
|
|||
// Arrange 1
|
||||
var endpoint1 = CreateEndpoint("/a");
|
||||
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
|
||||
var objectPoolProvider = new DefaultObjectPoolProvider();
|
||||
var objectPool = objectPoolProvider.Create(new UriBuilderContextPooledObjectPolicy());
|
||||
|
||||
// Act 1
|
||||
var finder = new CustomRouteValuesBasedEndpointFinder(
|
||||
new CompositeEndpointDataSource(new[] { dynamicDataSource }),
|
||||
objectPool);
|
||||
var finder = new CustomRouteValuesBasedEndpointFinder(new CompositeEndpointDataSource(new[] { dynamicDataSource }));
|
||||
|
||||
// Assert 1
|
||||
Assert.NotNull(finder.AllMatches);
|
||||
|
|
@ -218,14 +213,9 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return CreateEndpointFinder(new DefaultEndpointDataSource(endpoints));
|
||||
}
|
||||
|
||||
private CustomRouteValuesBasedEndpointFinder CreateEndpointFinder(params EndpointDataSource[] endpointDataSources)
|
||||
private CustomRouteValuesBasedEndpointFinder CreateEndpointFinder(params EndpointDataSource[] dataSources)
|
||||
{
|
||||
var objectPoolProvider = new DefaultObjectPoolProvider();
|
||||
var objectPool = objectPoolProvider.Create(new UriBuilderContextPooledObjectPolicy());
|
||||
|
||||
return new CustomRouteValuesBasedEndpointFinder(
|
||||
new CompositeEndpointDataSource(endpointDataSources),
|
||||
objectPool);
|
||||
return new CustomRouteValuesBasedEndpointFinder(new CompositeEndpointDataSource(dataSources));
|
||||
}
|
||||
|
||||
private RouteEndpoint CreateEndpoint(
|
||||
|
|
@ -256,10 +246,8 @@ namespace Microsoft.AspNetCore.Routing
|
|||
|
||||
private class CustomRouteValuesBasedEndpointFinder : RouteValuesBasedEndpointFinder
|
||||
{
|
||||
public CustomRouteValuesBasedEndpointFinder(
|
||||
CompositeEndpointDataSource endpointDataSource,
|
||||
ObjectPool<UriBuildingContext> objectPool)
|
||||
: base(endpointDataSource, objectPool)
|
||||
public CustomRouteValuesBasedEndpointFinder(CompositeEndpointDataSource dataSource)
|
||||
: base(dataSource)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue