Как избежать исключения Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost в Blazor на стороне сервера?

Вопрос или проблема

Я запустил веб-приложение Blazor, которое работает в режиме серверного рендеринга. Приложение отслеживает изменения в базе данных и отображает измененную информацию в браузере. Код, который следит за изменениями в базе данных и уведомляет браузер, приведен ниже:

    public async Task WatchAppLogChangesAsync(CancellationToken cancellationToken)
    {
        var pipeline = new EmptyPipelineDefinition<ChangeStreamDocument<AppLog>>().Match(x => x.OperationType == ChangeStreamOperationType.Insert);
        using (var cursor = await _appLogsCollection.WatchAsync(pipeline))
        {
            await cursor.ForEachAsync(async change =>
            {
                var playerId = change.FullDocument.UserId;
                if (_playerClientMap.TryGetValue(playerId, out var clients))
                {
                    foreach (var clientId in clients)
                    {
                        Console.WriteLine($"уведомить клиента: {clientId} о изменении лога приложения: {change.FullDocument.Message}");
                        await _hubContext.Clients.Client(clientId).SendAsync("AppLogUpdated", $"{change.FullDocument.Message}\r\n{change.FullDocument.RawData}");
                    }
                }
            });
        }
    }

Код для отображения лога приведен ниже:

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
            return;
        hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .Build();

        hubConnection.On<string>("AppLogUpdated", async (logMsg) =>
        {
            @* Console.WriteLine($"получено от hub2: {logMsg}"); *@
            messages.Add(logMsg);
            await InvokeAsync(StateHasChanged);

        });
        await hubConnection.StartAsync();
        await Send();
        await GetLatestLogs(2);
    }

Когда большое количество логов внезапно вставляется, я получаю следующую исключительную ситуацию:

warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100]
      Необработанное исключение при рендеринге компонента: Коллекция была изменена; операция перечисления может не выполниться.
      System.InvalidOperationException: Коллекция была изменена; операция перечисления может не выполниться.
         в System.Collections.Generic.List`1.Enumerator.MoveNextRare()
         в System.Collections.Generic.List`1.Enumerator.MoveNext()
         в Radzen.Blazor.Rendering.Markers`1.BuildRenderTree(RenderTreeBuilder __builder)
         в Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
         в Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
         в Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
         в Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
      Необработанное исключение в цепочке 'ZqYGFNtGl3fN67EQprc7TtLm6bTk1DNfRUOpqxgDQJQ'.
      System.InvalidOperationException: Коллекция была изменена; операция перечисления может не выполниться.
         в System.Collections.Generic.List`1.Enumerator.MoveNextRare()
         в System.Collections.Generic.List`1.Enumerator.MoveNext()
         в Radzen.Blazor.Rendering.Markers`1.BuildRenderTree(RenderTreeBuilder __builder)
         в Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
         в Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
         в Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
         в Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]

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

Ответ или решение

Чтобы избежать исключения Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost в приложении Blazor Server, связанного с изменением коллекции во время ее перебора, необходимо применять несколько подходов. Это исключение возникает, когда вы добавляете или удаляете элементы из коллекции, когда она уже перебирается (например, в процессе рендеринга компонента).

Возможные решения проблемы:

1. Использование блокировки

Синхронизация или блокировка может помочь предотвратить изменения коллекции, пока она перебирается. Например, можно использовать lock в C#:

private readonly object _lock = new object();

hubConnection.On<string>("AppLogUpdated", async (logMsg) =>
{
    lock (_lock)
    {
        messages.Add(logMsg);
    }
    await InvokeAsync(StateHasChanged);
});

2. Запись изменений в отдельный список

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

private List<string> _pendingMessages = new List<string>();

hubConnection.On<string>("AppLogUpdated", async (logMsg) =>
{
    _pendingMessages.Add(logMsg);
    await InvokeAsync(StateHasChanged);
});

// В методе, который будет вызываться после обновления
private void UpdateMessages()
{
    messages.AddRange(_pendingMessages);
    _pendingMessages.Clear();
    StateHasChanged();
}

3. Использование InvokeAsync для обновления состояния

InvokeAsync гарантирует, что код будет выполнен на основном потоке UI, что может помочь избежать каких-либо конфликтов при обновлении состояния вашего компонента.

4. Отложенное обновление

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

hubConnection.On<string>("AppLogUpdated", (logMsg) =>
{
    messages.Add(logMsg);
    _ = InvokeAsync(UpdateMessages); // Отложенное обновление
});

5. Разделение логики визуализации и работы с данными

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

Заключение

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

Если у вас остались вопросы или нужна дополнительная помощь, пожалуйста, дайте знать!

Оцените материал
Добавить комментарий

Капча загружается...