代码改变世界

基于ParsedRoute的Domain Parser

2009-08-24 18:27 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

之前谈了不少关于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的体会吧。对于现在的实现,您对此有什么想法呢?