共用方式為


在 ASP.NET Core Blazor 應用程式中處理錯誤

注意

這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前的版本,請參閱 本文的 .NET 9 版本。

本文描述 Blazor 如何管理未處理的例外狀況,以及如何開發可偵測和處理錯誤的應用程式。

開發期間的詳細錯誤

當 Blazor 應用程式在開發期間無法正常運作時,接收來自應用程式的詳細錯誤資訊可協助進行疑難排解並修正問題。 發生錯誤時,Blazor 應用程式會在畫面底部顯示淺黃色列:

  • 在開發期間,該列會將您導向瀏覽器主控台,您可以在其中查看例外狀況。
  • 在生產環境中,該列會通知使用者發生錯誤,並建議重新整理瀏覽器。

此錯誤處理體驗的 UI 是 Blazor 專案範本的一部分。 並非所有版本的 Blazor 專案範本都會使用 data-nosnippet 屬性 向瀏覽器發出不要快取錯誤 UI 內容的訊號,但 Blazor 文件的所有版本都會套用該屬性。

在 Blazor Web App中,於 MainLayout 元件中自訂該體驗。 因為 Razor 元件中不支援環境標記協助程式 (例如 <environment include="Production">...</environment>),下列範例會插入 IHostEnvironment 來設定不同環境的錯誤訊息。

MainLayout.razor 頂端:

@inject IHostEnvironment HostEnvironment

建立或修改 Blazor 錯誤 UI 標記:

<div id="blazor-error-ui" data-nosnippet>
    @if (HostEnvironment.IsProduction())
    {
        <span>An error has occurred.</span>
    }
    else
    {
        <span>An unhandled exception occurred.</span>
    }
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

在 Blazor Server 應用程式中,在 Pages/_Host.cshtml 檔案中自訂該體驗。 下列範例會使用環境標記協助程式來設定不同環境的錯誤訊息。

在 Blazor Server 應用程式中,在 Pages/_Layout.cshtml 檔案中自訂該體驗。 下列範例會使用環境標記協助程式來設定不同環境的錯誤訊息。

在 Blazor Server 應用程式中,在 Pages/_Host.cshtml 檔案中自訂該體驗。 下列範例會使用環境標記協助程式來設定不同環境的錯誤訊息。

建立或修改 Blazor 錯誤 UI 標記:

<div id="blazor-error-ui" data-nosnippet>
    <environment include="Staging,Production">
        An error has occurred.
    </environment>
    <environment include="Development">
        An unhandled exception occurred.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

在 Blazor WebAssembly 應用程式中,在 wwwroot/index.html 檔案中自訂該體驗:

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

blazor-error-ui 元素通常會隱藏,因為應用程式自動產生的樣式表中存在 blazor-error-ui CSS 類別的 display: none 樣式。 發生錯誤時,架構會將 display: block 套用至元素。

blazor-error-ui 元素通常會隱藏,因為 wwwroot/css 資料夾的網站樣式表中存在 blazor-error-ui CSS 類別的 display: none 樣式。 發生錯誤時,架構會將 display: block 套用至元素。

詳細的線路錯誤

本章節適用於在電路運作的 Blazor Web App。

本節適用 Blazor Server 應用程式。

用戶端錯誤不會包含呼叫堆疊,也不會提供錯誤原因的詳細資料,但伺服器記錄確實會包含此類資訊。 基於開發目的,您可以藉由啟用詳細的錯誤,將敏感性線路錯誤資訊提供給用戶端。

CircuitOptions.DetailedErrors 設定為 true。 如需詳細資訊和範例,請參閱 ASP.NET Core BlazorSignalR 指導

設定 CircuitOptions.DetailedErrors 的替代方法是在應用程式的 Development 環境設定檔案 (appsettings.Development.json) 中將 DetailedErrors 設定金鑰設定為 true。 此外,將 SignalR 伺服器端記錄 (Microsoft.AspNetCore.SignalR) 設定為 [偵錯] 或 [追蹤] 以取得詳細的 SignalR 記錄。

appsettings.Development.json

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

DetailedErrors 設定金鑰也可以設定為 true,在 Development/Staging 環境伺服器或您的本機系統上,使用 ASPNETCORE_DETAILEDERRORS 環境變數搭配 true 的值。

警告

請務必避免將錯誤資訊公開給網際網路上的用戶端,這是安全性風險。

Razor 元件伺服器端轉譯的詳細錯誤

本章節適用 Blazor Web App。

使用 RazorComponentsServiceOptions.DetailedErrors 選項來控制 Razor 元件伺服器端轉譯發生錯誤時產生的詳細資訊。 預設值是 false

下列範例會啟用詳細錯誤:

builder.Services.AddRazorComponents(options => 
    options.DetailedErrors = builder.Environment.IsDevelopment());

警告

只在 Development 環境中啟用詳細錯誤。 詳細錯誤可能包含惡意使用者可在攻擊中使用的應用程式相關敏感性資訊。

上述範例會根據 IsDevelopment 所傳回的值來設定 DetailedErrors 的值,以提供一定程度的安全性。 當應用程式位於 Development 環境中時,DetailedErrors 會設定為 true。 這種方法並非萬無一失,因為可以在 Development 環境中的公用伺服器上裝載生產應用程式。

在開發人員程式碼中管理未處理的例外狀況

若要讓應用程式在錯誤之後繼續,應用程式必須具有錯誤處理邏輯。 本文稍後的小節會描述未處理的例外狀況的潛在來源。

在生產環境中,請勿在 UI 中轉譯架構例外狀況訊息或堆疊追蹤。 轉譯例外狀況訊息或堆疊追蹤可以:

  • 將敏感性資訊揭露給終端使用者。
  • 協助惡意使用者探索應用程式中可能會危害應用程式、伺服器或網路安全性的弱點。

線路未處理的例外狀況

本節適用於透過線路運作的伺服器端應用程式。

已啟用伺服器互動功能的 Razor 元件在伺服器上會是具狀態。 當使用者與伺服器上的元件互動時,他們會保有與伺服器的連線,稱為線路。 線路會保存作用中的元件執行個體,加上狀態的其他許多層面,例如:

  • 元件的最新轉譯輸出。
  • 可以由用戶端事件觸發的目前事件處理委派集。

如果使用者在多個瀏覽器索引標籤中開啟應用程式,使用者就會建立多個獨立線路。

Blazor 會將大部分未處理的例外狀況視為發生所在線路的嚴重情況。 如果線路因未處理的例外狀況而終止,使用者只能重新載入頁面以建立新的線路,以便繼續與應用程式互動。 終止的線路以外的線路,也就是用於其他使用者或其他瀏覽器索引標籤的線路,不會受到影響。 此案例類似於損毀的桌面應用程式。 必須重新啟動損毀的應用程式,但其他應用程式不會受影響。

架構會在發生未處理的例外狀況時終止線路,原因如下:

  • 未處理的例外狀況通常會讓線路處於未定義的狀態。
  • 在未處理的例外狀況之後,無法保證應用程式的正常作業。
  • 如果線路繼續處於未定義狀態,則應用程式可能會出現安全性弱點。

全域例外狀況處理

如需全域處理例外狀況的方法,請參閱下列各節:

  • 錯誤界限:適用於所有 Blazor 應用程式。
  • 替代全域例外狀況處理:適用於採用全域互動式轉譯模式的 Blazor Server、 Blazor WebAssembly和 Blazor Web App(8.0 或更新版本)。

錯誤界限

錯誤界限提供處理例外狀況的便利方法。 ErrorBoundary 元件:

  • 尚未發生錯誤時轉譯其子內容。
  • 當錯誤界限內的任何元件擲回未處理的例外狀況時,轉譯錯誤 UI。

若要定義錯誤界限,請使用 ErrorBoundary 元件來包裝現有的內容。 錯誤界限會管理包裝元件擲回的未處理例外狀況。

<ErrorBoundary>
    ...
</ErrorBoundary>

若要以全域方式實作錯誤界限,請在應用程式主要配置本文內容周圍新增界限。

MainLayout.razor 中:

<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>

在只會對靜態 MainLayout 元件套用錯誤界限的 Blazor Web App 中,界限只會在靜態伺服器端轉譯 (靜態 SSR) 期間起作用。 界限不會啟動,只是因為元件階層更下層的元件是互動式的。

無法將互動式轉譯模式套用至 MainLayout 元件,因為元件的 Body 參數是 RenderFragment 委派,這是任意程式碼且無法序列化。 若要廣泛啟用 MainLayout 元件的互動功能,以及元件階層下方的元件 rest ,應用程式必須採用全域互動式轉譯模式,方法是將互動式轉譯模式套用至應用程式根元件中的 HeadOutletRoutes 元件執行個體,通常是 App 元件。 下列範例在全域採用互動式伺服器 (InteractiveServer) 轉譯模式。

Components/App.razor 中:

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

如果您不想啟用全域互動功能,請將錯誤界限放在元件階層更遠的地方。 要記住的重要概念是,無論錯誤界限放置於何處:

  • 如果放置錯誤界限的元件不是互動式,則錯誤界線只能在靜態 SSR 期間在伺服器上啟用。 例如,當元件生命週期方法中擲回錯誤時,界限可以啟動,但對於元件內用戶互動所觸發的事件,例如按鈕點選處理程序擲回的錯誤。
  • 如果放置錯誤界限的元件不是互動式,則錯誤界能啟用包覆的互動式元件。

注意

上述考慮與獨立 Blazor WebAssembly 應用程式無關,因為 Blazor WebAssembly 應用程式的用戶端轉譯 (CSR) 是完全互動式的。

請考慮下列範例,其中內嵌計數器元件擲回的例外狀況是由採用互動式轉譯模式之 Home 元件中的錯誤界限攔截。

EmbeddedCounter.razor

<h1>Embedded Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        if (currentCount > 5)
        {
            throw new InvalidOperationException("Current count is too big!");
        }
    }
}

Home.razor

@page "/"
@rendermode InteractiveServer

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<ErrorBoundary>
    <EmbeddedCounter />
</ErrorBoundary>

請考慮下列範例,其中內嵌計數器元件擲回的例外狀況是由 Home 元件中的錯誤界限攔截。

EmbeddedCounter.razor

<h1>Embedded Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        if (currentCount > 5)
        {
            throw new InvalidOperationException("Current count is too big!");
        }
    }
}

Home.razor

@page "/"

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<ErrorBoundary>
    <EmbeddedCounter />
</ErrorBoundary>

如果針對 currentCount 擲出未處理的例外狀況超過五個:

  • 錯誤會正常記錄 (System.InvalidOperationException: Current count is too big!)。
  • 例外狀況會由錯誤界限處理。
  • 預設錯誤 UI 會由錯誤界限轉譯。

ErrorBoundary 元件會使用 blazor-error-boundary CSS 類別做為其錯誤內容,轉譯空白 <div> 元素。 預設 UI 的色彩、文字和圖示是在應用程式樣式表 wwwroot 資料夾中定義,因此您可以自由自訂錯誤 UI。

錯誤界限所呈現的預設錯誤 UI,其背景為紅色、文字「發生錯誤」,以及內含驚嘆號的黃色警告圖示。

若要變更預設的錯誤內容:

下列範例會包裝 EmbeddedCounter 元件並提供自訂錯誤內容:

<ErrorBoundary>
    <ChildContent>
        <EmbeddedCounter />
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

針對上述範例,應用程式的樣式表單大概包含 errorUI CSS 類別來設定內容樣式。 錯誤內容會從沒有區塊層級項目的 ErrorContent 屬性轉譯。 區塊層級專案,例如除法 (<div>) 或段落 (<p>) 元素,可以包裝錯誤內容標記,但不需要。

或者,使用 ErrorContent 的內容 (@context) 來取得錯誤資料:

<ErrorContent>
    @context.HelpLink
</ErrorContent>

ErrorContent 也可以命名內容。 在下列範例中,內容會命名為 exception

<ErrorContent Context="exception">
    @exception.HelpLink
</ErrorContent>

警告

請務必避免將錯誤資訊公開給網際網路上的用戶端,這是安全性風險。

如果錯誤界限是在應用程式配置中定義,因此不論發生錯誤之後使用者導覽到哪個頁面,都會看到錯誤 UI。 在大部分情況下,我們建議將錯誤界限的範圍縮小。 如果您廣泛界定錯誤界限的範圍,可以藉由呼叫錯誤界限的 Recover 方法,在後續頁面導覽事件上將它重設為非錯誤狀態。

MainLayout.razor 中:

...

<ErrorBoundary @ref="errorBoundary">
    @Body
</ErrorBoundary>

...

@code {
    private ErrorBoundary? errorBoundary;

    protected override void OnParametersSet()
    {
        errorBoundary?.Recover();
    }
}

若要避免無限迴圈,其中的復原只會重新轉譯再次擲出錯誤的元件,請勿從轉譯邏輯呼叫 Recover。 只在下列情況下呼叫 Recover

  • 使用者執行 UI 手勢,例如選取按鈕以指出他們想要重試程序,或當使用者導覽至新元件時。
  • 執行的其他邏輯也會清除例外狀況。 重新轉譯元件時,錯誤不會重複發生。

下列範例可讓使用者使用按鈕從例外狀況復原:

<ErrorBoundary @ref="errorBoundary">
    <ChildContent>
        <EmbeddedCounter />
    </ChildContent>
    <ErrorContent>
        <div class="alert alert-danger" role="alert">
            <p class="fs-3 fw-bold">😈 A rotten gremlin got us. Sorry!</p>
            <p>@context.HelpLink</p>
            <button class="btn btn-info" @onclick="_ => errorBoundary?.Recover()">
                Clear
            </button>
        </div>
    </ErrorContent>
</ErrorBoundary>

@code {
    private ErrorBoundary? errorBoundary;
}

您也可以覆寫 OnErrorAsync來進行自訂處理的子類別 ErrorBoundary 。 下列範例只會記錄錯誤,但您可以實作您想要的任何錯誤處理程式碼。 如果您的程式碼等候非同步工作,您可以移除傳回 CompletedTask 的行。

CustomErrorBoundary.razor

@inherits ErrorBoundary
@inject ILogger<CustomErrorBoundary> Logger

@if (CurrentException is null)
{
    @ChildContent
}
else if (ErrorContent is not null)
{
    @ErrorContent(CurrentException)
}

@code {
    protected override Task OnErrorAsync(Exception ex)
    {
        Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
        return Task.CompletedTask;
    }
}

上述範例也可以實作為類別。

CustomErrorBoundary.cs

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace BlazorSample;

public class CustomErrorBoundary : ErrorBoundary
{
    [Inject]
    ILogger<CustomErrorBoundary> Logger {  get; set; } = default!;

    protected override Task OnErrorAsync(Exception ex)
    {
        Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
        return Task.CompletedTask;
    }
}

元件中使用的上述任一實作:

<CustomErrorBoundary>
    ...
</CustomErrorBoundary>

替代全域例外狀況處理

本章節所描述的方法適用於採用全域互動式轉譯模式的 Blazor Server、 Blazor WebAssembly和 Blazor Web App(InteractiveServerInteractiveWebAssemblyInteractiveAuto)。 此方法不適用於採用每頁/元件轉譯模式或靜態伺服器端轉譯 (靜態 SSR) 的 Blazor Web App,因為此方法依賴 CascadingValue/CascadingParameter,其無法跨越轉譯模式界限或搭配採用靜態 SSR 的元件運作。

使用錯誤界限 (ErrorBoundary) 的替代方法是將自訂錯誤元件當做 CascadingValue 傳遞至子元件。 使用元件優於使用插入的服務或自訂記錄器實作的優點是,串聯元件可以在發生錯誤時轉譯內容並套用 CSS 樣式。

下列 ProcessError 元件範例只會記錄錯誤,但元件的方法可以以應用程式所需的任何方式處理錯誤,包括透過使用多個錯誤處理方法。

ProcessError.razor

@inject ILogger<ProcessError> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public void LogError(Exception ex)
    {
        Logger.LogError("ProcessError.LogError: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);

        // Call StateHasChanged if LogError directly participates in 
        // rendering. If LogError only logs or records the error,
        // there's no need to call StateHasChanged.
        //StateHasChanged();
    }
}

注意

如需 RenderFragment 的詳細資訊,請參閱 ASP.NET Core Razor 元件

在 Blazor Web App中使用此方法時,請開啟 Routes 元件,以 ProcessError 元件包裝 Router 元件 (<Router>...</Router>)。 這可讓 ProcessError 元件串聯至 ProcessError 元件接收為 CascadingParameter 的應用程式的任何元件。

Routes.razor 中:

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

在 Blazor Server 或 Blazor WebAssembly 應用程式中使用此方法時,請開啟 App 元件,以 ProcessError 元件包裝 Router 元件 (<Router>...</Router>)。 這可讓 ProcessError 元件串聯至 ProcessError 元件接收為 CascadingParameter 的應用程式的任何元件。

App.razor 中:

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

若要處理元件中的錯誤:

  • ProcessError 元件指定為 @code 區塊中的 CascadingParameter。 在以 Blazor 專案範本為基礎的應用程式中的範例 Counter 元件中,新增下列 ProcessError 屬性:

    [CascadingParameter]
    public ProcessError? ProcessError { get; set; }
    
  • 在具有適當例外狀況類型的任何 catch 區塊中呼叫錯誤處理方法。 範例 ProcessError 元件只提供單一 LogError 方法,但錯誤處理元件可以提供任意數目的錯誤處理方法,以解決整個應用程式中的替代錯誤處理需求。 下列 Counter 元件 @code 區塊範例包含 ProcessError 串聯參數,並在計數大於五時截獲記錄的例外狀況:

    @code {
        private int currentCount = 0;
    
        [CascadingParameter]
        public ProcessError? ProcessError { get; set; }
    
        private void IncrementCount()
        {
            try
            {
                currentCount++;
    
                if (currentCount > 5)
                {
                    throw new InvalidOperationException("Current count is over five!");
                }
            }
            catch (Exception ex)
            {
                ProcessError?.LogError(ex);
            }
        }
    }
    

記錄的錯誤:

fail: {COMPONENT NAMESPACE}.ProcessError[0]
ProcessError.LogError: System.InvalidOperationException Message: Current count is over five!

如果 LogError 方法直接參與轉譯,例如顯示自訂錯誤訊息列或變更轉譯元素的 CSS 樣式,請在 LogError 方法結尾呼叫 StateHasChanged 以重新轉譯 UI。

由於本節中的方法會處理 try-catch 陳述式的錯誤,因此當發生錯誤且線路保持運作時,用戶端與伺服器之間的應用程式 SignalR 連線不會中斷。 其他未處理的例外狀況對線路來說仍是嚴重情況。 如需詳細資訊,請參閱線路如何回應未處理的例外狀況一節。

應用程式可以使用錯誤處理元件作為串聯值,以集中方式處理錯誤。

下列 ProcessError 元件會將本身當作 CascadingValue 傳遞至子元件。 下列範例只會記錄錯誤,但元件的方法可以以應用程式所需的任何方式處理錯誤,包括透過使用多個錯誤處理方法。 使用元件優於使用插入的服務或自訂記錄器實作的優點是,串聯元件可以在發生錯誤時轉譯內容並套用 CSS 樣式。

ProcessError.razor

@using Microsoft.Extensions.Logging
@inject ILogger<ProcessError> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public void LogError(Exception ex)
    {
        Logger.LogError("ProcessError.LogError: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);
    }
}

注意

如需 RenderFragment 的詳細資訊,請參閱 ASP.NET Core Razor 元件

App 元件中,使用 ProcessError 元件包裝 Router 元件。 這可讓 ProcessError 元件串聯至 ProcessError 元件接收為 CascadingParameter 的應用程式的任何元件。

App.razor

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

若要處理元件中的錯誤:

  • ProcessError 元件指定為 @code 區塊中的 CascadingParameter

    [CascadingParameter]
    public ProcessError ProcessError { get; set; }
    
  • 在具有適當例外狀況類型的任何 catch 區塊中呼叫錯誤處理方法。 範例 ProcessError 元件只提供單一 LogError 方法,但錯誤處理元件可以提供任意數目的錯誤處理方法,以解決整個應用程式中的替代錯誤處理需求。

    try
    {
        ...
    }
    catch (Exception ex)
    {
        ProcessError.LogError(ex);
    }
    

使用上述範例 ProcessError 元件和 LogError 方法,瀏覽器的開發人員工具主控台會指出已設陷、記錄的錯誤:

fail: {COMPONENT NAMESPACE}.Shared.ProcessError[0]
ProcessError.LogError: System.NullReferenceException Message: Object reference not set to an instance of an object.

如果 LogError 方法直接參與轉譯,例如顯示自訂錯誤訊息列或變更轉譯元素的 CSS 樣式,請在 LogError 方法結尾呼叫 StateHasChanged 以重新轉譯 UI。

由於本節中的方法會處理 try-catch 陳述式的錯誤,因此當發生錯誤且線路保持運作時,用戶端與伺服器之間的 Blazor 應用程式 SignalR 連線不會中斷。 任何未處理的例外狀況對線路來說是嚴重情況。 如需詳細資訊,請參閱線路如何回應未處理的例外狀況一節。

使用持續性提供者記錄錯誤

如果發生未處理的例外狀況,則會將例外狀況記錄至服務容器中設定的 ILogger 執行個體。 Blazor 應用程式會使用主控台記錄提供者記錄主控台輸出。 考慮使用會管理記錄大小和記錄輪替的提供者,記錄到伺服器上的位置 (或用戶端應用程式的後端 Web API)。 或者,應用程式可以使用應用程式效能管理 (APM) 服務,例如 Azure Application Insights (Azure 監視器)

注意

原生 Application Insights 功能以支援用戶端應用程式,以及 Google Analytics 的原生 Blazor 架構支援,可能會在這些技術未來的版本中提供。 如需詳細資訊,請參閱在 Blazor WASM 用戶端中支援 App Insights (microsoft/ApplicationInsights-dotnet #2143)Web 分析和診斷 (包括社群實作的連結) (dotnet/aspnetcore #5461)。 同時,用戶端應用程式可以使用 Application Insights JavaScript SDK 搭配 JS Interop,直接從用戶端應用程式將錯誤記錄至 Application Insights。

在透過線路運作的 Blazor 應用程式開發期間,應用程式通常會將例外狀況的完整詳細資料傳送至瀏覽器的主控台,以協助偵錯。 在生產環境中,詳細錯誤不會傳送至用戶端,但會在伺服器上記錄例外狀況的完整詳細資料。

您必須決定要記錄的事件,以及記錄的事件嚴重性層級。 惡意使用者可能會故意觸發錯誤。 例如,不要記錄來自顯示產品詳細資料的元件 URL 中提供未知 ProductId 的錯誤事件。 並非所有錯誤都應該被視為記錄的事件。

如需詳細資訊,請參閱下列文章:

‡適用於伺服器端 Blazor 應用程式和其他伺服器端 ASP.NET Core 應用程式,其為 Blazor 的 Web API 後端應用程式。 用戶端應用程式可以將用戶端上的錯誤資訊設陷,並將其傳送至 Web API,以將錯誤資訊記錄至持續性記錄提供者。

如果發生未處理的例外狀況,則會將例外狀況記錄至服務容器中設定的 ILogger 執行個體。 Blazor 應用程式會使用主控台記錄提供者記錄主控台輸出。 考慮將錯誤資訊傳送至會使用記錄提供者搭配記錄大小管理和記錄輪替的後端 Web API,以記錄到伺服器上的更永久位置。 或者,後端 Web API 應用程式可以使用應用程式效能管理 (APM) 服務,例如 Azure Application Insights (Azure 監視器)†,來記錄它從用戶端接收的錯誤資訊。

您必須決定要記錄的事件,以及記錄的事件嚴重性層級。 惡意使用者可能會故意觸發錯誤。 例如,不要記錄來自顯示產品詳細資料的元件 URL 中提供未知 ProductId 的錯誤事件。 並非所有錯誤都應該被視為記錄的事件。

如需詳細資訊,請參閱下列文章:

†原生 Application Insights 功能以支援用戶端應用程式,以及 Google Analytics 的原生 Blazor 架構支援,可能會在這些技術未來的版本中提供。 如需詳細資訊,請參閱在 Blazor WASM 用戶端中支援 App Insights (microsoft/ApplicationInsights-dotnet #2143)Web 分析和診斷 (包括社群實作的連結) (dotnet/aspnetcore #5461)。 同時,用戶端應用程式可以使用 Application Insights JavaScript SDK 搭配 JS Interop,直接從用戶端應用程式將錯誤記錄至 Application Insights。

‡適用於伺服器端 ASP.NET Core 應用程式,其為 Blazor 應用程式的 Web API 後端應用程式。 用戶端應用程式會錯誤資訊設陷,並將其傳送至 Web API,以將錯誤資訊記錄至持續性記錄提供者。

可能發生錯誤的位置

架構和應用程式程式碼可能會在下列任何位置觸發未處理的例外狀況,本文下列各節會進一步描述:

元件具現化

Blazor 建立元件的執行個體時:

  • 叫用元件的建構函式。
  • 叫用透過 @inject 指示詞或 [Inject] 屬性提供給元件建構函式的 DI 服務的建構函式。

任何 [Inject] 屬性的執行建構函式或 setter 中的錯誤會導致未處理的例外狀況,並阻止架構具現化元件。 如果應用程式透過線路運作,線路就會失敗。 如果建構函式邏輯可能會擲出例外狀況,應用程式應該使用 try-catch 陳述式來設陷例外狀況,並搭配錯誤處理和記錄。

生命週期方法

在元件的存留期間,Blazor 會叫用生命週期方法。 如果任何生命週期方法會以同步或非同步方式擲出例外狀況,則例外狀況對線路來說是嚴重情況。 若要讓元件處理生命週期方法中的錯誤,請新增錯誤處理邏輯。

在下列範例中,其中的 OnParametersSetAsync 會呼叫方法來取得產品:

  • ProductRepository.GetProductByIdAsync 方法中擲出的例外狀況會由 try-catch 陳述式處理。
  • 執行 catch 區塊時:
    • loadFailed 設定為 true,其用來向使用者顯示錯誤訊息。
    • 記錄錯誤。
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}

轉譯邏輯

Razor 元件檔案 (.razor) 中的宣告式標記會編譯成名為 BuildRenderTree 的 C# 方法。 元件轉譯時,BuildRenderTree 會執行並建置資料結構,描述所轉譯元件的元素、文字和子元件。

轉譯邏輯可能會擲出例外狀況。 評估 @someObject.PropertyName@someObjectnull 時,就是此案例的範例。 對於透過線路操作的 Blazor 應用程式,轉譯邏輯擲出的未處理例外狀況對應用程式的線路來說是嚴重情況。

若要防止轉譯邏輯中發生 NullReferenceException,請在存取物件成員之前先檢查 null 物件。 在下列範例中,如果 person.Addressnull,則不會存取 person.Address 屬性:

@if (person.Address != null)
{
    <div>@person.Address.Line1</div>
    <div>@person.Address.Line2</div>
    <div>@person.Address.City</div>
    <div>@person.Address.Country</div>
}

上述程式碼假設 person 不是 null。 程式碼的結構通常會保證轉譯元件時物件存在。 在這些情況下,不需要在轉譯邏輯中檢查 null。 在先前的範例中,可能會保證 person 存在,因為 person 會在具現化元件時建立,如下列範例所示:

@code {
    private Person person = new();

    ...
}

事件處理常式

使用下列項目建立事件處理常式時,用戶端程式代碼會觸發叫用 C# 程式碼:

  • @onclick
  • @onchange
  • 其他 @on... 屬性
  • @bind

事件處理常式程式碼可能會在這些案例中擲出未處理的例外狀況。

如果應用程式呼叫可能會因為外部原因而失敗的程式碼,請使用 try-catch 陳述式來設陷例外狀況,並搭配錯誤處理和記錄。

如果事件處理常式擲出未由開發人員程式碼設陷和處理的未處理的例外狀況 (例如,資料庫查詢失敗):

  • 架構會記錄例外狀況。
  • 在透過線路運作的 Blazor 應用程式中,例外情況對應用程式線路來說是嚴重情況。

元件處置

例如,因為使用者已導覽至另一個頁面,因此可能會從 UI 移除某個元件。 移除從 UI 實作 System.IDisposable 的元件時,架構會呼叫元件的 Dispose 方法。

如果元件的 Dispose 方法在透過線路操作的 Blazor 應用程式中擲出未處理的例外狀況,則例外狀況對應用程式的線路來說是嚴重情況。

如果處置邏輯可能會擲出例外狀況,應用程式應該使用 try-catch 陳述式來設陷例外狀況,並搭配錯誤處理和記錄。

如需元件處置的詳細資訊,請參閱 ASP.NET Core Razor 元件生命週期

JavaScript Interop

IJSRuntime 會由 Blazor 架構進行註冊。 IJSRuntime.InvokeAsync 會允許 .NET 程式碼在使用者的瀏覽器中對 JavaScript (JS) 執行階段進行非同步呼叫。

下列條件適用使用 InvokeAsync 的錯誤處理:

  • 如果呼叫 InvokeAsync 同步失敗,就會發生 .NET 例外狀況。 例如,對 InvokeAsync 的呼叫可能會失敗,因為提供的引數無法序列化。 開發人員程式碼必須攔截該例外狀況。 如果事件處理常式或元件生命週期方法中的應用程式程式碼不會處理透過線路運作的 Blazor 應用程式中的例外狀況,則產生的例外狀況對應用程式的線路來說是嚴重情況。
  • 如果呼叫 InvokeAsync 非同步失敗,.NET Task 就會失敗。 例如,呼叫 InvokeAsync 可能會失敗,因為 JS 端程式碼擲出例外狀況,或傳回以 rejected 形式完成的 Promise。 開發人員程式碼必須攔截該例外狀況。 如果使用 await 運算子,請考慮在 try-catch 陳述式中包裝方法呼叫,並搭配錯誤處理和記錄。 否則,在透過線路運作的 Blazor 應用程式中,失敗的程式碼會導致未處理的例外狀況,對應用程式的線路來說是嚴重情況。
  • InvokeAsync 的呼叫必須在特定期間內完成,否則呼叫會逾時。預設逾時期間為一分鐘。 逾時可保護程式碼在網路連線中斷時遭受損失,或永遠不會傳回完成訊息的 JS 程式碼。 如果呼叫逾時,產生的 System.Threading.Tasks 會失敗並出現 OperationCanceledException。 設陷和處理例外狀況並記錄。

同樣地,JS 程式碼可能會起始對 [JSInvokable] 屬性所指出的 .NET 方法的呼叫。 如果這些 .NET 方法擲出未處理的例外狀況:

  • 在透過線路運作的 Blazor 應用程式中,例外狀況不會被視為對應用程式線路來說是嚴重錯誤。
  • JS 端 Promise 遭到拒絕。

您可以選擇在 .NET 端或方法呼叫的 JS 端使用錯誤處理常式程式碼。

如需詳細資訊,請參閱下列文章:

預先轉譯

預設會預先轉譯 Razor 元件,以便將其轉譯的 HTML 標記隨著使用者初始 HTTP 要求的一部分傳回。

在透過線路運作的 Blazor 應用程式中,預先轉譯的運作方式如下:

  • 為所有屬於相同頁面一部分的預先轉譯元件建立新的線路。
  • 產生初始 HTML。
  • 將線路視為 disconnected,直到使用者的瀏覽器重新建立與相同伺服器的 SignalR 連線為止。 建立連線時,線路上的互動功能會繼續,並更新元件的 HTML 標記。

針對預先轉譯的用戶端元件,預先轉譯的運作方式如下:

  • 針對屬於相同頁面的所有預先轉譯元件,在伺服器上產生初始 HTML。
  • 在瀏覽器載入應用程式編譯的程式碼和背景中的 .NET 執行階段 (如果尚未載入) 之後,讓元件在用戶端上成為互動式。

如果元件在預先轉譯期間擲出未處理的例外狀況,例如,在生命週期方法期間或轉譯邏輯中:

  • 在透過線路運作的 Blazor 應用程式中,例外情況對線路來說是嚴重情況。 對於預先轉譯的用戶端元件,例外狀況會防止轉譯元件。
  • 例外狀況從 ComponentTagHelper 擲出呼叫堆疊。

在預先轉譯失敗的一般情況下,繼續建置和轉譯元件沒有意義,因為無法轉譯工作元件。

若要容許在預先轉譯期間可能發生的錯誤,必須將錯誤處理邏輯放置在可能會擲出例外狀況的元件內。 使用 try-catch 陳述式搭配錯誤處理和記錄。 不要將 ComponentTagHelper 包裝在 try-catch 陳述式中,而是將錯誤處理邏輯放置在 ComponentTagHelper 轉譯的元件中。

進階案例

遞迴轉譯

元件可以遞迴方式巢狀化。 這適合用來呈現遞迴資料結構。 例如,TreeNode 元件可以為每個節點的子系轉譯更多 TreeNode 元件。

以遞迴方式轉譯時,請避免會產生無限遞迴的編碼模式:

  • 不要以遞迴方式轉譯包含循環的資料結構。 例如,不要轉譯子系包含本身的樹狀節點。
  • 不要建立包含循環的配置鏈結。 例如,請勿建立配置為其本身的配置。
  • 不要允許終端使用者透過惡意資料輸入或 JavaScript Interop 呼叫違反遞迴變異數 (規則)。

轉譯期間的無限迴圈:

  • 導致轉譯程序永遠繼續。
  • 相當於建立不會結束的迴圈。

在這些案例中,Blazor 會失敗且通常會嘗試:

  • 無限期地取用作業系統所允許的 CPU 時間。
  • 取用無限量的記憶體。 取用無限制的記憶體相當於不會結束的迴圈,會在每個反覆運算上增加項目至集合。

若要避免無限遞迴模式,請確保遞迴轉譯程式碼包含適當的停止條件。

自訂轉譯樹狀目錄邏輯

大部分 Razor 元件會實作為 Razor 元件檔案 (.razor),並由架構編譯,以產生在 RenderTreeBuilder 上運作的邏輯以轉譯其輸出。 不過,開發人員可以使用程序性 C# 程式碼手動實作 RenderTreeBuilder 邏輯。 如需詳細資訊,請參閱 ASP.NET Core Blazor 進階案例 (轉譯樹狀目錄建構)

警告

使用手動轉譯樹狀目錄產生器邏輯會被視為進階和不安全的案例,不建議用於一般元件開發。

如果是撰寫 RenderTreeBuilder 程式碼,開發人員必須保證程式碼的正確性。 例如,開發人員必須確保:

不正確的手動轉譯樹狀目錄建立器邏輯可能會導致任意未定義的行為,包括損毀、應用程式或伺服器停止回應,以及安全性漏洞。

考慮在相同層級的複雜度上手動轉譯樹狀目錄產生器邏輯,並使用與手動撰寫組件程式碼或 Microsoft Intermediate Language (MSIL) 指令相同的危險層級。

其他資源

†適用用戶端 Blazor 應用程式用於記錄的後端 ASP.NET Core Web API 應用程式。