diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs index c507434429..df5c1b5910 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs @@ -1,24 +1,13 @@ // 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.Collections.Generic; -using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing { public abstract class LinkGenerator { - public abstract bool TryGetLink( - HttpContext httpContext, - IEnumerable endpoints, - RouteValueDictionary explicitValues, - RouteValueDictionary ambientValues, - out string link); + public abstract bool TryGetLink(LinkGeneratorContext context, out string link); - public abstract string GetLink( - HttpContext httpContext, - IEnumerable endpoints, - RouteValueDictionary explicitValues, - RouteValueDictionary ambientValues); + public abstract string GetLink(LinkGeneratorContext context); } } diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGeneratorContext.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGeneratorContext.cs new file mode 100644 index 0000000000..c1e04c17e0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGeneratorContext.cs @@ -0,0 +1,36 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing +{ + public class LinkGeneratorContext + { + public HttpContext HttpContext { get; set; } + + public IEnumerable Endpoints { get; set; } + + public RouteValueDictionary ExplicitValues { get; set; } + + public RouteValueDictionary AmbientValues { get; set; } + + /// + /// Gets or sets a value indicating whether all generated paths URLs are lower-case. + /// Use to configure the behavior for query strings. + /// + public bool? LowercaseUrls { get; set; } + + /// + /// Gets or sets a value indicating whether a generated query strings are lower-case. + /// This property will be unless is also true. + /// + public bool? LowercaseQueryStrings { get; set; } + + /// + /// Gets or sets a value indicating whether a trailing slash should be appended to the generated URLs. + /// + public bool? AppendTrailingSlash { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index a52382e50d..73ccbe092a 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -2,7 +2,6 @@ // 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; @@ -11,32 +10,38 @@ using Microsoft.AspNetCore.Routing.Matchers; using Microsoft.AspNetCore.Routing.Template; 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 _uriBuildingContextPool; + private readonly RouteOptions _options; private readonly ILogger _logger; public DefaultLinkGenerator( MatchProcessorFactory matchProcessorFactory, ObjectPool uriBuildingContextPool, + IOptions routeOptions, ILogger logger) { _matchProcessorFactory = matchProcessorFactory; _uriBuildingContextPool = uriBuildingContextPool; + _options = routeOptions.Value; _logger = logger; } - public override string GetLink( - HttpContext httpContext, - IEnumerable endpoints, - RouteValueDictionary explicitValues, - RouteValueDictionary ambientValues) + public override string GetLink(LinkGeneratorContext context) { - if (TryGetLink(httpContext, endpoints, explicitValues, ambientValues, out var link)) + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (TryGetLink(context, out var link)) { return link; } @@ -44,21 +49,21 @@ namespace Microsoft.AspNetCore.Routing throw new InvalidOperationException("Could not find a matching endpoint to generate a link."); } - public override bool TryGetLink( - HttpContext httpContext, - IEnumerable endpoints, - RouteValueDictionary explicitValues, - RouteValueDictionary ambientValues, - out string link) + public override bool TryGetLink(LinkGeneratorContext context, out string link) { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + link = null; - if (endpoints == null) + if (context.Endpoints == null) { return false; } - var matcherEndpoints = endpoints.OfType(); + var matcherEndpoints = context.Endpoints.OfType(); if (!matcherEndpoints.Any()) { //todo:log here @@ -67,7 +72,7 @@ namespace Microsoft.AspNetCore.Routing foreach (var endpoint in matcherEndpoints) { - link = GetLink(httpContext, endpoint, explicitValues, ambientValues); + link = GetLink(endpoint, context); if (link != null) { return true; @@ -77,11 +82,7 @@ namespace Microsoft.AspNetCore.Routing return false; } - private string GetLink( - HttpContext httpContext, - MatcherEndpoint endpoint, - RouteValueDictionary explicitValues, - RouteValueDictionary ambientValues) + private string GetLink(MatcherEndpoint endpoint, LinkGeneratorContext context) { var templateBinder = new TemplateBinder( UrlEncoder.Default, @@ -89,28 +90,34 @@ namespace Microsoft.AspNetCore.Routing new RouteTemplate(endpoint.RoutePattern), new RouteValueDictionary(endpoint.RoutePattern.Defaults)); - var templateValuesResult = templateBinder.GetValues(ambientValues, explicitValues); + var templateValuesResult = templateBinder.GetValues( + ambientValues: context.AmbientValues, + values: context.ExplicitValues); if (templateValuesResult == null) { // We're missing one of the required values for this route. return null; } - if (!Match(httpContext, endpoint, templateValuesResult.CombinedValues)) + if (!MatchesConstraints(context.HttpContext, endpoint, templateValuesResult.CombinedValues)) { return null; } - return templateBinder.BindValues(templateValuesResult.AcceptedValues); + var url = templateBinder.BindValues(templateValuesResult.AcceptedValues); + return Normalize(context, url); } - private bool Match(HttpContext httpContext, MatcherEndpoint endpoint, RouteValueDictionary routeValues) + 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 @@ -128,5 +135,47 @@ namespace Microsoft.AspNetCore.Routing return true; } + + private string Normalize(LinkGeneratorContext context, string url) + { + var lowercaseUrls = context.LowercaseUrls.HasValue ? context.LowercaseUrls.Value : _options.LowercaseUrls; + var lowercaseQueryStrings = context.LowercaseQueryStrings.HasValue ? + context.LowercaseQueryStrings.Value : _options.LowercaseQueryStrings; + var appendTrailingSlash = context.AppendTrailingSlash.HasValue ? + context.AppendTrailingSlash.Value : _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; + } } } diff --git a/src/Microsoft.AspNetCore.Routing/RouteOptions.cs b/src/Microsoft.AspNetCore.Routing/RouteOptions.cs index b5c5bd544f..efabde03a1 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteOptions.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteOptions.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Routing /// /// Gets or sets a value indicating whether a generated query strings are lower-case. - /// This property will be unless is also true + /// This property will not be used unless is also true. /// public bool LowercaseQueryStrings { get; set; } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index ff6d0ac25c..0321c730f2 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.EndpointFinders; @@ -32,10 +31,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home", link); @@ -53,10 +54,12 @@ namespace Microsoft.AspNetCore.Routing // Act & Assert var exception = Assert.Throws( () => linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues)); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + })); Assert.Equal(expectedMessage, exception.Message); } @@ -70,10 +73,12 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -93,10 +98,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint1, endpoint2, endpoint3 }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint1, endpoint2, endpoint3 }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/10", link); @@ -114,10 +121,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint1, endpoint2, endpoint3 }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint1, endpoint2, endpoint3 }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index", link); @@ -135,10 +144,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", link); @@ -156,10 +167,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index?color=red&color=green&color=blue", link); @@ -177,10 +190,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index?items=10&items=20&items=30", link); @@ -198,10 +213,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index", link); @@ -219,10 +236,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", link); @@ -240,15 +259,259 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index", link); } + [Fact] + public void GetLink_GeneratesLowercaseUrl_SetOnRouteOptions() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index" }, + ambientValues: new { controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); + + // Assert + Assert.Equal("/home/index", link); + } + + [Fact] + public void GetLink_GeneratesLowercaseQueryString_SetOnRouteOptions() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator( + new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + ambientValues: new { controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); + + // Assert + Assert.Equal("/home/index?showstatus=true&info=detailed", link); + } + + [Fact] + public void GetLink_GeneratesLowercaseQueryString_OnlyIfLowercaseUrlIsTrue_SetOnRouteOptions() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator( + new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = true }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + ambientValues: new { controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); + + // Assert + Assert.Equal("/Home/Index?ShowStatus=True&INFO=DETAILED", link); + } + + [Fact] + public void GetLink_AppendsTrailingSlash_SetOnRouteOptions() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(new RouteOptions() { AppendTrailingSlash = true }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index" }, + ambientValues: new { controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); + + // Assert + Assert.Equal("/Home/Index/", link); + } + + [Fact] + public void GetLink_GeneratesLowercaseQueryStringAndTrailingSlash_SetOnRouteOptions() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator( + new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true, AppendTrailingSlash = true }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + ambientValues: new { controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); + + // Assert + Assert.Equal("/home/index/?showstatus=true&info=detailed", link); + } + + [Fact] + public void GetLink_LowercaseUrlSetToTrue_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "InDex" }, + ambientValues: new { controller = "HoMe" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues, + LowercaseUrls = false + }); + + // Assert + Assert.Equal("/HoMe/InDex", link); + } + + [Fact] + public void GetLink_LowercaseUrlSetToFalse_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = false }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "InDex" }, + ambientValues: new { controller = "HoMe" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues, + LowercaseUrls = true + }); + + // Assert + Assert.Equal("/home/index", link); + } + + [Fact] + public void GetLink_LowercaseUrlQueryStringsSetToTrue_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator( + new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + ambientValues: new { controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues, + LowercaseUrls = false, + LowercaseQueryStrings = false + }); + + // Assert + Assert.Equal("/Home/Index?ShowStatus=True&INFO=DETAILED", link); + } + + [Fact] + public void GetLink_LowercaseUrlQueryStringsSetToFalse_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator( + new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = false }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + ambientValues: new { controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues, + LowercaseUrls = true, + LowercaseQueryStrings = true + }); + + // Assert + Assert.Equal("/home/index?showstatus=true&info=detailed", link); + } + + [Fact] + public void GetLink_AppendTrailingSlashSetToFalse_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = CreateEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(new RouteOptions() { AppendTrailingSlash = false }); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index" }, + ambientValues: new { controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues, + AppendTrailingSlash = true + }); + + // Assert + Assert.Equal("/Home/Index/", link); + } + [Fact] public void RouteGenerationRejectsConstraints() { @@ -263,10 +526,13 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -287,10 +553,13 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -312,10 +581,13 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -336,10 +608,13 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -372,10 +647,13 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -406,10 +684,13 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -440,10 +721,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/slug/Home/Store", link); @@ -471,10 +755,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/slug/Shopping", link); @@ -503,10 +790,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/slug/Home/Store", link); @@ -528,10 +818,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/4", link); @@ -545,17 +838,20 @@ namespace Microsoft.AspNetCore.Routing var endpoint = CreateEndpoint( template: "Home/Index/{id}", defaults: new { controller = "Home", action = "Index" }, - constraints: new {id = "int"}); + constraints: new { id = "int" }); var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -576,10 +872,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/98", link); @@ -600,10 +899,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index", link); @@ -624,10 +926,13 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -649,10 +954,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/14", link); @@ -673,10 +981,13 @@ namespace Microsoft.AspNetCore.Routing // Act var canGenerateLink = linkGenerator.TryGetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues, + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, out var link); // Assert @@ -699,10 +1010,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/products", link); @@ -719,10 +1033,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/products", link); @@ -739,10 +1055,60 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); + + // Assert + Assert.Equal("/Home/Index", link); + } + + [Fact] + public void GetLink_OptionalParameter_ParameterPresentInValuesAndDefaults() + { + // Arrange + var endpoint = CreateEndpoint( + template: "{controller}/{action}/{name}", + defaults: new { name = "default-products" }); + var linkGenerator = CreateLinkGenerator(); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); + + // Assert + Assert.Equal("/Home/Index/products", link); + } + + [Fact] + public void GetLink_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() + { + // Arrange + var endpoint = CreateEndpoint( + template: "{controller}/{action}/{name}", + defaults: new { name = "products" }); + var linkGenerator = CreateLinkGenerator(); + var context = CreateRouteValuesContext( + suppliedValues: new { action = "Index", controller = "Home" }); + + // Act + var link = linkGenerator.GetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index", link); @@ -759,10 +1125,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/products?format=json", link); @@ -781,10 +1149,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/.products", link); @@ -802,10 +1173,13 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new DefaultHttpContext(), - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + HttpContext = new DefaultHttpContext(), + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index/", link); @@ -822,10 +1196,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/Home/Index", link); @@ -843,10 +1219,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/a/15/17", link); @@ -864,10 +1242,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/a/15/17", link); @@ -885,10 +1265,12 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }); // Assert Assert.Equal("/a", link); @@ -904,7 +1286,7 @@ namespace Microsoft.AspNetCore.Routing private MatcherEndpoint CreateEndpoint( string template, - object defaults = null, + object defaults = null, object constraints = null, int order = 0, EndpointMetadataCollection metadata = null) @@ -918,13 +1300,16 @@ namespace Microsoft.AspNetCore.Routing null); } - private LinkGenerator CreateLinkGenerator() + private LinkGenerator CreateLinkGenerator(RouteOptions routeOptions = null) { + routeOptions = routeOptions ?? new RouteOptions(); + var options = Options.Create(routeOptions); return new DefaultLinkGenerator( new DefaultMatchProcessorFactory( - Options.Create(new RouteOptions()), + options, Mock.Of()), new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), + options, NullLogger.Instance); } }