You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免Blazor Server中CircuitHost组件的集合修改异常?

Blazor Server SSR模式下大量日志插入导致集合修改异常

我运行着Server Side Render模式的Blazor Web应用,用于监控数据库变更并将变更信息渲染至浏览器。

监控数据库变更并通知浏览器的代码

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($"tell client: {clientId} when app log changed: {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($"receive from hub2: {logMsg}");
        messages.Add(logMsg);
        await InvokeAsync(StateHasChanged);

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

异常信息

warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100]
      Unhandled exception rendering component: Collection was modified; enumeration operation may not execute.
      System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
         at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
         at System.Collections.Generic.List`1.Enumerator.MoveNext()
         at Radzen.Blazor.Rendering.Markers`1.BuildRenderTree(RenderTreeBuilder __builder)
         at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
         at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
      Unhandled exception in circuit 'ZqYGFNtGl3fN67EQprc7TtLm6bTk1DNfRUOpqxgDQJQ'.
      System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
         at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
         at System.Collections.Generic.List`1.Enumerator.MoveNext()
         at Radzen.Blazor.Rendering.Markers`1.BuildRenderTree(RenderTreeBuilder __builder)
         at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
         at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]

问题原因及解决方法

核心原因

异常本质是集合的并发修改:Blazor组件在UI线程渲染时会遍历messages集合(Radzen组件内部正在执行遍历操作),而SignalR的回调是在后台线程执行,此时调用messages.Add修改集合,List<T>并非线程安全集合,遍历过程中修改会触发该异常。

解决方法

方法1:加锁同步集合访问

通过锁确保集合的读写操作互斥,避免并发冲突:

  1. 在组件中定义锁对象和集合:
private readonly object _messagesLock = new object();
private List<string> messages = new List<string>();
  1. 修改SignalR回调中的集合添加逻辑:
hubConnection.On<string>("AppLogUpdated", async (logMsg) =>
{
    lock(_messagesLock)
    {
        messages.Add(logMsg);
    }
    await InvokeAsync(StateHasChanged);
});
  1. 渲染时获取集合快照(避免渲染过程中集合被修改):
private List<string> GetMessagesSnapshot()
{
    lock(_messagesLock)
    {
        return messages.ToList();
    }
}
  1. 组件绑定数据时使用快照:
<!-- 假设使用RadzenList组件展示日志 -->
<RadzenList Data="@GetMessagesSnapshot()" TItem="string">
    <Template Context="msg">
        <p>@msg</p>
    </Template>
</RadzenList>

方法2:使用线程安全集合并取快照渲染

用ConcurrentQueue<string>(线程安全的队列集合)替代List<T>,渲染时转换为List快照:

  1. 替换集合类型:
private ConcurrentQueue<string> messages = new ConcurrentQueue<string>();
  1. 修改SignalR回调的添加逻辑:
hubConnection.On<string>("AppLogUpdated", async (logMsg) =>
{
    messages.Enqueue(logMsg);
    await InvokeAsync(StateHasChanged);
});
  1. 渲染时取快照:
<RadzenList Data="@messages.ToList()" TItem="string">
    <Template Context="msg">
        <p>@msg</p>
    </Template>
</RadzenList>

额外优化

如果日志量极大,频繁触发StateHasChanged会导致UI频繁更新,可考虑批量更新:比如累计N条日志后再触发一次渲染,或设置固定间隔合并更新,减少渲染压力。

内容的提问来源于stack exchange,提问作者Penney Wang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 21:57:13