代码改变世界

简化DomainRoute的配置

2009-10-15 09:57  Jeffrey Zhao  阅读(...)  评论(...编辑  收藏

昨天有朋友写邮件告诉我说,他正在项目中尝试着使用我提供的DomainRoute组件。我很高兴,这说明我的努力不是在自娱自乐,是对别人有实际帮助的,也有一些朋友会尝试着自行对项目进行扩展,而不总是靠微软提供的食物来过活。不过他说,他发现DomainRoute的配置非常繁琐,需要为每个Route使用WithDomain,提供了大量重复的信息。他说他也在构建了辅助API,不过似乎效果不够好,问我有没有更好的解决方法。其实是有的,因为我在使用DomainRoute的初期也遇到了这个问题,不过现在已经在MvcPatch中提供了个人认为比较令人满意解决方案。

DomainRoute使用了装饰器模式,是在某个RouteBase对象外部再包裹一层,得到一个新的DomainRoute对象。因此它的构造函数是这样的:

public DomainRoute(RouteBase innerRoute, string pattern, RouteValueDictionary defaults, ...)
{
    ...
}

此外,还提供了一个扩展方法辅助DomainRoute的构造方式:

public static class RouteExtensions
{
    public static DomainRoute WithDomain(this RouteBase route, string pattern, ...)
    {
        return new DomainRoute(route, pattern, ...);
    }
}

于是在使用的时候:

var route = new Route(
    "{controller}/{action}/{id}",                           // url
    new RouteValueDictionary(new { controller = "Home" }),  // defaults
    new MvcRouteHandler());
routes.Add(
    "Default",
    route.WithDomain(
        "http://{area}.mysite.com",                         // domain
        new { area = "products" }));                        // defaults

这只是一条配置规则而已,的确够繁琐的。在实际项目中,这样的配置大概会出现十几次甚至更多,更重要的是,这些在同一个域名的配置规则,需要为每一个重复一遍配置的信息,这严重违反了DRY原则。最容易想到的做法,可能是将WithDomain方法的调用提取到另一个方法中去,然后反复调用同一个方法,便可以缓解这个状况了。

不过,和微软在ASP.NET MVC提供的配置Route规则的做法相比,还是有比较大的差距。我们来回忆一下,它是怎么做的呢?

routes.MapRoute(
    "Default",                                                  // name
    "{controller}/{action}/{id}",                               // url
    new { controller = "Home", action = "Index", id = "" });    // defaults

多容易,你看它根本没有“构造”Route类,指定MvcRouteHandler对象这样的操作。这就是在告诉系统“我们要做什么”,而不是“我们该怎么做”。MapRoute是一个扩展方法,会向RouteCollection对象中(也就是上面的routes)添加Route规则。于是,我们也可以为DomainRoute添加一个类似的辅助方法。那么先来看看它的使用方式吧:

routes.MapDomain(
    "Blog",                                                     // prefix
    "http://{userName}.blogs.{*domain}",                        // domain
    new { area = "Blog" },                                      // defaults
    innerRoutes =>
    {
        innerRoutes.MapRoute(
            "Index",                                            // name
            "",                                                 // url
            new { controller = "Blog", action = "Index" });     // defaults

        innerRoutes.MapRoute(
            "Default",
            "{controller}/{action}/{id}",
            new { controller = "Home", action = "Index", id = "" });
    });

routes.MapDomain(
    "API",
    "http://api.{*domain}",
    new { area = "API" },
    innerRoutes =>
    {
        ...
    });

上面的代码表示为当前项目配置了两个域名,而在第一个DomainRoute下面还配置了两条Route规则。MapDomain方法的最后一个参数是一个“Route收集器”,它的作用是收集一些Route规则。这个收集器上面也定义了MapRoute方法,它的使用方式和ASP.NET MVC中自带的做法保持统一。使用了这样的API之后,我们可以看到上面的信息没有任何的重复,也不需要定义额外的辅助方法。

值得一提的是,虽然这个API的形式从一开始我打算简化DomainRoute配置时就定下了,但是它的内部的实现却有过很大的改变。一开始,我是打算把DomainRoute使用组合模式做成一个容器,其中包含了N个Route配置。但是昨天的文章提到,由于Route规则是有“命名”的,因此不能使用这种方式。因此,目前的API并不是这样实现的。

还是以上面的代码为例,我们在MapDomain方法中指定了一个名称“Blog”,它的收集器收集了两个Route规则,分别叫做“Index”和“Default”。因此,最终RouteCollection上同样会放入两个Route对象,分别叫做“Blog.Index”和“Blog.Default”——这就是MapDomain的第一个参数是“前缀(prefix)”而不是一个“名称(name)”。于是,在生成URL的时候,我们就可以通过指定名称的方式来访问到两个Route规则了。此外,如果您将prefix指定为空字符串或null,它便会保留收集器中的Route命名(如上面的Index和Default)。

关于MapDomain方法的实现,您可以参考MvcPatch中MvcPatch.Extensions项目的RouteCollectionExtensions类。MvcPatch包含我在博客上提过的几乎所有扩展,并且根据平时实际使用情况进行了改进。如果您对直接使用一个“修改版”的ASP.NET MVC框架有所顾虑的话,也可以参考其中的实现,有选择地运用到自己的项目中去。