Making UrlHelper's methods virtual

This commit is contained in:
Kiran Challa 2014-09-19 11:50:23 -07:00
parent 71964a813c
commit 43c7ddb9b7
17 changed files with 475 additions and 26 deletions

15
Mvc.sln
View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22013.1
VisualStudioVersion = 14.0.22115.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -78,6 +78,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FiltersWebSite", "test\WebS
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "XmlSerializerWebSite", "test\WebSites\XmlSerializerWebSite\XmlSerializerWebSite.kproj", "{96107AC0-18E2-474D-BAB4-2FFF2185FBCD}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "UrlHelperWebSite", "test\WebSites\UrlHelperWebSite\UrlHelperWebSite.kproj", "{A192E504-2881-41DC-90D1-B7F1DD1134E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -398,6 +400,16 @@ Global
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD}.Release|x86.ActiveCfg = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Debug|x86.ActiveCfg = Debug|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Any CPU.Build.0 = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -435,5 +447,6 @@ Global
{6A0B65CE-6B01-40D0-840D-EFF3680D1547} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{1976AC4A-FEA4-4587-A158-D9F79736D2B6} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{A192E504-2881-41DC-90D1-B7F1DD1134E8} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
EndGlobal

View File

@ -3,14 +3,52 @@
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Defines the contract for the helper to build URLs for ASP.NET MVC within an application.
/// </summary>
public interface IUrlHelper
{
/// <summary>
/// Generates a fully qualified or absolute URL for an action method by using the specified action name,
/// controller name, route values, protocol to use, host name and fragment.
/// </summary>
/// <param name="action">The name of the action method.</param>
/// <param name="controller">The name of the controller.</param>
/// <param name="values">An object that contains the parameters for a route.</param>
/// <param name="protocol">The protocol for the URL, such as "http" or "https".</param>
/// <param name="host">The host name for the URL.</param>
/// <param name="fragment">The fragment for the URL.</param>
/// <returns>The fully qualified or absolute URL to an action method.</returns>
string Action(string action, string controller, object values, string protocol, string host, string fragment);
/// <summary>
/// Converts a virtual (relative) path to an application absolute path.
/// </summary>
/// <remarks>
/// If the specified content path does not start with the tilde (~) character,
/// this method returns <paramref name="contentPath"/> unchanged.
/// </remarks>
/// <param name="contentPath">The virtual path of the content.</param>
/// <returns>The application absolute path.</returns>
string Content(string contentPath);
/// <summary>
/// Returns a value that indicates whether the URL is local.
/// </summary>
/// <param name="url">The URL.</param>
/// <returns>true if the URL is local; otherwise, false.</returns>
bool IsLocalUrl(string url);
/// <summary>
/// Generates a fully qualified or absolute URL for the specified route values by
/// using the specified route name, protocol to use, host name and fragment.
/// </summary>
/// <param name="routeName">The name of the route that is used to generate URL.</param>
/// <param name="values">An object that contains the parameters for a route.</param>
/// <param name="protocol">The protocol for the URL, such as "http" or "https".</param>
/// <param name="host">The host name for the URL.</param>
/// <param name="fragment">The fragment for the URL.</param>
/// <returns>The fully qualified or absolute URL.</returns>
string RouteUrl(string routeName, object values, string protocol, string host, string fragment);
}
}

View File

@ -11,6 +11,10 @@ using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// An implementation of <see cref="IUrlHelper"/> that contains methods to
/// build URLs for ASP.NET MVC within an application.
/// </summary>
public class UrlHelper : IUrlHelper
{
private readonly HttpContext _httpContext;
@ -18,6 +22,14 @@ namespace Microsoft.AspNet.Mvc
private readonly IDictionary<string, object> _ambientValues;
private readonly IActionSelector _actionSelector;
/// <summary>
/// Initializes a new instance of the <see cref="UrlHelper"/> class using the specified action context and action selector.
/// </summary>
/// <param name="contextAccessor">The <see cref="IContextAccessor{TContext}"/> to access the action context
/// of the current request.</param>
/// <param name="actionSelector">The <see cref="IActionSelector"/> to be used for verifying the correctness of
/// supplied parameters for a route.
/// </param>
public UrlHelper(IContextAccessor<ActionContext> contextAccessor, IActionSelector actionSelector)
{
_httpContext = contextAccessor.Value.HttpContext;
@ -26,12 +38,13 @@ namespace Microsoft.AspNet.Mvc
_actionSelector = actionSelector;
}
public string Action(
string action,
string controller,
object values,
string protocol,
string host,
/// <inheritdoc />
public virtual string Action(
string action,
string controller,
object values,
string protocol,
string host,
string fragment)
{
var valuesDictionary = TypeHelper.ObjectToDictionary(values);
@ -55,6 +68,7 @@ namespace Microsoft.AspNet.Mvc
return GenerateUrl(protocol, host, path, fragment);
}
/// <inheritdoc />
public bool IsLocalUrl(string url)
{
return
@ -67,9 +81,11 @@ namespace Microsoft.AspNet.Mvc
(url.Length > 1 && url[0] == '~' && url[1] == '/'));
}
public string RouteUrl(string routeName, object values, string protocol, string host, string fragment)
/// <inheritdoc />
public virtual string RouteUrl(string routeName, object values, string protocol, string host, string fragment)
{
var valuesDictionary = TypeHelper.ObjectToDictionary(values);
var path = GeneratePathFromRoute(routeName, valuesDictionary);
if (path == null)
{
@ -84,7 +100,14 @@ namespace Microsoft.AspNet.Mvc
return GeneratePathFromRoute(routeName: null, values: values);
}
private string GeneratePathFromRoute(string routeName, IDictionary<string, object> values)
/// <summary>
/// Generates the absolute path of the url for the specified route values by
/// using the specified route name.
/// </summary>
/// <param name="routeName">The name of the route that is used to generate the URL.</param>
/// <param name="values">A dictionary that contains the parameters for a route.</param>
/// <returns>The absolute path of the URL.</returns>
protected virtual string GeneratePathFromRoute(string routeName, IDictionary<string, object> values)
{
var context = new VirtualPathContext(_httpContext, _ambientValues, values, routeName);
var path = _router.GetVirtualPath(context);
@ -110,7 +133,8 @@ namespace Microsoft.AspNet.Mvc
}
}
public string Content([NotNull] string contentPath)
/// <inheritdoc />
public virtual string Content([NotNull] string contentPath)
{
return GenerateClientUrl(_httpContext.Request.PathBase, contentPath);
}

View File

@ -38,10 +38,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test
[InlineData(null, "~/Home/About", "/Home/About")]
[InlineData("/", "~/Home/About", "/Home/About")]
[InlineData("/", "~/", "/")]
[InlineData("", "~/Home/About", "/Home/About")]
[InlineData("/myapproot", "~/", "/myapproot/")]
[InlineData("", "~/Home/About", "/Home/About")]
[InlineData("/myapproot", "~/", "/myapproot/")]
[InlineData("/myapproot", "~/Content/bootstrap.css", "/myapproot/Content/bootstrap.css")]
public void Content_ReturnsAppRelativePath_WhenItStartsWithToken(string appRoot,
string contentPath,
string expectedPath)
@ -382,11 +381,11 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new
{
Action = "newaction",
Controller = "home2",
id = "someid"
},
{
Action = "newaction",
Controller = "home2",
id = "someid"
},
protocol: "https");
// Assert
@ -436,19 +435,74 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new
{
Action = "newaction",
Controller = "home2",
id = "someid"
});
{
Action = "newaction",
Controller = "home2",
id = "someid"
});
// Assert
Assert.Equal("/app/named/home2/newaction/someid", url);
}
[Fact]
public void UrlAction_RouteValuesAsDictionary_CaseSensitive()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// We're using a dictionary with a case-sensitive comparer and loading it with data
// using casings differently from the route. This should still successfully generate a link.
var dict = new Dictionary<string, object>();
var id = "suppliedid";
var isprint = "true";
dict["ID"] = id;
dict["isprint"] = isprint;
// Act
var url = urlHelper.Action(
action: "contact",
controller: "home",
values: dict);
// Assert
Assert.Equal(2, dict.Count);
Assert.Same(id, dict["ID"]);
Assert.Same(isprint, dict["isprint"]);
Assert.Equal("/app/home/contact/suppliedid?isprint=true", url);
}
[Fact]
public void UrlRouteUrl_RouteValuesAsDictionary_CaseSensitive()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// We're using a dictionary with a case-sensitive comparer and loading it with data
// using casings differently from the route. This should still successfully generate a link.
var dict = new Dictionary<string, object>();
var action = "contact";
var controller = "home";
var id = "suppliedid";
dict["ACTION"] = action;
dict["Controller"] = controller;
dict["ID"] = id;
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute", values: dict);
// Assert
Assert.Equal(3, dict.Count);
Assert.Same(action, dict["ACTION"]);
Assert.Same(controller, dict["Controller"]);
Assert.Same(id, dict["ID"]);
Assert.Equal("/app/named/home/contact/suppliedid", url);
}
private static HttpContext CreateHttpContext(string appRoot, ILoggerFactory factory = null)
{
if(factory == null)
if (factory == null)
{
factory = NullLoggerFactory.Instance;
}
@ -553,7 +607,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
serviceProviderMock.Setup(o => o.GetService(typeof(IInlineConstraintResolver)))
.Returns(new DefaultInlineConstraintResolver(serviceProviderMock.Object,
accessorMock.Object));
rt.ServiceProvider = serviceProviderMock.Object;
rt.MapRoute(string.Empty,
"{controller}/{action}/{id}",

View File

@ -0,0 +1,79 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
/// <summary>
/// The tests here verify the extensibility of <see cref="UrlHelper"/>.
///
/// Following are some of the scenarios exercised here:
/// 1. Based on configuration, generate Content urls pointing to local or a CDN server
/// 2. Based on configuration, generate lower case urls
/// </summary>
public class CustomUrlHelperTests
{
private readonly IServiceProvider _services = TestHelper.CreateServices("UrlHelperWebSite");
private readonly Action<IApplicationBuilder> _app = new UrlHelperWebSite.Startup().Configure;
private const string _cdnServerBaseUrl = "http://cdn.contoso.com";
[Fact]
public async Task CustomUrlHelper_GeneratesUrlFromController()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/Home/UrlContent");
string responseData = await response.Content.ReadAsStringAsync();
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(_cdnServerBaseUrl + "/bootstrap.min.css", responseData);
}
[Fact]
public async Task CustomUrlHelper_GeneratesUrlFromView()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/Home/Index");
string responseData = await response.Content.ReadAsStringAsync();
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(_cdnServerBaseUrl + "/bootstrap.min.css", responseData);
}
[Theory]
[InlineData("http://localhost/Home/LinkByUrlRouteUrl", "/api/simplepoco/10")]
[InlineData("http://localhost/Home/LinkByUrlAction", "/home/urlcontent")]
public async Task LowercaseUrls_LinkGeneration(string url, string expectedLink)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync(url);
string responseData = await response.Content.ReadAsStringAsync();
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedLink, responseData, ignoreCase: false);
}
}
}

View File

@ -9,7 +9,7 @@
"BasicWebSite": "",
"CompositeViewEngine": "",
"ConnegWebsite": "",
"FiltersWebSite": "",
"FiltersWebSite": "",
"FormatterWebSite": "",
"InlineConstraintsWebSite": "",
"Microsoft.AspNet.TestHost": "1.0.0-*",
@ -27,6 +27,7 @@
"RazorWebSite": "",
"ValueProvidersSite": "",
"XmlSerializerWebSite": "",
"UrlHelperWebSite": "",
"Xunit.KRunner": "1.0.0-*"
},
"commands": {

View File

@ -0,0 +1,13 @@
using System;
namespace UrlHelperWebSite
{
public class AppOptions
{
public bool ServeCDNContent { get; set; }
public string CDNServerBaseUrl { get; set; }
public bool GenerateLowercaseUrls { get; set; }
}
}

View File

@ -0,0 +1,5 @@
{
"ServeCDNContent": "true",
"CDNServerBaseUrl" : "http://cdn.contoso.com",
"GenerateLowercaseUrls": "true"
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
// 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 Microsoft.AspNet.Mvc;
namespace UrlHelperWebSite.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public string UrlContent()
{
return Url.Content("~/Bootstrap.min.css");
}
public string LinkByUrlAction()
{
return Url.Action("UrlContent", "Home", null);
}
public string LinkByUrlRouteUrl()
{
return Url.RouteUrl("SimplePocoApi", new { id = 10 });
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using Microsoft.AspNet.Mvc;
namespace UrlHelperWebSite.Controllers
{
[Route("api/[controller]/{id?}", Name = "SimplePocoApi")]
public class SimplePocoController
{
private readonly IUrlHelper _urlHelper;
public SimplePocoController(IUrlHelper urlHelper)
{
_urlHelper = urlHelper;
}
[HttpGet]
public string GetById(int id)
{
return "value:" + id;
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel;
namespace UrlHelperWebSite
{
/// <summary>
/// Following are some of the scenarios exercised here:
/// 1. Based on configuration, generate Content urls pointing to local or a CDN server
/// 2. Based on configuration, generate lower case urls
/// </summary>
public class CustomUrlHelper : UrlHelper
{
private readonly IOptionsAccessor<AppOptions> _appOptions;
private readonly HttpContext _httpContext;
public CustomUrlHelper(IContextAccessor<ActionContext> contextAccessor, IActionSelector actionSelector,
IOptionsAccessor<AppOptions> appOptions)
: base(contextAccessor, actionSelector)
{
_appOptions = appOptions;
_httpContext = contextAccessor.Value.HttpContext;
}
/// <summary>
/// Depending on config data, generates an absolute url pointing to a CDN server
/// or falls back to the default behavior
/// </summary>
/// <param name="contentPath"></param>
/// <returns></returns>
public override string Content(string contentPath)
{
if (_appOptions.Options.ServeCDNContent
&& contentPath.StartsWith("~/", StringComparison.Ordinal))
{
var segment = new PathString(contentPath.Substring(1));
return ConvertToLowercaseUrl(_appOptions.Options.CDNServerBaseUrl + segment);
}
return ConvertToLowercaseUrl(base.Content(contentPath));
}
public override string RouteUrl(string routeName, object values, string protocol, string host, string fragment)
{
return ConvertToLowercaseUrl(base.RouteUrl(routeName, values, protocol, host, fragment));
}
public override string Action(string action, string controller, object values, string protocol, string host, string fragment)
{
return ConvertToLowercaseUrl(base.Action(action, controller, values, protocol, host, fragment));
}
private string ConvertToLowercaseUrl(string url)
{
if (!string.IsNullOrEmpty(url)
&& _appOptions.Options.GenerateLowercaseUrls)
{
return url.ToLowerInvariant();
}
return url;
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.ConfigurationModel;
using Microsoft.Framework.DependencyInjection;
namespace UrlHelperWebSite
{
public class Startup
{
public void Configure(IApplicationBuilder app)
{
var configuration = app.GetTestConfiguration();
configuration.AddJsonFile("config.json");
// Set up application services
app.UseServices(services =>
{
services.SetupOptions<AppOptions>(optionsSetup =>
{
optionsSetup.ServeCDNContent = Convert.ToBoolean(configuration.Get("ServeCDNContent"));
optionsSetup.CDNServerBaseUrl = configuration.Get("CDNServerBaseUrl");
optionsSetup.GenerateLowercaseUrls = Convert.ToBoolean(configuration.Get("GenerateLowercaseUrls"));
});
// Add MVC services to the services container
services.AddMvc(configuration);
services.AddScoped<IUrlHelper, CustomUrlHelper>();
});
// Add MVC to the request pipeline
app.UseMvc(routes =>
{
routes.MapRoute("Default", "{controller=Home}/{action=Index}");
});
}
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>a192e504-2881-41dc-90d1-b7f1dd1134e8</ProjectGuid>
<OutputType>Web</OutputType>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Console'">
<DebuggerFlavor>ConsoleDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Web'">
<DebuggerFlavor>WebDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>35856</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1 @@
@Url.Content("~/Bootstrap.min.css")

View File

@ -0,0 +1 @@
@Url.Content("~/Bootstrap.min.css")

View File

@ -0,0 +1,13 @@
{
"dependencies": {
"Microsoft.AspNet.Mvc": "",
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Mvc.TestConfiguration": "",
"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*",
"Microsoft.Framework.OptionsModel": "1.0.0-*"
},
"frameworks": {
"aspnet50": { },
"aspnetcore50": { }
}
}