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:
Ryan Nowak 2018-09-10 18:54:06 -07:00 committed by GitHub
commit 087e6d05ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1006 additions and 37 deletions

View File

@ -4,6 +4,7 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
namespace RoutingSample.Web
{

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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