﻿# 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** 專案：

[![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/1a130bda6fdc88197fcc-image_thumb_178.png)](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/9a523cdbe33662b2c44c-image_2145.png)

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

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/8197f4a2acde056655ae-image_2146.png)

接著我們開啟專案下 **App\_Start\\RouteConfig.cs** 檔案，試圖修改預設路由的 url 參數定義：

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/31e4f8e1db6007c8b820-image_2147.png)

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

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/f7a9c1dac6a39b739913-image_2148.png)

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

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

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

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/006dd2fb06ed456658c7-image_2149.png "image")

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

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/2a3fdba5f54daf457c9f-image_2150.png)

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

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

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

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

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

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/e7f31fa5ebc9eaae1db8-image_2151.png)

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

[![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/0370c50b1163ebefa142-image_thumb_179.png)](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/2646bbcc6f1d221f9f65-image_2152.png)

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

> **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](http://winmerge.org/) 工具比對兩個 ASP.NET MVC 專案資料夾的差異，這才發現，雖然兩個都是 ASP.NET MVC 4 專案，但是 web.config 的預設內容卻改變了，是在 <system.webServer> 區段下的 <modules> 區段，原本在 .NET 4.0 專案範本中有下圖這段 **runAllManagedModulesForAllRequests** 設定，但是在 .NET 4.5 卻被移除，進而導致此差異：

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/eaadf272df5b9e3116fe-image_2153.png)

然而，這段設定最主要是針對 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 進行處理：

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/8cbf27e12e31273fddd6-image_2154.png)

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/fc9d82392678a68f17eb-image_2155.png)

以下是從**聖哥**的【[Windows Server 2008 應用程式新平台 - IIS 7.0](http://teacher.syset.com/viewtopic.mspx?t=3917&highlight=IIS)】簡報中擷取的一段圖示，你可以從這裡看出 IIS 6.0 在處理要求的流程。

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/a29d22c774f574af4698-image_2156.png)

這裡你將會發現到，如果我們要透過「身分驗證」模組處裡本次要求，他是 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 的檔案](/post/2007/11/01/How-to-protect-html-file-when-using-aspnet-FormsAuthentication.aspx)

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

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/eaa4e15b4a253daddbfd-image_2157.png "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 處理器！

[![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/956d8faeb64e0bffba92-image_thumb_180.png)](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/c3a821b77b105bb97eab-image_2158.png)

文字版本如下：

```
<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 模組的開發人員來說非常重要！

**相關連結**

-   [ASP.NET MVC 開發心得分享 (21)：Routing 觀念與技巧](/post/2011/08/01/ASPNET-MVC-Developer-Note-Part-21-Routing-Concepts-and-Skills.aspx)
-   [Windows Server 2008 應用程式新平台 - IIS 7.0](http://teacher.syset.com/viewtopic.mspx?t=3917&highlight=IIS)
-   [Introduction to IIS Architectures : The Official Microsoft IIS Site](http://www.iis.net/learn/get-started/introduction-to-iis/introduction-to-iis-architecture)
-   [Caveats with the runAllManagedModulesForAllRequests in IIS 7/8](https://weblog.west-wind.com/posts/2012/Oct/25/Caveats-with-the-runAllManagedModulesForAllRequests-in-IIS-78) !!
-   [Don't use runAllManagedModulesForAllRequests="true" when getting your MVC routing to work](http://www.britishdeveloper.co.uk/2010/06/dont-use-modules-runallmanagedmodulesfo.html)
-   [Modules <modules> : The Official Microsoft IIS Site](http://www.iis.net/configreference/system.webserver/modules)
-   [ASP.NET 開發人員應使用 IIS Express 進行開發與測試](/post/2011/07/22/ASPNET-developer-should-use-IIS-Express-in-Visual-Studio-2010.aspx)
