经常有朋友问我,如何对域名作URL Routing,他们可能希望根据域名(或自域名)来获得一些值,最终影响Controller,Action或某些参数的选择。之前我只是简单地说“扩展一下ASP.NET Routing吧”,而现在由于自己也正好需要使用这个功能,便实现了一个扩展。使用下来,效果不错。
ASP.NET Routing已经实现了针对Path的匹配和构造,而如今我们是希望在这个基础上提供额外的Domain支持,而扩展的结果依旧是对URL的Routing支持。这种增加职责而不改变其外观的需求让我想到了装饰器模式。也就是说,如果我们的目标是构造一个RouteDomain,那么它可能就是这样的:
public class DomainRoute : RouteBase { private DomainParser m_domainParser; public RouteBase InnerRoute { get; private set; } public string Pattern { get; private set; } public DomainRoute(RouteBase innerRoute, string pattern) { this.InnerRoute = innerRoute; this.Pattern = pattern; this.m_domainParser = new DomainParser(pattern); } public override RouteData GetRouteData(HttpContextBase httpContext) { ... } public override VirtualPathData GetVirtualPath( RequestContext requestContext, RouteValueDictionary values) { ... } }
DomainRoute会封装一个内部Route对象,将匹配或构造Path的任务交给这个内部对象“之余”,再把对Domain的处理工作交给DomainParser进行,而DomainRoute的主要逻辑,实际上便是将上两者进行组合。如GetRouteData方法:
public override RouteData GetRouteData(HttpContextBase httpContext) { // match domain var domainValues = this.m_domainParser.Match(httpContext.Request.Url); if (domainValues == null) return null; // match path var routeData = this.InnerRoute.GetRouteData(httpContext); if (routeData == null) return null; // merge routeData.Values.CopyFrom(domainValues); routeData.Route = this; return routeData; }
GetRouteData的功能是匹配URL,分三步走,第一步是匹配Domain,第二步是使用内部Route匹配Path,然后通过常用辅助方法中的CopyFrom方法,把一个字典中的所有数据复制到RouteData中并返回即可。可见,由于我们把任务进行了细小地拆分,每个类的职责均非常简单,可以进行独立的单元测试,因此代码也可以显得非常简单易懂。
ASP.NET Routing的功能是构造URL和构造URL,因此我们还需要实现一个GetVirtualPath方法:
public override VirtualPathData GetVirtualPath( RequestContext requestContext, RouteValueDictionary values) { // bind domain var domain = this.m_domainParser.Bind(requestContext.RouteData.Values, values); if (domain == null) return null; // bind path var innerValues = new RouteValueDictionary(); innerValues.CopyFrom(values).RemoveKeys(this.m_domainParser.Segments); var pathData = this.InnerRoute.GetVirtualPath(requestContext, innerValues); if (pathData == null) return null; // merge pathData.Route = this; pathData.VirtualPath = Merge(requestContext.HttpContext, domain, pathData.VirtualPath); return pathData; } private static string Merge(HttpContextBase context, string domain, string path) { var domainWithSlash = domain + "/"; var ignoreDomain = context.Request.Url.ToString().StartsWith(domainWithSlash); return ignoreDomain ? path : domainWithSlash + path; }
与GetRouteData的逻辑类似,GetVirtualPath方法首先根据所得的Route Value组装出Domain,再使用内部Route对象构造一个Path,并将其合并(Merge)起来。略有不同的是,再调用内部Route对象之前,必须去除所有用于Domain的部分(Segment),否则这些会出现在URL的QueryString部分中。在合并Domain和Path的时候,也有些许逻辑。Merge方法会判断当前请求与目标的Domain,如果两者相同,则会返回一个相对路径,省略URL前完整的域名。
方便起见,我们也可以使用一个扩展方法来辅助DomainRoute的构造:
public static class RouteExtensions { public static DomainRoute WithDomain(this RouteBase route, string pattern) { return new DomainRoute(route, pattern); } }
最后还是进行几个单元测试吧。首先,我们可以捕获整个URL中的数据(关于MockHelper请参考这里):
[Fact] public void Capture_Request_Scheme() { Mock<HttpRequestWrapper> mockRequest; var mockContext = MockHelper.MockRequest("http://jeffz.space.cnblogs.com/posts/2009", out mockRequest); var route = new Route("{section}/{data}", null); var domainRoute = route.WithDomain("{scheme}://{user}.{area}.{*domain}"); var routeData = domainRoute.GetRouteData(mockContext.Object); Assert.Equal("http", routeData.Values["scheme"]); Assert.Equal("space", routeData.Values["area"]); Assert.Equal("cnblogs.com", routeData.Values["domain"]); Assert.Equal("jeffz", routeData.Values["user"]); Assert.Equal("posts", routeData.Values["section"]); Assert.Equal("2009", routeData.Values["data"]); }
其次,对于无法匹配的URL,也能够返回null:
[Fact] public void Specified_Request_Scheme() { Mock<HttpRequestWrapper> mockRequest; var mockContext = MockHelper.MockRequest("http://space.cnblogs.com/Home", out mockRequest); var sslRoute = new Route("{controller}", null).WithDomain("https://{sub_domain}.{*domain}"); var sslData = sslRoute.GetRouteData(mockContext.Object); Assert.Null(sslData); }
最后,我们也可以成功地构造整段URL:
[Fact] public void Build_Url() { Mock<HttpRequestWrapper> mockRequest; var mockContext = MockHelper.MockRequest("http://wiki.cnblogs.com/Home/Index", out mockRequest); var route = new Route("{controller}/{action}", null).WithDomain("{scheme}://{area}.{*domain}"); var routeData = route.GetRouteData(mockContext.Object); var requestContext = new RequestContext(mockContext.Object, routeData); Assert.Equal("http", routeData.Values["scheme"]); Assert.Equal("wiki", routeData.Values["area"]); Assert.Equal("cnblogs.com", routeData.Values["domain"]); Assert.Equal("Home", routeData.Values["controller"]); Assert.Equal("Index", routeData.Values["action"]); // same domain var values = new RouteValueDictionary(new { controller = "Account", action = "List" }); var pathData = route.GetVirtualPath(requestContext, values); Assert.Equal("Account/List", pathData.VirtualPath); // different domain var spaceRoute = new Route("{controller}/{action}", null).WithDomain("http://{user}.{area}.{*domain}"); var spaceHash = new { controller = "Account", action = "List", area = "space", user = "jeffz" }; var spaceValues = new RouteValueDictionary(spaceHash); var spacePathData = spaceRoute.GetVirtualPath(requestContext, spaceValues); Assert.Equal("http://jeffz.space.cnblogs.com/Account/List", spacePathData.VirtualPath); }
整个DomainRoute类就这样完成了,除了单元测试外,总共也就60多行代码,但已经实现了我们所需要的常用功能。当然,目前还不支持“端口”,如果您需要的话,也可以修改代码,让其为您所用。
不过,虽然DomainRoute已经准备好了,但是在视图中“构造”URL时的辅助方法还需要一些额外的实现。这个下次再说吧(已发布,请参考《支持DomainRoute的URL构造辅助方法》)。
如果您有什么其他的想法或建议也请提出,我们可以一起讨论一下。