The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

ASP.NET MVC 4 在 .NET 4.0 與 .NET 4.5 的專案範本差異

昨天在【ASP.NET MVC 4 開發實戰】課程中,學員們發現了一個問題,就是在實作與測試 ASP.NET MVC 的 Routing (路由) 機制的過程中,發現有個功能有些人做得出來,有些人卻做不出來,當我前去查看時也沒立即發現問題癥結,中午休息的空檔終於找到的這個細微的差異之處,所以還是寫篇文章提醒正準備開始使用 .NET Framework 4.5 的 ASP.NET MVC 開發人員。

在此我們先建立一個以 .NET Framework 4.0 為主的 ASP.NET MVC 4 專案:

專案範本則以「網際網路應用程式」為主要範本:

接著我們開啟專案下 App_Start\RouteConfig.cs 檔案,試圖修改預設路由的 url 參數定義:

我們試著將 {action} 修改成 {action}.aspx

這時,我們可以測試該專案,連接以下網址

http://localhost:26618/Home/Index.aspx

你會看到網頁是可以正常顯示的,如下圖示:

image

接著,我們試著將 {action} 修改成 {action}.{ext},把 .aspx 修改成一個路由變數

我們定義了 .{ext} 這個路由變數,代表我們可以在 {action} 後面加上任意 "副檔名”,然而事實上這不是個 “副檔名”,而是一個路由值而已!

這時,我們可以測試該專案,連接以下網址

http://localhost:26618/Home/Index.php

  ( 備註:這是為了示範路由與網址的對應而設計的一個範例,你就算把 .php 改成 .jsp 都能執行 )

你會看到網頁是可以正常顯示的,如下圖示:

 

接下來,我們建立一個以 .NET Framework 4.5 為主的 ASP.NET MVC 4 專案:

然後完全使用上述的範例執行一次,但練習到最後,你卻會發現,使用以下網址可行:

http://localhost:26636/Home/Index.aspx

 

但使用以下網址卻會引發 HTTP 錯誤 404.0 – Not Found 的錯誤 ( 而且該錯誤是從 IIS 傳來的 )

http://localhost:26636/Home/Index.php

 

但是如果我在 .php 後面加上一條斜線 ( / ),問題竟然可以解決,例如:

http://localhost:26636/Home/Index.php/

 

後來我用 WinMerge 工具比對兩個 ASP.NET MVC 專案資料夾的差異,這才發現,雖然兩個都是 ASP.NET MVC 4 專案,但是 web.config 的預設內容卻改變了,是在 <system.webServer> 區段下的 <modules> 區段,原本在 .NET 4.0 專案範本中有下圖這段 runAllManagedModulesForAllRequests 設定,但是在 .NET 4.5 卻被移除,進而導致此差異:

 

然而,這段設定最主要是針對 IIS7+ ( 含 IIS 7.5, IIS 8.0 ) 的 整合 (Integrated) 模式下進行設定,如果你不加上 <modules runAllManagedModulesForAllRequests="true" /> 的話,在預設的情況下,透過 .NET 撰寫的 HttpModules 只能針對 Managed 要求進行處裡 (也就是那些透過 .NET 執行的程式),那些透過原生模組 (native modules) 處裡的檔案,例如 JPEG, CSS, JavaScript 等靜態檔案,就不會流經這些 HttpModules 進行處裡,在這種情況下,如果你沒有明確註冊 HttpHandler 在 IIS 裡面的話,將會導致 IIS 無法判定明確的 MIME Type 而導致找不到網頁的情況,也就是我們這次遇到的問題。

當然,最簡單的應變措施就是直接把這段設定加回去,不過,加回去之前請三思,這條設定被拿掉是有原因的,而原因就出在:效能衝擊 ( Performance Impact )!

在 IIS6 以前,IIS 在處理 HTTP 要求的過程中,只能透過 原生模組 (native modules) 處裡所有要求,並且預設透過副檔名來決定如何對應不同的原生模組,以 ASP.NET 為例,就會有一個 .aspx 對應到 c:\windows\microsoft.net\framework\v2.0.50727\aspnet_isapi.dll 的設定,相關設定畫面可參考如下圖示,你可以發現,所有 HTTP 要求都必須透過「對應」的方式,才能得知正確的 Handler,接下來才能將 HTTP 要求委派給對應的 Handler 進行處理:

以下是從聖哥的【Windows Server 2008 應用程式新平台 - IIS 7.0】簡報中擷取的一段圖示,你可以從這裡看出 IIS 6.0 在處理要求的流程。

這裡你將會發現到,如果我們要透過「身分驗證」模組處裡本次要求,他是 IIS 內建的一個原生模組,雖然我們也可以在 ASP.NET 撰寫 HttpHandler 或 HttpModules 做到相同的事情,但是這些由 ASP.NET 撰寫的程式預設只能處理所有與 ASP.NET 相關的要求而已,也就是那些副檔名註冊在 IIS 裡面,並且設定到 aspnet_isapi.dll 的那些副檔名,才真正能執行到這段程式碼。反之,如果今天從用戶端傳來的 HTTP 要求是 *.htm 副檔名,他所對應到的只要不是 aspnet_isapi.dll 的話,就執行不到這段由 ASP.NET 開發框架下撰寫的 HttpHandler 或 HttpModules。我在 2007 年曾經寫過的一篇文章,就是試圖解決這個問題,參見:如何讓 ASP.NET 的表單驗證功能保護 .htm 的檔案

 

從 IIS7 開始,這個所謂的 IIS Request Pipeline (要求管線) 做了一次重大個改進,一個名為「整合」模式的新式要求管線,把整個模組執行的過程做出了改善,他允許你用 .NET 撰寫 Managed Modules,並且可以套用在任何 HTTP 要求上面,徹底打破架構上的限制,以下是 IIS7+ 新式要求管線示意圖:
( :若想深入研究的人,建議可閱讀 Introduction to IIS Architectures 文章。)

image

 

回到本文提出的問題,當我們沒有 <modules runAllManagedModulesForAllRequests="true" /> 這項預設設定的話 ( 預設為 false ),代表的是,我們在 ASP.NET 定義的模組 (modules),只想處理 Managed 要求 (就是那些註冊為 .NET 相關的要求) 而已,而非 所有 HTTP 要求 (AllRequests),所以當你試圖要求 *.php 等尚未註冊的 handler 時,自然就會引發 HTTP 錯誤 404.0 – Not Found 的錯誤!

不過,可能會有人想問,為什麼我在 .php 後面加上一條斜線 ( / ),問題竟然可以解決呢?

因為從 ASP.NET 4.0 開始,新增了一個 ExtensionlessUrlHandler 要求處理器 (handler),這是一個專門用來處理那些「沒有副檔名的 HTTP 要求」。因為我們在撰寫 ASP.NET MVC 的時候,通常不會在網址加上所謂的「副檔名」,對於這些要求,IIS 會不知道應該將要求如何派送給那個 handler 處理,所以才衍生出這樣的需求,可以說是為了 ASP.NET MVC 量身打造的!

像是 ExtensionlessUrlHandler 這樣的處理器,如果要在 IIS6 實現,必須要註冊所謂的萬用字元應用程式對應才有可能 (設定了這個會影響 IIS6 的執行效能),最主要還是 IIS 架構上的差異使然。

你可以從下圖看到 ASP.NET MVC 專案範本中,預設就會載入所謂的 ExtensionlessUrlHandler 處理器!

文字版本如下:

<handlers>
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
  <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit"
       path="*."
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       modules="IsapiModule"
       scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll"
       preCondition="classicMode,runtimeVersionv4.0,bitness32"
       responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit"
       path="*."
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       modules="IsapiModule"
       scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll"
       preCondition="classicMode,runtimeVersionv4.0,bitness64"
       responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0"
       path="*."
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       type="System.Web.Handlers.TransferRequestHandler"
       preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

如果你想註冊 *.php 並且讓 ASP.NET MVC 路由可以辨識的話,可以參考以下設定:

<handlers>
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
  <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit"
       path="*."
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       modules="IsapiModule"
       scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll"
       preCondition="classicMode,runtimeVersionv4.0,bitness32"
       responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit"
       path="*."
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       modules="IsapiModule"
       scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll"
       preCondition="classicMode,runtimeVersionv4.0,bitness64"
       responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0"
       path="*."
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       type="System.Web.Handlers.TransferRequestHandler"
       preCondition="integratedMode,runtimeVersionv4.0" />
  <add name="(PHP) ExtensionlessUrlHandler-ISAPI-4.0_32bit"
       path="*.php"
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       modules="IsapiModule"
       scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll"
       preCondition="classicMode,runtimeVersionv4.0,bitness32"
       responseBufferLimit="0" />
  <add name="(PHP) ExtensionlessUrlHandler-ISAPI-4.0_64bit"
       path="*.php"
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       modules="IsapiModule"
       scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll"
       preCondition="classicMode,runtimeVersionv4.0,bitness64"
       responseBufferLimit="0" />
  <add name="(PHP) ExtensionlessUrlHandler-Integrated-4.0"
       path="*.php"
       verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
       type="System.Web.Handlers.TransferRequestHandler"
       preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

 

最後想要補充說明的是,如果你自行開發 HTTP 模組 ( HttpModules ) 的話,就算你手動加上 <modules runAllManagedModulesForAllRequests="false" /> 設定,可能會認為「任何原生模組相關的 HTTP 要求」( 例如: *.htm, *.css 之類的 ) 都不會再流經你自行開發的 HTTP 模組,然而,這是個錯誤觀念!

使用這個 runAllManagedModulesForAllRequests 設定,最主要的差異在於:

  • 當設定為 true 的時候,所有 HTTP 要求,包含已註冊的未註冊的附檔名對應,都會流經你自己撰寫的 HTTP 模組。
  • 當設定為 false 的時候,所有 HTTP 要求,所有已註冊的附檔名對應,還是會流經你自己撰寫的 HTTP 模組。

這個小細節對於寫 HTTP 模組的開發人員來說非常重要!

 

相關連結