Hello World
Spiga

配合域名作URL Routing

2009-08-25 16:00 by 老赵, 7028 visits

经常有朋友问我,如何对域名作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构造辅助方法》)。

如果您有什么其他的想法或建议也请提出,我们可以一起讨论一下。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

15 条回复

  1. Felix Yan
    *.*.*.*
    链接

    Felix Yan 2009-08-25 16:15:00

    哈哈,抢沙发!

  2. 迭戈
    *.*.*.*
    链接

    迭戈 2009-08-25 16:21:00

    占个位置先

  3. Gnie
    *.*.*.*
    链接

    Gnie 2009-08-25 16:25:00

    老赵最近产量很高吗。

  4. 老赵
    admin
    链接

    老赵 2009-08-25 16:33:00

    @Gnie
    因为最近思考比较少,琐事比较多,所以这些个《XX编程三百例》形式的文章比较容易些得出来。

  5. Sky101[未注册用户]
    *.*.*.*
    链接

    Sky101[未注册用户] 2009-08-25 20:16:00

    很喜欢这样的小文章:)

  6. 老赵
    admin
    链接

    老赵 2009-08-25 20:36:00

    @Sky101
    其实已经不小了,要从ParsedRoute看起,到DomainParser,才到DomainRoute,呵呵。

  7. Gnie
    *.*.*.*
    链接

    Gnie 2009-08-25 20:47:00

    @Jeffrey Zhao
    今天用宽屏看你的Blog的确舒服多了,不愧是老赵啊:)

  8. Xuefly
    *.*.*.*
    链接

    Xuefly 2009-08-25 21:12:00


    IIS主机名留空即设置成通配符,再把自己的域名泛解析一下*.xuefly.com,二级域名就出来了
    System.Web.Routing.dll果然可以完全控制URL!

  9. 老赵
    admin
    链接

    老赵 2009-08-25 21:19:00

    @Gnie
    嗯嗯,我不喜欢浪费空间,只能委屈那1/4的1024宽度用户了。

  10. 老赵
    admin
    链接

    老赵 2009-08-25 21:19:00

    @Xuefly
    能够自行扩展的东西,总归可以的咯。

  11. Dev BBs[未注册用户]
    *.*.*.*
    链接

    Dev BBs[未注册用户] 2009-08-26 11:17:00

    这些东西还没有研究过。

  12. ling
    113.12.105.*
    链接

    ling 2010-06-23 16:19:11

    您好!看了你的相关代码,没有发现PropertyAccessor这个类,

  13. 在路上
    123.67.5.*
    链接

    在路上 2010-10-18 01:51:54

    ASP.NET MVC 实现二级域名

    麻烦老赵帮我看下这篇文章怎么实现二级域名的,我写出来没有反应的呢,

    routes.Add("DomainRoute", new DomainRoute(
        "{user}.viiiii.com", //作为二级域名
        "{controller}/{action}/{id}", // URL with parameters
        new { user = "", controller = "Home", action = "Index", id = "" }  // 默认值
    ));
    
  14. 链接

    0557163 2011-10-26 23:13:34

    老赵,这个能用到asp.net routing in (3.5)吗?

  15. hotdancing
    61.177.29.*
    链接

    hotdancing 2013-09-20 22:04:01

    老赵,我们使用您的方法进行二级域名解析,但是,在使用页面缓存(outputCache)时,不同二级域名的缓存竟然是一样的(第一个被缓存的二级域名页面),尝试多种方式无果,请教!

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我