// 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 Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Routing { public class DefaultLinkGeneratorTest { [Fact] public void GetLink_Success() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress(new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home", link); } [Fact] public void GetLink_Fail_ThrowsException() { // Arrange var expectedMessage = "Could not find a matching endpoint to generate a link."; var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress(new { controller = "Home" }); // Act & Assert var exception = Assert.Throws( () => linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues })); Assert.Equal(expectedMessage, exception.Message); } [Fact] public void TryGetLink_Fail() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress(new { controller = "Home" }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.False(canGenerateLink); Assert.Null(link); } [Fact] public void GetLink_MultipleEndpoints_Success() { // Arrange var endpoint1 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{id?}"); var endpoint2 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var endpoint3 = EndpointFactory.CreateMatcherEndpoint("{controller}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress(new { controller = "Home", action = "Index", id = "10" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint1, endpoint2, endpoint3 }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/10", link); } [Fact] public void GetLink_MultipleEndpoints_Success2() { // Arrange var endpoint1 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{id}"); var endpoint2 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var endpoint3 = EndpointFactory.CreateMatcherEndpoint("{controller}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress(new { controller = "Home", action = "Index" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint1, endpoint2, endpoint3 }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index", link); } [Fact] public void GetLink_EncodesValues() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { name = "name with %special #characters" }, ambientValues: new { controller = "Home", action = "Index" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", link); } [Fact] public void GetLink_ForListOfStrings() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( new { color = new List { "red", "green", "blue" } }, new { controller = "Home", action = "Index" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index?color=red&color=green&color=blue", link); } [Fact] public void GetLink_ForListOfInts() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( new { items = new List { 10, 20, 30 } }, new { controller = "Home", action = "Index" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index?items=10&items=20&items=30", link); } [Fact] public void GetLink_ForList_Empty() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( new { color = new List { } }, new { controller = "Home", action = "Index" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index", link); } [Fact] public void GetLink_ForList_StringWorkaround() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }, new { controller = "Home", action = "Index" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", link); } [Fact] public void GetLink_Success_AmbientValues() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index", link); } [Fact] public void GetLink_GeneratesLowercaseUrl_SetOnRouteOptions() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/home/index", link); } [Fact] public void GetLink_GeneratesLowercaseQueryString_SetOnRouteOptions() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/home/index?showstatus=true&info=detailed", link); } [Fact] public void GetLink_GeneratesLowercaseQueryString_OnlyIfLowercaseUrlIsTrue_SetOnRouteOptions() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = true }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index?ShowStatus=True&INFO=DETAILED", link); } [Fact] public void GetLink_AppendsTrailingSlash_SetOnRouteOptions() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { AppendTrailingSlash = true }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/", link); } [Fact] public void GetLink_GeneratesLowercaseQueryStringAndTrailingSlash_SetOnRouteOptions() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true, AppendTrailingSlash = true }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/home/index/?showstatus=true&info=detailed", link); } [Fact] public void GetLink_LowercaseUrlSetToTrue_OnRouteOptions_OverridenByCallsiteValue() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }); var address = CreateRouteValuesAddress( explicitValues: new { action = "InDex" }, ambientValues: new { controller = "HoMe" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues, LowercaseUrls = false }); // Assert Assert.Equal("/HoMe/InDex", link); } [Fact] public void GetLink_LowercaseUrlSetToFalse_OnRouteOptions_OverridenByCallsiteValue() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = false }); var address = CreateRouteValuesAddress( explicitValues: new { action = "InDex" }, ambientValues: new { controller = "HoMe" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues, LowercaseUrls = true }); // Assert Assert.Equal("/home/index", link); } [Fact] public void GetLink_LowercaseUrlQueryStringsSetToTrue_OnRouteOptions_OverridenByCallsiteValue() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.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 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = false }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.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 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { AppendTrailingSlash = false }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index" }, ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues, AppendTrailingSlash = true }); // Assert Assert.Equal("/Home/Index/", link); } [Fact] public void RouteGenerationRejectsConstraints() { // Arrange var address = CreateRouteValuesAddress(new { p1 = "abcd" }); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = "\\d{4}" }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.False(canGenerateLink); } [Fact] public void RouteGenerationAcceptsConstraints() { // Arrange var address = CreateRouteValuesAddress(new { p1 = "hello", p2 = "1234" }); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = new RegexRouteConstraint("\\d{4}"), }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.True(canGenerateLink); Assert.Equal("/hello/1234", link); } [Fact] public void RouteWithCatchAllRejectsConstraints() { // Arrange var address = CreateRouteValuesAddress(new { p1 = "abcd" }); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{*p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = new RegexRouteConstraint("\\d{4}") }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.False(canGenerateLink); } [Fact] public void RouteWithCatchAllAcceptsConstraints() { // Arrange var address = CreateRouteValuesAddress(new { p1 = "hello", p2 = "1234" }); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{*p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = new RegexRouteConstraint("\\d{4}") }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.True(canGenerateLink); Assert.Equal("/hello/1234", link); } [Fact] public void GetLinkWithNonParameterConstraintReturnsUrlWithoutQueryString() { // Arrange var address = CreateRouteValuesAddress(new { p1 = "hello", p2 = "1234" }); var linkGenerator = CreateLinkGenerator(); var target = new Mock(); target .Setup( e => e.Match( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(true) .Verifiable(); var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = target.Object }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.True(canGenerateLink); Assert.Equal("/hello/1234", link); target.VerifyAll(); } // Any ambient values from the current request should be visible to constraint, even // if they have nothing to do with the route generating a link [Fact] public void GetLink_ConstraintsSeeAmbientValues() { // Arrange var constraint = new CapturingConstraint(); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store" }, constraints: new { c = constraint }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Store" }, ambientValues: new { controller = "Home", action = "Blog", extra = "42" }); var expectedValues = new RouteValueDictionary( new { controller = "Home", action = "Store", extra = "42" }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.True(canGenerateLink); Assert.Equal("/slug/Home/Store", link); Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); } // Non-parameter default values from the routing generating a link are not in the 'values' // collection when constraints are processed. [Fact] public void GetLink_ConstraintsDontSeeDefaults_WhenTheyArentParameters() { // Arrange var constraint = new CapturingConstraint(); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store", otherthing = "17" }, constraints: new { c = constraint }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Store" }, ambientValues: new { controller = "Home", action = "Blog" }); var expectedValues = new RouteValueDictionary( new { controller = "Home", action = "Store" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/slug/Home/Store", link); Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); } // Default values are visible to the constraint when they are used to fill a parameter. [Fact] public void GetLink_ConstraintsSeesDefault_WhenThereItsAParamter() { // Arrange var constraint = new CapturingConstraint(); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/{controller}/{action}", defaults: new { action = "Index" }, constraints: new { c = constraint, }); var address = CreateRouteValuesAddress( explicitValues: new { controller = "Shopping" }, ambientValues: new { controller = "Home", action = "Blog" }); var expectedValues = new RouteValueDictionary( new { controller = "Shopping", action = "Index" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/slug/Shopping", link); Assert.Equal(expectedValues, constraint.Values); } // Default values from the routing generating a link are in the 'values' collection when // constraints are processed - IFF they are specified as values or ambient values. [Fact] public void GetLink_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient() { // Arrange var constraint = new CapturingConstraint(); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }, constraints: new { c = constraint, }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Store", thirdthing = "13" }, ambientValues: new { controller = "Home", action = "Blog", otherthing = "17" }); var expectedValues = new RouteValueDictionary( new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/slug/Home/Store", link); Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); } [Fact] public void GetLink_InlineConstraints_Success() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", id = 4 }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/4", link); } [Fact] public void GetLink_InlineConstraints_NonMatchingvalue() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.False(canGenerateLink); } [Fact] public void GetLink_InlineConstraints_OptionalParameter_ValuePresent() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", id = 98 }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/98", link); } [Fact] public void GetLink_InlineConstraints_OptionalParameter_ValueNotPresent() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index", link); } [Fact] public void GetLink_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.False(canGenerateLink); } [Fact] public void GetLink_InlineConstraints_MultipleInlineConstraints() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int:range(1,20)}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", id = 14 }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/14", link); } [Fact] public void GetLink_InlineConstraints_CompositeInlineConstraint_Fails() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int:range(1,20)}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", id = 50 }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.False(canGenerateLink); } [Fact] public void GetLink_InlineConstraints_CompositeConstraint_FromConstructor() { // Arrange var constraint = new MaxLengthRouteConstraint(20); var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{name}", defaults: new { controller = "Home", action = "Index" }, constraints: new { name = constraint }); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", name = "products" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/products", link); } [Fact] public void GetLink_OptionalParameter_ParameterPresentInValues() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", name = "products" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/products", link); } [Fact] public void GetLink_OptionalParameter_ParameterNotPresentInValues() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index", link); } [Fact] public void GetLink_OptionalParameter_ParameterPresentInValuesAndDefaults() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/{name}", defaults: new { name = "default-products" }); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", name = "products" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/products", link); } [Fact] public void GetLink_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/{name}", defaults: new { name = "products" }); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index", link); } [Fact] public void GetLink_ParameterNotPresentInTemplate_PresentInValues() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", name = "products", format = "json" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/products?format=json", link); } [Fact] public void GetLink_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/.{name?}"); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home", name = "products" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/.products", link); } [Fact] public void GetLink_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/.{name?}"); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { HttpContext = new DefaultHttpContext(), Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index/", link); } [Fact] public void GetLink_OptionalParameter_InSimpleSegment() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/Home/Index", link); } [Fact] public void GetLink_TwoOptionalParameters_OneValueFromAmbientValues() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}/{d?}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { }, ambientValues: new { c = "17" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/a/15/17", link); } [Fact] public void GetLink_OptionalParameterAfterDefault_OneValueFromAmbientValues() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { }, ambientValues: new { c = "17" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/a/15/17", link); } [Fact] public void GetLink_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues() { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}/{d?}"); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { }, ambientValues: new { d = "17" }); // Act var link = linkGenerator.GetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }); // Assert Assert.Equal("/a", link); } public static TheoryData DoesNotDiscardAmbientValuesData { get { // - ambient values // - explicit values // - required values // - defaults return new TheoryData { // link to same action on same controller { new { controller = "Products", action = "Edit", id = 10 }, new { controller = "Products", action = "Edit" }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } }, // link to same action on same controller - ignoring case { new { controller = "ProDUcts", action = "EDit", id = 10 }, new { controller = "ProDUcts", action = "EDit" }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } }, // link to same action and same controller on same area { new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, new { area = "Admin", controller = "Products", action = "Edit" }, new { area = "Admin", controller = "Products", action = "Edit", page = (string)null }, new { area = "Admin", controller = "Products", action = "Edit", page = (string)null } }, // link to same action and same controller on same area { new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, new { controller = "Products", action = "Edit" }, new { area = "Admin", controller = "Products", action = "Edit", page = (string)null }, new { area = "Admin", controller = "Products", action = "Edit", page = (string)null } }, // link to same action and same controller { new { controller = "Products", action = "Edit", id = 10 }, new { controller = "Products", action = "Edit" }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } }, { new { controller = "Products", action = "Edit", id = 10 }, new { controller = "Products", action = "Edit" }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } }, { new { controller = "Products", action = "Edit", id = 10 }, new { controller = "Products", action = "Edit" }, new { area = "", controller = "Products", action = "Edit", page = "" }, new { area = "", controller = "Products", action = "Edit", page = "" } }, // link to same page { new { page = "Products/Edit", id = 10 }, new { page = "Products/Edit" }, new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" }, new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } }, }; } } [Theory] [MemberData(nameof(DoesNotDiscardAmbientValuesData))] public void TryGetLink_DoesNotDiscardAmbientValues_IfAllRequiredKeysMatch( object ambientValues, object explicitValues, object requiredValues, object defaults) { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint( "Products/Edit/{id}", requiredValues: requiredValues, defaults: defaults); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: explicitValues, ambientValues: ambientValues); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.True(canGenerateLink); Assert.Equal("/Products/Edit/10", link); } [Fact] public void TryGetLink_DoesNotDiscardAmbientValues_IfAllRequiredValuesMatch_ForGenericKeys() { // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint( "Products/Edit/{id}", requiredValues: new { c = "Products", a = "Edit" }, defaults: new { c = "Products", a = "Edit" }); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { c = "Products", a = "Edit" }, ambientValues: new { c = "Products", a = "Edit", id = 10 }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.True(canGenerateLink); Assert.Equal("/Products/Edit/10", link); } [Fact] public void TryGetLink_DiscardsAmbientValues_ForGenericKeys() { // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint( "Products/Edit/{id}", requiredValues: new { c = "Products", a = "Edit" }, defaults: new { c = "Products", a = "Edit" }); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: new { c = "Products", a = "List" }, ambientValues: new { c = "Products", a = "Edit", id = 10 }); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.False(canGenerateLink); Assert.Null(link); } public static TheoryData DiscardAmbientValuesData { get { // - ambient values // - explicit values // - required values // - defaults return new TheoryData { // link to different action on same controller { new { controller = "Products", action = "Edit", id = 10 }, new { controller = "Products", action = "List" }, new { area = (string)null, controller = "Products", action = "List", page = (string)null }, new { area = (string)null, controller = "Products", action = "List", page = (string)null } }, // link to different action on same controller and same area { new { area = "Customer", controller = "Products", action = "Edit", id = 10 }, new { area = "Customer", controller = "Products", action = "List" }, new { area = "Customer", controller = "Products", action = "List", page = (string)null }, new { area = "Customer", controller = "Products", action = "List", page = (string)null } }, // link from one area to a different one { new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, new { area = "Consumer", controller = "Products", action = "Edit" }, new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null }, new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null } }, // link from non-area to a area one { new { controller = "Products", action = "Edit", id = 10 }, new { area = "Consumer", controller = "Products", action = "Edit" }, new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null }, new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null } }, // link from area to a non-area based action { new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, new { area = "", controller = "Products", action = "Edit" }, new { area = "", controller = "Products", action = "Edit", page = (string)null }, new { area = "", controller = "Products", action = "Edit", page = (string)null } }, // link from controller-action to a page { new { controller = "Products", action = "Edit", id = 10 }, new { page = "Products/Edit" }, new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit"}, new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit"} }, // link from a page to controller-action { new { page = "Products/Edit", id = 10 }, new { controller = "Products", action = "Edit" }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } }, // link from one page to a different page { new { page = "Products/Details", id = 10 }, new { page = "Products/Edit" }, new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" }, new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } }, }; } } [Theory] [MemberData(nameof(DiscardAmbientValuesData))] public void TryGetLink_DiscardsAmbientValues_IfAnyAmbientValue_IsDifferentThan_EndpointRequiredValues( object ambientValues, object explicitValues, object requiredValues, object defaults) { // Linking to a different action on the same controller // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint( "Products/Edit/{id}", requiredValues: requiredValues, defaults: defaults); var linkGenerator = CreateLinkGenerator(); var address = CreateRouteValuesAddress( explicitValues: explicitValues, ambientValues: ambientValues); // Act var canGenerateLink = linkGenerator.TryGetLink( new LinkGeneratorContext { Endpoints = new[] { endpoint }, ExplicitValues = address.ExplicitValues, AmbientValues = address.AmbientValues }, out var link); // Assert Assert.False(canGenerateLink); Assert.Null(link); } private RouteValuesAddress CreateRouteValuesAddress( object explicitValues, object ambientValues = null) { var address = new RouteValuesAddress(); address.ExplicitValues = new RouteValueDictionary(explicitValues); address.AmbientValues = new RouteValueDictionary(ambientValues); return address; } private LinkGenerator CreateLinkGenerator(RouteOptions routeOptions = null) { routeOptions = routeOptions ?? new RouteOptions(); var options = Options.Create(routeOptions); return new DefaultLinkGenerator( new DefaultMatchProcessorFactory( options, Mock.Of()), new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), options, NullLogger.Instance); } } }