這是開發 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 內容。

來個一鏡到底展示:

  1. 原本沒有 secret.txt
  2. default.aspx 如預期發生 TypeInitializationException,根本原因是靜態建構式找不到 secret.txt
  3. 寫入 secret.txt,確認檔案已存在
  4. 再次執行 default.aspx,仍在抱怨找不到 secret.txt,但由時間標籤可發現 TypeInitializationException 訊息與前次相同,但 default.aspx 加註的時間是新的
  5. 回收 AppPool
  6. 再執行 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

Post a comment