淺談ASP.NET核心對象
想當(dāng)初在只使用WebForms框架并以服務(wù)端為中心的開發(fā)模式時,發(fā)現(xiàn)ASP.NET好復(fù)雜。一大堆服務(wù)端控件,各有各的使用方法,有些控件的事件也很重要,必須在合適地時機去響應(yīng),還真有些復(fù)雜。后來逐漸發(fā)現(xiàn)這些復(fù)雜的根源其實就是服務(wù)器控件相關(guān)的抽象邏輯。隨著Ajax越用越多,可能有些人也做過這些事情:【新建一個ashx文件,讀取一些用戶的輸入數(shù)據(jù),F(xiàn)orm, QueryString,然后調(diào)用業(yè)務(wù)邏輯代碼,將處理后的結(jié)果序列化成JSON字符串再發(fā)給客戶端】,這樣也能完成一次請求。不知大家有沒有做過這類事情,反正我是做過的。慢慢地,我也嫌煩了,這些事情中除了調(diào)用業(yè)務(wù)邏輯部分,都是些體力活嘛。于是想,寫點代碼把這些事情交給它們?nèi)プ霭桑抑惶幚砼c請求有關(guān)的數(shù)據(jù)處理就好了。終于,我寫了個簡陋的框架,并自稱為【我的Ajax服務(wù)端框架】以及【我的MVC框架】。寫完這些東西后,發(fā)現(xiàn)ASP.NET的東西變少了,但是仍可以實現(xiàn)很多功能。
其實,我們可以從另一角度來看ASP.NET,它就是一個底層框架平臺,它負責(zé)接收HTTP請求(從IIS傳入),將請求分配給一個線程,再把請求放到它的處理管道中,由一些其它的【管道事件訂閱者】來處理它們,最后將處理結(jié)果返回給客戶端。而WebForms或者MVC框架,都屬于ASP.NET平臺上的【管道事件訂閱者】而已,Web Service也是哦。如果你不想受限于WebForms或者MVC框架,或者您還想用ASP.NET做點其它的事情,比如:自己的服務(wù)框架,就像WebService那樣。但希望用其它更簡單的序列化方式來減少網(wǎng)絡(luò)流量,或者還有加密要求。那么了解ASP.NET提供了哪些功能就很有必要了。
本文將站在ASP.NET平臺的角度,來看看ASP.NET的一些基礎(chǔ)功能。雖然不會涉及任何其它上層框架,但所講述的內(nèi)容其實是適合其它上層框架的。
前面我說到:ASP.NET負責(zé)接收請求,并將請求分配給一個線程來執(zhí)行。最終執(zhí)行什么呢?當(dāng)然就是我們的處理邏輯。但我們在處理時,用戶輸入的數(shù)據(jù)又是從哪里來的呢?只能是HTTP請求。但它又可分為二個部分:請求頭和請求體。在ASP.NET中,我們并不需要去分析請求頭和請求體,比如:我們可以直接訪問QueryString,F(xiàn)orm就可以得到用戶傳過來的數(shù)據(jù)了,然而QueryString其實是放在請求頭上,在請求頭上的還有Cookie,F(xiàn)orm以及PostFile則放在請求體中。如果對這些內(nèi)容不清楚的可以參考我的博客:【細說Cookie】和【細說 Form (表單)】。在我這二篇博客中,您應(yīng)該可以看出:要是讓您從請求頭請求體中讀取這些數(shù)據(jù),還是很麻煩的。幸好,ASP.NET做為底層平臺,在每次處理請求時,都將這些數(shù)據(jù)轉(zhuǎn)成方便我們處理的對象了。今天我將只談這些基礎(chǔ)對象以及它們可以實現(xiàn)的功能。
在我的眼里,ASP.NET有三大核心對象:HttpContext, HttpRequest, HttpResponse。
除此之外,還有二個對象雖然稱不上核心,但仍然比較重要:HttpRuntime,HttpServerUtility
事實上,這些類的實例在其它的一些類型中也經(jīng)常被引用到,從出現(xiàn)的頻率也可以看出它們的重要性。
中國人喜歡把較重要的東西放在最后,做為壓軸出場。今天我也將按照這個風(fēng)俗習(xí)慣做為這些對象的出場順序來分別說說它們有哪些【重要的功能】。
HttpRuntime
第一個出場的是HttpRuntime,其實這個對象算是整個ASP.NET平臺最核心的對象,從名字可以看出它的份量。但它包含的很多方法都不是public類型的,它在整個請求的處理過程中,做了許多默默無聞但非常重要的工作。反而公開的東西并不多,因此需要我們掌握的東西也較少。不能讓它做為壓軸出場就讓它第一個出場吧。這就是我的想法。
HttpRuntime公開了一個方法靜態(tài) UnloadAppDomain() ,這個方法可以讓我們用代碼重新啟動網(wǎng)站。通常用于用戶通過程序界面修改了一個比較重要的參數(shù),這時需要重啟程序了。
HttpRuntime還公開了一個大家都熟知的靜態(tài)屬性 Cache ??赡苡行┤苏J為他/她在使用Page.Cache或者HttpContext.Cache,事實上后二個屬性都是HttpRuntime.Cache的【快捷方式】。HttpRuntime.Cache是個非常強大的東西,主要用于緩存一些數(shù)據(jù)對象,提高程序性能。雖然緩存實現(xiàn)方式比較多,一個static變量也算是能起到緩存的作用,但HttpRuntime.Cache的功能絕不僅限于一個簡單的緩存集合,如果說實現(xiàn)“緩存項的滑動過期和絕對過期”算是小兒科的話,緩存依賴的功能應(yīng)該可以算是個強大的特性吧。更有意義的是:它緩存的內(nèi)容還可以在操作系統(tǒng)內(nèi)存不足時能將一些緩存項釋放(可指定優(yōu)先級),從而獲得那些對象的內(nèi)存,并能在移除這些緩項時能通知您的代碼。可能有人認為當(dāng)內(nèi)存不足時自動釋放一些緩存對象容易啊,使用WeakReference類來包裝一下就可以了。但WeakReference不提供移除時的通知功能。
HttpRuntime.Cache還有個非??岬墓δ苁牵核⒎侵荒茉贏SP.NET環(huán)境中使用,也能在其它編程模型中使用,比如大家熟知的WinForm編程模型。如何使用呢,直接訪問HttpRuntime.Cache這個靜態(tài)屬性肯定是不行的。我們只要在程序初始化時創(chuàng)建一個HttpRuntime的實例,當(dāng)然還要保證它不會被GC回收掉。然后就可以像在ASP.NET中一樣使用HttpRuntime.Cache了,就這么簡單。是的,就是這樣簡單,您就可以在其它編程模型中使用Cache的強大功能:線程安全的集合,2種過期時間的選擇,緩存依賴,內(nèi)存不足時自動釋放且有回調(diào)通知。
這里我還想說說緩存依賴。我曾經(jīng)見過一個使用場景:有人從一堆文件(分為若干類別)中加載數(shù)據(jù)到Cache中,但是他為了想在這些數(shù)據(jù)文件修改時能重新加載,而采用創(chuàng)建線程并輪詢文件的最后修改時間的方式來實現(xiàn),總共開了60多個線程,那些線程每隔15去檢查各自所“管轄”的文件是否已修改。如果您也是這樣處理的,我今天就告訴您:真的沒必要這么復(fù)雜,您只要在添加緩存項時創(chuàng)建一個CacheDependency的實例并調(diào)用相應(yīng)的重載方法就可以了。具體CacheDependency有哪些參數(shù),您還是參考一下MSDN吧。這里我只告訴您:它能在一個文件或者目錄,或者多個文件在修改時,自動通知Cache將緩存項清除,而且還可以設(shè)置到依賴其它的緩存項,甚至能將這些依賴關(guān)系組合使用,非常強大。
可能還有人會擔(dān)心往Cache里放入太多的東西會不會影響性能,因此有人還想到控制緩存數(shù)量的辦法。我只想說:緩存容器決定一個對象的保存位置是使用Hash算法的,并不會因為緩存項變多而影響性能,更有趣的是ASP.NET的Cache的容器還并非只有一個,它能隨著CPU的數(shù)量而調(diào)整,看這個架式,應(yīng)該在設(shè)計Cache時還想到了高并發(fā)訪問的性能問題。如果這時你還在統(tǒng)計緩存數(shù)量并手工釋放某些緩存項,我只能說您在寫損害性能的代碼。
HttpServerUtility , HttpUtility
不要覺得奇怪,這次我一下子請了二個對象出場了。由于HttpServerUtility的實例通常以Server的屬性公開,但它的提供一些Encode, Decode方法其實調(diào)用的是HttpUtility類的靜態(tài)方法。所以我就把它們倆一起請出來了。
HttpUtility公開了一些靜態(tài)方法,如:
HtmlEncode(),應(yīng)該是使用頻率比較高的方法,用于防止注入攻擊,它負責(zé)安全地生成一段HTML代碼。
有時我們還需要生成一個URL,那么UrlEncode()方法就能派上用場了,因為URL中并不能包含所有字符,所以要做相應(yīng)的編碼。
HttpUtility還有一個方法HtmlAttributeEncode(),它也是用于防止注入攻擊,安全地輸出一個HTML屬性。
在.net4中,HttpUtility還提供了另一個方法:JavaScriptStringEncode(),也是為了防止注入攻擊,安全地在服務(wù)端輸出一段JS代碼。
HttpUtility還公開了一些靜態(tài)方法,如:
HtmlDecode(), UrlDecode(),通常來說,我們并不需要使用它們。尤其是UrlDecode ,除非您要自己的框架,一般來說,在我們訪問QueryString, Form時,已經(jīng)做過UrlDecode了,您就不用再去調(diào)用了。
HttpServerUtility除了公開了比較常用的Encode, Decode方法外,還公開了一個非常有用的方法:Execute(),是的,它非常有用,尤其是您需要在服務(wù)端獲取一個頁面或者用戶控件的HTML輸出時。如果您對這個功能有興趣可以參考我的博客:【我的Ajax服務(wù)端框架 - (4) JS直接請求ascx用戶控件】
HttpRequest
現(xiàn)在總算輪到第一個核心對象出場了。MSDN給它作了一個簡短的解釋:“使 ASP.NET 能夠讀取客戶端在 Web 請求期間發(fā)送的 HTTP 值。”
這個解釋還算是到位的。HttpRequest的實例包含了所有來自客戶端的所有數(shù)據(jù),我們可以把這些數(shù)據(jù)看成是輸入數(shù)據(jù), Handler以及Module就相當(dāng)于是處理過程,HttpResponse就是輸出了。
在HttpRequest包含的所有輸入數(shù)據(jù)中,有我們經(jīng)常使用的QueryString, Form, Cookie,它還允許我們訪問一些HTTP請求頭、瀏覽器的相關(guān)信息、請求映射的相關(guān)文件路徑、URL詳細信息、請求的方法、請求是否已經(jīng)過身份驗證,是否為SSL等等。
HttpRequest的公開屬性絕大部分都是比較重要的,這里就簡單地列舉一下吧。
- // 獲取服務(wù)器上 ASP.NET 應(yīng)用程序的虛擬應(yīng)用程序根路徑。
- public string ApplicationPath { get;}
- // 獲取應(yīng)用程序根的虛擬路徑,并通過對應(yīng)用程序根使用波形符 (~) 表示法(例如,以“~/page.aspx”的形式)使該路徑成為相對路徑。
- public string AppRelativeCurrentExecutionFilePath { get;}
- // 獲取或設(shè)置有關(guān)正在請求的客戶端的瀏覽器功能的信息。
- public HttpBrowserCapabilities Browser { get;set;}
- // 獲取客戶端發(fā)送的 cookie 的集合。
- public HttpCookieCollection Cookies { get;}
- // 獲取當(dāng)前請求的虛擬路徑。
- public string FilePath { get;}
- // 獲取采用多部分 MIME 格式的由客戶端上載的文件的集合。
- public HttpFileCollection Files { get;}
- // 獲取或設(shè)置在讀取當(dāng)前輸入流時要使用的篩選器。
- public Stream Filter { get;set;}
- // 獲取窗體變量集合。
- public NameValueCollection Form { get;}
- // 獲取 HTTP 頭集合。
- public NameValueCollection Headers { get;}
- // 獲取客戶端使用的 HTTP 數(shù)據(jù)傳輸方法(如 GET、POST 或 HEAD)。
- public string HttpMethod { get;}
- // 獲取傳入的 HTTP 實體主體的內(nèi)容。
- public Stream InputStream { get;}
- // 獲取一個值,該值指示是否驗證了請求。
- public bool IsAuthenticated { get;}
- // 獲取當(dāng)前請求的虛擬路徑。
- public string Path { get;}
- // 獲取 HTTP 查詢字符串變量集合。
- public NameValueCollection QueryString { get;}
- // 獲取當(dāng)前請求的原始 URL。
- public string RawUrl { get;}
- // 獲取有關(guān)當(dāng)前請求的 URL 的信息。
- public Uri Url { get;}
- // 從 QueryString、Form、Cookies 或 ServerVariables 集合中獲取指定的對象。
- public string this[string key] { get;}
- // 將指定的虛擬路徑映射到物理路徑。
- // 參數(shù): virtualPath: 當(dāng)前請求的虛擬路徑(絕對路徑或相對路徑)。
- // 返回結(jié)果: 由 virtualPath 指定的服務(wù)器物理路徑。
- public string MapPath(string virtualPath);
下面我來說說一些不被人注意的細節(jié)。
HttpRequest的QueryString, Form屬性的類型都是NameValueCollection,它個集合類型有一個特點:允許在一個鍵下存儲多個字符串值。
以下代碼演示了這個特殊的現(xiàn)象:
- protected void Page_Load(object sender, EventArgs e)
- {
- string[] allkeys = Request.QueryString.AllKeys;
- if( allkeys.Length == 0 )
- Response.Redirect(
- Request.RawUrl + "?aa=1&bb=2&cc=3&aa=" + HttpUtility.UrlEncode("5,6,7"), true);
- StringBuilder sb = new StringBuilder();
- foreach( string key in allkeys )
- sb.AppendFormat("{0} = {1}<br />",
- HttpUtility.HtmlEncode(key), HttpUtility.HtmlEncode(Request.QueryString[key]));
- this.labResult.Text = sb.ToString();
- }
頁面最終顯示結(jié)果如下(注意鍵值為aa的結(jié)果):
說明:
1. HttpUtility.ParseQueryString(string)這個靜態(tài)方法能幫助我們解析一個URL字符串,返回的結(jié)果也是NameValueCollection類型。
2. NameValueCollection是一個不區(qū)分大小寫的集合。
HttpRequest有一個Cookies屬性,MSDN給它的解釋是:“獲取客戶端發(fā)送的 Cookie 的集合。”,這次MSDN的解釋就不完全準(zhǔn)確了。
請看如下代碼:
- protected void Page_Load(object sender, EventArgs e)
- {
- string key = "Key1";
- HttpCookie c = new HttpCookie(key, DateTime.Now.ToString());
- Response.Cookies.Add(c);
- HttpCookie cookie = Request.Cookies[key];
- if( cookie != null )
- this.labResult.Text = cookie.Value;
- Response.Cookies.Remove(key);
- }
這段代碼的運行結(jié)果就是【能顯示當(dāng)前時間】,我就不貼圖了。
如果寫成如下形式:
- protected void Page_Load(object sender, EventArgs e)
- {
- string key = "Key1";
- HttpCookie cookie = Request.Cookies[key];
- if( cookie != null )
- this.labResult.Text = cookie.Value;
- HttpCookie c = new HttpCookie(key, DateTime.Now.ToString());
- Response.Cookies.Add(c);
- Response.Cookies.Remove(key);
- }
此時就讀不到Cookie了。這也提示我們:Cookie的讀寫次序可能會影響我們的某些判斷。
HttpRequest還有二個用于方便獲取HTTP數(shù)據(jù)的屬性Params,Item ,后者是個默認的索引器。
這二個屬性都可以讓我們方便地根據(jù)一個KEY去【同時搜索】QueryString、Form、Cookies 或 ServerVariables這4個集合。通常如果請求是用GET方法發(fā)出的,那我們一般是訪問QueryString去獲取用戶的數(shù)據(jù),如果請求是用POST方法提交的,我們一般使用Form去訪問用戶提交的表單數(shù)據(jù)。而使用Params,Item可以讓我們在寫代碼時不必區(qū)分是GET還是POST。這二個屬性唯一不同的是:Item是依次訪問這4個集合,找到就返回結(jié)果,而Params是在訪問時,先將4個集合的數(shù)據(jù)合并到一個新集合(集合不存在時創(chuàng)建),然后再查找指定的結(jié)果。
為了更清楚地演示這們的差別,請看以下示例代碼:
- <body>
- <p>Item結(jié)果:<%= this.ItemValue %></p>
- <p>Params結(jié)果:<%= this.ParamsValue %></p>
- <hr />
- <form action="<%= Request.RawUrl %>" method="post">
- <input type="text" name="name" value="123" />
- <input type="submit" value="提交" />
- </form>
- </body>
- public partial class ShowItem : System.Web.UI.Page
- {
- protected string ItemValue;
- protected string ParamsValue;
- protected void Page_Load(object sender, EventArgs e)
- {
- string[] allkeys = Request.QueryString.AllKeys;
- if( allkeys.Length == 0 )
- Response.Redirect("ShowItem.aspx?name=abc", true);
- ItemValue = Request["name"];
- ParamsValue = Request.Params["name"];
- }
- }
頁面在未提交前瀏覽器的顯示:
點擊提交按鈕后,瀏覽器的顯示:
差別很明顯,我也不多說了。說下我的建議吧:盡量不要使用Params,不光是上面的結(jié)果導(dǎo)致的判斷問題,沒必要多創(chuàng)建一個集合出來吧,而且更糟糕的是寫Cookie后,也會更新集合。
HttpRequest還有二個很【低調(diào)】的屬性:InputStream, Filter ,這二位的能量很巨大,卻不經(jīng)常被人用到。
HttpResponse也有這二個對應(yīng)的屬性,本文的后面部分將向您展示它們的強大功能。
HttpResponse
我們處理HTTP請求的最終目的只有一個:向客戶端返回結(jié)果。而所有需要向客戶端返回的操作都要調(diào)用HttpResponse的方法。它提供的功能集中在操作HTTP響應(yīng)部分,如:響應(yīng)流,響應(yīng)頭。
我把一些認為很重要的成員簡單列舉了一下:
- // 獲取網(wǎng)頁的緩存策略(過期時間、保密性、變化子句)。
- public HttpCachePolicy Cache { get;}
- // 獲取或設(shè)置輸出流的 HTTP MIME 類型。默認值為“text/html”。
- public string ContentType { get;set;}
- // 獲取響應(yīng) Cookie 集合。
- public HttpCookieCollection Cookies { get;}
- // 獲取或設(shè)置一個包裝篩選器對象,該對象用于在傳輸之前修改 HTTP 實體主體。
- public Stream Filter { get;set;}
- // 啟用到輸出 Http 內(nèi)容主體的二進制輸出。
- public Stream OutputStream { get;}
- // 獲取或設(shè)置返回給客戶端的輸出的 HTTP 狀態(tài)代碼。默認值為 200 (OK)。
- public int StatusCode { get;set;}
- // 將 HTTP 頭添加到輸出流。
- public void AppendHeader(string name, string value);
- // 將當(dāng)前所有緩沖的輸出發(fā)送到客戶端,停止該頁的執(zhí)行,并引發(fā)EndRequest事件。
- public void End();
- // 將客戶端重定向到新的 URL。指定新的 URL 并指定當(dāng)前頁的執(zhí)行是否應(yīng)終止。
- public void Redirect(string url, bool endResponse);
- // 將指定的文件直接寫入 HTTP 響應(yīng)輸出流,而不在內(nèi)存中緩沖該文件。
- public void TransmitFile(string filename);
- // 將 System.Object 寫入 HTTP 響應(yīng)流。
- public void Write(object obj);
這些成員都有簡單的解釋,應(yīng)該了解它們。
這里請關(guān)注一下屬性StatusCode。我們經(jīng)常用JQuery來實現(xiàn)Ajax,比如:使用ajax()函數(shù),雖然你可以設(shè)置error回調(diào)函數(shù),但是,極有可能在服務(wù)端即使拋黃頁了,也不會觸發(fā)這個回調(diào)函數(shù),除非是設(shè)置了dataType="json",這時在解析失敗時,才會觸發(fā)這個回調(diào)函數(shù),如果是dataType="html",就算是黃頁了,也能【正常顯示】。
怎么辦?在服務(wù)端發(fā)生異常不能返回正確結(jié)果時,請設(shè)置StatusCode屬性,比如:Response.StatusCode = 500;
HttpContext
終于輪到大人物出場了。
應(yīng)該可以這么說:有了HttpRequest, HttpResponse分別控制了輸入輸出,就應(yīng)該沒有更重要的東西了。但我們用的都是HttpRequest, HttpResponse的實例,它們在哪里創(chuàng)建的呢,哪里保存有它們最原始的引用呢?答案當(dāng)然是:HttpContext 。沒有老子哪有兒子,就這么個關(guān)系。更關(guān)鍵的是:這個老子還很牛,【在任何地方都能找到它】,而且我前面提到另二個實力不錯的選手(HttpServerUtility和Cache),也都是它的手下。因此,任何事情,找到它就算是有辦法了。你說它是不是最牛。
不僅如此,在ASP.NET的世界,還有黑白二派。Module像個土匪,什么請求都要去“檢查”一下,Handler更像白道上的人物,點名了只做某某事。有趣的是:HttpContext真像個大人物,黑白道的人物有時都要找它幫忙。幫什么忙呢?可憐的土匪沒有倉庫,它有東西沒地方存放,只能存放在HttpContext那里,有時惹得Handler也盯上了它,去HttpContext去拿土匪的戰(zhàn)利品。
這位大人物的傳奇故事大致就這樣。我們再來從技術(shù)的角度來觀察它的功能。
雖然HttpContext也公開了一些屬性和方法,但我認為最重要的還是上面提到的那些對象的引用。
這里再補充二個上面沒提到的實例屬性:User, Items
User屬性保存于當(dāng)前請求的用戶身份信息。如果判斷當(dāng)前請求的用戶是不是已經(jīng)過身份認證,可以訪問:Request.IsAuthenticated這個實例屬性。
前面我在故事中提到:“可憐的土匪沒有倉庫,它有東西沒地方存放,只能存放在HttpContext那里”,其實這些東西就是保存在Items屬性中。這是個字典,因此適合以Key/Value的方式來訪問。如果希望在一次請求的過程中保存一些臨時數(shù)據(jù),那么,這個屬性是最理想的存放容器了。它會在下次請求重新創(chuàng)建,因此,不同的請求之間,數(shù)據(jù)不會被共享。
如果希望提供一些靜態(tài)屬性,并且,只希望與一次請求關(guān)聯(lián),那么建議借助HttpContext.Items的實例屬性來實現(xiàn)。
我曾經(jīng)見過有人用ThreadStaticAttribute來實現(xiàn)這個功能,然后在Page.Init事件中去修改那個字段。
哎,哥啊,MSDN上說:【用 ThreadStaticAttribute 標(biāo)記的 static 字段不在線程之間共享。每個執(zhí)行線程都有單獨的字段實例,并且獨立地設(shè)置及獲取該字段的值。如果在不同的線程中訪問該字段,則該字段將包含不同的值?!?注意了:一個線程可以執(zhí)行多次請求過程,且Page.Init事件在ASP.NET的管道中屬于較中間的事件啊,要是請求不使用Page呢,您再想想吧。
前面我提到HttpContext有種超能力:【在任何地方都能找到它】,是的,HttpContext有個靜態(tài)屬性Current,你說是不是【在任何地方都能找到它】。千萬別小看這個屬性,沒有它,HttpContext根本牛不起來。
也正是因為這個屬性,在ASP.NET的世界里,您可以在任何地方訪問Request, Response, Server, Cache,還能在任何地方將一些與請求有關(guān)的臨時數(shù)據(jù)保存起來,這絕對是個非常強大的功能。Module的在不同的事件階段,以及與Handler的”溝通“有時就通過這個方式來完成。
還記得我上篇博客【Session,有沒有必要使用它?】中提到的事情嗎:每個頁面使用Session的方式是使用Page指令來說明的,但Session是由SessionStateModule來實現(xiàn)的, SessionStateModule會處理所有的請求,所以,它不知道當(dāng)前要請求的要如何使用Session,但是,HttpContext提供了一個屬性Handler讓它們之間有機會溝通,才能處理這個問題。
這個例子反映了Module與Handler溝通的方式,我再來舉個Module自身溝通的例子,就說UrlRoutingModule吧,它訂閱了二個事件:
- protected virtual void Init(HttpApplication application)
- {
- application.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache);
- application.PostMapRequestHandler += new EventHandler(this.OnApplicationPostMapRequestHandler);
- }
在OnApplicationPostResolveRequestCache方法中,最終做了以下調(diào)用:
- public virtual void PostResolveRequestCache(HttpContextBase context)
- {
- // ...............
- RequestData data2 = new RequestData {
- OriginalPath = context.Request.Path,
- HttpHandler = httpHandler
- };
- context.Items[_requestDataKey] = data2;
- context.RewritePath("~/UrlRouting.axd");
- }
再來看看OnApplicationPostMapRequestHandler方法中,最終做了以下調(diào)用:
- public virtual void PostMapRequestHandler(HttpContextBase context)
- {
- RequestData data = (RequestData)context.Items[_requestDataKey];
- if( data != null ) {
- context.RewritePath(data.OriginalPath);
- context.Handler = data.HttpHandler;
- }
- }
看到了嗎,HttpContext.Items為Module在不同的事件中保存了臨時數(shù)據(jù),而且很方便。
強大的背后也有麻煩事
前面我們看到了HttpContext的強大,而且還提供HttpContext.Current這個靜態(tài)屬性。這樣一來,的確是【在任何地方都能找到它】。想想我們能做什么?我們可以在任何一個類庫中都可以訪問QueryString, Form,夠靈活吧。我們還可以在任何地方(比如BLL中)調(diào)用Response.Redirect()讓請求重定向,是不是很強大?
不過,有個很現(xiàn)實的問題擺在面前:到處訪問這些對象會讓代碼很難測試。原因很簡單:在測試時,這些對象沒法正常工作,因為HttpRuntime很多幕后的事情還沒做,沒有運行它們的環(huán)境。是不是很掃興?沒辦法,現(xiàn)在的測試水平很難駕馭這些功能強大的對象。
很多人都說WebForms框架搞得代碼沒法測試,通常也是的確如此。
我看到很多人在頁面的CodeFile中寫了一大堆的控件操作代碼,還混有很多調(diào)用業(yè)務(wù)邏輯的代碼,甚至在類庫項目中還中訪問QueryString, Cookie。再加上諸如ViewState, Session這類【有狀態(tài)】的東西大量使用,這樣的代碼是很難測試。
換個視角,看看MVC框架為什么說可測試性會好很多,理由很簡單,你很少會需要使用HttpRequest, HttpRespons,從Controller開始,您需要的數(shù)據(jù)已經(jīng)給您準(zhǔn)備好了,直接用就可以了。但MVC框架并不能保證寫的代碼就一定能方便的測試,比如:您繼續(xù)使用HttpContext.Current.XXXXX而不使用那些HttpXxxxxBase對象。
一般說來,很多人會采用三層或者多層的方式來組織他們的項目代碼。此時,如果您希望您的核心代碼是可測試的,并且確實需要使用這些對象,那么應(yīng)該盡量集中使用這些強大的對象,應(yīng)該在最靠近UI層的地方去訪問它們。可以把調(diào)用業(yè)務(wù)邏輯的代碼再提取到一個單獨的層中,比如就叫“服務(wù)層”吧,由服務(wù)層去調(diào)用下面的BLL(假設(shè)BLL的API的粒度較小),服務(wù)層由表示層調(diào)用,調(diào)用服務(wù)層的參數(shù)由表示層從HttpRequest中取得。需要操作Response對象時,比如:重定向這類操作,則應(yīng)該在表示層中完成。
記?。褐挥斜硎緦硬拍茉L問前面提到的對象,而且要讓表示層盡量簡單,簡單到不需要測試,真正需要測試的代碼(與業(yè)務(wù)邏輯有關(guān))放在表示層以下。如此設(shè)計,您的表示層將非常簡單,以至于不用測試(MVC框架中的View也能包含代碼,但也沒法測試,是一樣的道理)。甚至,服務(wù)層還可以單獨部署。
如果您的項目真的采用分層的設(shè)計,那么,就應(yīng)該可以讓界面與業(yè)務(wù)處理分離。比如您可以這樣設(shè)計:
1. 表示層只處理輸入輸出的事情,它應(yīng)該僅負責(zé)與用戶的交互處理,建議這層代碼簡單到可以忽略測試。
2. 處理請求由UI層以下的邏輯層來完成,它負責(zé)請求的具體實現(xiàn)過程,它的方法參數(shù)來自于表示層。
為了檢驗?zāi)姆謱釉O(shè)計是否符合這個原則,有個很簡單的方法:
寫個console小程序模擬UI層調(diào)用下層方法,能正常運行,就說明您的分層是正確的,否則,建議改進它們。
換一種方式使用ASP.NET框架
前面我提到HttpRequest有個InputStream屬性, HttpResponse有一個OutputStream屬性,它們對應(yīng)的是輸入輸出流。直接使用它們,我們可以非常簡單地提供一些服務(wù)功能,比如:我希望直接使用JSON格式來請求和應(yīng)答。如果采用這種方案來設(shè)計,我們只需要定義好輸入輸出的數(shù)據(jù)結(jié)構(gòu),并使用這們來傳輸數(shù)據(jù)就好了。當(dāng)然了,也有其它的方法能實現(xiàn),但它們不是本文的主題,我也比較喜歡這種簡單又直觀地方式來解決某些問題。
2007年我做過一個短信的接口,人家就提供幾個URL做為服務(wù)的地址,調(diào)用參數(shù)以及返回值就直接通過HTTP請求一起傳遞。
2009年做過一個項目是調(diào)用Experian Precise ID服務(wù)(Java寫的),那個服務(wù)也直接使用HTTP協(xié)議,數(shù)據(jù)格式采用XML,輸出輸入的數(shù)據(jù)結(jié)構(gòu)由他們定義的自定義類型。
2010年,我做過一個數(shù)據(jù)訪問層服務(wù),與C++的客戶端通信,采用ASP.NET加JSON數(shù)據(jù)格式的方式。
基本上這三個項目都有一個共同點:直接使用HTTP協(xié)議,數(shù)據(jù)結(jié)構(gòu)有著明確的定義格式,直接隨HTTP一起傳遞。就這么簡單,卻非常有用,而且適用性很廣,基本上什么語言都能很好地相互調(diào)用。
下面我以一個簡單的示例演示這二個屬性的強大之處。
在示例中,服務(wù)端要求數(shù)據(jù)的輸入輸出采用JSON格式,服務(wù)的功能是一個訂單查詢功能,輸入輸出的類型定義如下:
- // 查詢訂單的輸入?yún)?shù)
- public sealed class QueryOrderCondition
- {
- public int? OrderId;
- public int? CustomerId;
- public DateTime StartDate;
- public DateTime EndDate;
- }
- // 查詢訂單的輸出參數(shù)類型
- public sealed class Order
- {
- public int OrderID { get;set;}
- public int CustomerID { get;set;}
- public string CustomerName { get;set;}
- public DateTime OrderDate { get;set;}
- public double SumMoney { get;set;}
- public string Comment { get;set;}
- public bool Finished { get;set;}
- public List<OrderDetail>Detail { get;set;}
- }
- public sealed class OrderDetail
- {
- public int OrderID { get;set;}
- public int Quantity { get;set;}
- public int ProductID { get;set;}
- public string ProductName { get;set;}
- public string Unit { get;set;}
- public double UnitPrice { get;set;}
- }
服務(wù)端的實現(xiàn):創(chuàng)建一個QueryOrderService.ashx,具體實現(xiàn)代碼如下:
- public class QueryOrderService : IHttpHandler
- {
- public void ProcessRequest(HttpContext context)
- {
- context.Response.ContentType = "application/json";
- string input = null;
- JavaScriptSerializer jss = new JavaScriptSerializer();
- using( StreamReader sr = new StreamReader(context.Request.InputStream) ) {
- input = sr.ReadToEnd();
- }
- QueryOrderCondition query = jss.Deserialize<QueryOrderCondition>(input);
- // 模擬查詢過程,這里就直接返回一個列表。
- List<Order>list = new List<Order>();
- for( int i = 0;i <10;i++ )
- list.Add(DataFactory.CreateRandomOrder());
- string json = jss.Serialize(list);
- context.Response.Write(json);
- }
代碼很簡單,經(jīng)過了以下幾個步驟:
1. 從Request.InputStream中讀取客戶端發(fā)送過來的JSON字符串,
2. 反序列化成需要的輸入?yún)?shù),
3. 執(zhí)行查詢訂單的操作,生成結(jié)果數(shù)據(jù),
4. 將結(jié)果做JSON序列化,轉(zhuǎn)成字符串,
5. 寫入到響應(yīng)流。
很簡單吧,我可以把它看作是一個服務(wù)吧,但它沒有其它服務(wù)框架的種種約束,而且相當(dāng)靈活,比如我可以讓服務(wù)采用GZIP的方式來壓縮傳輸數(shù)據(jù):
- public void ProcessRequest(HttpContext context)
- {
- context.Response.ContentType = "application/json";
- string input = null;
- JavaScriptSerializer jss = new JavaScriptSerializer();
- using( GZipStream gzip = new GZipStream(context.Request.InputStream, CompressionMode.Decompress) ) {
- using( StreamReader sr = new StreamReader(gzip) ) {
- input = sr.ReadToEnd();
- }
- }
- QueryOrderCondition query = jss.Deserialize<QueryOrderCondition>(input);
- // 模擬查詢過程,這里就直接返回一個列表。
- List<Order>list = new List<Order>();
- for( int i = 0;i <10;i++ )
- list.Add(DataFactory.CreateRandomOrder());
- string json = jss.Serialize(list);
- using( GZipStream gzip = new GZipStream(context.Response.OutputStream, CompressionMode.Compress) ) {
- using( StreamWriter sw = new StreamWriter(gzip) ) {
- context.Response.AppendHeader("Content-Encoding", "gzip");
- sw.Write(json);
- }
- }
- }
修改也很直觀,在輸入輸出的地方,加上Gzip的操作就可以了。
如果您想加密傳輸內(nèi)容,也可以在讀寫之間做相應(yīng)的處理,或者,想換個序列化方式,也簡單,我想您應(yīng)該懂的。
總之,如何讀寫數(shù)據(jù),全由您來決定。喜歡怎樣處理就怎樣處理,這就是自由。
不僅如此,我還可以讓服務(wù)端判斷客戶端是否要求使用GZIP方式來傳輸數(shù)據(jù),如果客戶端要求使用GZIP壓縮,服務(wù)就自動適應(yīng),最后把結(jié)果也做GZIP壓縮處理,是不是更酷?
- public void ProcessRequest(HttpContext context)
- {
- context.Response.ContentType = "application/json";
- string input = null;
- JavaScriptSerializer jss = new JavaScriptSerializer();
- bool enableGzip = (context.Request.Headers["Content-Encoding"] == "gzip");
- if( enableGzip )
- context.Request.Filter = new GZipStream(context.Request.Filter, CompressionMode.Decompress);
- using( StreamReader sr = new StreamReadercontext.Request.InputStream) ) {
- input = sr.ReadToEnd();
- }
- QueryOrderCondition query = jss.Deserialize<QueryOrderCondition>(input);
- // 模擬查詢過程,這里就直接返回一個列表。
- List<Order>list = new List<Order>();
- for( int i = 0;i <10;i++ )
- list.Add(DataFactory.CreateRandomOrder());
- string json = jss.Serialize(list);
- if( enableGzip ) {
- context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
- context.Response.AppendHeader("Content-Encoding", "gzip");
- }
- context.Response.Write(json);
- }
注意:這次我為了不想寫二套代碼,使用了Request.Filter屬性。前面我就說過這是個功能強大的屬性。這個屬性實現(xiàn)的效果就是裝飾器模式,因此您可以繼續(xù)對輸入輸出流進行【裝飾】,但是要保證輸入和輸出的裝飾順序要相反。所以使用多次裝飾后,會把事情搞復(fù)雜,因此,建議需要多次裝飾時,做個封裝可能會好些。不過,這個屬性的更強大之處或許在這里體現(xiàn)的并不明顯,要談它的強大之處已不是本文的主題,我以后再說。
想想:我這幾行代碼與此服務(wù)完全沒有關(guān)系,而且照這種做法,每個服務(wù)都要寫一遍,是不是太麻煩了?
- bool enableGzip = (context.Request.Headers["Content-Encoding"] == "gzip");
- if( enableGzip )
- context.Request.Filter = new GZipStream(context.Request.Filter, CompressionMode.Decompress);
- // .............................................................
- if( enableGzip ) {
- context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
- context.Response.AppendHeader("Content-Encoding", "gzip");
- }
其實,豈止是這一個地方麻煩。照這種做法,每個服務(wù)都要創(chuàng)建一個ahsx文件,讀輸入,寫輸出,也是重復(fù)勞動。但是,如何改進這些地方,就不是本文的主題了,我將在后面的博客中改進它們。今天的主題是展示這些對象的強大功能。
從以上的示例中,您有沒有發(fā)現(xiàn):只要使用這幾個對象就可以實現(xiàn)一個服務(wù)所必需的基礎(chǔ)功能!
在后續(xù)博客中,我將引入其它一些ASP.NET的基礎(chǔ)對象,并把本次實現(xiàn)的一部分處理抽取出來,實現(xiàn)一個簡單的服務(wù)框架。有興趣的同學(xué),可以繼續(xù)關(guān)注。
每個對象都是一個不朽的傳奇,每個傳奇背后都有一個精彩的故事。
我是Fish Li, 感謝大家閱讀我的博客,請繼續(xù)關(guān)注我的后續(xù)博客。
原文:http://www.cnblogs.com/fish-li/archive/2011/08/21/2148640.html
【編輯推薦】