ASP.NET 實戰手札:重啟網站才能解決的 TypeInitializationException 錯誤
| | 6 | | ![]() |
這是開發 ASP.NET 網站可能遇到的問題:當類別的靜態建構式、靜態欄位初始化出錯,將導致 TypeInitializationException 「'XXXX' 的類型初始設定式發生例外狀況。 The type initializer for 'XXXX' threw an exception.」錯誤。一旦發生,即使修正錯誤來源,靜態建構式、靜態屬性也不會重新執行,呼叫相關方法會得到相同例外(可想像成一直傳回被 Cache 的 TypeInitializationException),有時會讓人混淆,例如:缺少的檔案、Registry 明明已經補上,為什麼系統還是一直抱怨找不到?
用一個範例來重現問題。我在 IIS 開了一個 ASP.NET Web Site 應用程式 - MyWebSite,App_Code/AppUtil.cs 的靜態建構式會由 ~/secret.txt 讀取字串,之後透過 public static string GetSecret() 方法供外界讀取。(註:為便於觀察,讀檔動作加了 try catch 在錯誤訊息加註時間戳記。)
using System;
using System.Web.Hosting;
using System.IO;
public class AppUtil
{
static string _secret;
static AppUtil()
{
try
{
_secret = File.ReadAllText(
HostingEnvironment.MapPath("~/secret.txt"));
}
catch (Exception ex)
{
throw new ApplicationException(ex.Message + "/" +
DateTime.Now.ToString("HH:mm:ss.fff"));
}
}
public static string GetSecret()
{
return _secret;
}
}
測試網頁 default.aspx 如下:
<%@ Page Language="C#" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
Response.ContentType = "text/plain";
try
{
var secret = AppUtil.GetSecret();
Response.Write("Secret = " + secret);
}
catch (Exception ex)
{
Response.Write(ex.GetType().ToString() + ":\n");
Response.Write(ex.Message + "\n");
if (ex.InnerException != null)
{
Response.Write(ex.InnerException.Message + "\n");
}
Response.Write(DateTime.Now.ToString("HH:mm:ss.fff") + "\n");
}
}
</script>
開始測試時 secret.txt 不存在,呼叫 default.aspx 不意外地得到錯誤:
System.TypeInitializationException:
'AppUtil' 的類型初始設定式發生例外狀況。
找不到檔案 'C:\WWW\MyWebSite\secret.txt'。
HH:mm:ss.fff
接著我們現場建立 secret.txt,再執行一次 default.aspx,發現它仍然在抱怨找不到 secret.txt,但檔案明明在呀!
重啟 IIS AppPool 或 IISRESET 後,才會順利讀到 secret.txt 內容。
來個一鏡到底展示:
- 原本沒有 secret.txt
- default.aspx 如預期發生 TypeInitializationException,根本原因是靜態建構式找不到 secret.txt
- 寫入 secret.txt,確認檔案已存在
- 再次執行 default.aspx,仍在抱怨找不到 secret.txt,但由時間標籤可發現 TypeInitializationException 訊息與前次相同,但 default.aspx 加註的時間是新的
- 回收 AppPool
- 再執行 default.aspx,這才成功讀到 secret.txt
【結論】由實驗結果可知,ASP.NET 網站若因型別因靜態建構式、靜態欄位初始化出錯導致 TypeInitializationException 錯誤,即使排除錯誤根源仍會得到同一例外,直到重啟 AppPool 或 IISRESET 才能修正。明白這點,就不會被「明明已經調整過,卻一直彈出一模一樣錯誤訊息」所迷惑囉。
(不過,我找不到能解釋 TypeInitializationException 類似被 Cache 住現象的技術文件,歡迎大家補充。)
Demostrating the "cached" behavior of TypeInitializationException in ASP.NET website.
Comments
# by zNiangko
https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/classes-and-structs/static-constructors 看起來備註的第八點有提到這回事
# by Jeffrey
to zNiangko, 謝謝補充。我更好奇「TypeInitializationException 像被 Cache 住 」的運作原理。
# by ByTim
有時候寫完程式碼,也建置完了,本機端還是舊的,神奇的是開DEBUG模式是新的,之後再讀一次本機端,還是舊的,最後清除>重建>關IIS網站站台>重啟站台,本機端終於正常了,也不知道是硬碟,還是什麼地方有問題。
# by ChrisTorng
看過這個嗎 https://docs.microsoft.com/en-us/dotnet/api/system.typeinitializationexception?view=net-6.0#Static ,我也不很懂。 我都開全部 C# 程式碼分析告警,有一項 CA1810: Initialize reference type static fields inline https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1810 說為了效能起見,不建議使用 static constructor,應改用 inline static field initialize。 記得以前也曾看過分析 static 欄位 IL 內容的文章。若詳細了解 C# compiler 如何產出 static field/constructor 之 IL 的內容後,應該就可完全明白原理了。不過我還沒研究過...
# by ChrisTorng
就我所能理解與想像,並不是在程式一啟動就初始化所有 static 欄位,而是有執行到相關引用才初始化。但也不能用「== null」這樣的條件下一律執行初始化,如果的確有 static field 一直保持是 null 的話就會錯誤地反覆執行。總之 compiler 出來的 IL 程式保證只會執行一次 static initializer。如果那一次是錯誤的,也不會在後續的引用中再次執行了。另看起來 static constructor 需要更多的引用前檢查,而 static field initializer 比較不用,因此以效能考量上建議使用 static field initializer。
# by Jeffrey
to ChrisTorng, 感謝分享。我補充 Jon Skeet 這篇 https://csharpindepth.com/articles/BeforeFieldInit 有提到 CLI 規格對靜態建構式、靜態欄位初始化執行時機的定義,以及 BeforeFieldInit 造成的奇妙現象。不過,如該文開頭所提,型別初始化實作會依不同版本 .NET 有所差異,像是 .NET 4 跟 .NET 3.5 就不一樣 ( https://codeblog.jonskeet.uk/2010/01/26/type-initialization-changes-in-net-4-0/ ),這段如要細究水很深 :P