ASP.NET MVC Controller的激活

最近抽空看了一下ASP.NET MVC的部分源碼,順帶寫篇文章作個筆記以便往後查看。緩存

在UrlRoutingModule模塊中,將請求處理程序映射到了MvcHandler中,所以,提及Controller的激活,首先要從MvcHandler入手,MvcHandler實現了三個接口:IHttpAsyncHandler, IHttpHandler, IRequiresSessionState。 其處理邏輯主要實如今同步和異步的ProcessRequest方法中,總的來講,該方法在執行的時候,大體經歷如下幾個步驟:閉包

  1. 預處理(在響應頭中添加版本信息並去除未賦值的可選路由參數)
  2. 經過ControllerBuilder獲取ControlerFactory,並使用Controller工廠建立Controller
  3. 根據是不是異步處理,調用Controller中相應的方法(ExecuteCore或BeginExecute)
  4. 釋放Controller

其中第一步在ProcessRequestInit方法中進行處理,本文主要是分析第兩步中的controller是如何建立出來的。less

Controller的建立是經過ControllerFactory實現的,而ControllerFactory的建立又是在ControllerBuilder中完成的,所以咱們先了解一下ControllerBuilder的工做原理。異步

ControllerBuilder

從源碼中能夠看出,在ControllerBuilder類中,並無直接實現對controller工廠的建立,ControllerFactory的建立其實是委託給一個繼承自IResolver接口的SingleServiceResolver類的實例來實現的,這一點從GetControllerFactory方法中能夠看出,它是經過調用SingleServiceResolver對象的Current屬性來完成controller工廠的建立的。ide

public IControllerFactory GetControllerFactory()
{
    return _serviceResolver.Current;  //依賴IResolver接口建立工廠
}

而且在源碼中還發現,SingleServiceResolver類是internal級別的,這意味着外部沒法直接訪問,那麼ControllerBuilder是如何藉助SingleServiceResolver來實現工廠的註冊呢?繼續看代碼,ControllerBuilder類和SingleServiceResolver類都有一個Func<IControllerFactory>類型的委託字段,咱們姑且稱爲工廠委託,函數

//ControllerBuilder.cs
private Func<IControllerFactory> _factoryThunk = () => null;  //工廠委託
//SingleServiceResolver.cs
private Func<TService> _currentValueThunk;  //工廠委託

該委託實現了工廠的建立,而經過SetControllerFactory方法僅僅是更改了ControllerBuilder類的工廠委託字段,並無更改SingleServiceResolver類的工廠委託字段,post

public void SetControllerFactory(IControllerFactory controllerFactory)
{
    if (controllerFactory == null)
    {
        throw new ArgumentNullException("controllerFactory");
    }

    _factoryThunk = () => controllerFactory;  //更改ControllerBuilder的工廠委託字段
}

所以必須將相應的更改應用到SingleServiceResolver類中才能實現真正的註冊,咱們知道,若是是單純的引用賦值,那麼更改一個引用並不會對另一個引用形成改變,好比:測試

Func<object> f1 = ()=>null;
Func<object> f2 = f1;  //f1與f2指向同一個對象
object o = new object();
f1 = ()=>o;  //更改f1後,f2仍然指向以前的對象
bool b1 = f1() == o;   //true
bool b2 = f2() == null;  //true,  f1()!=f2()

因此,ControllerBuilder在實例化SingleServiceResolver對象的時候,並無將自身的工廠委託字段直接賦值給SingleServiceResolver對象的對應字段(由於這樣的話SetControllerFactory方法註冊的委託沒法應用到SingleServiceResolver對象中),而是經過委託來進行了包裝,這樣就會造成一個閉包,在閉包中進行引用,以下所示:ui

Func<object> f1 = ()=>null;
Func<object> f2 = ()=>f1();  //經過委託包裝f1,造成閉包
object o = new object();
f1 = ()=>o;  //更改f1後,f2與f1保持同步
bool b1 = f1() == o;  //true
bool b2 = f2() == o;  //true,  f1()==f2()

//ControllerBuilder.cs
internal ControllerBuilder(IResolver<IControllerFactory> serviceResolver)
{
    _serviceResolver = serviceResolver ?? new SingleServiceResolver<IControllerFactory>(
                                              () => _factoryThunk(),  //封裝委託,閉包引用
                                              new DefaultControllerFactory { ControllerBuilder = this },
                                              "ControllerBuilder.GetControllerFactory");
}

這樣SingleServiceResolver對象中的工廠委託就會與ControllerBuilder對象中的對應字段保持同步了,SetControllerFactory方法也就達到了替換默認工廠的目的。this

閉包引用測試代碼:

using System;

class Program
{
    public static void Main(string[] args)
    {
        Func<object> f1 = ()=>null;
        Func<object> f2 = f1;  //f1與f2指向同一個對象
        object o = new object();
        f1 = ()=>o;  //更改f1後,f2仍然指向以前的對象
        bool b1 = f1() == o;   //true
        bool b2 = f2() == null;  //true,  f1()!=f2()

        Print("直接賦值:");
        Print(f1(),"f1() == {0}");
        Print(f2(),"f2() == {0}");
        Print(f1() == f2(),"f1() == f2() ? {0}");

        Func<object> ff1 = ()=>null;
        Func<object> ff2 = ()=>ff1();  //經過委託包裝f1,造成閉包
        object oo = new object();
        ff1 = ()=>oo;  //更改f1後,f2與f1保持同步
        bool bb1 = ff1() == oo;  //true
        bool bb2 = ff2() == oo;  //true,  f1()==f2()

        Print("委託賦值:");
        Print(ff1(),"ff1() == {0}");
        Print(ff2(),"ff2() == {0}");
        Print(ff1() == ff2(),"ff1() == ff2() ? {0}");
        
        Console.ReadLine();
    }

    static void Print(object mess,string format = "{0}")
    {
        string message = mess == null ? "null" : mess.ToString();
        Console.WriteLine(string.Format(format,message));
    }
}

下面看一下SingleServiceResolver類是如何實現對象的建立的,該類是個泛型類,這意味着能夠構造任何類型的對象,不只限於ControllerFactory,實際上在MVC中,該類在不少地方都獲得了應用,例如:ControllerBuilder、DefaultControllerFactory、BuildManagerViewEngine等,實現了對多種對象的建立。

SingleServiceResolver

該類實現了IResolver接口,主要用來提供指定類型的實例,在SingleServiceResolver類中有三種方式來建立對象:

一、private Lazy<TService> _currentValueFromResolver;  //內部調用_resolverThunk
二、private Func<TService> _currentValueThunk;  //委託方式
三、private TService _defaultValue;   //默認值方式

private Func<IDependencyResolver> _resolverThunk;  //IDependencyResolver方式

從Current方法中能夠看出他們的優先級:

public TService Current
{
    get { return _currentValueFromResolver.Value ?? _currentValueThunk() ?? _defaultValue; }
}

_currentValueFromResolver其實是對_resolverThunk的封裝,內部仍是調用_resolverThunk來實現對象的構造,因此優先級是:_resolverThunk > _currentValueThunk > _defaultValue,即:IDependencyResolver方式 > 委託方式 > 默認值方式。

SingleServiceResolver在構造函數中默認實現了一個DefaultDependencyResolver對象封裝到委託字段_resolverThunk中,該默認的Resolver是以Activator.CreateInstance(type)的方式建立對象的,可是有個前提,指定的type不能是接口或者抽象類,不然直接返回null。
在ControllerBuilder類中實例化SingleServiceResolver對象的時候指定的是IControllerFactory接口類型,因此其內部的SingleServiceResolver對象沒法經過IDependencyResolver方式建立對象,那麼建立ControllerFactory對象的職責就落到了_currentValueThunk(委託方式)和_defaultValue(默認值方式)這兩個方式上,前面說過,SingleServiceResolver類中的委託字段其實是經過閉包引用ControllerBuilder類中的相應委託來建立對象的,而在ControllerBuilder類中,這個對應的委託默認是返回null,

private Func<IControllerFactory> _factoryThunk = () => null;

所以,默認狀況下SingleServiceResolver類的第二種方式也失效了,那麼此時也只能依靠默認值方式來提供對象了,在ControllerBuilder類中這個默認值是DefaultControllerFactory:

internal ControllerBuilder(IResolver<IControllerFactory> serviceResolver)
{
    _serviceResolver = serviceResolver ?? new SingleServiceResolver<IControllerFactory>(
                                              () => _factoryThunk(),
                                              new DefaultControllerFactory { ControllerBuilder = this }, //默認值
                                              "ControllerBuilder.GetControllerFactory");
}

因此,在默認狀況下是使用DefaultControllerFactory類來構造Controller的。
在建立SingleServiceResolver對象的時候,能夠從三個地方判斷出真正建立對象的方法是哪一種:

new SingleServiceResolver<IControllerFactory>(   //一、看泛型接口,若是爲接口或抽象類,則IDependencyResolver方式失效
    () => _factoryThunk(),  //二、看_factoryThunk()是否返回null,若是是則委託方式失效
    new DefaultControllerFactory { ControllerBuilder = this },  //三、以上兩種都失效,則使用該默認值
    "ControllerBuilder.GetControllerFactory");

經過以上建立對象的過程能夠得知,有兩種方式能夠替換默認的對象提供器:

  1. 替換默認的DependencyResolver,能夠經過DependencyResolver類的靜態方法SetResolver方法來實現:

    CustomDependencyResolver customResolver = new  CustomDependencyResolver();
     DependencyResolver.SetResolver(customResolver);

    將以上語句放在程序啓動的地方,例如:Application_Start

  2. 經過前面介紹的ControllerBuilder類的SetControllerFactory方法

注:第一種方式的優先級更高。

ControllerFactory

經過ControllerBuilder建立出ControllerFactory對象後,下面就要利用該對象完成具體Controller的建立,ControllerFactory都實現了IControllerFactory接口,經過實現CreateController方法完成對Controller的實例化,CreateController的內部邏輯很是簡單,就兩步:獲取Controller類型,而後建立Controller對象。

獲取Controller類型

根據控制器名稱獲取控制器Type的過程,有必要深刻了解一下,以便於咱們在往後遇到相關問題的時候可以更好的進行錯誤定位。獲取類型的邏輯都封裝在GetControllerType方法中,該過程根據路由數據中是否含有命名空間信息,分爲三個階段進行類型搜索:

  • 首先,若是當前路由數據中存在命名空間信息,則在緩存中根據控制器名稱和命名空間搜索對應的類型,若是找到惟一一個類型,則返回該類型,找到多個直接拋異常
  • 其次,若是當前路由數據中不存在命名空間信息,或在第一階段的搜索沒有找到對應的類型,而且UseNamespaceFallback==true,此時會獲取ControllerBuilder中設置的命名空間信息,利用該信息和控制器名稱在緩存中進行類型搜索,若是找到惟一一個類型,則返回該類型,找到多個直接拋異常
  • 最後,若是路由數據和ControllerBuilder中都沒有命名空間信息,或者在以上兩個階段都沒有搜索到對應的Controller類型,那麼會忽略命名空間,在緩存中僅按照控制器名稱進行類型搜索,若是找到惟一一個類型,則返回該類型,找到多個直接拋異常

所以,命名空間的優先級是:RouteData > ControllerBuilder

在緩存中搜索類型的時候,若是是第一次查找,會調用ControllerTypeCache.EnsureInitialized方法將保存在硬盤中的Xml緩存文件加載到一個字典類型的內存緩存中。若是該緩存文件不存在,則會遍歷當前應用引用的全部程序集,找出全部public權限的Controller類型(判斷條件:實現IController接口、非抽象類、類名以Controller結尾),而後將這些類型信息進行xml序列化,生成緩存文件保存在硬盤中,以便於下次直接從緩存文件中加載,同時將類型信息分組以字典的形式緩存在內存中,提升搜索效率,字典的key爲ControllerName(不帶命名空間)。

Controller類型搜索流程以下圖所示:

獲取Controller類型

建立Controller對象

獲取Controller類型之後,接下來就要進行Controller對象的建立。在DefaultControllerFactory類的源碼中能夠看到,同ControllerBuilder相似,該類的構造函數中也實例化了一個SingleServiceResolver對象,按照以前介紹的方法,咱們一眼就能夠看出,該對象是利用默認值的方式提供了一個DefaultControllerActivator對象。

_activatorResolver = activatorResolver ?? new SingleServiceResolver<IControllerActivator>(  //一、泛型爲接口,IDependencyResolver方式失效
                     () => null,  //二、返回了null,委託方式失效
                     new DefaultControllerActivator(dependencyResolver),  //三、以上兩種方式均失效,則使用該提供方式
                     "DefaultControllerFactory constructor");

實際上DefaultControllerFactory類僅實現了類型的搜索,對象的真正建立過程須要由DefaultControllerActivator類來完成,默認狀況下,DefaultControllerActivator建立Controller的過程是很簡單的,由於它實際上使用的是一個叫作DefaultDependencyResolver的類來進行Controller建立的,在該類內部直接調用Activator.CreateInstance(serviceType)方法完成對象的實例化。

從DefaultControllerFactory和DefaultControllerActivator這兩個類的建立過程能夠發現,MVC提供了多種方式(IDependencyResolver方式、委託方式 、默認值方式)來提供對象,所以在對MVC相關模塊進行擴展的時候,也有多種方式能夠採用。

Controller中的數據容器

Controller中涉及到幾個給view傳值的數據容器:TempData、ViewData和ViewBag。前二者的不一樣之處在於TempData僅存儲臨時數據,裏面的數據在第一次讀取以後會被移除,即:只能被讀取一次;ViewData和ViewBag保存的是同一份數據,只不過ViewBag是動態對象,對ViewData進行了封裝。

public dynamic ViewBag
{
    get
    {
        if (_dynamicViewDataDictionary == null)
        {
            _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData); //封裝ViewData
        }
        return _dynamicViewDataDictionary;
    }
}

下面簡單說一下TempData的實現原理。

TempData

首先看下MSDN上是如何解釋的:

你能夠按使用 ViewDataDictionary 對象的相同方式使用 TempDataDictionary 對象傳遞數據。 可是,TempDataDictionary 對象中的數據僅從一個請求保持到下一個請求,除非你使用 Keep 方法將一個或多個鍵標記爲需保留。 若是鍵已標記爲需保留,則會爲下一個請求保留該鍵。
TempDataDictionary 對象的典型用法是,在數據重定向到一個操做方法時從另外一個操做方法傳遞數據。 例如,操做方法可能會在調用 RedirectToAction 方法以前,將有關錯誤的信息存儲在控制器的 TempData 屬性(該屬性返回 TempDataDictionary 對象)中。 而後,下一個操做方法能夠處理錯誤並呈現顯示錯誤消息的視圖。

TempData的特性就是能夠在兩個Action之間傳遞數據,它會保存一份數據到下一個Action,並隨着再下一個Action的到來而失效。因此它被用在兩個Action之間來保存數據,好比,這樣一個場景,你的一個Action接受一些post的數據,而後交給另外一個Action來處理,並顯示到頁面,這時就可使用TempData來傳遞這份數據。

TempData實現了IDictionary<string, object>接口,同時內部含有一個IDictionary<string, object>類型的私有字段,並添加了相關方法對字典字段的操做進行了控制,這明顯是代理模式的一個應用。由於TempData須要在Action之間傳遞數據,所以要求其可以對自身的數據進行保存,TempData依賴ITempDataProvider接口實現了數據的加載與保存,默認狀況下是使用SessionStateTempDataProvider對象將TempData中的數據存放在Session中。

下面看一下TempData是如何控制數據操做的,TempDataDictionary源碼中有這樣一段定義:

internal const string TempDataSerializationKey = "__tempData";

private Dictionary<string, object> _data;
private HashSet<string> _initialKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private HashSet<string> _retainedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

私有字典字段_data是真正存放數據的地方,哈希集合_initialKeys和_retainedKeys用來標記數據,_initialKeys中存放還沒有被讀取的數據key,_retainedKeys存放能夠被屢次訪問的key。
TempDataDictionary對數據操做的控制行爲主要體如今在讀取數據的時候並不會當即從_data中刪除對應的數據,而是經過_initialKeys和_retainedKeys這兩個hashset標記每條數據的狀態,最後在經過ITempDataProvider進行保存的時候再根據以前標記的狀態對數據進行過濾,這時纔去除已訪問過的數據。

相關的控制方法有:TryGetValue、Add、Keep、Peek、Remove、Clear

一、TryGetValue

public bool TryGetValue(string key, out object value)
{
    _initialKeys.Remove(key);
    return _data.TryGetValue(key, out value);
}

該方法在讀取數據的時候,會從_initialKeys集合中移除對應的key,前面說過,由於_initialKeys是用來標記數據未訪問狀態的,從該集合中刪除了key,以後在經過ITempDataProvider保存的時候就會將數據從_data字典中刪除,下一次請求就沒法再從TempData訪問該key對應的數據了,即:數據只能在一次請求中使用。

二、Add

public void Add(string key, object value)
{
    _data.Add(key, value);
    _initialKeys.Add(key);
}

添加數據的時候在_initialKeys中打上標記,代表該key對應的數據能夠被訪問。

三、Keep

public void Keep(string key)
{
    _retainedKeys.Add(key);
}

調用Keep方法的時候,會將key添加到_retainedKeys中,代表該條記錄能夠被屢次訪問,爲何能夠被屢次訪問呢,能夠從Save方法中找到緣由:

public void Save(ControllerContext controllerContext, ITempDataProvider tempDataProvider)
{
    // Frequently called so ensure delegate is stateless
    _data.RemoveFromDictionary((KeyValuePair<string, object> entry, TempDataDictionary tempData) =>
        {
            string key = entry.Key;
            return !tempData._initialKeys.Contains(key) 
                && !tempData._retainedKeys.Contains(key);
        }, this);

    tempDataProvider.SaveTempData(controllerContext, _data);
}

能夠看出,在保存的時候,會從_data中取出每一條數據,判斷該數據的key是否存在於_initialKeys和_retainedKeys中,若是都不存在纔會從_data中移除,因此keep方法將key添加到_retainedKeys後,該數據就不會被刪除了,即:能夠在多個請求中被訪問了。

四、Peek

public object Peek(string key)
{
    object value;
    _data.TryGetValue(key, out value);
    return value;
}

從代碼中能夠看出,該方法在讀取數據的時候,僅僅是從_data中進行了獲取,並無移除_initialKeys集合中對應的key,所以經過該方法讀取數據不影響數據的狀態,該條數據依然能夠在下一次請求中被使用。

五、Remove 與 Clear

public bool Remove(string key)
{
    _retainedKeys.Remove(key);
    _initialKeys.Remove(key);
    return _data.Remove(key);
}

public void Clear()
{
    _data.Clear();
    _retainedKeys.Clear();
    _initialKeys.Clear();
}

這兩個方法沒什麼多說的,只是在刪除數據的時候同時刪除其對應的狀態。

相關文章
相關標籤/搜索