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

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

想當初在只使用WebForms框架並以服務端爲中心的開發模式時,發現Asp.net好複雜。一大堆服務端控件,各有各的使用方法, 有些控件的事件也很重要,必須在合適地時機去響應,還真有些複雜。後來逐漸發現這些複雜的根源其實就是服務器控件相關的抽象邏輯。 隨着Ajax越用越多,可能有些人也作過這些事情:【新建一個ashx文件,讀取一些用戶的輸入數據,Form, QueryString, 而後調用業務邏輯代碼,將處理後的結果序列化成JSON字符串再發給客戶端】,這樣也能完成一次請求。 不知你們有沒有作過這類事情,反正我是作過的。慢慢地,我也嫌煩了,這些事情中除了調用業務邏輯部分, 都是些體力活嘛。因而想,寫點代碼把這些事情交給它們去作吧,我只處理與請求有關的數據處理就行了。 終於,我寫了個簡陋的框架,並自稱爲【個人Ajax服務端框架】 以及【個人MVC框架】。 寫完這些東西后,發現Asp.net的東西變少了,可是仍能夠實現不少功能。ajax

其實,咱們能夠從另外一角度來看Asp.net,它就是一個底層框架平臺,它負責接收HTTP請求(從IIS傳入),將請求分配給一個線程, 再把請求放到它的處理管道中,由一些其它的【管道事件訂閱者】來處理它們,最後將處理結果返回給客戶端。 而WebForms或者MVC框架,都屬於Asp.net平臺上的【管道事件訂閱者】而已,Web Service也是哦。若是你不想受限於WebForms或者MVC框架, 或者您還想用Asp.net作點其它的事情,好比:本身的服務框架,就像WebService那樣。 但但願用其它更簡單的序列化方式來減小網絡流量,或者還有加密要求。 那麼瞭解Asp.net提供了哪些功能就頗有必要了。算法

本文將站在Asp.net平臺的角度,來看看Asp.net的一些基礎功能。雖然不會涉及任何其它上層框架, 但所講述的內容實際上是適合其它上層框架的。json

前面我說到: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的結果):

說明:
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 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的基礎對象,並把本次實現的一部分處理抽取出來,實現一個簡單的服務框架。 有興趣的同窗,能夠繼續關注。

每一個對象都是一個不朽的傳奇,每一個傳奇背後都有一個精彩的故事。
我是Fish Li, 感謝你們閱讀個人博客,請繼續關注個人後續博客。

 

點擊此處下載示例代碼

 
分類:  Asp.net
 
好文要頂  已關注  收藏該文   
 
榮譽: 推薦博客
我在關注他  取消關注
665
9
 
(請您對文章作出評價)
 
相關文章
相關標籤/搜索