基于ParsedRoute的Domain Parser
2009-08-24 18:27 by 老赵, 5401 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的体会吧。对于现在的实现,您对此有什么想法呢?
SF