Как Blazor Server обрабатывает circuit disconnect/reconnect и состояние пользователя?
Circuit — серверный объект с состоянием пользователя в Blazor Server. При разрыве SignalR circuit сохраняется на сервере до истечения DisconnectedCircuitRetentionPeriod (по умолчанию 3 мин), после чего уничтожается и состояние теряется.
Circuit disconnect/reconnect в Blazor Server
В Blazor Server каждое соединение пользователя с сервером называется circuit. Это объект на стороне сервера, хранящий состояние всех компонентов, DI-сервисов со Scoped-временем жизни и очередь обновлений DOM. Связь осуществляется через SignalR WebSocket.
Жизненный цикл circuit
- Connected — активное соединение, события обрабатываются в реальном времени.
- Disconnected — соединение прервано (мобильная сеть, перезагрузка вкладки). Circuit остаётся живым на сервере в течение настроенного таймаута.
- Reconnecting — браузер пытается восстановить соединение, показывая встроенный UI.
- Disposed — таймаут истёк или пользователь закрыл вкладку, circuit уничтожается.
Настройка таймаута и circuit
// Program.cs
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddServerSideBlazor(options =>
{
// Таймаут неактивного circuit (default: 3 minutes)
options.DisconnectedCircuitMaxRetained = 100;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(5);
// JSInterop timeout
options.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds(60);
});
Кастомные обработчики disconnect/reconnect
Реализуйте CircuitHandler для отслеживания жизненного цикла circuit в сервисах:
using Microsoft.AspNetCore.Components.Server.Circuits;
public class TrackingCircuitHandler : CircuitHandler
{
private readonly ILogger<TrackingCircuitHandler> _logger;
public TrackingCircuitHandler(ILogger<TrackingCircuitHandler> logger)
{
_logger = logger;
}
public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken ct)
{
_logger.LogInformation("Circuit {Id} connected", circuit.Id);
return Task.CompletedTask;
}
public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken ct)
{
_logger.LogWarning("Circuit {Id} disconnected", circuit.Id);
return Task.CompletedTask;
}
public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken ct)
{
_logger.LogInformation("Circuit {Id} closed", circuit.Id);
return Task.CompletedTask;
}
}
// Регистрация
builder.Services.AddScoped<CircuitHandler, TrackingCircuitHandler>();
Состояние пользователя при разрыве
Пока circuit жив на сервере (в пределах таймаута), все Scoped-сервисы и состояние компонентов сохраняются. При реконнекте браузер повторно синхронизирует DOM. Если circuit истёк — состояние теряется, пользователь видит ошибку и должен перезагрузить страницу.
Для критически важных данных используйте одну из стратегий персистентности:
- Server-side store: хранить состояние в Redis/БД, связав с userId.
- PersistentComponentState: serialization state при prerendering.
- localStorage через JSInterop: сохранять черновики форм на клиенте.
// Пример сохранения в localStorage при изменении
@inject IJSRuntime JS
private async Task OnFormChanged()
{
var json = JsonSerializer.Serialize(_formModel);
await JS.InvokeVoidAsync("localStorage.setItem", "draft", json);
}
Подводные камни
- Scoped DI = per-circuit, не per-request. Scoped-сервисы живут всё время circuit. Если храните в них состояние — оно не обнуляется между навигациями.
- Goroutine leak при disconnected. Async-операции, запущенные в circuit (background timer, HttpClient запросы), продолжают работать пока circuit жив. Используйте CancellationToken из
ComponentBase.CancellationToken. - Масштабирование без sticky sessions. При нескольких серверах реконнект может попасть на другой узел, где нет circuit. Нужен SignalR Redis Backplane или sticky sessions в load balancer.
- Concurrent updates из background tasks. При обновлении UI из фонового потока обязательно вызывайте
InvokeAsync(StateHasChanged), иначе thread-safety нарушена. - Таймаут по умолчанию 3 минуты. Для мобильных пользователей с нестабильной сетью этого может не хватить. Увеличивайте
DisconnectedCircuitRetentionPeriodс учётом нагрузки на память сервера. - Потеря state при истечении circuit. Нет автоматического восстановления — пользователь видит ошибку. Используйте
<ReconnectionModal>кастомный UI для лучшего UX.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.