Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 43c7ddb

Browse files
committed
Making UrlHelper's methods virtual
1 parent 71964a8 commit 43c7ddb

File tree

17 files changed

+475
-26
lines changed

17 files changed

+475
-26
lines changed

Mvc.sln

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 14
4-
VisualStudioVersion = 14.0.22013.1
4+
VisualStudioVersion = 14.0.22115.0
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
77
EndProject
@@ -78,6 +78,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FiltersWebSite", "test\WebS
7878
EndProject
7979
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "XmlSerializerWebSite", "test\WebSites\XmlSerializerWebSite\XmlSerializerWebSite.kproj", "{96107AC0-18E2-474D-BAB4-2FFF2185FBCD}"
8080
EndProject
81+
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "UrlHelperWebSite", "test\WebSites\UrlHelperWebSite\UrlHelperWebSite.kproj", "{A192E504-2881-41DC-90D1-B7F1DD1134E8}"
82+
EndProject
8183
Global
8284
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8385
Debug|Any CPU = Debug|Any CPU
@@ -398,6 +400,16 @@ Global
398400
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
399401
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
400402
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD}.Release|x86.ActiveCfg = Release|Any CPU
403+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
404+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
405+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
406+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
407+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|x86.ActiveCfg = Debug|Any CPU
408+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
409+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Any CPU.Build.0 = Release|Any CPU
410+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
411+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.Build.0 = Release|Any CPU
412+
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|x86.ActiveCfg = Release|Any CPU
401413
EndGlobalSection
402414
GlobalSection(SolutionProperties) = preSolution
403415
HideSolutionNode = FALSE
@@ -435,5 +447,6 @@ Global
435447
{6A0B65CE-6B01-40D0-840D-EFF3680D1547} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
436448
{1976AC4A-FEA4-4587-A158-D9F79736D2B6} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
437449
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
450+
{A192E504-2881-41DC-90D1-B7F1DD1134E8} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
438451
EndGlobalSection
439452
EndGlobal

src/Microsoft.AspNet.Mvc.Core/IUrlHelper.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,52 @@
33

44
namespace Microsoft.AspNet.Mvc
55
{
6+
/// <summary>
7+
/// Defines the contract for the helper to build URLs for ASP.NET MVC within an application.
8+
/// </summary>
69
public interface IUrlHelper
710
{
11+
/// <summary>
12+
/// Generates a fully qualified or absolute URL for an action method by using the specified action name,
13+
/// controller name, route values, protocol to use, host name and fragment.
14+
/// </summary>
15+
/// <param name="action">The name of the action method.</param>
16+
/// <param name="controller">The name of the controller.</param>
17+
/// <param name="values">An object that contains the parameters for a route.</param>
18+
/// <param name="protocol">The protocol for the URL, such as "http" or "https".</param>
19+
/// <param name="host">The host name for the URL.</param>
20+
/// <param name="fragment">The fragment for the URL.</param>
21+
/// <returns>The fully qualified or absolute URL to an action method.</returns>
822
string Action(string action, string controller, object values, string protocol, string host, string fragment);
923

24+
/// <summary>
25+
/// Converts a virtual (relative) path to an application absolute path.
26+
/// </summary>
27+
/// <remarks>
28+
/// If the specified content path does not start with the tilde (~) character,
29+
/// this method returns <paramref name="contentPath"/> unchanged.
30+
/// </remarks>
31+
/// <param name="contentPath">The virtual path of the content.</param>
32+
/// <returns>The application absolute path.</returns>
1033
string Content(string contentPath);
11-
34+
35+
/// <summary>
36+
/// Returns a value that indicates whether the URL is local.
37+
/// </summary>
38+
/// <param name="url">The URL.</param>
39+
/// <returns>true if the URL is local; otherwise, false.</returns>
1240
bool IsLocalUrl(string url);
1341

42+
/// <summary>
43+
/// Generates a fully qualified or absolute URL for the specified route values by
44+
/// using the specified route name, protocol to use, host name and fragment.
45+
/// </summary>
46+
/// <param name="routeName">The name of the route that is used to generate URL.</param>
47+
/// <param name="values">An object that contains the parameters for a route.</param>
48+
/// <param name="protocol">The protocol for the URL, such as "http" or "https".</param>
49+
/// <param name="host">The host name for the URL.</param>
50+
/// <param name="fragment">The fragment for the URL.</param>
51+
/// <returns>The fully qualified or absolute URL.</returns>
1452
string RouteUrl(string routeName, object values, string protocol, string host, string fragment);
1553
}
1654
}

src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,25 @@
1111

1212
namespace Microsoft.AspNet.Mvc
1313
{
14+
/// <summary>
15+
/// An implementation of <see cref="IUrlHelper"/> that contains methods to
16+
/// build URLs for ASP.NET MVC within an application.
17+
/// </summary>
1418
public class UrlHelper : IUrlHelper
1519
{
1620
private readonly HttpContext _httpContext;
1721
private readonly IRouter _router;
1822
private readonly IDictionary<string, object> _ambientValues;
1923
private readonly IActionSelector _actionSelector;
2024

25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="UrlHelper"/> class using the specified action context and action selector.
27+
/// </summary>
28+
/// <param name="contextAccessor">The <see cref="IContextAccessor{TContext}"/> to access the action context
29+
/// of the current request.</param>
30+
/// <param name="actionSelector">The <see cref="IActionSelector"/> to be used for verifying the correctness of
31+
/// supplied parameters for a route.
32+
/// </param>
2133
public UrlHelper(IContextAccessor<ActionContext> contextAccessor, IActionSelector actionSelector)
2234
{
2335
_httpContext = contextAccessor.Value.HttpContext;
@@ -26,12 +38,13 @@ public UrlHelper(IContextAccessor<ActionContext> contextAccessor, IActionSelecto
2638
_actionSelector = actionSelector;
2739
}
2840

29-
public string Action(
30-
string action,
31-
string controller,
32-
object values,
33-
string protocol,
34-
string host,
41+
/// <inheritdoc />
42+
public virtual string Action(
43+
string action,
44+
string controller,
45+
object values,
46+
string protocol,
47+
string host,
3548
string fragment)
3649
{
3750
var valuesDictionary = TypeHelper.ObjectToDictionary(values);
@@ -55,6 +68,7 @@ public string Action(
5568
return GenerateUrl(protocol, host, path, fragment);
5669
}
5770

71+
/// <inheritdoc />
5872
public bool IsLocalUrl(string url)
5973
{
6074
return
@@ -67,9 +81,11 @@ public bool IsLocalUrl(string url)
6781
(url.Length > 1 && url[0] == '~' && url[1] == '/'));
6882
}
6983

70-
public string RouteUrl(string routeName, object values, string protocol, string host, string fragment)
84+
/// <inheritdoc />
85+
public virtual string RouteUrl(string routeName, object values, string protocol, string host, string fragment)
7186
{
7287
var valuesDictionary = TypeHelper.ObjectToDictionary(values);
88+
7389
var path = GeneratePathFromRoute(routeName, valuesDictionary);
7490
if (path == null)
7591
{
@@ -84,7 +100,14 @@ private string GeneratePathFromRoute(IDictionary<string, object> values)
84100
return GeneratePathFromRoute(routeName: null, values: values);
85101
}
86102

87-
private string GeneratePathFromRoute(string routeName, IDictionary<string, object> values)
103+
/// <summary>
104+
/// Generates the absolute path of the url for the specified route values by
105+
/// using the specified route name.
106+
/// </summary>
107+
/// <param name="routeName">The name of the route that is used to generate the URL.</param>
108+
/// <param name="values">A dictionary that contains the parameters for a route.</param>
109+
/// <returns>The absolute path of the URL.</returns>
110+
protected virtual string GeneratePathFromRoute(string routeName, IDictionary<string, object> values)
88111
{
89112
var context = new VirtualPathContext(_httpContext, _ambientValues, values, routeName);
90113
var path = _router.GetVirtualPath(context);
@@ -110,7 +133,8 @@ private string GeneratePathFromRoute(string routeName, IDictionary<string, objec
110133
}
111134
}
112135

113-
public string Content([NotNull] string contentPath)
136+
/// <inheritdoc />
137+
public virtual string Content([NotNull] string contentPath)
114138
{
115139
return GenerateClientUrl(_httpContext.Request.PathBase, contentPath);
116140
}

test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,9 @@ public void Content_ReturnsContentPath_WhenItDoesNotStartWithToken(string appRoo
3838
[InlineData(null, "~/Home/About", "/Home/About")]
3939
[InlineData("/", "~/Home/About", "/Home/About")]
4040
[InlineData("/", "~/", "/")]
41-
[InlineData("", "~/Home/About", "/Home/About")]
4241
[InlineData("/myapproot", "~/", "/myapproot/")]
4342
[InlineData("", "~/Home/About", "/Home/About")]
44-
[InlineData("/myapproot", "~/", "/myapproot/")]
43+
[InlineData("/myapproot", "~/Content/bootstrap.css", "/myapproot/Content/bootstrap.css")]
4544
public void Content_ReturnsAppRelativePath_WhenItStartsWithToken(string appRoot,
4645
string contentPath,
4746
string expectedPath)
@@ -382,11 +381,11 @@ public void RouteUrlWithProtocol()
382381
// Act
383382
var url = urlHelper.RouteUrl(routeName: "namedroute",
384383
values: new
385-
{
386-
Action = "newaction",
387-
Controller = "home2",
388-
id = "someid"
389-
},
384+
{
385+
Action = "newaction",
386+
Controller = "home2",
387+
id = "someid"
388+
},
390389
protocol: "https");
391390

392391
// Assert
@@ -436,19 +435,74 @@ public void RouteUrlWithRouteNameAndObjectProperties()
436435
// Act
437436
var url = urlHelper.RouteUrl(routeName: "namedroute",
438437
values: new
439-
{
440-
Action = "newaction",
441-
Controller = "home2",
442-
id = "someid"
443-
});
438+
{
439+
Action = "newaction",
440+
Controller = "home2",
441+
id = "someid"
442+
});
444443

445444
// Assert
446445
Assert.Equal("/app/named/home2/newaction/someid", url);
447446
}
448447

448+
[Fact]
449+
public void UrlAction_RouteValuesAsDictionary_CaseSensitive()
450+
{
451+
// Arrange
452+
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
453+
454+
// We're using a dictionary with a case-sensitive comparer and loading it with data
455+
// using casings differently from the route. This should still successfully generate a link.
456+
var dict = new Dictionary<string, object>();
457+
var id = "suppliedid";
458+
var isprint = "true";
459+
dict["ID"] = id;
460+
dict["isprint"] = isprint;
461+
462+
// Act
463+
var url = urlHelper.Action(
464+
action: "contact",
465+
controller: "home",
466+
values: dict);
467+
468+
// Assert
469+
Assert.Equal(2, dict.Count);
470+
Assert.Same(id, dict["ID"]);
471+
Assert.Same(isprint, dict["isprint"]);
472+
Assert.Equal("/app/home/contact/suppliedid?isprint=true", url);
473+
}
474+
475+
[Fact]
476+
public void UrlRouteUrl_RouteValuesAsDictionary_CaseSensitive()
477+
{
478+
// Arrange
479+
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
480+
481+
// We're using a dictionary with a case-sensitive comparer and loading it with data
482+
// using casings differently from the route. This should still successfully generate a link.
483+
var dict = new Dictionary<string, object>();
484+
var action = "contact";
485+
var controller = "home";
486+
var id = "suppliedid";
487+
488+
dict["ACTION"] = action;
489+
dict["Controller"] = controller;
490+
dict["ID"] = id;
491+
492+
// Act
493+
var url = urlHelper.RouteUrl(routeName: "namedroute", values: dict);
494+
495+
// Assert
496+
Assert.Equal(3, dict.Count);
497+
Assert.Same(action, dict["ACTION"]);
498+
Assert.Same(controller, dict["Controller"]);
499+
Assert.Same(id, dict["ID"]);
500+
Assert.Equal("/app/named/home/contact/suppliedid", url);
501+
}
502+
449503
private static HttpContext CreateHttpContext(string appRoot, ILoggerFactory factory = null)
450504
{
451-
if(factory == null)
505+
if (factory == null)
452506
{
453507
factory = NullLoggerFactory.Instance;
454508
}
@@ -553,7 +607,7 @@ private static IRouter GetRouter(string mockRouteName, string mockTemplateValue)
553607
serviceProviderMock.Setup(o => o.GetService(typeof(IInlineConstraintResolver)))
554608
.Returns(new DefaultInlineConstraintResolver(serviceProviderMock.Object,
555609
accessorMock.Object));
556-
610+
557611
rt.ServiceProvider = serviceProviderMock.Object;
558612
rt.MapRoute(string.Empty,
559613
"{controller}/{action}/{id}",
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using System.IO;
3+
using System.Net;
4+
using System.Net.Http;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNet.Builder;
8+
using Microsoft.AspNet.TestHost;
9+
using Xunit;
10+
11+
namespace Microsoft.AspNet.Mvc.FunctionalTests
12+
{
13+
/// <summary>
14+
/// The tests here verify the extensibility of <see cref="UrlHelper"/>.
15+
///
16+
/// Following are some of the scenarios exercised here:
17+
/// 1. Based on configuration, generate Content urls pointing to local or a CDN server
18+
/// 2. Based on configuration, generate lower case urls
19+
/// </summary>
20+
public class CustomUrlHelperTests
21+
{
22+
private readonly IServiceProvider _services = TestHelper.CreateServices("UrlHelperWebSite");
23+
private readonly Action<IApplicationBuilder> _app = new UrlHelperWebSite.Startup().Configure;
24+
private const string _cdnServerBaseUrl = "http://cdn.contoso.com";
25+
26+
[Fact]
27+
public async Task CustomUrlHelper_GeneratesUrlFromController()
28+
{
29+
// Arrange
30+
var server = TestServer.Create(_services, _app);
31+
var client = server.CreateClient();
32+
33+
// Act
34+
var response = await client.GetAsync("http://localhost/Home/UrlContent");
35+
36+
string responseData = await response.Content.ReadAsStringAsync();
37+
38+
//Assert
39+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
40+
Assert.Equal(_cdnServerBaseUrl + "/bootstrap.min.css", responseData);
41+
}
42+
43+
[Fact]
44+
public async Task CustomUrlHelper_GeneratesUrlFromView()
45+
{
46+
// Arrange
47+
var server = TestServer.Create(_services, _app);
48+
var client = server.CreateClient();
49+
50+
// Act
51+
var response = await client.GetAsync("http://localhost/Home/Index");
52+
53+
string responseData = await response.Content.ReadAsStringAsync();
54+
55+
//Assert
56+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
57+
Assert.Contains(_cdnServerBaseUrl + "/bootstrap.min.css", responseData);
58+
}
59+
60+
[Theory]
61+
[InlineData("http://localhost/Home/LinkByUrlRouteUrl", "/api/simplepoco/10")]
62+
[InlineData("http://localhost/Home/LinkByUrlAction", "/home/urlcontent")]
63+
public async Task LowercaseUrls_LinkGeneration(string url, string expectedLink)
64+
{
65+
// Arrange
66+
var server = TestServer.Create(_services, _app);
67+
var client = server.CreateClient();
68+
69+
// Act
70+
var response = await client.GetAsync(url);
71+
72+
string responseData = await response.Content.ReadAsStringAsync();
73+
74+
//Assert
75+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
76+
Assert.Equal(expectedLink, responseData, ignoreCase: false);
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)