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