Hello World
Spiga

支持Area的ControllerFactory

2009-08-20 11:33 by 老赵, 5519 visits

几个星期之前,有个朋友对我说,他的项目中需要将前后台区分开来,也就是类似分Area的功能。不过Area只在MVC 2中出现,因此现在想在1.0版本中先实现类似的功能了。他打算,根据Route中捕获的内容(如“area”),然后去找对应命名空间下的Controller。这样看来不难,似乎只要在Route上做点配置,而默认的DefaultControllerFactory已经对命名空间的查询提供支持了(可惜有线程安全的问题)。

不过他说,最后发现似乎这块功能不是他想象的那样,因此希望我可以看看到底是什么问题。由于当时没有扩展ASP.NET MVC的需求,后来我事情一多就忘了,现在先说声抱歉。最近开始对ASP.NET MVC动手动脚了,发现这样一个Area的功能非常有用,而且巧合的是,我也打算把Area和命名空间对应起来。

只是我选择的路和那位兄弟不一样,我打算自己写一个简单的ControllerFactory来替换掉默认的DefaultControllerFactory。这么做的主要原因是:我不知道DefaultControllerFactory已经提供对命名空间的支持了,微软默默地实现了却没有对外公开过,我也是后来阅读代码时才发现的。同时又意识到线程安全的问题,于是就还是打算自己写了。

好在ASP.NET MVC从设计之初就提供了扩展的能力,每个组件粒度都很小,大部分组件都是可以独立拔插的(Controller类除外,如果你使用自己的IController实现,就会发现大部分功能,如各Filter都失效了)。而要实现一个Controller Factory,只要实现一个简单的IControllerFactory就可以了(我喜欢接口):

public interface IControllerFactory
{
    IController CreateController(RequestContext requestContext, string controllerName);
    void ReleaseController(IController controller);
}

于是构建一个AreaControllerFactory也大致只需要以下一些代码:

public class AreaControllerFactory : IControllerFactory
{
    public IController CreateController(RequestContext requestContext, string controllerName)
    {
        ...
    }

    public void ReleaseController(IController controller)
    {
        IDisposable disposable = controller as IDisposable;
        if (disposable != null)
        {
            disposable.Dispose();
        }
    }
}

然后按照惯例,还是一步步谈起。首先是构造函数,我们的策略是根据不同的Area加载不同命名空间下的Controller类型。方便起见,我选择“基础命名空间”和“扩展部分”两块,它们从构造函数中传入:

private Dictionary<string, string> m_areaPartMapping = new Dictionary<string, string>();

public string NamespaceBase { get; private set; }

public AreaControllerFactory(string namespaceBase)
    : this(namespaceBase, null)
{ }

public AreaControllerFactory(string namespaceBase, IDictionary<string, string> areaPartMapping)
{
    this.NamespaceBase = namespaceBase.EndsWith(".") ? namespaceBase : namespaceBase + ".";

    if (areaPartMapping != null)
    {
        foreach (var pair in areaPartMapping)
        {
            this.m_areaPartMapping.Add(pair.Key.ToLowerInvariant(), pair.Value);
        }
    }
}

于是我们就可以这样使用:

var controllerFactory = new AreaControllerFactory("MyApp.Controllers");
ControllerBuilder.Current.SetControllerFactory(controllerFactory);

如果在需要的时候,还可以指定Area与特定命名空间“部分”的映射关系。因此,我们需要从Area来获取这个“Part”:

private string GetNamespacePart(string area)
{
    if (String.IsNullOrEmpty(area)) return "";

    string part;
    if (this.m_areaPartMapping.TryGetValue(area.ToLowerInvariant(), out part))
    {
        return part;
    }

    return area;
}

这里我选择“配置”和“约定”相结合的方式。得到一个Area之后,我们会在映射表里进行查找Part,如果没有,则Area本身便是Part。根据Part和Controller名称,我们便可以获得Controller的类型:

private ReaderWriterLockSlim m_rwLock = new ReaderWriterLockSlim();
private Dictionary<string, Type> m_controllerTypes = new Dictionary<string, Type>();

private Type GetControllerType(string area, string controllerName)
{
    string part = this.GetNamespacePart(area);

    string typeName = String.IsNullOrEmpty(part) ?
        this.NamespaceBase + controllerName.ToLowerInvariant() + "Controller" :
        this.NamespaceBase + part + "." + controllerName.ToLowerInvariant() + "Controller";

    Type type;

    this.m_rwLock.EnterReadLock();
    try
    {
        if (this.m_controllerTypes.TryGetValue(typeName, out type))
        {
            return type;
        }
    }
    finally
    {
        this.m_rwLock.ExitReadLock();
    }

    type = Type.GetType(typeName, false, true);

    if (type != null)
    {
        this.m_rwLock.EnterWriteLock();
        try
        {
            this.m_controllerTypes[typeName] = type;
        }
        finally
        {
            this.m_rwLock.ExitWriteLock();
        }
    }

    return type;
}

由于我选择在应用程序中使用同一个AreaControllerFactory对象,因此线程安全是一定要有保证的。这里我们用到了读写锁,不过请注意,红色那句话并不保证对于每个相同的typeName只执行一次,也不保证相同的typeName对于m_controllerTypes字典只会进行一次写操作(所以我没有Add,而是使用了下标操作)。不过,由于这些“重复”不会造成问题,因此就没有去涉及太多这方面的考虑。

最后,便是那CreateControlle方法:

public IController CreateController(RequestContext requestContext, string controllerName)
{
    Type controllerType;
    object area;
    if (requestContext.RouteData.Values.TryGetValue("area", out area))
    {
        controllerType = this.GetControllerType(area.ToString(), controllerName);
    }
    else
    {
        controllerType = this.GetControllerType(null, controllerName);
    }

    if (controllerType == null)
    {
        throw new HttpException(404,
            String.Format(
                "Controller of path {0} not found.",
                requestContext.HttpContext.Request.Path));
    }

    try
    {
        return (IController)Activator.CreateInstance(controllerType);
    }
    catch (Exception ex)
    {
        string message = String.Format("Error creating controller {0}" + controllerType);
        throw new InvalidOperationException(message, ex);
    }
}

似乎没有什么可谈的:我们从RouteData中获取出area对应的值,并且调用GetControllerType方法获得Controller的类型,并使用Activator.CreateInstance创建对象。在不合法的情况下,抛出合适的异常即可。

至此,AreaControllerFactory就完成了,很容易,不是吗?很显然,这个组件的功能非常有限,例如为什么所有的Controller一定要在同一个命名空间下?没错,它其实只是符合“我要求”的一个东西。但是,在项目中很多东西都是如此,我只实现我够用的功能。例如,我可能不会向对外公开的API那样,严格检查每个问题,抛出严谨的异常。我可能倾向于在项目中使用接口,而不是使用抽象类。因为是我的项目,我可以快速反馈,需要修改的时候就修改吧。

同样的,如果DefaultControllerFactory真在某些特别情况下有问题,或者支持的有些复杂。那么不如我们就自己动手吧。一次性投入,而且这样的小组件也花不了多少时间。

 

完整代码:http://gist.github.com/170798

Creative Commons License

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

Add your comment

33 条回复

  1. bravf
    *.*.*.*
    链接

    bravf 2009-08-20 11:47:00

    shafa?

  2. AlexLiu
    *.*.*.*
    链接

    AlexLiu 2009-08-20 12:25:00

    赵老师最近真是量产啊。。

  3. Vadin Wang
    *.*.*.*
    链接

    Vadin Wang 2009-08-20 12:43:00

    c

    AlexLiu:赵老师最近真是量产啊。。


    纯引

  4. 阿不
    *.*.*.*
    链接

    阿不 2009-08-20 12:48:00

    默认的ControllerFactory是可以带有Namespace的约束的,做法是在dataTokens加一个Namespaces约束

    修改:再回头看你的原文,发现你已经发现可以了。不过算是补充吧

  5. 老赵
    admin
    链接

    老赵 2009-08-20 12:50:00

    @阿不
    不过1.0版中,直接利用这个DataToken有线程安全问题。

  6. ︶ㄣ木べ头
    *.*.*.*
    链接

    ︶ㄣ木べ头 2009-08-20 12:50:00

    很早很早就见一个外国人写过,我还用了他写的。

  7. 老赵
    admin
    链接

    老赵 2009-08-20 12:52:00

    @︶ㄣ木べ头
    是么,好吧,又被抢先了。

  8. 阿不
    *.*.*.*
    链接

    阿不 2009-08-20 13:04:00

    @ Jeffrey Zhao
    再次看了你的那篇文章了,:)

  9. 东方的1[未注册用户]
    *.*.*.*
    链接

    东方的1[未注册用户] 2009-08-20 13:15:00

    呵呵,如果我是老板我会把老赵给辞退了,这家伙一天8小时花了6小时写博客,花了1小时59分回帖,呵呵!

  10. Lovell
    *.*.*.*
    链接

    Lovell 2009-08-20 13:16:00

    mark

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

    hehehehe[未注册用户] 2009-08-20 13:17:00

    具体怎么应用到项目中去呢?
    就是怎么让 mvc 默认用自定义的 AreaControllerFactory
    去创建 Controller 呢?
    我是新手。

  12. 老赵
    admin
    链接

    老赵 2009-08-20 13:22:00

    东方的1:呵呵,如果我是老板我会把老赵给辞退了,这家伙一天8小时花了6小时写博客,花了1小时59分回帖,呵呵!


    放心,这篇文章我只写了40分钟。

  13. 老赵
    admin
    链接

    老赵 2009-08-20 13:23:00

    @hehehehe
    文章里已经有了啊。

  14. 雅虎鸭毛[未注册用户]
    *.*.*.*
    链接

    雅虎鸭毛[未注册用户] 2009-08-20 13:33:00

    老赵MVP实至名归!顶!
    MVP是对社区的贡献的衡量,老赵实在是实至名归

  15. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-08-20 14:34:00

    像老赵这样的人多多益善

  16. Jeff Wong
    *.*.*.*
    链接

    Jeff Wong 2009-08-20 14:44:00

    老赵博客的存在让很多要面子的程序员不敢在园子里发首页,嗯哈哈哈...

  17. ibei
    *.*.*.*
    链接

    ibei 2009-08-20 15:36:00

    嗯嗯。楼上说的好。
    持续关注老赵书托系列~~

  18. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2009-08-20 15:42:00

    你都把代码放github喇?

  19. 老赵
    admin
    链接

    老赵 2009-08-20 15:59:00

    @Cat Chen
    放一些,蛮方便的。不过github在chrome里显示不正常……

  20. Sojin Liu
    *.*.*.*
    链接

    Sojin Liu 2009-08-20 16:08:00

    ...

    type = Type.GetType(typeName, false, true);

    if (type != null)
    {
    ...
    }

    老赵这段代码和mvc中的ReaderWriterCache.FetchOrCreateItem
    方法非常相似,ReaderWriterCache也是不保证对象被创建多次,(在你这是获取类型多次)
    但他保证了对象不被赋值多次。因为他在写锁里面再次检查了字典是否已经含有对应对象并返回。

    TValue newEntry = creator();
    _rwLock.AcquireWriterLock(Timeout.Infinite);
    try {
    TValue existingEntry;
    if (_cache.TryGetValue(key, out existingEntry)) {
    return existingEntry;
    }
    _cache[key] = newEntry;
    return newEntry;
    }
    finally {
    _rwLock.ReleaseWriterLock();
    }
    不过我也有个疑问,他为何不将TValue newEntry = creator();放到
    if (_cache.TryGetValue(key, out existingEntry))
    的后面防止多次创建呢?这可能也是个很复杂的“平衡”的问题。

    ========================================================
    另外老赵可能还没看我上次说的那个问题,我就在这里简单说下吧。

    mvc 2虽然将DefaultControllerFactory修复为线程安全,但用来实现area还是有问题的,因为GetControllerType的行为是先在route指定的namespace下查找符合条件的controller,如果找不到,他会继续去所有controller中查找。

    这样会有两种情况:
    1、用户请求某个area下并不存在的controller,mvc会帮你把另外一个area下已存在的controller找出来。
    2、您已经在blog和news area下都有一个叫Home的Controller,这个时候您去请求notice area下的homeController,如/notice/home/index
    这时候mvc会抛InvalidOperationException并告诉你ControllerName Ambiguous。

  21. 老赵
    admin
    链接

    老赵 2009-08-20 16:10:00

    @Sojin Liu
    这样看来,估计真说是by design……也过得去,呵呵。

  22. Sojin Liu
    *.*.*.*
    链接

    Sojin Liu 2009-08-20 17:22:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @Sojin Liu
    这样看来,估计真说是by design……也过得去,呵呵。



    您是指我评论后半段的area问题是by design?
    我觉得应该不会吧,因为您上次引用给我的那个外国人的实现
    也是通过route的datatoken传递namespace的,我想他应该不会想让用户有可能在请求一个area的情况下被定向到另外的area吧(比如请求前台area被定向到后台并告诉你请登录。)

    或者是另一种情况,DefaultControllerFactory抛出异常(ControllerNameAmbiguous)(并且我们无法通过mvc一般手段处理,如exceptionfilter,因为这里抛出异常时还未进入actionInvoke),这样我们就只能通过类似于elmah的手法来进一步处理,来告诉用户这个链接是没有对应页面的,这就很麻烦了。

    所以我觉得这个应该算是bug,而不是by design.

  23. 老赵
    admin
    链接

    老赵 2009-08-20 22:31:00

    @Sojin Liu
    我觉得可能算是设计的可用性不高吧。
    有一点可能不合适,就是一旦某个Namespace找不到,就会找出另一个namespace的controller。
    而Ambiguous错误,我觉得是合理的。
    有机会我再看看。

  24. 吴辉军
    *.*.*.*
    链接

    吴辉军 2009-08-24 18:26:00

    老赵您好,我很喜欢你做研究的风格。

    就这篇支持Area的ControllerFactory,我向您提一个小小的建议。
    如果仅仅用Type.GetType()的方法来得到Controller的类型会导致Controller、ControllerFactory以及WebApplication三个程序集的耦合,也就是必须把三者写在同一个程序集Type才能正确地通过反射得到合适的ControllerType,否则会报错。

    如果这个细节您能处理一下,这篇文章将会更好地帮助园友提高.net开发水平。

    以上拙见,纯属班门弄斧。

  25. 老赵
    admin
    链接

    老赵 2009-08-24 18:30:00

    @吴辉军
    没有听懂你的意思啊,什么叫做三者写在一个程序集下?
    其实在我使用的时候,AreaControllerFactory和我的Controller是在一个程序集里的,和WebApp是分开的。

  26. 吴辉军
    *.*.*.*
    链接

    吴辉军 2009-08-24 21:53:00

    如果其他的程序集里面还有controller呢?AreaControllerFactory是否能正确解析独立程序集中的controller呢?

    我的这种情形主要出现在“不同功能的controller由不同的开发者在不同的程序集中实现”的情况。

  27. 老赵
    admin
    链接

    老赵 2009-08-24 23:00:00

    @吴辉军
    只要在WebApp里引用了,应该也没有问题吧。我不倾向于“五中生有”地直接加载一个程序集。
    有机会我试试看,如果这里的限制影响了你,自己设法改一下吧。:)

  28. xjb
    *.*.*.*
    链接

    xjb 2009-08-26 14:21:00

    博客园的老赵文章不能全文浏览,有些郁闷

  29. 老赵
    admin
    链接

    老赵 2009-08-26 14:33:00

    @xjb
    是指RSS吗?博客园没有这个统一的设置,我也不想一篇一篇再改了……

  30. xjb
    *.*.*.*
    链接

    xjb 2009-08-26 15:02:00

    是rss,如果看不到全文,订阅就没什么意义了

  31. 老赵
    admin
    链接

    老赵 2009-08-26 15:06:00

    @xjb
    还好吧,我也订阅了不少非全文的RSS,就是一种“提醒”啊,多点一次链接吧。

  32. Vincent.Xiaojie[未注册用户…
    *.*.*.*
    链接

    Vincent.Xiaojie[未注册用户] 2009-10-19 23:42:00

    Jeffrey Zhao:
    @吴辉军
    只要在WebApp里引用了,应该也没有问题吧。我不倾向于“五中生有”地直接加载一个程序集。
    有机会我试试看,如果这里的限制影响了你,自己设法改一下吧。:)



    -----------
    被 Type.GetType() 搞晕了。引用了不行,Type.GetType() 只能get本程序集的,我用System.Web.Compilation.BuildManager.GetType()就能跨程序集GetType,暂时还不知道有什么缺点。
    谢谢老赵的代码。

  33. 感觉没了
    *.*.*.*
    链接

    感觉没了 2009-12-07 17:29:00

    源码我下载了,为啥用记事本打开,源码全是不换行的

    用编辑器打开就是正常换行的?

    而且文件我用编辑器编辑,不管我怎么换行

    换用记事本打开都不换行

    我自己创建的文件,都是正常换行的。

    求解老赵

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我