Вопрос или проблема
Я запустил веб-приложение 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. Разделение логики визуализации и работы с данными
Убедитесь, что логика рендеринга компонентов не погружена в обработку событий в реальном времени. Если вы будете обрабатывать обновления потоков отдельно, это поможет избежать проблем с изменением во время перебора.
Заключение
Эти методы помогут вам избежать исключений, связанных с изменением коллекции, что важно для стабильной работы приложения. Выберите подходящий способ для вашего конкретного случая. Каждое решение имеет свои плюсы и минусы, и вам следует протестировать каждый подход, чтобы понять, что лучше всего подходит для вашей архитектуры приложения.
Если у вас остались вопросы или нужна дополнительная помощь, пожалуйста, дайте знать!