Blazor Server用户间消息传递:接收方异常致发送方崩溃求助
解决方案
针对Blazor Server中接收方处理消息异常导致发送方崩溃的问题,核心是切断发送方与接收方的异常传导链路,同时保证发送方无需关注后续处理状态。以下是几个实用的落地方案:
方案1:为每个事件订阅者添加独立异常捕获
问题根源在于触发MessageReceived事件时,若某个接收方的处理逻辑抛出未捕获异常,会沿着调用链回传到发送方的调用上下文。解决思路是在分发消息时,为每个订阅者的处理逻辑单独包裹异常捕获,确保单个接收方的异常不会影响其他订阅者和发送方。
修改NotifyMessageReceived方法示例:
public void NotifyMessageReceived(List<string> notifyIds, MessageDto messageDto) { var handlers = MessageReceived; if (handlers == null) return; // 遍历所有订阅者,逐个调用并捕获异常 foreach (var handler in handlers.GetInvocationList()) { // 根据业务逻辑筛选需要通知的接收方(示例:从handler关联的用户ID判断) var receiverUserId = GetUserIdFromHandler(handler); if (!notifyIds.Contains(receiverUserId)) continue; try { handler.DynamicInvoke(this, new MessageReceivedEventArgs(messageDto)); } catch (Exception ex) { // 记录异常日志,便于排查问题 _logger.LogError(ex, "接收方 {UserId} 处理消息失败", receiverUserId); } } }
方案2:异步事件+后台任务解耦
将消息处理逻辑放到独立的后台任务中执行,发送方调用后直接返回,不等待接收方处理完成,彻底切断同步调用的异常传导路径。
步骤1:定义异步事件
// 替换原有的同步事件 public event Func<object, MessageReceivedEventArgs, Task> MessageReceivedAsync;
步骤2:异步分发消息
public async Task NotifyMessageReceivedAsync(List<string> notifyIds, MessageDto messageDto) { var handlers = MessageReceivedAsync; if (handlers == null) return; foreach (var handler in handlers.GetInvocationList().Cast<Func<object, MessageReceivedEventArgs, Task>>()) { var receiverUserId = GetUserIdFromHandler(handler); if (!notifyIds.Contains(receiverUserId)) continue; // 用fire-and-forget的后台任务执行,发送方无需等待 _ = Task.Run(async () => { try { // 若接收方需要更新UI,必须通过InvokeAsync切换到Blazor电路上下文 await handler.Invoke(this, new MessageReceivedEventArgs(messageDto)); } catch (Exception ex) { _logger.LogError(ex, "接收方 {UserId} 异步处理消息失败", receiverUserId); } }); } }
发送方调用时直接执行await _usersStateContainer.NotifyMessageReceivedAsync(notifyIds, messageDto)即可,调用完成后立即返回,不受接收方处理结果影响。
方案3:引入中间消息队列(彻底解耦)
通过内存队列或第三方队列实现发送方与接收方的完全解耦,发送方仅负责将消息写入队列,后续分发逻辑由后台服务独立处理,接收方的异常绝对不会传导到发送方。
示例:内存消息队列实现
public interface IMessageQueue { void Enqueue(List<string> notifyIds, MessageDto messageDto); } public class InMemoryMessageQueue : IMessageQueue { private readonly ConcurrentQueue<MessageQueueItem> _queue = new(); private readonly IUsersStateContainer _usersStateContainer; private readonly ILogger<InMemoryMessageQueue> _logger; private readonly CancellationToken _stoppingToken; public InMemoryMessageQueue(IUsersStateContainer usersStateContainer, ILogger<InMemoryMessageQueue> logger, IHostApplicationLifetime appLifetime) { _usersStateContainer = usersStateContainer; _logger = logger; _stoppingToken = appLifetime.ApplicationStopping; // 启动队列处理后台任务 _ = ProcessQueueAsync(); } public void Enqueue(List<string> notifyIds, MessageDto messageDto) { _queue.Enqueue(new MessageQueueItem { NotifyIds = notifyIds, Message = messageDto }); } private async Task ProcessQueueAsync() { while (!_stoppingToken.IsCancellationRequested) { if (_queue.TryDequeue(out var item)) { try { await _usersStateContainer.NotifyMessageReceivedAsync(item.NotifyIds, item.Message); } catch (Exception ex) { _logger.LogError(ex, "队列消息分发失败,将重新入队"); // 失败消息重新入队,避免丢失 _queue.Enqueue(item); await Task.Delay(1000, _stoppingToken); } } else { await Task.Delay(100, _stoppingToken); } } } private class MessageQueueItem { public List<string> NotifyIds { get; set; } public MessageDto Message { get; set; } } }
发送方只需调用_messageQueue.Enqueue(notifyIds, messageDto)即可完成消息发送,后续所有分发、处理逻辑由后台服务独立执行,完全与发送方解耦。
关键注意事项
- 若接收方处理逻辑涉及UI更新,必须调用
InvokeAsync切换到Blazor电路上下文,避免线程异常(但该异常已被捕获,不会影响发送方)。 - 所有异常必须记录日志,便于后续排查接收方的处理问题。
- 若使用异步任务,务必在任务内部捕获所有异常,避免未处理异常导致应用崩溃。
内容的提问来源于stack exchange,提问作者Paul
相关产品推荐
相关产品推荐

