Обработка ошибок в приложениях ASP.NET Core Blazor
Примечание.
Это не последняя версия этой статьи. Для текущего выпуска см. статью версии .NET 9.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске см. версию .NET 9 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см. версию .NET 9 этой статьи.
В этой статье описывается, как Blazor управляет необработанными исключениями и как разрабатывать приложения, которые обнаруживают и обрабатывают ошибки.
Подробные сведения об ошибках во время разработки
Если во время разработки приложение Blazor работает неправильно, подробные сведения об ошибках в приложении могут помочь в устранении неполадок. При возникновении ошибки в приложении Blazor в нижней части экрана отображается светло-желтая полоса:
- Во время разработки полоса направляет вас в консоль браузера, где вы можете увидеть исключение.
- В рабочей среде эта полоса уведомляет пользователя о том, что произошла ошибка, и рекомендует обновить содержимое окна браузера.
Пользовательский интерфейс для этого процесса обработки ошибок входит в состав шаблонов проектов Blazor. Не все версии Blazor шаблонов проектов используют data-nosnippet
атрибут для сигнала браузерам не кэшировать содержимое пользовательского интерфейса ошибки, но все версии Blazor документации применяют атрибут.
В Blazor Web App настройте работу в компоненте 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
файле. В следующем примере используется вспомогательный тег Environment для настройки сообщений об ошибках для разных сред.
Создайте или измените разметку ошибки пользовательского интерфейса 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 Web Appработающим через цепь.
Этот раздел относится к приложениям 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 Web Apps.
Используйте параметр RazorComponentsServiceOptions.DetailedErrors, чтобы контролировать выдачу подробной информации об ошибках для Razor, связанных с отрисовкой компонента на стороне сервера. Значение по умолчанию — false
.
В следующем примере приводятся подробные ошибки:
builder.Services.AddRazorComponents(options =>
options.DetailedErrors = builder.Environment.IsDevelopment());
Предупреждение
Включайте подробные ошибки только в Development
среде. Подробные ошибки могут содержать конфиденциальную информацию о приложении, которое злоумышленники могут использовать в атаке.
В предыдущем примере обеспечивается степень безопасности путем задания значения DetailedErrors на основе возвращаемого IsDevelopmentзначения. Если приложение находится в Development
среде, DetailedErrors задается значение true
. Такой подход не является надежным, так как в среде Development
можно разместить рабочее приложение на общедоступном сервере.
Управление необработанными исключениями в коде разработчика
Чтобы приложение продолжало работу после возникновения ошибки, в нем должна быть логика обработки ошибок. В последующих разделах этой статьи описываются потенциальные источники необработанных исключений.
В рабочей среде не следует отображать сообщения об исключениях платформы или трассировки стека в пользовательском интерфейсе. Отображение сообщений об исключениях или трассировок стека может:
- Раскрыть конфиденциальные сведения конечным пользователям.
- Помочь злоумышленнику обнаружить слабые места в приложении, что может нарушить безопасность приложения, сервера или сети.
Необработанные исключения для цепей
Этот раздел применяется к серверным приложениям, работающим над цепью.
Razor компоненты с включенной интерактивностью сервера находятся на сервере с отслеживанием состояния. Хотя пользователи взаимодействуют с компонентом на сервере, они поддерживают подключение к серверу, известному как цепь. Цепь содержит экземпляры активных компонентов, а также многие другие аспекты состояния, например:
- Последние отображаемые выходные данные компонентов.
- Текущий набор делегатов обработки событий, которые могут быть активированы событиями на стороне клиента.
Если пользователь открывает приложение на нескольких вкладках браузера, он создает несколько независимых каналов.
Blazor обрабатывает большинство необработанных исключений как фатальные для цепи, в которой они происходят. Если канал завершается из-за необработанного исключения, пользователь может продолжить взаимодействие с приложением только путем перезагрузки страницы для создания нового канала. Процессы, отличные от завершённого, такие как процессы для других пользователей или вкладок браузера, не затрагиваются. Этот сценарий аналогичен сценарию с настольным приложением, которое аварийно завершает работу. Это приложение необходимо перезапустить, но остальные приложения не затрагиваются.
Платформа завершает канал при возникновении необработанного исключения по следующим причинам:
- Необработанное исключение часто оставляет схему в неопределенном состоянии.
- Гарантировать нормальную работу приложения после необработанного исключения невозможно.
- Если цепь продолжит находиться в неопределенном состоянии, в приложении могут появиться уязвимости безопасности.
Глобальная обработка исключений
Подходы к обработке исключений глобально см. в следующих разделах:
- Границы ошибок: применяется ко всем Blazor приложениям.
- Альтернативная глобальная обработка исключений: применяется к Blazor Server, Blazor WebAssemblyи Blazor Web Apps (8.0 или более поздней версии), которые применяют глобальный интерактивный режим отрисовки.
Границы ошибок
Границы ошибок предоставляют удобный подход к обработке исключений. Компонент ErrorBoundary:
- Отображает свое дочернее содержимое, если ошибка не произошла.
- Отображает UI ошибки при необработанном исключении, вызываемом любым компонентом внутри границы ошибки.
Чтобы определить границу ошибки, используйте ErrorBoundary компонент для упаковки одного или нескольких других компонентов. Граница ошибки управляет необработанными исключениями, создаваемыми компонентами, которые он упаковывает.
<ErrorBoundary>
...
</ErrorBoundary>
Чтобы реализовать границу ошибок в глобальном режиме, добавьте границу вокруг основного макета приложения.
В MainLayout.razor
:
<article class="content px-4">
<ErrorBoundary>
@Body
</ErrorBoundary>
</article>
В Blazor Web App, где граница ошибки применяется только к статическому компоненту MainLayout
, она активна только во время статической серверной отрисовки. Граница не активируется только потому, что компонент дальше вниз иерархии компонентов является интерактивным.
Не удается применить интерактивный режим отрисовки к MainLayout
компоненту, так как параметр компонента Body
является делегатом RenderFragment , который является произвольным кодом и не может быть сериализован. Чтобы включить интерактивное взаимодействие для компонента MainLayout
и остальных компонентов ниже иерархии компонентов, приложение должно применить глобальный интерактивный режим отрисовки, применяя интерактивный режим отрисовки к HeadOutlet
и Routes
экземплярам компонентов в корневом компоненте приложения, который обычно является компонентом App
. В следующем примере глобально используется режим отрисовки интерактивного сервера (InteractiveServer
).
В Components/App.razor
:
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />
Если вы предпочитаете не включать глобальную интерактивность, поместите границу ошибки дальше вниз по иерархии компонентов. Ключевые концепции, которые важно учитывать, заключаются в том, где именно разместить границу ошибки.
- Если компонент, на котором размещена граница ошибки, не является интерактивным, граница ошибки может активироваться только на сервере во время статического SSR. Например, граница может активироваться при возникновении ошибки в методе жизненного цикла компонента, но не для события, активируемого взаимодействием пользователей в компоненте, например ошибка, возникаемая обработчиком нажатия кнопки.
- Если компонент, на котором размещена граница ошибки, является интерактивным, граница ошибки может активироваться для интерактивных компонентов, которые он упаковывает.
Примечание.
Приведенные выше рекомендации не относятся к автономным Blazor WebAssembly приложениям, так как клиентская отрисовка (CSR) Blazor WebAssembly приложения полностью интерактивна.
Рассмотрим следующий пример, где исключение, вызываемое внедренным компонентом счетчика, перехватывается границей ошибки в 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!
). - Исключение обработано границей ошибки.
- Интерфейс обработки ошибок по умолчанию отображается границей обработки ошибок.
Компонент ErrorBoundary отображает пустой элемент <div>
, используя класс CSS blazor-error-boundary
для содержимого ошибки. Цвета, текст и значок интерфейса по умолчанию определяются в таблице стилей приложения в папке wwwroot
, поэтому вы можете настроить интерфейс отображения ошибок.
Чтобы изменить содержимое ошибки по умолчанию, выполните следующие действия.
- Оберните компоненты границы ошибки в свойство ChildContent.
- Задайте для свойства содержимое ErrorContent ошибки.
В следующем примере оборачивается EmbeddedCounter
компонент и предоставляется настраиваемое содержимое ошибки.
<ErrorBoundary>
<ChildContent>
<EmbeddedCounter />
</ChildContent>
<ErrorContent>
<p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
</ErrorContent>
</ErrorBoundary>
В предыдущем примере таблица стилей приложения, предположительно, включает errorUI
класс CSS для стиля содержимого. Содержимое ошибки формируется из свойства ErrorContent, без включения элемента уровня блока. Элемент уровня блока, например деление (<div>
) или элемент абзаца (<p>
), может упаковать разметку содержимого ошибки, но она не требуется.
При необходимости используйте контекст (@context
) для получения данных об ошибках ErrorContent :
<ErrorContent>
@context.HelpLink
</ErrorContent>
Также ErrorContent можно назвать контекст. В следующем примере контекст называется exception
:
<ErrorContent Context="exception">
@exception.HelpLink
</ErrorContent>
Предупреждение
Никогда не предоставляйте сведения об ошибках клиентам в Интернете, так как это подвергает безопасность угрозе.
Если граница ошибки определена в макете приложения, пользовательский интерфейс ошибки отображается независимо от того, на какую страницу пользователь переходит после возникновения ошибки. В большинстве случаев рекомендуется сузить границы ошибок. Если границе ошибки предоставлена широкая область ответственности, ее можно сбросить в состояние без ошибок при следующих событиях навигации страницы, вызывая метод границы ошибки Recover.
В MainLayout.razor
:
- Добавьте поле для ErrorBoundaryсохранения ссылки на него с помощью директивы атрибута
@ref
. - В методе
OnParameterSet
жизненного цикла можно активировать восстановление в границе обработки ошибок с помощью Recover, чтобы очистить ошибку при переходе пользователя к другому компоненту.
...
<ErrorBoundary @ref="errorBoundary">
@Body
</ErrorBoundary>
...
@code {
private ErrorBoundary? errorBoundary;
protected override void OnParametersSet()
{
errorBoundary?.Recover();
}
}
Чтобы избежать бесконечного цикла, когда восстановление просто перерисовывает компонент, который снова вызывает ошибку, не вызывайте Recover из логики отрисовки. Используйте Recover только в следующих случаях:
- Пользователь выполняет жест пользовательского интерфейса, например нажатие кнопки, чтобы указать, что они хотят повторить процедуру или когда пользователь переходит к новому компоненту.
- Дополнительная логика, которая выполняется, также очищает исключение. Если компонент перерендеривается, ошибка не повторяется.
В следующем примере пользователь может восстановиться из исключения с помощью кнопки:
<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;
}
Можно также создать подкласс на основе ErrorBoundary, переопределив OnErrorAsync для пользовательской обработки. Следующий пример просто регистрирует ошибку, но вы можете реализовать любой нужный код обработки ошибок. Вы можете удалить строку, которая возвращает 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 Apps, которые применяют глобальный интерактивный режим отрисовки (InteractiveServer
, InteractiveWebAssembly
или InteractiveAuto
). Подход не работает с Blazor Web Appами, которые используют режимы отрисовки на уровне страницы или компонента, или статическую серверную отрисовку (SSR), потому что этот метод основан на 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 см. в статье Компоненты Razor ASP.NET Core.
При использовании этого подхода в Blazor Web App, откройте компонент Routes
и оберните компонент Router (<Router>...</Router>
) с помощью компонента ProcessError
. Благодаря этому компонент ProcessError
сможет каскадно передаваться любому компоненту приложения, где компонент ProcessError
принят в качестве CascadingParameter
.
В Routes.razor
:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
При использовании этого подхода в приложении Blazor Server или Blazor WebAssembly откройте компонент App
, оберните компонент Router (<Router>...</Router>
) с помощью компонента ProcessError
. Благодаря этому компонент ProcessError
может передаваться вниз к любому компоненту приложения, где компонент ProcessError
получен как CascadingParameter
.
В App.razor
:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
Чтобы обработать ошибки в компоненте, выполните приведенные ниже действия.
Назначьте компонент
ProcessError
какCascadingParameter
в блоке@code
. В примере компонентаCounter
в приложении, основанном на шаблоне проекта Blazor, добавьте следующее свойство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 отрисованных элементов, вызовите StateHasChanged
в конце метода LogError
для повторной отрисовки пользовательского интерфейса.
Поскольку методы в этом разделе воспользуются оператором 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 см. в статье Компоненты Razor ASP.NET Core.
В компоненте App
оберните компонент Router с помощью компонента ProcessError
. Это позволяет компоненту ProcessError
распространяться на любой компонент приложения, в котором компонент ProcessError
используется в качестве CascadingParameter
.
App.razor
:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
Чтобы обработать ошибки в компоненте, выполните приведенные ниже действия.
Назначьте компонент
ProcessError
какCascadingParameter
в блоке@code
:[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 отрисованных элементов, вызовите StateHasChanged
в конце метода LogError
для повторной отрисовки пользовательского интерфейса.
Так как подходы в этом разделе предусматривают обработку ошибок с помощью инструкции try-catch
, соединение Blazor приложения SignalR между клиентом и сервером не прерывается при возникновении ошибки и канал остается активным. Необработанное исключение является фатальным для цепи. Дополнительные сведения см. в разделе о том, как канал реагирует на необработанные исключения.
Регистрация ошибок с персистентным поставщиком
При возникновении необработанного исключения оно регистрируется в экземплярах ILogger, которые настроены в контейнере службы. Blazor Выходные данные консоли журнала приложений с поставщиком ведения журнала консоли. Рассмотрите возможность логирования в расположение на сервере (или серверном веб-API для клиентских приложений) с помощью поставщика, который управляет размером логов и их ротацией. Кроме того, приложение может использовать службу управления производительностью приложений (APM), например Azure Application Insights (Azure Monitor).
Примечание.
Функции Native Application Insights для поддержки клиентских приложений и собственной Blazor платформы поддержки Google Analytics могут стать доступными в будущих выпусках этих технологий. Дополнительные сведения см. в статьях Поддержка App Insights на стороне клиента Blazor WASM (microsoft/ApplicationInsights-dotnet #2143) и Веб-аналитика и диагностика (содержит ссылки на реализации сообщества) (dotnet/aspnetcore #5461). В то же время клиентское приложение может использовать JavaScript SDK для Application Insights с помощью interop для логирования ошибок непосредственно в Application Insights из клиентского приложения.
Во время разработки в приложении Blazor, работающем на схеме, приложение обычно отправляет полные сведения об исключениях в консоль браузера, чтобы помочь в отладке. В рабочей среде подробные сообщения об ошибках не отправляются клиентам, но все сведения об исключениях записываются на сервер.
Необходимо решить, какие инциденты следует заносить в журнал, и выбрать уровень серьезности регистрируемых инцидентов. Злоумышленники могут попытаться вызывать ошибки намеренно. Например, не заносите в журнал инцидент в результате ошибки, когда в URL-адресе компонента, отображающего сведения о продукте, указан неизвестный ProductId
. Не все ошибки следует рассматривать как инциденты, подлежащие записи в журнал.
Дополнительные сведения см. в следующих статьях:
- Ведение журналов BlazorASP.NET Core
- Обработка ошибок в ASP.NET Core‡
- Создание веб-API с помощью ASP.NET Core
.Применяется к серверным приложениям и другим серверным Blazor приложениям ASP.NET Core, которые являются внутренними приложениями веб-API для Blazor. Клиентские приложения могут перехватывать и отправлять информацию об ошибках, возникающих на стороне клиента, в веб-API, который записывает эту информацию в постоянного поставщика ведения журнала.
При возникновении необработанного исключения оно регистрируется в экземплярах ILogger, которые настроены в контейнере службы. Blazor приложения записывают вывод консоли с использованием поставщика ведения журнала консоли. Рассмотрите возможность сохранения записей журнала в более постоянной директории на сервере, отправляя данные об ошибках в backend веб-API, который использует поставщика ведения журнала с функциями управления размером логов и ротацией логов. Кроме того, серверное приложение веб-API может использовать службу управления производительностью приложений (APM), например Azure Application Insights (Azure Monitor)†, для записи сведений об ошибках, получаемых от клиентов.
Необходимо решить, какие инциденты следует заносить в журнал, и выбрать уровень серьезности регистрируемых инцидентов. Злоумышленники могут попытаться вызывать ошибки намеренно. Например, не заносите в журнал инцидент в результате ошибки, когда в URL-адресе компонента, отображающего сведения о продукте, указан неизвестный ProductId
. Не все ошибки следует рассматривать как инциденты, подлежащие записи в журнал.
Дополнительные сведения см. в следующих статьях:
- Ведение журналов BlazorASP.NET Core
- Обработка ошибок в ASP.NET Core‡
- Создание веб-API с помощью ASP.NET Core
функции †Native Application Insights для поддержки клиентских приложений и собственной Blazor платформы поддержки Google Analytics могут стать доступными в будущих выпусках этих технологий. Дополнительные сведения см. в статьях Поддержка App Insights на стороне клиента Blazor WASM (microsoft/ApplicationInsights-dotnet #2143) и Веб-аналитика и диагностика (содержит ссылки на реализации сообщества) (dotnet/aspnetcore #5461). В то же время клиентское приложение может использовать JavaScript SDK для Application Insights с JS интероперабельностью, чтобы отправлять ошибки непосредственно в Application Insights из клиентского приложения.
‡Применяется к приложениям 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?}"
@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
с обработкой ошибок и ведением журнала.
Дополнительные сведения об удалении компонентов см. в разделе Удаление компонентов в ASP.NET Core Razor.
Взаимодействие с 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 В приложении, работающем по схеме, исключение не рассматривается как неустранимая для цепи приложения.
-
JS на стороне
Promise
отклоняется.
Вы можете использовать код обработки ошибок либо на стороне .NET, либо на стороне JS вызова метода.
Дополнительные сведения см. в следующих статьях:
- Вызов функций JavaScript из методов .NET в ASP.NET Core Blazor
- Вызов методов .NET из функций JavaScript в Blazor ASP.NET Core
Предварительная отрисовка
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 используют для ведения журнала.
ASP.NET Core