Обработка ошибок в приложениях ASP.NET Core Blazor

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 8 этой статьи.

В этой статье описывается, как Blazor управляет необработанными исключениями и как разрабатывать приложения, которые обнаруживают и обрабатывают ошибки.

Подробные сведения об ошибках во время разработки

Если во время разработки приложение Blazor работает неправильно, подробные сведения об ошибках в приложении могут помочь в устранении неполадок. При возникновении ошибки в приложении Blazor в нижней части экрана отображается светло-желтая полоса:

  • Во время разработки из этой полосы можно перейти в консоль браузера, где можно просмотреть исключение.
  • В рабочей среде эта полоса уведомляет пользователя о том, что произошла ошибка, и рекомендует обновить содержимое окна браузера.

Пользовательский интерфейс для этого процесса обработки ошибок входит в состав шаблонов проектов Blazor. Не все версии Blazor шаблонов проектов используют data-nosnippet атрибут для сигнала браузерам не кэшировать содержимое пользовательского интерфейса ошибки, но все версии Blazor документации применяют атрибут.

Blazor В веб-приложении настройте интерфейс компонентаMainLayout. Так как вспомогательный компонент тега среды (например, <environment include="Production">...</environment>) не поддерживается в Razor компонентах, в следующем примере внедряется IHostEnvironment настройка сообщений об ошибках для разных сред.

В верхней части MainLayout.razor:

@inject IHostEnvironment HostEnvironment

Создайте или измените разметку пользовательского Blazor интерфейса ошибки:

<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 интерфейса ошибки:

<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 обычно скрыт из-за наличия display: none стиля blazor-error-ui класса CSS в автоматически созданной таблице стилей приложения. При возникновении ошибки платформа применяет display: block к элементу.

Элемент blazor-error-ui обычно скрыт из-за наличия display: none стиля blazor-error-ui класса CSS в таблице стилей сайта в папке wwwroot/css . При возникновении ошибки платформа применяет display: block к элементу.

Подробное описание ошибок каналов

Этот раздел относится к Blazor веб-приложения операционной системе по каналу.

Этот раздел относится к приложениям Blazor Server.

Ошибки на стороне клиента не включают сведения о стеке вызовов и не предоставляют сведения о причине ошибки, но журналы сервера содержат эти сведения. В целях разработки конфиденциальные сведения об ошибках каналов можно сделать доступными для клиента, включив подробное описание ошибок.

Задайте для параметра CircuitOptions.DetailedErrors значение true. Дополнительные сведения и пример см. в разделе Руководство поBlazorSignalR ASP.NET Core .

Альтернативой параметру CircuitOptions.DetailedErrors является установка DetailedErrors ключа true конфигурации в файле параметров среды приложения Development (appsettings.Development.json). Кроме того, задайте для параметра 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 для использования ASPNETCORE_DETAILEDERRORS переменной среды со значением true на Development/Staging серверах среды или локальной системе.

Предупреждение

Никогда не предоставляйте сведения об ошибках клиентам в Интернете, так как это подвергает безопасность угрозе.

Подробные ошибки для Razor отрисовки на стороне сервера компонента

Этот раздел относится к Blazor веб-приложения.

RazorComponentsServiceOptions.DetailedErrors Используйте параметр для управления получением подробных сведений об ошибках для Razor отрисовки на стороне сервера компонента. Значение по умолчанию — false.

В следующем примере приводятся подробные ошибки:

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

Предупреждение

Включите только подробные Development ошибки в среде. Подробные ошибки могут содержать конфиденциальную информацию о приложении, которое злоумышленники могут использовать в атаке.

В предыдущем примере обеспечивается степень безопасности путем задания значения DetailedErrors на основе возвращаемого IsDevelopmentзначения. Если приложение находится в Development среде, DetailedErrors задается значение true. Такой подход не является обманным, так как в среде можно разместить рабочее приложение на общедоступном сервере Development .

Управление необработанными исключениями в коде разработчика

Чтобы приложение продолжало работу после возникновения ошибки, в нем должна быть логика обработки ошибок. В последующих разделах этой статьи описываются потенциальные источники необработанных исключений.

В рабочей среде не следует отображать сообщения об исключениях платформы или трассировки стека в пользовательском интерфейсе. Отображение сообщений об исключениях или трассировок стека может:

  • Раскрыть конфиденциальные сведения конечным пользователям.
  • Помочь злоумышленнику обнаружить слабые места в приложении, что может нарушить безопасность приложения, сервера или сети.

Необработанные исключения для каналов

Этот раздел относится к приложениям на стороне сервера, работающим над каналом.

Razor компоненты с включенной интерактивностью сервера находятся на сервере с отслеживанием состояния. Хотя пользователи взаимодействуют с компонентом на сервере, они поддерживают подключение к серверу, известному как канал. Канал содержит экземпляры активных компонентов, а также многие другие аспекты состояния, например:

  • Последние отображаемые выходные данные компонентов.
  • Текущий набор делегатов обработки событий, которые могут вызываться событиями на стороне клиента.

Если пользователь открывает приложение на нескольких вкладках браузера, он создает несколько независимых каналов.

Blazor обрабатывает большинство необработанных исключений как неустранимые для канала, где они происходят. Если канал завершается из-за необработанного исключения, пользователь может продолжить взаимодействие с приложением только путем перезагрузки страницы для создания нового канала. Это не влияет на остальные каналы для других пользователей или других вкладок браузера. Этот сценарий аналогичен сценарию с настольным приложением, которое аварийно завершает работу. Такое приложение необходимо перезапустить, но остальные приложения это не затрагивает.

Платформа завершает канал при возникновении необработанного исключения по следующим причинам:

  • Необработанное исключение часто оставляет канал в неопределенном состоянии.
  • После необработанного исключения нормальную работу приложения невозможно гарантировать.
  • Дальнейшая работа канала в неопределенном состоянии может стать причиной уязвимостей системы безопасности.

Глобальная обработка исключений

Сведения о глобальной обработке исключений см. в следующих разделах:

Границы ошибок

Границы ошибок предоставляют удобный подход к обработке исключений. Компонент ErrorBoundary:

  • Отображает свое дочернее содержимое при возникновении ошибки.
  • Отображает пользовательский интерфейс ошибки при возникновении необработанного исключения.

Чтобы определить границу ошибки, используйте компонент ErrorBoundary для охвата существующего содержимого. Приложение продолжит работать в обычном режиме, но граница ошибки будет обрабатывать необработанные исключения.

<ErrorBoundary>
    ...
</ErrorBoundary>

Чтобы реализовать границу ошибок в глобальном режиме, добавьте границу вокруг основного макета приложения.

В MainLayout.razor:

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

В Blazor веб-приложения с границей ошибки, применяемой только к стаическому MainLayout компоненту, граница активна только во время этапа отрисовки на стороне статического сервера (статический SSR). Граница не активируется только потому, что компонент дальше вниз иерархии компонентов является интерактивным. Чтобы включить интерактивное взаимодействие для MainLayout компонента и остальных компонентов в иерархии компонентов, включите интерактивную отрисовку для HeadOutletRoutes экземпляров компонентов в App компоненте (Components/App.razor). В следующем примере используется режим отрисовки интерактивного сервера(InteractiveServer

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Если вы предпочитаете не включать взаимодействие сервера во всем приложении из Routes компонента, поместите границу ошибки дальше вниз иерархии компонентов. Например, поместите границу ошибки вокруг разметки в отдельных компонентах, которые обеспечивают интерактивность, а не в главном макете приложения. Важные понятия, которые следует учитывать, заключается в том, что везде, где размещается граница ошибки:

  • Если граница ошибки не является интерактивной, она может активироваться только на сервере во время статической отрисовки. Например, граница может активироваться при возникновении ошибки в методе жизненного цикла компонента.
  • Если граница ошибки является интерактивной, она может активироваться для компонентов, отрисовываемых на интерактивном сервере, которые он упаковывает.

Рассмотрим следующий пример, в котором компонент Counter создает исключение, если число увеличивается до пяти.

В Counter.razor:

private void IncrementCount()
{
    currentCount++;

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

Если необработанное исключение создается для currentCount более пяти:

  • Ошибка регистрируется обычно (System.InvalidOperationException: Current count is too big!).
  • Созданное исключение обрабатывается границей ошибки.
  • Пользовательский интерфейс ошибки отображается границей ошибки со следующим сообщением об ошибке по умолчанию: An error has occurred.

По умолчанию компонент ErrorBoundary отображает пустой элемент <div> с классом CSS blazor-error-boundary для содержимого ошибки. Цвета, текст и значок пользовательского интерфейса по умолчанию определяются с помощью CSS в таблице стилей приложения в папке wwwroot, поэтому вы можете настроить пользовательский интерфейс ошибки.

Измените содержимое ошибки по умолчанию, задав ErrorContent свойство:

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

Так как граница ошибки определена в макете в предыдущих примерах, пользовательский интерфейс ошибки отображается независимо от того, на какую страницу пользователь переходит после возникновения ошибки. В большинстве случаев рекомендуется сузить границы ошибок. Если вы широко область границу ошибки, ее можно сбросить в состояние без ошибок на последующих событиях навигации страниц, вызвав метод границы Recover ошибки.

В MainLayout.razor:

...

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

...

@code {
    private ErrorBoundary? errorBoundary;

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

Чтобы избежать бесконечного цикла, когда восстановление просто rerenders компонента, который снова вызывает ошибку, не вызывайте Recover логику отрисовки. Вызов выполняется Recover только в том случае, если:

  • Пользователь выполняет жест пользовательского интерфейса, например нажатие кнопки, чтобы указать, что они хотят повторить процедуру или когда пользователь переходит к новому компоненту.
  • Дополнительная логика также очищает исключение. Если компонент перерисовывается, ошибка не повторяется.

Альтернативная глобальная обработка исключений

Альтернативой использованию границ ошибок (ErrorBoundary) является передача пользовательского компонента ошибки в качестве CascadingValue дочерним компонентам. Преимущество использования компонента по сравнению с использованием внедренной службы или реализации пользовательского средства ведения журнала заключается в том, что при возникновении ошибки каскадный компонент может отрисовывать содержимое и применять стили CSS.

В следующем примере компонента Error просто регистрируются ошибки, но методы компонента могут обрабатывать ошибки любым способом, требуемым для приложения, в том числе с использованием нескольких методов обработки ошибок.

Error.razor:

@inject ILogger<Error> Logger

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

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

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

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

Примечание.

Дополнительные сведения о RenderFragment см. в статье Компоненты Razor ASP.NET Core.

В компоненте Routes обтекайте Router компонент (<Router>...</Router>) компонентом Error . Благодаря этому компонент Error сможет переходить к любому компоненту приложения, в котором компонент Error получен как CascadingParameter.

В Routes.razor:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

В компоненте App обтекайте Router компонент (<Router>...</Router>) компонентом Error . Благодаря этому компонент Error сможет переходить к любому компоненту приложения, в котором компонент Error получен как CascadingParameter.

В App.razor:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

Чтобы обработать ошибки в компоненте, выполните приведенные ниже действия.

  • Назначьте компонент Error как CascadingParameter в блоке @code. В примере компонента Counter в приложении, основанном на шаблоне проекта Blazor, добавьте следующее свойство Error:

    [CascadingParameter]
    public Error? Error { get; set; }
    
  • Вызовите метод обработки ошибок в любом блоке catch с соответствующим типом исключения. В примере компонента Error есть только один метод ProcessError. Однако компонент обработки ошибок может предоставить любое количество соответствующих методов, чтобы удовлетворить альтернативные требования к обработке ошибок во всем приложении. В следующем примере компонента Counter создается исключение, которое перехватывается, когда число больше пяти:

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

Используя предыдущий компонент Error с предыдущими изменениями, внесенными в компонент Counter, консоль инструментов разработчика обозревателя указывает на перехваченную, зарегистрированную ошибку:

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

Если метод ProcessError напрямую участвует в отрисовке, например показывает пользовательскую панель сообщений об ошибках или изменяет стили CSS отрисованных элементов, вызовите StateHasChanged в конце метода ProcessErrors для повторной отрисовки пользовательского интерфейса.

Так как подходы в этом разделе обрабатывают ошибки с оператором try-catch , подключение приложения SignalR между клиентом и сервером не нарушается при возникновении ошибки, и канал остается живым. Другие необработанные исключения остаются неустранимыми для канала. Дополнительные сведения см. в разделе о том, как канал реагирует на необработанные исключения.

Приложение может использовать компонент обработки ошибок в качестве каскадного значения для обработки ошибок централизованно.

Следующий компонент Error передает себя дочерним компонентам в качестве CascadingValue. В следующем примере просто регистрируется ошибка, но методы компонента могут обрабатывать ошибки любым способом, требуемым для приложения, в том числе с использованием нескольких методов обработки ошибок. Преимущество использования компонента по сравнению с использованием внедренной службы или реализации пользовательского средства ведения журнала заключается в том, что при возникновении ошибки каскадный компонент может отрисовывать содержимое и применять стили CSS.

Error.razor:

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

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

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

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

Примечание.

Дополнительные сведения о RenderFragment см. в статье Компоненты Razor ASP.NET Core.

В компоненте App создайте программу-оболочку для компонента Router с помощью компонента Error. Благодаря этому компонент Error сможет переходить к любому компоненту приложения, в котором компонент Error получен как CascadingParameter.

App.razor:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

Чтобы обработать ошибки в компоненте, выполните приведенные ниже действия.

  • Назначьте компонент Error как CascadingParameter в блоке @code:

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Вызовите метод обработки ошибок в любом блоке catch с соответствующим типом исключения. В примере компонента Error есть только один метод ProcessError. Однако компонент обработки ошибок может предоставить любое количество соответствующих методов, чтобы удовлетворить альтернативные требования к обработке ошибок во всем приложении.

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

Используя компонент Error и метод ProcessError из предыдущего примера, консоль инструментов разработчика обозревателя указывает на перехваченную, зарегистрированную ошибку:

Сбой: BlazorSample.Shared.Error[0] Error:ProcessError - Тип: System.NullReferenceException Сообщение: Ссылка на объект не указывает на экземпляр объекта.

Если метод ProcessError напрямую участвует в отрисовке, например показывает пользовательскую панель сообщений об ошибках или изменяет стили CSS отрисованных элементов, вызовите StateHasChanged в конце метода ProcessErrors для повторной отрисовки пользовательского интерфейса.

Так как подходы в этом разделе предусматривают обработку ошибок с помощью инструкции try-catch, соединение SignalR приложения Blazor между клиентом и сервером не прерывается при возникновении ошибки и канал остается активным. Необработанное исключение является неустранимым для канала. Дополнительные сведения см. в разделе о том, как канал реагирует на необработанные исключения.

Регистрация ошибок с помощью постоянного поставщика

При возникновении необработанного исключения оно заносится в журнал экземпляров ILogger, настроенных в контейнере службы. По умолчанию приложения Blazor записываются в выходные данные консоли с помощью поставщика ведения журнала консоли. Рассмотрите возможность ведения журнала в расположение на сервере (или серверном веб-API для клиентских приложений) с помощью поставщика, который управляет размером журнала и поворотом журнала. Кроме того, приложение может использовать службу управления производительностью приложений (APM), например Azure Application Insights (Azure Monitor).

Примечание.

Функции собственного приложения Аналитика для поддержки клиентских приложений и собственной Blazor платформы поддержки Google Analytics могут стать доступными в будущих выпусках этих технологий. Дополнительные сведения см. в статьях Поддержка App Insights на стороне клиента Blazor WASM (microsoft/ApplicationInsights-dotnet #2143) и Веб-аналитика и диагностика (содержит ссылки на реализации сообщества) (dotnet/aspnetcore #5461). В то же время клиентское приложение может использовать пакет SDK JavaScript для приложения Аналитика JavaScript, JS чтобы регистрировать ошибки непосредственно в приложение Аналитика из клиентского приложения.

Во время разработки в Blazor приложении, работающем над каналом, приложение обычно отправляет полные сведения об исключениях в консоль браузера, чтобы помочь в отладке. В рабочей среде подробные сообщения об ошибках не отправляются клиентам, но все сведения об исключениях записываются на сервер.

Необходимо решить, какие инциденты следует заносить в журнал, и выбрать уровень серьезности регистрируемых инцидентов. Злоумышленники могут попытаться вызывать ошибки намеренно. Например, не заносите в журнал инцидент в результате ошибки, когда в URL-адресе компонента, отображающего сведения о продукте, указан неизвестный ProductId. Не все ошибки следует рассматривать как инциденты, подлежащие записи в журнал.

Дополнительные сведения см. в следующих статьях:

.Применяется к серверным приложениям и другим серверным Blazor приложениям ASP.NET Core, которые являются внутренними приложениями веб-API для Blazor. Клиентские приложения могут перехватывать и отправлять сведения об ошибках клиента в веб-API, который записывает сведения об ошибке в постоянный поставщик ведения журнала.

При возникновении необработанного исключения оно заносится в журнал экземпляров ILogger, настроенных в контейнере службы. По умолчанию приложения Blazor записываются в выходные данные консоли с помощью поставщика ведения журнала консоли. Возможно, следует сохранять записи журнала в более постоянное расположение на сервере, отправляя сведения об ошибке в серверный веб-API, который использует поставщик ведения журнала с возможностью управления размером журнала и ротации журналов. Кроме того, серверное приложение веб-API может использовать службу управления производительностью приложений (APM), например Azure Application Insights (Azure Monitor)†, для записи сведений об ошибках, получаемых от клиентов.

Необходимо решить, какие инциденты следует заносить в журнал, и выбрать уровень серьезности регистрируемых инцидентов. Злоумышленники могут попытаться вызывать ошибки намеренно. Например, не заносите в журнал инцидент в результате ошибки, когда в URL-адресе компонента, отображающего сведения о продукте, указан неизвестный ProductId. Не все ошибки следует рассматривать как инциденты, подлежащие записи в журнал.

Дополнительные сведения см. в следующих статьях:

†Native Application Аналитика функции для поддержки клиентских приложений и собственной Blazor платформы поддержки Google Analytics могут стать доступными в будущих выпусках этих технологий. Дополнительные сведения см. в статьях Поддержка App Insights на стороне клиента Blazor WASM (microsoft/ApplicationInsights-dotnet #2143) и Веб-аналитика и диагностика (содержит ссылки на реализации сообщества) (dotnet/aspnetcore #5461). В то же время клиентское приложение может использовать пакет SDK JavaScript для приложения Аналитика JavaScript, JS чтобы регистрировать ошибки непосредственно в приложение Аналитика из клиентского приложения.

‡Применяется к приложениям ASP.NET Core на стороне сервера, которые являются серверными приложениями веб-API для приложений Blazor. Приложения на стороне клиента регистрируют и отправляют сведения об ошибках в веб-API, который регистрирует сведения об ошибке в постоянном поставщике ведения журнала.

Места, где могут возникнуть ошибки

Код платформы и приложения может активировать необработанные исключения в любом из следующих расположений, которые описаны в следующих разделах этой статьи.

Экземпляр компонента

Когда Blazor создает экземпляр компонента:

  • Вызывается конструктор компонента.
  • Вызываются конструкторы служб DI, которые передаются в конструктор компонента с помощью директивы @inject или атрибута [Inject].

Ошибка в выполненном конструкторе или методе задания для любого свойства [Inject] приводит к возникновению необработанного исключения и предотвращает создание экземпляра компонента на платформе. Если приложение работает над каналом, канал завершается ошибкой. Если логика конструктора может вызывать исключения, приложение должно выполнить перехват исключений с помощью инструкции 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}"
@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) компилируется в метод C# с именем BuildRenderTree. Когда компонент отрисовывается, BuildRenderTree выполняет и создает структуру данных, описывающую элементы, текст и дочерние компоненты отрисованного компонента.

Логика отрисовки может вызывать исключение. Пример этого сценария происходит, когда @someObject.PropertyName вычисляется, но @someObject имеет значение null. Для Blazor приложений, работающих над каналом, необработанное исключение, вызываемое логикой отрисовки, является неустранимым для канала приложения.

Чтобы предотвратить NullReferenceException в логике отрисовки, проверьте наличие объекта null перед обращением к его элементам. В следующем примере свойства person.Address недоступны, если person.Address имеет значение null:

@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 В приложении, работающем над каналом, исключение является смертельным для канала приложения.

Удаление компонентов

Компонент может быть удален из пользовательского интерфейса, например, потому, что пользователь перешел на другую страницу. Когда компонент, реализующий System.IDisposable, удаляется из пользовательского интерфейса, платформа вызывает метод Dispose компонента.

Если метод компонента Dispose создает необработанное исключение в приложении, работающем над Blazor каналом, исключение неустранимо для канала приложения.

Если логика удаления может вызывать исключения, приложение должно выполнить перехват исключений с помощью инструкции try-catch с обработкой ошибок и ведением журнала.

Дополнительные сведения об удалении компонентов см. в разделе Жизненный цикл компонента RazorASP.NET Core.

Взаимодействие с JavaScript

IJSRuntime регистрируется платформой Blazor. IJSRuntime.InvokeAsync позволяет коду .NET выполнять асинхронные вызовы к среде выполнения JavaScript (JS) в браузере пользователя.

К обработке ошибок с помощью InvokeAsync применяются следующие условия:

  • Если вызов к InvokeAsync выполняется синхронно, возникает исключение .NET. Вызов к InvokeAsync может завершиться ошибкой, например, поскольку передаваемые аргументы не могут быть сериализованы. Код разработчика должен перехватить исключение. Если код приложения в обработчике событий или методе жизненного цикла компонентов не обрабатывает исключение в Blazor приложении, работающем над каналом, результирующее исключение является неустранимым для канала приложения.
  • Если вызов InvokeAsync асинхронно завершается сбоем, .NET Task также завершается сбоем. Вызов к InvokeAsync может завершиться ошибкой, например, потому что код на стороне JS вызывает исключение или возвращает Promise, который был завершен как rejected. Код разработчика должен перехватить исключение. При использовании оператора awaitрекомендуется заключить вызов метода в инструкцию try-catch с обработкой ошибок и ведением журнала. В противном случае в Blazor приложении, работающем над каналом, неисправный код приводит к необработанным исключениям, которое является неустранимым для канала приложения.
  • По умолчанию вызовы InvokeAsync должны выполняться в течение определенного периода или в противном случае время ожидания вызова. Период времени ожидания по умолчанию составляет одну минуту. Время ожидания защищает код от потери сетевого подключения или кода JS, который никогда не возвращает сообщение о завершении. Если время ожидания вызова истекает, полученный элемент System.Threading.Tasks завершается с исключением OperationCanceledException. Перехватите и обработайте исключение с ведением журнала.

Аналогичным образом код JS может инициировать вызовы методов .NET, указанных в атрибуте [JSInvokable]. Если эти методы .NET создают необработанное исключение:

  • Blazor В приложении, работающем над каналом, исключение не рассматривается как неустранимая для канала приложения.
  • Promise на стороне JS отклоняется.

Вы можете использовать код обработки ошибок либо на стороне .NET, либо на стороне JS вызова метода.

Дополнительные сведения см. в следующих статьях:

Предварительная отрисовка

Razor Компоненты по умолчанию предопределяются таким образом, чтобы их отрисовка HTML-разметки возвращалась в рамках первоначального HTTP-запроса пользователя.

В приложении, работающем над каналом Blazor , предварительная отрисовка работает следующим образом:

  • Создается новый канал для всех предварительно отрисованных компонентов, которые являются частью одной страницы.
  • Создается исходный HTML-код.
  • Канал рассматривается как disconnected, пока браузер пользователя не установит подключение SignalR к тому же серверу. Если соединение установлено, взаимодействие с каналом возобновляется, а HTML-разметка компонентов обновляется.

Для предварительно подготовленных клиентских компонентов предварительная отрисовка работает следующим образом:

  • Создается исходный HTML-код на сервере для всех предварительно отображенных компонентов, которые являются частью одной страницы.
  • Компонент делается интерактивным на клиенте после того, как браузер загрузил скомпилированный код приложения и среду выполнения .NET (если она еще не загружена) в фоновом режиме.

Если компонент создает необработанное исключение во время предварительной отрисовки, например во время метода жизненного цикла или в логике отрисовки:

  • В приложении, работающем Blazor над каналом, исключение неустранимо для канала. Для предопределенных клиентских компонентов исключение предотвращает отрисовку компонента.
  • Исключение вызывается в стеке вызовов из ComponentTagHelper.

В обычных обстоятельствах при сбое предварительной отрисовки продолжение сборки и отрисовки компонента не имеет смысла, так как невозможно отрисовать рабочий компонент.

Чтобы допускать ошибки, которые могут возникнуть во время предварительной отрисовки, логика обработки ошибок должна размещаться внутри компонента, который может вызывать исключения. Используйте инструкции try-catch с обработкой ошибок и ведением журнала. Вместо того чтобы упаковывать ComponentTagHelper в инструкцию try-catch, поместите логику обработки ошибок в компонент, отрисовываемый ComponentTagHelper.

Расширенные сценарии

Рекурсивная отрисовка

Компоненты можно вкладывать друг в друга рекурсивно. Это полезно для представления рекурсивных структур данных. Например, компонент TreeNode может отрисовывать другие компоненты TreeNode для каждого из дочерних элементов узла.

При рекурсивной отрисовке избегайте шаблонов кодирования, которые приводят к бесконечной рекурсии:

  • Не следует рекурсивно отрисовывать структуру данных, содержащую цикл. Например, не следует отрисовывать узел дерева, дочерние элементы которого содержат сами себя.
  • Не создавайте цепочку макетов, содержащих цикл. Например, не создавайте макет, макетом которого является он сам.
  • Не разрешайте пользователю нарушать инварианты рекурсии (правила) с помощью ввода вредоносных данных или вызовов взаимодействия JavaScript.

Бесконечные циклы во время отрисовки:

  • Приводят к тому, что процесс отрисовки будет выполняться бесконечно.
  • Эквивалентны созданию незавершенного цикла.

В этих сценариях происходит сбой Blazor и обычно выполняется попытка:

  • Использовать столько процессорных ресурсов, сколько разрешено операционной системой, в неограниченном объеме.
  • Использовать неограниченный объем памяти. Использование неограниченного объема памяти эквивалентно сценарию, в котором незавершенный цикл добавляет записи в коллекцию при каждой итерации.

Чтобы избежать бесконечного шаблона рекурсии, убедитесь, что рекурсивный код отрисовки содержит подходящие условия остановки.

Пользовательская логика дерева отрисовки

Большинство компонентов Razor реализуются как файлы компонента Razor (.razor) и компилируются платформой для создания логики, которая работает в RenderTreeBuilder для отрисовки выходных данных. Разработчик может вручную реализовать логику RenderTreeBuilder с помощью процедурного кода C#. Дополнительные сведения см. в разделе «Расширенные сценарии ASP.NET Core Blazor (построение дерева визуализации)».

Предупреждение

Использование выполняемой вручную логики построителя дерева считается сложным и небезопасным сценарием, не рекомендуемым для обычной разработки компонентов.

При написании кода RenderTreeBuilder разработчик должен гарантировать правильность кода. Например, разработчик должен убедиться в том, что:

  • Вызовы к OpenElement и CloseElement правильно сбалансированы.
  • Атрибуты добавляются только в нужные места.

Неправильная логика построителя дерева отображения вручную может вызвать произвольное неопределенное поведение, включая сбои, зависание приложения или сервера и уязвимости системы безопасности.

Считайте, что выполняемая вручную логика построителя дерева отрисовки имеет тот же уровень сложности и опасности, что и написание кода сборки или инструкций MSIL вручную.

Дополнительные ресурсы

†Применяется к серверным веб-API ASP.NET Core, которые клиентские приложения Blazor используют для ведения журнала.