鍍金池/ 教程/ C#/ ASP.NET MVC 隨想錄(7)——鋒利的 KATANA
ASP.NET MVC 使用 Bootstrap 系列(4)——使用 JavaScript 插件
ASP.NET MVC 隨想錄(6)——漫談 OWIN
ASP.NET MVC 隨想錄(5)——?jiǎng)?chuàng)建 ASP.NET MVC Bootstrap Helpers
ASP.NET MVC 隨想錄(3)——使用 Bootstrap 組件
ASP.NET MVC 隨想錄(7)——鋒利的 KATANA
ASP.NET MVC 隨想錄(1)——開始使用 Bootstrap
ASP.NET MVC 隨想錄(8)——?jiǎng)?chuàng)建自定義的 Middleware 中間件
ASP.NET MVC 隨想錄(2)——使用 Bootstrap CSS 和 HTML 元素
作者簡介

ASP.NET MVC 隨想錄(7)——鋒利的 KATANA

正如上篇文章所講解的,OWIN 在 Web Server 與 Web Application 之間定義了一套規(guī)范(Specs),意在解耦 Web Server 與 Web Application,從而推進(jìn)跨平臺(tái)的實(shí)現(xiàn)。若要真正使用 OWIN 規(guī)范,那么必須要對他們進(jìn)行實(shí)現(xiàn)。目前有兩個(gè)產(chǎn)品實(shí)現(xiàn)了 OWIN 規(guī)范——一是由微軟主導(dǎo)的 Katana,二是第三方的 Nowin。本文主要關(guān)注的還是 Katana,由微軟團(tuán)隊(duì)主導(dǎo),開源到 CodePlex 上。

在介紹 Katana 之前,我覺得有必要先為大家梳理一下十幾年以來 ASP.NET 發(fā)展歷程。

ASP.NET 發(fā)展歷程

ASP.NET Web Form

ASP.NET Web Form 在 2002 正式發(fā)布時(shí),面向的開發(fā)者主要有兩類:

  • 使用混合 HTML 標(biāo)記和服務(wù)端腳本開發(fā)動(dòng)態(tài)網(wǎng)站的 ASP 開發(fā)者,另外,ASP 運(yùn)行時(shí)抽象了底層的 HTTP 連接和 Web Server,并為開發(fā)者提供了一系列的對象模型用于交互 Http 請求,當(dāng)然也提供了額外的服務(wù)諸如 Session、Cache、State 等。
  • 開發(fā) WinForm 的程序員,他們可能對 HTTP 和 HTML 一無所知,但熟悉拖控件的方式來構(gòu)建應(yīng)用程序。

為了迎合這兩類開發(fā)者,ASP.NET Web Form 通過使用沉重的 ViewState 來保存頁面回傳過程中的狀態(tài)值,因?yàn)?HTTP 協(xié)議是無狀態(tài)的,通過 ViewState,使原本沒有記憶的 Http 協(xié)議變得有記憶起來。這在當(dāng)時(shí)是非常好的設(shè)計(jì),能通過拖拽控件的形式快速開發(fā) Web,而不必過多的去關(guān)注底層原理。同時(shí) ASP.NET 團(tuán)隊(duì)還為 ASP.NET 豐富了更多的功能,諸如:Session、Cache、Configuration 等等。

這在當(dāng)時(shí)無疑是成功的,ASP.NET 的發(fā)布迅速拉攏了開發(fā)者,在 Web 開發(fā)中形成了一股新的勢力,但同時(shí)也買下來一些隱患:

  • 所有的功能、特性都發(fā)布在一個(gè)整體框架上并且緊耦合核心的 Web 抽象庫——System.Web
  • System.Web 是.NET Framework 的重要組成部分,這意味著要修復(fù)更新 System.Web 必須更新.NET Framework,但.NET Framework 是操作系統(tǒng)的基礎(chǔ),為了穩(wěn)定性往往不會(huì)頻繁更新。
  • ASP.NET Framework (System.Web)緊耦合 IIS
  • IIS 只能運(yùn)行在 Windows系統(tǒng)

ASP.NET MVC

由于 Web Form 產(chǎn)生一大堆 ViewState 和客戶端腳本,這對開發(fā)者來說慢慢變成一種累贅,因?yàn)槲覀冎幌氘a(chǎn)生純凈的 HTML 標(biāo)記。所以開發(fā)者更想去主動(dòng)控制而非被動(dòng)產(chǎn)生額外 HTML 標(biāo)記。

所以微軟基于 MVC 設(shè)計(jì)模式推出了其重要的 Web Framework——ASP.NET MVC Framework,通過 Model-View-Control 解耦了業(yè)務(wù)邏輯和表現(xiàn)邏輯,同時(shí)沒有了服務(wù)器端控件,將頁面的控制權(quán)完全交給了開發(fā)者。

為了快速更新迭代,通過 Nuget 來獲取更新,故從.NET Framework 中分離開了。但唯一不足的是,ASP.NET MVC 還是基于 ASP.NET Framework(注:ASP.NET MVC 6 已經(jīng)不依賴 System.Web),所以 Web Application 和 Web Server 依舊沒有解耦。

ASP.NET Web API

隨著時(shí)間的推移,一些問題開始暴露出來了,由于 Web Server 和 Web Application 緊耦合在一起,微軟在開發(fā)獨(dú)立、簡單的 Framework 上越發(fā)捉襟見肘,這和其他平臺(tái)下開源社區(qū)蓬勃發(fā)展形成鮮明對比,幸運(yùn)的是微軟做出了改變,推出了獨(dú)立的 Web Framework ——ASP.NET Web API,它適用于移動(dòng)互聯(lián)網(wǎng)并可以快速通過 Nuget 安裝,更為重要的是,它不依賴 System.Web,也不依賴 IIS,你可以使用 Self-Host 或者在其他 Web Server 部署。

Katana

隨著 Web API 能夠運(yùn)行在自己的輕量級(jí)的宿主中,并且越來越多簡單、模塊化、專一的 Framework 問世,開發(fā)人員有時(shí)候不得不啟動(dòng)單獨(dú)的進(jìn)程來處理 Web 應(yīng)用程序的各種組件(模塊)、如靜態(tài)文件、動(dòng)態(tài)文件、Web API 和 Socket。為了避免進(jìn)程擴(kuò)散,所有的進(jìn)程必須啟動(dòng)、停止并且獨(dú)立進(jìn)行管理。這時(shí),我們需要一個(gè)公共的宿主進(jìn)程來管理這些模塊。

這就是 OWIN 誕生的原因,解耦成最小粒度的組件,然后這些標(biāo)準(zhǔn)化框架和組件可以很容易地插入到 OWIN Pipeline 中,從而對組件進(jìn)行統(tǒng)一管理。而 Katana 正是 OWIN 的實(shí)現(xiàn),為我們提供了豐富的 Host 和 Server。

走進(jìn)Katana的世界

Katana 作為 OWIN 的規(guī)范實(shí)現(xiàn),除了實(shí)現(xiàn) Host 和 Server 之外,還提供了一系列的 API 幫助開發(fā)應(yīng)用程序,其中已經(jīng)包括一些功能組件如身份驗(yàn)證(Authentication)、診斷(Diagnostics)、靜態(tài)文件處理(Static Files)、ASP.NET Web API 和 SignalR 的綁定等。

Katana的基本原則

  • 可移植性:從 HostàServeràMiddleware,每個(gè) Pipeline 中的組件都是可替換的,并且第三方公司和開源項(xiàng)目的 Framework 都是可以在 OWIN Server 上運(yùn)行,也就是說不受平臺(tái)限制,從而實(shí)現(xiàn)跨平臺(tái)。
  • 模塊化:每一個(gè)組件都必須保持足夠獨(dú)立性,通常只做一件事,以混合模塊的形式來滿足實(shí)際的開發(fā)需求
  • 輕量和高效:因?yàn)槊恳粋€(gè)組件都是模塊化開發(fā),而且可以輕松的在 Pipeline 中插拔組件,實(shí)現(xiàn)高效開發(fā)

Katana 體系結(jié)構(gòu)

Katana 實(shí)現(xiàn)了 OWIN 的 Layers,所以 Katana 的體系結(jié)構(gòu)和 OWIN 一致,如下所示:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/2.png" alt="" />

1.)Host :宿主 Host 被 OWIN 規(guī)范定義在第一層(最底層),他的職責(zé)是管理底層的進(jìn)程(啟動(dòng)、關(guān)閉)、初始化 OWIN Pipeline、選擇 Server 運(yùn)行等。

Katana 為我們提供了 3 種選擇:

  • IIS / ASP.NET :使用 IIS 是最簡單和向后兼容方式,在這種場景中 OWIN Pipeline 通過標(biāo)準(zhǔn)的 HttpModule 和 HttpHandler 啟動(dòng)。使用此 Host 你必須使用 System.Web 作為 OWIN Server
  • Custom Host :如果你想要使用其他 Server 來替換掉 System.Web,并且可以有更多的控制權(quán),那么你可以選擇創(chuàng)建一個(gè)自定義宿主,如使用 Windows Service、控制臺(tái)應(yīng)用程序、Winform 來承載Server。
  • OwinHost :如果你對上面兩種 Host 還不滿意,那么最后一個(gè)選擇是使用 Katana 提供的OwinHost.exe:他是一個(gè)命令行應(yīng)用程序,運(yùn)行在項(xiàng)目的根部,啟動(dòng) HttpListener Server并找到基于約束的 Startup 啟動(dòng)項(xiàng)。OwinHost 提供了命令行選項(xiàng)來自定義他的行為,比如:手動(dòng)指定 Startup 啟動(dòng)項(xiàng)或者使用其他 Server(如果你不需要默認(rèn)的 HttpListener Server)。

2.)Server

Host 之后的 Layer 被稱為 Server,他負(fù)責(zé)打開套接字并監(jiān)聽 Http 請求,一旦請求到達(dá),根據(jù)Http 請求來構(gòu)建符合 OWIN 規(guī)范的 Environment Dictionary(環(huán)境字典)并將它發(fā)送到 Pipeline 中交由 Middleware 處理。Katana 對 OWIN Server 的實(shí)現(xiàn)分為如下幾類:

  • System.Web:如前所述那樣,System.Web 和 IIS/ASP.NET Host 兩者彼此耦合,當(dāng)你選擇使用System.Web 作為 Server ,Katana System.Web Server 把自己注冊為 HttpModule 和HttpHandler 并且處理發(fā)送給 IIS 的請求,最后將 HttpRequest、HttpResponse 對象映射為 OWIN 環(huán)境字典并將它發(fā)送至 Pipeline 中處理。
  • HttpListener:這是 OwinHost.exe 和自定義 Host 默認(rèn)的 Server。
  • WebListener:這是 ASP.NET vNext 默認(rèn)的輕量級(jí) Server,他目前無法使用在 Katana 中

3)Middleware

Middleware(中間件)位于 Host、Server 之后,用來處理 Pipeline 中的請求,Middleware 可以理解為實(shí)現(xiàn)了 OWIN 應(yīng)用程序委托 AppFun 的組件。

Middleware 處理請求之后并可以交由下一個(gè) Pipeline 中的 Middleware 組件處理,即鏈?zhǔn)教幚碚埱?,通過環(huán)境字典可以獲取到所有的 Http 請求數(shù)據(jù)和自定義數(shù)據(jù)。Middleware 可以是簡單的 Log 組件,亦可以為復(fù)雜的大型 Web Framework,諸如:ASP.NET Web API、Nancy、SignlR 等,如下圖所示:Pipeline 中的 Middleware 用來處理請求:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/3.png" alt="" />

4.)Application

最后一層即為 Application,是具體的代碼實(shí)現(xiàn),比如 ASP.NET Web API、SignalR 具體代碼的實(shí)現(xiàn)。

現(xiàn)在,我想你應(yīng)該了解了什么事 Katana 以及 Katana 的基本原則和體系結(jié)構(gòu),那么現(xiàn)在就是具體應(yīng)用到實(shí)際當(dāng)中去了。

使用 ASP.NET/IIS 托管 Katana-based 應(yīng)用程序

在 Startup 的 Configuration 方法中實(shí)現(xiàn) OWIN Pipeline 處理邏輯,如下代碼所示:

public class Startup
{
    public void Configuration(IAppBuilder app)

    {
        app.Run(context =>

        {

            context.Response.ContentType = "text/plain";

            return context.Response.WriteAsync("Hello World");

        });

    }

}

app.Run 方法將一個(gè)接受 IOwinContext 對象最為輸入?yún)?shù)并返回 Task 的 Lambda 表達(dá)式作為 OWIN Pipeline 的最后處理步驟,IOwinContext 強(qiáng)類型對象是對 Environment Dictionary 的封裝,然后異步輸出"Hello World"字符串。

細(xì)心的你可能觀察到,在 Nuget 安裝 Microsoft.Owin.Host.SystemWeb 程序集時(shí),默認(rèn)安裝了依賴項(xiàng) Microsoft.Owin 程序集,正式它為我們提供了擴(kuò)展方法 Run 和 IOwinContext 接口,當(dāng)然我們也可以使用最原始的方式來輸出"Hello World"字符串,即 Owin 程序集為我們提供的最原始方式,這僅僅是學(xué)習(xí)上參考,雖然我們不會(huì)在正式場景下使用:

using AppFunc = Func<IDictionary<string, object>, Task>;

public class Startup

{

    public void Configuration(IAppBuilder app)

    {

        app.Use(new Func<AppFunc, AppFunc>(next => (env =>

        {

            string text = "Hello World";

            var response = env["owin.ResponseBody"] as Stream;

            var headers = env["owin.ResponseHeaders"] as IDictionary<string, string[]>;

            headers["Content-Type"] = new[] { "text/plain" };

            return response.WriteAsync(Encoding.UTF8.GetBytes(text), 0, text.Length);

        })));

    }

}

使用自定義 Host(self-host)托管 Katana-based 應(yīng)用程序

使用自定義 Host 托管 Katana 應(yīng)用程序與使用 IIS 托管差別不大,你可以使用控制臺(tái)、WinForm、WPF 等實(shí)現(xiàn)托管,但要記住,這會(huì)失去 IIS 帶有的一些功能(SSL、Event Log、Diagnostics、Management…),當(dāng)然這可以自己來實(shí)現(xiàn)。

  • 創(chuàng)建控制臺(tái)應(yīng)用程序*
  • Install-Package Microsoft.Owin.SelfHost
  • 在 Main 方法中使用 Startup 配置項(xiàng)構(gòu)建 Pipeline 并監(jiān)聽端口

      using (WebApp.Start("http://localhost:10002"))  
      {
    
         System.Console.WriteLine("啟動(dòng)站點(diǎn):http://localhost:10002");
    
         System.Console.ReadLine();
    
      }

使用自定義的 Host 將失去 IIS 的一些功能,當(dāng)然我們可以自己去實(shí)現(xiàn)。幸運(yùn)的是,Katana 為我們默認(rèn)實(shí)現(xiàn)了部分功能,比如 Diagnostic,包含在程序集 Microsoft.Owin.Diagnostic 中。

public void Configuration(IAppBuilder app)
{

    app.UseWelcomePage("/");

    app.UseErrorPage();

    app.Run(context =>
    {

        //將請求記錄在控制臺(tái)

        Trace.WriteLine(context.Request.Uri);

        //顯示錯(cuò)誤頁

        if (context.Request.Path.ToString().Equals("/error"))

        {

            throw new Exception("拋出異常");

        }

        context.Response.ContentType = "text/plain";

        return context.Response.WriteAsync("Hello World");

    });

}

在上述代碼中,當(dāng)請求的路徑(Request.Path)為根目錄時(shí),渲染輸出 Webcome Page并且不繼續(xù)執(zhí)行 Pipeline 中的其余 Middleware 組件,如下所示:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/4.png" alt="" />

如果請求的路徑為 Error時(shí),拋出異常,顯示錯(cuò)誤頁,如下所示:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/5.png" alt="" />

使用 OwinHost.exe 托管 Katana-based 應(yīng)用程序

當(dāng)然我們還可以使用 Katana 提供的 OwinHost.exe 來托管應(yīng)用程序,毫無疑問,通過 Nuget 來安裝 OwinHost。

如果你按照我的例子一步一步執(zhí)行的話,你會(huì)發(fā)現(xiàn)不管使用 ASP.NET/IIS 托管還是自托管,Startup 配置類都是不變的,改變的僅僅是托管方式。同理 OwinHost 也是一樣的,但它更靈活,我們可以使用類庫或者 Web 應(yīng)用程序來作為 Application。

類庫作為 Application,可以最小的去引用程序集,創(chuàng)建一個(gè)類庫后,刪除默認(rèn)的 Class1.cs,然后并且添加 Startup 啟動(dòng)項(xiàng),這會(huì)默認(rèn)像類庫中添加 Owin 和 Microsoft.Owin 程序集的引用。

然后,使用 Nuget 來安裝 OwinHost.exe,如 Install-Package OwinHost,注意它并不是一個(gè)程序集,而是.exe 應(yīng)用程序位于/packages/OwinHost.(version)/tools 文件夾。

因?yàn)轭悗觳荒苤苯舆\(yùn)行,那么只能在它的根目錄調(diào)用 OwinHost.exe 來托管,它將加載.bin 文件下所有的程序集,所以需要改變類庫的默認(rèn)輸出,如下所示:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/6.png" alt="" />

然后編譯解決方案,打開 cmd,鍵入如下命令:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/7.png" alt="" />

如上圖成功啟動(dòng)了宿主 Host 并且默認(rèn)監(jiān)聽 5000 端口。

OwinHost.exe 還提供自定義參數(shù),通過追加-h 來查看,如下所示:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/8.png" alt="" />

既然類庫不能直接運(yùn)行,當(dāng)然你也不能直接進(jìn)行調(diào)試,我們可以附加 OwinHost 進(jìn)程來進(jìn)行調(diào)試,如下所示:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/9.png" alt="" />

注:我在使用 OwinHost.exe 3.0.1 時(shí),Startup 如果是如下情況下,它提示轉(zhuǎn)換失敗,不知是否是該版本的 Bug。

   using AppFunc = Func<idictionary<string, object="">, Task>;
   public class Startup  
   {  
       public void Configuration(IAppBuilder app)
       {

       //使用OwinHost.exe,報(bào)錯(cuò),提示轉(zhuǎn)換失敗

       app.Run(context=>context.Response.WriteAsync("Hello World"));

       //使用OwinHost.exe 不報(bào)錯(cuò)

       //app.Use(new Func<appfunc, appfunc="">(next => (env =>

       //{

           // string text = "Hello World";

           // var response = env["owin.ResponseBody"] as Stream;

           // var headers = env["owin.ResponseHeaders"] as IDictionary<string, string[]="">;

           // headers["Content-Type"] = new[] { "text/plain" };

           // return response.WriteAsync(Encoding.UTF8.GetBytes(text), 0, text.Length);

       //})));

       }

   }

報(bào)錯(cuò)信息如下:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/10.png" alt="" />

Web Application 比類庫使用起來輕松多了,你可以直接運(yùn)行和調(diào)試,唯一比較弱的可能是它引用較多的程序集,你完全可以刪掉,比如 System.Web。

通過 Nuget 安裝了 OwinHost.exe 之后,可以在 Web 中使用它,如下所示:

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/11.png" alt="" />

幾種指定啟動(dòng)項(xiàng) Startup 的方法

  • 默認(rèn)名稱約束:默認(rèn)情況下 Host 會(huì)去查找 root namespace 下的名為 Startup 的類作為啟動(dòng)項(xiàng)。
  • OwinStartup Attribute:當(dāng)創(chuàng)建 Owin Startup 類時(shí),自動(dòng)會(huì)加上 Attribute 如:[**assembly: OwinStartup(typeof(JKXY.KatanaDemo.OwinHost.Startup))]**
  • 配置文件,如:
  • 如果使用自定義 Host,那么可以通過 WebApp.Start("http://localhost:10002") 來設(shè)置啟動(dòng)項(xiàng)。
  • 如果使用 OwinHost,那么可以通過命令行參數(shù)來實(shí)現(xiàn),如下截圖所示

http://wiki.jikexueyuan.com/project/think-in-asp-net-mvc/images/Chapter7/12.png" alt="" />

啟動(dòng)項(xiàng) Startup 的高級(jí)應(yīng)用

啟動(dòng)項(xiàng) Startup 支持 Friendly Name,通過 Friendly Name 我們可以動(dòng)態(tài)切換 Startup。

比如在部署時(shí),我們會(huì)有 UAT 環(huán)境、Production 環(huán)境,在不同的環(huán)境中我們可以動(dòng)態(tài)切換 Startup 來執(zhí)行不同的操作。

舉個(gè)例子,我創(chuàng)建來兩個(gè)帶有 Friendly Name 的 Startup,如下所示:

  [assembly: OwinStartup("Production", typeof(JKXY.KatanaDemo.Web.StartupProduction))]
  namespace JKXY.KatanaDemo.Web
  {
      using AppFunc = Func<idictionary<string, object="">, Task&gt;;
      public class StartupProduction
      {
          public void Configuration(IAppBuilder app)
          {
             app.Run(context=&gt;context.Response.WriteAsync("Production"));
         }
     }
 }

  [assembly: OwinStartup("UAT",typeof(JKXY.KatanaDemo.Web.StartupUAT))]

  namespace JKXY.KatanaDemo.Web
  {
      public class StartupUAT
      {
          public void Configuration(IAppBuilder app)
         {
             app.Run(context=&gt;context.Response.WriteAsync("UAT"));
         }
     }
 }

根據(jù) Friendly Name 使用配置文件或者 OwinHost 參數(shù)來切換 Startup

  <appsettings>
    <add key="owin:appStartup" value="Production">
  </add></appsettings>

小結(jié)

這篇博客為大家講解了 Katana 的世界,那么接下來我將繼續(xù) OWIN & Katana 之旅,探索 Middleware 的創(chuàng)建,謝謝大家支持。