BlazorMiddleTechnical

Как 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.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics