523 lines
22 KiB
C#
523 lines
22 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Http.Extensions;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
using Microsoft.AspNetCore.Routing.Internal;
|
|
using Microsoft.AspNetCore.Routing.Template;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.ObjectPool;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Microsoft.AspNetCore.Routing
|
|
{
|
|
internal sealed class DefaultLinkGenerator : LinkGenerator
|
|
{
|
|
private readonly ParameterPolicyFactory _parameterPolicyFactory;
|
|
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
|
|
private readonly ILogger<DefaultLinkGenerator> _logger;
|
|
private readonly IServiceProvider _serviceProvider;
|
|
|
|
// A LinkOptions object initialized with the values from RouteOptions
|
|
// Used when the user didn't specify something more global.
|
|
private readonly LinkOptions _globalLinkOptions;
|
|
|
|
// Caches TemplateBinder instances
|
|
private readonly DataSourceDependentCache<ConcurrentDictionary<RouteEndpoint, TemplateBinder>> _cache;
|
|
|
|
// Used to initialize TemplateBinder instances
|
|
private readonly Func<RouteEndpoint, TemplateBinder> _createTemplateBinder;
|
|
|
|
public DefaultLinkGenerator(
|
|
ParameterPolicyFactory parameterPolicyFactory,
|
|
CompositeEndpointDataSource dataSource,
|
|
ObjectPool<UriBuildingContext> uriBuildingContextPool,
|
|
IOptions<RouteOptions> routeOptions,
|
|
ILogger<DefaultLinkGenerator> logger,
|
|
IServiceProvider serviceProvider)
|
|
{
|
|
_parameterPolicyFactory = parameterPolicyFactory;
|
|
_uriBuildingContextPool = uriBuildingContextPool;
|
|
_logger = logger;
|
|
_serviceProvider = serviceProvider;
|
|
|
|
// We cache TemplateBinder instances per-Endpoint for performance, but we want to wipe out
|
|
// that cache is the endpoints change so that we don't allow unbounded memory growth.
|
|
_cache = new DataSourceDependentCache<ConcurrentDictionary<RouteEndpoint, TemplateBinder>>(dataSource, (_) =>
|
|
{
|
|
// We don't eagerly fill this cache because there's no real reason to. Unlike URL matching, we don't
|
|
// need to build a big data structure up front to be correct.
|
|
return new ConcurrentDictionary<RouteEndpoint, TemplateBinder>();
|
|
});
|
|
|
|
// Cached to avoid per-call allocation of a delegate on lookup.
|
|
_createTemplateBinder = CreateTemplateBinder;
|
|
|
|
_globalLinkOptions = new LinkOptions()
|
|
{
|
|
AppendTrailingSlash = routeOptions.Value.AppendTrailingSlash,
|
|
LowercaseQueryStrings = routeOptions.Value.LowercaseQueryStrings,
|
|
LowercaseUrls = routeOptions.Value.LowercaseUrls,
|
|
};
|
|
}
|
|
|
|
public override string GetPathByAddress<TAddress>(
|
|
HttpContext httpContext,
|
|
TAddress address,
|
|
RouteValueDictionary values,
|
|
RouteValueDictionary ambientValues = default,
|
|
PathString? pathBase = default,
|
|
FragmentString fragment = default,
|
|
LinkOptions options = null)
|
|
{
|
|
if (httpContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(httpContext));
|
|
}
|
|
|
|
var endpoints = GetEndpoints(address);
|
|
if (endpoints.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return GetPathByEndpoints(
|
|
endpoints,
|
|
values,
|
|
ambientValues,
|
|
pathBase ?? httpContext.Request.PathBase,
|
|
fragment,
|
|
options);
|
|
}
|
|
|
|
public override string GetPathByAddress<TAddress>(
|
|
TAddress address,
|
|
RouteValueDictionary values,
|
|
PathString pathBase = default,
|
|
FragmentString fragment = default,
|
|
LinkOptions options = null)
|
|
{
|
|
var endpoints = GetEndpoints(address);
|
|
if (endpoints.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return GetPathByEndpoints(
|
|
endpoints,
|
|
values,
|
|
ambientValues: null,
|
|
pathBase: pathBase,
|
|
fragment: fragment,
|
|
options: options);
|
|
}
|
|
|
|
public override string GetUriByAddress<TAddress>(
|
|
HttpContext httpContext,
|
|
TAddress address,
|
|
RouteValueDictionary values,
|
|
RouteValueDictionary ambientValues = default,
|
|
string scheme = default,
|
|
HostString? host = default,
|
|
PathString? pathBase = default,
|
|
FragmentString fragment = default,
|
|
LinkOptions options = null)
|
|
{
|
|
if (httpContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(httpContext));
|
|
}
|
|
|
|
var endpoints = GetEndpoints(address);
|
|
if (endpoints.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return GetUriByEndpoints(
|
|
endpoints,
|
|
values,
|
|
ambientValues,
|
|
scheme ?? httpContext.Request.Scheme,
|
|
host ?? httpContext.Request.Host,
|
|
pathBase ?? httpContext.Request.PathBase,
|
|
fragment,
|
|
options);
|
|
}
|
|
|
|
public override string GetUriByAddress<TAddress>(
|
|
TAddress address,
|
|
RouteValueDictionary values,
|
|
string scheme,
|
|
HostString host,
|
|
PathString pathBase = default,
|
|
FragmentString fragment = default,
|
|
LinkOptions options = null)
|
|
{
|
|
if (string.IsNullOrEmpty(scheme))
|
|
{
|
|
throw new ArgumentException("A scheme must be provided.", nameof(scheme));
|
|
}
|
|
|
|
if (!host.HasValue)
|
|
{
|
|
throw new ArgumentException("A host must be provided.", nameof(host));
|
|
}
|
|
|
|
var endpoints = GetEndpoints(address);
|
|
if (endpoints.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return GetUriByEndpoints(
|
|
endpoints,
|
|
values,
|
|
ambientValues: null,
|
|
scheme: scheme,
|
|
host: host,
|
|
pathBase: pathBase,
|
|
fragment: fragment,
|
|
options: options);
|
|
}
|
|
|
|
private List<RouteEndpoint> GetEndpoints<TAddress>(TAddress address)
|
|
{
|
|
var addressingScheme = _serviceProvider.GetRequiredService<IEndpointAddressScheme<TAddress>>();
|
|
var endpoints = addressingScheme.FindEndpoints(address).OfType<RouteEndpoint>().ToList();
|
|
|
|
if (endpoints.Count == 0)
|
|
{
|
|
Log.EndpointsNotFound(_logger, address);
|
|
}
|
|
else
|
|
{
|
|
Log.EndpointsFound(_logger, address, endpoints);
|
|
}
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
public string GetPathByEndpoints(
|
|
List<RouteEndpoint> endpoints,
|
|
RouteValueDictionary values,
|
|
RouteValueDictionary ambientValues,
|
|
PathString pathBase,
|
|
FragmentString fragment,
|
|
LinkOptions options)
|
|
{
|
|
for (var i = 0; i < endpoints.Count; i++)
|
|
{
|
|
var endpoint = endpoints[i];
|
|
if (TryProcessTemplate(
|
|
httpContext: null,
|
|
endpoint: endpoint,
|
|
values: values,
|
|
ambientValues: ambientValues,
|
|
options: options,
|
|
result: out var result))
|
|
{
|
|
var uri = UriHelper.BuildRelative(
|
|
pathBase,
|
|
result.path,
|
|
result.query,
|
|
fragment);
|
|
Log.LinkGenerationSucceeded(_logger, endpoints, uri);
|
|
return uri;
|
|
}
|
|
}
|
|
|
|
Log.LinkGenerationFailed(_logger, endpoints);
|
|
return null;
|
|
}
|
|
|
|
// Also called from DefaultLinkGenerationTemplate
|
|
public string GetUriByEndpoints(
|
|
List<RouteEndpoint> endpoints,
|
|
RouteValueDictionary values,
|
|
RouteValueDictionary ambientValues,
|
|
string scheme,
|
|
HostString host,
|
|
PathString pathBase,
|
|
FragmentString fragment,
|
|
LinkOptions options)
|
|
{
|
|
for (var i = 0; i < endpoints.Count; i++)
|
|
{
|
|
var endpoint = endpoints[i];
|
|
if (TryProcessTemplate(
|
|
httpContext: null,
|
|
endpoint: endpoint,
|
|
values: values,
|
|
ambientValues: ambientValues,
|
|
options: options,
|
|
result: out var result))
|
|
{
|
|
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;
|
|
}
|
|
|
|
private TemplateBinder CreateTemplateBinder(RouteEndpoint endpoint)
|
|
{
|
|
// Now create the constraints and parameter transformers from the pattern
|
|
var policies = new List<(string parameterName, IParameterPolicy policy)>();
|
|
foreach (var kvp in endpoint.RoutePattern.ParameterPolicies)
|
|
{
|
|
var parameterName = kvp.Key;
|
|
|
|
// It's possible that we don't have an actual route parameter, we need to support that case.
|
|
var parameter = endpoint.RoutePattern.GetParameter(parameterName);
|
|
|
|
// Use the first parameter transformer per parameter
|
|
var foundTransformer = false;
|
|
for (var i = 0; i < kvp.Value.Count; i++)
|
|
{
|
|
var parameterPolicy = _parameterPolicyFactory.Create(parameter, kvp.Value[i]);
|
|
if (!foundTransformer && parameterPolicy is IOutboundParameterTransformer parameterTransformer)
|
|
{
|
|
policies.Add((parameterName, parameterTransformer));
|
|
foundTransformer = true;
|
|
}
|
|
|
|
if (parameterPolicy is IRouteConstraint constraint)
|
|
{
|
|
policies.Add((parameterName, constraint));
|
|
}
|
|
}
|
|
}
|
|
|
|
return new TemplateBinder(
|
|
UrlEncoder.Default,
|
|
_uriBuildingContextPool,
|
|
endpoint.RoutePattern,
|
|
new RouteValueDictionary(endpoint.RoutePattern.Defaults),
|
|
endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>()?.RequiredValues.Keys,
|
|
policies);
|
|
}
|
|
|
|
// Internal for testing
|
|
internal TemplateBinder GetTemplateBinder(RouteEndpoint endpoint) => _cache.EnsureInitialized().GetOrAdd(endpoint, _createTemplateBinder);
|
|
|
|
// Internal for testing
|
|
internal bool TryProcessTemplate(
|
|
HttpContext httpContext,
|
|
RouteEndpoint endpoint,
|
|
RouteValueDictionary values,
|
|
RouteValueDictionary ambientValues,
|
|
LinkOptions options,
|
|
out (PathString path, QueryString query) result)
|
|
{
|
|
var templateBinder = GetTemplateBinder(endpoint);
|
|
|
|
var templateValuesResult = templateBinder.GetValues(ambientValues, values);
|
|
if (templateValuesResult == null)
|
|
{
|
|
// We're missing one of the required values for this route.
|
|
result = default;
|
|
Log.TemplateFailedRequiredValues(_logger, endpoint, ambientValues, values);
|
|
return false;
|
|
}
|
|
|
|
if (!templateBinder.TryProcessConstraints(httpContext, templateValuesResult.CombinedValues, out var parameterName, out var constraint))
|
|
{
|
|
result = default;
|
|
Log.TemplateFailedConstraint(_logger, endpoint, parameterName, constraint, templateValuesResult.CombinedValues);
|
|
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;
|
|
}
|
|
|
|
// Also called from DefaultLinkGenerationTemplate
|
|
public static RouteValueDictionary GetAmbientValues(HttpContext httpContext)
|
|
{
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|