Hello World
Spiga

基于ParsedRoute的Domain Parser

2009-08-24 18:27 by 老赵, 4112 visits

之前谈了不少关于ASP.NET Routing中ParsedRoute的内容,例如它的设计以及如何调用它的功能,其目的便是为了如今的使用作准备。现在我们就基于它构建一个Domain Parser,而这个Parser也是为今后的功能打基础的。

为了调用内部的ParsedRoute类的功能,我们写了一个简单的“外壳类”。这个类的源代码可以在这里获得,这里就不多重复了。我们主要关心如何借助这个类来实现一个Domain Parser。

Domain Parser的目的自然是对一个域名进行解析和组装。解析和组装也都是基于“模式”的,这个模式的形式便直接借鉴了ASP.NET Routing的Route类。例如使用{scheme}://{sub_domain}.{*domain}来匹配http://www.cnblogs.com,便可以得到对应的scheme、sub_domain和domain的值。换句话说,它必须通过这样的单元测试(嘿嘿,我们似乎也有点测试驱动开发的意味了):

[Fact]
public void Parse_Domain()
{
    var parser = new DomainParser("{scheme}://{sub_domain}.{*domain}");
    var values = parser.Match(new Uri("http://space.cnblogs.com"));
    Assert.Equal("http", values["scheme"]);
    Assert.Equal("space", values["sub_domain"]);
    Assert.Equal("cnblogs.com", values["domain"]);

    var sslParser = new DomainParser("https://{sub_domain}.{*domain}");
    Assert.Null(sslParser.Match(new Uri("http://www.cnblogs.com")));
}

反过来也一样,提供对应的scheme、sub_domain和domain的值,便可以组装出一个正确的域名。单元测试如下:

[Fact]
public void Build_Domain()
{
    var parser = new DomainParser("{scheme}://{sub_domain}.{*domain}");

    var currentValues = new RouteValueDictionary(
        new { sub_domain = "wiki", domain = "cnblogs.com" });
    
    var values = new RouteValueDictionary(
        new { scheme = "http", sub_domain = "space" });

    Assert.Equal("http://space.cnblogs.com", parser.Bind(currentValues, values));

    Assert.Null(parser.Bind(null, null));
}

只可惜,ParsedRoute本身只支持对斜杠(即“/”)分割的部分进行拆分,但是很显然域名的分割符比较特殊,如“://”或“.”,这意味着我们不能直接使用模式字符串构造一个ParsedRoute类,在此之前我们必须对其进行一定处理:

internal class DomainParser
{
    public DomainParser(string pattern)
    {
        this.Pattern = pattern;

        this.Segments = CaptureSegments(pattern);

        string routePattern = pattern.Replace("://", "/").Replace('.', '/');
        this.m_parsedRoute = RouteParser.Parse(routePattern);
    }

    private static ReadOnlyCollection<string> CaptureSegments(string domainPattern)
    {
        var regex = @"{\*?([^}]+)}";
        var matches = Regex.Matches(domainPattern, regex).Cast<Match>();
        var segments = matches.Select(m => m.Groups[1].Value);
        return new ReadOnlyCollection<string>(segments.ToList());
    }

    public ReadOnlyCollection<string> Segments { get; private set; }

    public string Pattern { get; private set; }

    ...
}

由于功能需要(稍后详谈),我们会提取其中所有需要捕获的“部分(segment)”,对此我们最好也通过单元测试来保证“提取”操作的正确性:

[Fact]
public void Capture_Segments()
{
    var parser = new DomainParser("http://{sub_domain}.{*domain}");
    var sorted = parser.Segments.OrderBy(s => s).ToList();
    Assert.Equal("domain", sorted[0]);
    Assert.Equal("sub_domain", sorted[1]);
}

自然,在解析域名时,也不能直接将其交给ParsedRoute类处理,而必须经过一定的转换:

private static string ConvertDomainToPath(Uri uri)
{
    return uri.Scheme + "/" + uri.Host.Replace('.', '/');
}

由此,我们便把“http://www.cnblogs.com”这个域名转化为“http/www/cnblogs/com”这个可以ParsedRoute可以解析的域名。至此,解析用的Match方法便可以轻易得出:

public RouteValueDictionary Match(Uri uri)
{
    var toParse = ConvertDomainToPath(uri);
    var domainValues = this.m_parsedRoute.Match(toParse, null);
    if (domainValues == null) return null;

    var result = new RouteValueDictionary();
    foreach (var pair in domainValues)
    {
        var value = pair.Value as string;
        if (value != null)
        {
            result.Add(pair.Key, value.Replace('/', '.'));
        }
        else
        {
            result.Add(pair.Key, pair.Value);
        }
    }

    return result;
}

在使用ParsedRoute获得解析结果之后,我们会把其中每个值中的斜杠替换成“.”,这样便恢复了域名匹配前的模样。例如,我们使用{*domain}来匹配域名的尾部,这样domain便可以得到cnblogs.com这个值。

作为Parse的逆操作,DomainParser的Bind方法自然会用到ParsedDomain的Bind方法。不过需要注意的是,ParsedDomain在遇到那些模式中没有出现的部分时,会将他们作为URL的Query String处理。您可以通过下面的单元测试代码来观察这一点:

[Fact]
public void Bind()
{
    var values = new RouteValueDictionary();
    values["controller"] = "Home";
    values["action"] = "Index";
    values["hello"] = "world";
    values["id"] = 5;

    var parsedRoute = RouteParser.Parse("{controller}/{action}/{*id}");
    var boundUrl = parsedRoute.Bind(null, values, null, null);
    Assert.Equal("Home/Index/5?hello=world", boundUrl.Url);
}

因此,在交由ParsedRoute处理之前,我们会从数据源中提取必要的值,并填充新的acceptValues集合,再使用ParsedRoute获取拼装结果:

public string Bind(RouteValueDictionary currentValues, RouteValueDictionary values)
{
    currentValues = currentValues ?? new RouteValueDictionary();
    values = values ?? new RouteValueDictionary();

    var acceptValues = new RouteValueDictionary();
    foreach (var name in this.Segments)
    {
        object segmentValue;
        if (values.TryGetValue(name, out segmentValue) ||
            currentValues.TryGetValue(name, out segmentValue))
        {
            acceptValues.Add(name, segmentValue);
        }
        else
        {
            return null;
        }
    }
    
    var boundUrl = this.m_parsedRoute.Bind(null, acceptValues, null, null);
    if (boundUrl == null) return null;

    return ConvertPathToDomain(boundUrl.Url);
}

我们这里只利用了ParsedRoute的第二个参数,这意味着我们提供的每个“部分”没有默认值,没有约束,完全通过直接提供,这样便避免涉及到ParsedRoute中较为复杂的部分。如果您需要这些额外的功能,也可以自行修改相关代码。不过,ParsedRoute的Bind方法获得的是一个类似于“http/www/cnblogs.com”这样的url,因此在返回之前还要进行额外的转化:

private static string ConvertUrlToDomain(string url)
{
    var domainParts = url.Split('/');
    var domain = domainParts[0];
    for (int i = 1; i < domainParts.Length; i++)
    {
        domain += (i == 1 ? "://" : ".");
        domain += domainParts[i];
    }

    return domain;
}

这倒都是最最简单的字符串转化而已,相信没有什么值得讨论的。

借助ParsedRoute的功能,我们构建了一个与现有匹配规则类似的DomainParser类,它可以帮助我们解析和组装域名,为接下去的功能做一些准备(这里可获得它的完整代码)。可能您已经猜到了这个功能是什么,不过现在我们还是分享一下关于DomainParser的体会吧。对于现在的实现,您对此有什么想法呢?

Creative Commons License

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

Add your comment

21 条回复

  1. Leejor[未注册用户]
    *.*.*.*
    链接

    Leejor[未注册用户] 2009-08-24 18:56:00

    SF

  2. Leejor.
    *.*.*.*
    链接

    Leejor. 2009-08-24 18:57:00

    为了防止老赵抢SF,我都没登陆就发出来了。@_@

  3. Steven Chen
    *.*.*.*
    链接

    Steven Chen 2009-08-24 21:22:00

    连地板也不给老赵留

  4. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-24 21:31:00

    我看楼上两位干脆做个抢沙发机器人算了。

  5. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-08-24 22:58:00

    下班的时候用手机抢沙发没有成功……好像手机还无法发评论

  6. 老赵
    admin
    链接

    老赵 2009-08-25 09:15:00

    大家写机器人然后发表出来吧,我有几本我不太需要的好书想送掉。

  7. Artech
    *.*.*.*
    链接

    Artech 2009-08-25 09:54:00

    老赵天天见,有点审美疲劳了:)

  8. 老赵
    admin
    链接

    老赵 2009-08-25 12:34:00

    @Artech
    没人讨论,不爽。

  9. Breeze Woo
    *.*.*.*
    链接

    Breeze Woo 2009-08-25 12:38:00

    赵兄,技术应该要面向大众化。
    最近你搞的几篇都看不明白啊!

  10. 老赵
    admin
    链接

    老赵 2009-08-25 12:42:00

    @Breeze Woo
    已经是最最贴近群众的ASP.NET MVC了……

  11. 路过者[未注册用户]
    *.*.*.*
    链接

    路过者[未注册用户] 2009-08-25 14:34:00

    好像老赵得了NIH症了,:-)


    也许微软应该充实System.UriTemplate(虽然处于一个WCF的程序集中,好像也没有×支持)类
    http://msdn.microsoft.com/en-us/library/system.uritemplate.aspx


    看例子
    http://hyperthink.net/blog/uritemplate-101/

    还有一个开源的
    http://uritemplate.codeplex.com/

  12. 老赵
    admin
    链接

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

    @路过者
    似乎是个好东西,多谢推荐,有机会我会看看的。:)

  13. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-08-25 17:05:00

    就这个例子而言,觉得Regex和String.Format也是【轻易】办到的,为什么专门要搬出一个ParsedRoute呢?

  14. 老赵
    admin
    链接

    老赵 2009-08-25 17:07:00

    @vczh
    Regex只能Match不能Bind。

  15. Xuefly
    *.*.*.*
    链接

    Xuefly 2009-08-25 20:27:00

    老赵这几篇互相联系是为了用System.Web.Routing.dll来实现二级域名做准备的吧

  16. 老赵
    admin
    链接

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

    @Xuefly
    是的,其实都是最近作的事情想的事情。

  17. Sojin Liu
    *.*.*.*
    链接

    Sojin Liu 2009-08-26 18:17:00

    @Jeffrey Zhao

    真的送吗?老赵?这个是我做的第一版简单的沙发机。
    blogId对应博客Id,在你博客的源码中可以查到
    http://www.cnblogs.com/Sojin/archive/2009/08/26/1554084.html

  18. 老赵
    admin
    链接

    老赵 2009-08-26 18:21:00

    @Sojin Liu
    http://www.china-pub.com/2382
    就这本,真送。
    不过你要写文章详细描述一下制作过程、思路等等啊,光演示或贴文不算,呵呵。

  19. Sojin Liu
    *.*.*.*
    链接

    Sojin Liu 2009-08-26 19:07:00

    @Jeffrey Zhao
    恩,这几天会把详细补齐的,只是先和你说一声把书预定了再说呵呵。

  20. kaka
    58.248.227.*
    链接

    kaka 2010-05-28 18:32:12

    private static PropertyAccessor s_urlAccessor;

    PropertyAccessor 这个类型的命名空间是那个?

    请教一下老赵,谢谢

  21. feelrain
    218.109.253.*
    链接

    feelrain 2015-02-05 12:42:55

    源码无法下载了,老赵能再提供一个地址吗,或者发一份到我的邮箱(feelrain@foxmail.com)谢谢!

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我