[#732] Attribute Routing: Implement `~/` for overriding a prefix
This change allows a user to override a route prefix set using
[Route("...")] on the controller by providing a route template
on the action that starts with "~/" or "/". For example,
[HttpGet("~/...")] or [HttpGet("/...")]
If the user specifies a template in [Route] that starts with "~/"
or "/", we will just strip the prefix from the template and use
the remaining part of the template.
The reason to do this is that there's a reasonable extensibility
scenario where a user can implement a global prefix for routes as
a convention (using IReflectedApplicationModelConvention), and use
~/ to escape that prefix (just like we support with action-level routes).
This commit is contained in:
parent
bbdb2dfbf2
commit
87c430ae19
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Routing
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -15,6 +17,12 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// <param name="right">The right template.</param>
|
||||
/// <returns>A combined template.</returns>
|
||||
public static string Combine(string left, string right)
|
||||
{
|
||||
var result = CombineCore(left, right);
|
||||
return CleanTemplate(result);
|
||||
}
|
||||
|
||||
private static string CombineCore(string left, string right)
|
||||
{
|
||||
if (left == null && right == null)
|
||||
{
|
||||
|
|
@ -22,28 +30,68 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
}
|
||||
else if (left == null)
|
||||
{
|
||||
return right.Trim('/');
|
||||
return right;
|
||||
}
|
||||
else if (right == null)
|
||||
{
|
||||
return left.Trim('/');
|
||||
return left;
|
||||
}
|
||||
|
||||
// Neither is null
|
||||
var trimmedLeft = left.Trim('/');
|
||||
var trimmedRight = right.Trim('/');
|
||||
|
||||
if (trimmedLeft == string.Empty)
|
||||
if (right.StartsWith("~/", StringComparison.OrdinalIgnoreCase) ||
|
||||
right.StartsWith("/", StringComparison.OrdinalIgnoreCase) ||
|
||||
left.Equals("~/", StringComparison.OrdinalIgnoreCase) ||
|
||||
left.Equals("/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return trimmedRight;
|
||||
return right;
|
||||
}
|
||||
else if (trimmedRight == string.Empty)
|
||||
|
||||
if (left.EndsWith("/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return trimmedLeft;
|
||||
return left + right;
|
||||
}
|
||||
|
||||
// Both templates contain some text.
|
||||
return trimmedLeft + '/' + trimmedRight;
|
||||
return left + '/' + right;
|
||||
}
|
||||
|
||||
private static string CleanTemplate(string result)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// This is an invalid combined template, so we don't want to
|
||||
// accidentally clean it and produce a valid template. For that
|
||||
// reason we ignore the clean up process for it.
|
||||
if (result.Equals("//", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var startIndex = 0;
|
||||
if (result.StartsWith("/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
startIndex = 1;
|
||||
}
|
||||
else if (result.StartsWith("~/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
startIndex = 2;
|
||||
}
|
||||
|
||||
// We are in the case where the string is "/" or "~/"
|
||||
if (startIndex == result.Length)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var subStringLength = result.Length - startIndex;
|
||||
if (result.EndsWith("/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
subStringLength--;
|
||||
}
|
||||
|
||||
return result.Substring(startIndex, subStringLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,13 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
[InlineData("", "/", "")]
|
||||
[InlineData("/", "/", "")]
|
||||
[InlineData("/", "/", "")]
|
||||
[InlineData("~/", null, "")]
|
||||
[InlineData("~/", "", "")]
|
||||
[InlineData("~/", "/", "")]
|
||||
[InlineData("~/", "~/", "")]
|
||||
[InlineData(null, "~/", "")]
|
||||
[InlineData("", "~/", "")]
|
||||
[InlineData("/", "~/", "")]
|
||||
public void Combine_EmptyTemplates(string left, string right, string expected)
|
||||
{
|
||||
// Arrange & Act
|
||||
|
|
@ -30,10 +37,19 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
[Theory]
|
||||
[InlineData("home", null, "home")]
|
||||
[InlineData("home", "", "home")]
|
||||
[InlineData("/home/", "/", "home")]
|
||||
[InlineData("/home/", "/", "")]
|
||||
[InlineData("/home/", "~/", "")]
|
||||
[InlineData(null, "GetEmployees", "GetEmployees")]
|
||||
[InlineData("/", "GetEmployees", "GetEmployees")]
|
||||
[InlineData("~/", "Blog/Index/", "Blog/Index")]
|
||||
[InlineData("", "/GetEmployees/{id}/", "GetEmployees/{id}")]
|
||||
[InlineData("~/home", null, "home")]
|
||||
[InlineData("~/home", "", "home")]
|
||||
[InlineData("~/home", "/", "")]
|
||||
[InlineData(null, "~/home", "home")]
|
||||
[InlineData("", "~/home", "home")]
|
||||
[InlineData("", "~/home/", "home")]
|
||||
[InlineData("/", "~/home", "home")]
|
||||
public void Combine_OneTemplateHasValue(string left, string right, string expected)
|
||||
{
|
||||
// Arrange & Act
|
||||
|
|
@ -45,8 +61,11 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
|
||||
[Theory]
|
||||
[InlineData("home", "About", "home/About")]
|
||||
[InlineData("home/", "/About", "home/About")]
|
||||
[InlineData("home/", "/About", "About")]
|
||||
[InlineData("home/", "/About/", "About")]
|
||||
[InlineData("/home/{action}", "{id}", "home/{action}/{id}")]
|
||||
[InlineData("home", "~/index", "index")]
|
||||
[InlineData("home", "~/index/", "index")]
|
||||
public void Combine_BothTemplatesHasValue(string left, string right, string expected)
|
||||
{
|
||||
// Arrange & Act
|
||||
|
|
@ -55,5 +74,29 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
// Assert
|
||||
Assert.Equal(expected, combined);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("~~/", null, "~~")]
|
||||
[InlineData("~~/", "", "~~")]
|
||||
[InlineData("~~/", "//", "//")]
|
||||
[InlineData("~~/", "~~/", "~~/~~")]
|
||||
[InlineData("~~/", "home", "~~/home")]
|
||||
[InlineData("~~/", "home/", "~~/home")]
|
||||
[InlineData("//", null, "//")]
|
||||
[InlineData("//", "", "//")]
|
||||
[InlineData("//", "//", "//")]
|
||||
[InlineData("//", "~~/", "/~~")]
|
||||
[InlineData("//", "home", "/home")]
|
||||
[InlineData("//", "home/", "/home")]
|
||||
[InlineData("////", null, "//")]
|
||||
[InlineData("~~//", null, "~~/")]
|
||||
public void Combine_InvalidTemplates(string left, string right, string expected)
|
||||
{
|
||||
// Arrange & Act
|
||||
var combined = AttributeRouteTemplate.Combine(left, right);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, combined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,12 +143,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/Store/Shop/Products");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
var body = await response.ReadBodyAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("/Store/Shop/Products", result.ExpectedUrls);
|
||||
Assert.Equal("Store", result.Controller);
|
||||
Assert.Equal("ListProducts", result.Action);
|
||||
|
|
@ -180,13 +181,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/Home/About");
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
var body = await response.ReadBodyAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("/Home/About", result.ExpectedUrls);
|
||||
Assert.Equal("Store", result.Controller);
|
||||
Assert.Equal("About", result.Action);
|
||||
|
|
@ -210,13 +211,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/Blog/Edit/5");
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
var body = await response.ReadBodyAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("/Blog/Edit/5", result.ExpectedUrls);
|
||||
Assert.Equal("Blog", result.Controller);
|
||||
Assert.Equal("Edit", result.Action);
|
||||
|
|
@ -245,9 +246,10 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/api/Employee");
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
var body = await response.ReadBodyAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
|
|
@ -267,13 +269,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/api/Employee/5/Boss");
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
var body = await response.ReadBodyAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("/api/Employee/5/Boss", result.ExpectedUrls);
|
||||
Assert.Equal("Employee", result.Controller);
|
||||
Assert.Equal("GetBoss", result.Action);
|
||||
|
|
@ -283,6 +285,31 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
result.RouteValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_ActionLevelRouteWithTildeSlash_OverridesControllerLevelRoute()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.Handler;
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/Manager/5");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
var body = await response.ReadBodyAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Contains("/Manager/5", result.ExpectedUrls);
|
||||
Assert.Equal("Employee", result.Controller);
|
||||
Assert.Equal("GetManager", result.Action);
|
||||
|
||||
Assert.Contains(
|
||||
new KeyValuePair<string, object>("id", "5"),
|
||||
result.RouteValues);
|
||||
}
|
||||
|
||||
// See TestResponseGenerator in RoutingWebSite for the code that generates this data.
|
||||
private class RoutingResult
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,5 +31,11 @@ namespace RoutingWebSite
|
|||
{
|
||||
return _generator.Generate("/api/Employee/" + id + "/Boss");
|
||||
}
|
||||
|
||||
[HttpGet("~/Manager/{id}")]
|
||||
public IActionResult GetManager(int id)
|
||||
{
|
||||
return _generator.Generate("/Manager/" + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue