天天看點

我心目中的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不提供移除時的通知功能。

這裡我還想說說緩存依賴。我曾經見過一個使用場景:有人從一堆檔案(分為若幹類别)中加載資料到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的結果):

我心目中的Asp.net核心對象

說明:

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"];        
    }
}
      

頁面在未送出前浏覽器的顯示:

我心目中的Asp.net核心對象

點選送出按鈕後,浏覽器的顯示:

我心目中的Asp.net核心對象

差别很明顯,我也不多說了。說下我的建議吧:盡量不要使用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 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);

    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的基礎對象,并把本次實作的一部分處理抽取出來,實作一個簡單的服務架構。 有興趣的同學,可以繼續關注。

每個對象都是一個不朽的傳奇,每個傳奇背後都有一個精彩的故事。

轉自:http://www.cnblogs.com/fish-li/archive/2011/08/21/2148640.html

繼續閱讀