aspnetcore/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs

333 lines
11 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.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matching;
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 class DefaultLinkGenerator : LinkGenerator
{
private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' };
private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
private readonly ILogger<DefaultLinkGenerator> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly RouteOptions _options;
public DefaultLinkGenerator(
MatchProcessorFactory matchProcessorFactory,
ObjectPool<UriBuildingContext> uriBuildingContextPool,
IOptions<RouteOptions> routeOptions,
ILogger<DefaultLinkGenerator> logger,
IServiceProvider serviceProvider)
{
_matchProcessorFactory = matchProcessorFactory;
_uriBuildingContextPool = uriBuildingContextPool;
_options = routeOptions.Value;
_logger = logger;
_serviceProvider = serviceProvider;
}
public override bool TryGetLink(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link)
{
return TryGetLinkByRouteValues(
httpContext,
routeName,
values,
options,
out link);
}
public override bool TryGetLinkByAddress<TAddress>(
HttpContext httpContext,
TAddress address,
object values,
LinkOptions options,
out string link)
{
return TryGetLinkByAddressInternal(
httpContext,
address,
explicitValues: values,
ambientValues: GetAmbientValues(httpContext),
options,
out link);
}
public override LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values)
{
var ambientValues = GetAmbientValues(httpContext);
var explicitValues = new RouteValueDictionary(values);
return GetTemplateInternal(
httpContext,
new RouteValuesAddress
{
RouteName = routeName,
ExplicitValues = explicitValues,
AmbientValues = ambientValues
},
ambientValues,
explicitValues,
values);
}
public override LinkGenerationTemplate GetTemplateByAddress<TAddress>(
HttpContext httpContext,
TAddress address)
{
return GetTemplateInternal(httpContext, address, values: null);
}
internal string MakeLink(
HttpContext httpContext,
MatcherEndpoint endpoint,
RouteValueDictionary ambientValues,
RouteValueDictionary explicitValues,
LinkOptions options)
{
var templateBinder = new TemplateBinder(
UrlEncoder.Default,
_uriBuildingContextPool,
endpoint.RoutePattern,
new RouteValueDictionary(endpoint.RoutePattern.Defaults));
var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
var templateValuesResult = templateBinder.GetValues(
ambientValues: ambientValues,
explicitValues: explicitValues,
requiredKeys: routeValuesAddressMetadata?.RequiredValues.Keys);
if (templateValuesResult == null)
{
// We're missing one of the required values for this route.
return null;
}
if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues))
{
return null;
}
var url = templateBinder.BindValues(templateValuesResult.AcceptedValues);
return Normalize(url, options);
}
private bool TryGetLinkByRouteValues(
HttpContext httpContext,
string routeName,
object values,
LinkOptions options,
out string link)
{
var ambientValues = GetAmbientValues(httpContext);
var address = new RouteValuesAddress
{
RouteName = routeName,
ExplicitValues = new RouteValueDictionary(values),
AmbientValues = ambientValues
};
return TryGetLinkByAddressInternal(
httpContext,
address,
explicitValues: values,
ambientValues: ambientValues,
options,
out link);
}
private bool TryGetLinkByAddressInternal<TAddress>(
HttpContext httpContext,
TAddress address,
object explicitValues,
RouteValueDictionary ambientValues,
LinkOptions options,
out string link)
{
link = null;
var endpoints = FindEndpoints(address);
if (endpoints == null)
{
return false;
}
foreach (var endpoint in endpoints)
{
link = MakeLink(
httpContext,
endpoint,
ambientValues,
new RouteValueDictionary(explicitValues),
options);
if (link != null)
{
return true;
}
}
return false;
}
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
HttpContext httpContext,
TAddress address,
object values)
{
var endpoints = FindEndpoints(address);
if (endpoints == null)
{
return null;
}
var ambientValues = GetAmbientValues(httpContext);
var explicitValues = new RouteValueDictionary(values);
return new DefaultLinkGenerationTemplate(
this,
endpoints,
httpContext,
explicitValues,
ambientValues);
}
private LinkGenerationTemplate GetTemplateInternal<TAddress>(
HttpContext httpContext,
TAddress address,
RouteValueDictionary ambientValues,
RouteValueDictionary explicitValues,
object values)
{
var endpoints = FindEndpoints(address);
if (endpoints == null)
{
return null;
}
return new DefaultLinkGenerationTemplate(
this,
endpoints,
httpContext,
explicitValues,
ambientValues);
}
private bool MatchesConstraints(
HttpContext httpContext,
MatcherEndpoint endpoint,
RouteValueDictionary routeValues)
{
if (routeValues == null)
{
throw new ArgumentNullException(nameof(routeValues));
}
foreach (var kvp in endpoint.RoutePattern.Constraints)
{
var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok
var constraintReferences = kvp.Value;
for (var i = 0; i < constraintReferences.Count; i++)
{
var constraintReference = constraintReferences[i];
var matchProcessor = _matchProcessorFactory.Create(parameter, constraintReference);
if (!matchProcessor.ProcessOutbound(httpContext, routeValues))
{
return false;
}
}
}
return true;
}
private string Normalize(string url, LinkOptions options)
{
var lowercaseUrls = options?.LowercaseUrls ?? _options.LowercaseUrls;
var lowercaseQueryStrings = options?.LowercaseQueryStrings ?? _options.LowercaseQueryStrings;
var appendTrailingSlash = options?.AppendTrailingSlash ?? _options.AppendTrailingSlash;
if (!string.IsNullOrEmpty(url) && (lowercaseUrls || appendTrailingSlash))
{
var indexOfSeparator = url.IndexOfAny(UrlQueryDelimiters);
var urlWithoutQueryString = url;
var queryString = string.Empty;
if (indexOfSeparator != -1)
{
urlWithoutQueryString = url.Substring(0, indexOfSeparator);
queryString = url.Substring(indexOfSeparator);
}
if (lowercaseUrls)
{
urlWithoutQueryString = urlWithoutQueryString.ToLowerInvariant();
}
if (lowercaseUrls && lowercaseQueryStrings)
{
queryString = queryString.ToLowerInvariant();
}
if (appendTrailingSlash && !urlWithoutQueryString.EndsWith("/", StringComparison.Ordinal))
{
urlWithoutQueryString += "/";
}
// queryString will contain the delimiter ? or # as the first character, so it's safe to append.
url = urlWithoutQueryString + queryString;
}
return url;
}
private RouteValueDictionary GetAmbientValues(HttpContext httpContext)
{
if (httpContext != null)
{
var feature = httpContext.Features.Get<IEndpointFeature>();
if (feature != null)
{
return feature.Values;
}
}
return new RouteValueDictionary();
}
private IEnumerable<MatcherEndpoint> FindEndpoints<TAddress>(TAddress address)
{
var finder = _serviceProvider.GetRequiredService<IEndpointFinder<TAddress>>();
var endpoints = finder.FindEndpoints(address);
if (endpoints == null)
{
return null;
}
var matcherEndpoints = endpoints.OfType<MatcherEndpoint>();
if (!matcherEndpoints.Any())
{
return null;
}
return matcherEndpoints;
}
}
}