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

.NET 7中Blazor WASM无法接收ASP.NET Core Web API的SignalR消息

Blazor WASM无法接收SignalR通知(CQRS投影场景)

问题背景

在.NET 7环境下搭建Blazor WebAssembly客户端+ASP.NET Core Web API服务端,采用CQRS模式开发时,查询投影无法即时就绪,因此通过SignalR向客户端推送投影的创建/更新状态,但客户端始终无法收到SignalR事件,自查服务端消息发送逻辑未发现问题,请求排查。

CQRS完整流程

  • Blazor WASM客户端向Web API发送POST/PUT请求
  • Web API控制器获取SignalR连接ID(代码中称为“client ID”)并传入MediatR请求
  • MediatR管道行为通过IHttpContextAccessor将该ID存入HTTP上下文
  • SaveChangesInterceptor从上下文读取ID,关联到数据库中的领域事件
  • 领域事件分发器发布包含该ID的事件
  • 领域事件处理器创建/更新查询投影,并尝试发送SignalR通知

排查方向建议

1. SignalR连接与ID一致性校验

  • 对比客户端与服务端的连接ID:客户端在HubConnection建立成功后,输出_hubConnection.ConnectionId;服务端在API控制器接收请求时,打印传入的client ID,确认二者完全匹配(注意大小写和格式)
  • 检查SignalR配置:
    • 服务端是否已添加SignalR服务(builder.Services.AddSignalR())并映射Hub端点(app.MapHub<NotificationHub>("/notification-hub"))
    • 客户端是否使用正确的Hub地址建立连接,确保地址与服务端映射路径一致

2. 领域事件中client ID的传递校验

  • 在SaveChangesInterceptor中添加日志,输出读取到的client ID值,确认从HTTP上下文成功获取
  • 直接查询数据库中的领域事件记录,验证client ID字段已被正确赋值

3. SignalR消息发送的上下文与方法校验

  • 检查领域事件处理器的执行上下文:如果处理器是在后台线程执行(如MediatR后台任务),确认IHubContext能被正确注入,不存在DI范围失效问题
  • 确认消息发送方法正确性:确保调用Clients.Client(clientId).SendAsync("EventName", payload),且事件名称与客户端订阅的名称完全一致(大小写敏感)

4. 客户端订阅逻辑校验

  • 确保订阅操作在HubConnection.StartAsync()完成后执行,避免连接未就绪时订阅失效
  • 检查订阅的事件名称与服务端发送的名称完全匹配,无大小写或拼写错误
  • 为订阅添加错误处理,捕获并输出可能的订阅异常:
    _hubConnection.On<ProjectionUpdatedDto>("ProjectionUpdated", 
        dto => { /* 处理逻辑 */ },
        error => Console.WriteLine($"订阅错误: {error.Message}")
    );
    

关键代码片段

服务端API控制器

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateCommand command)
{
    var clientId = HttpContext.Request.Headers["X-SignalR-ClientId"].FirstOrDefault();
    command.ClientId = clientId;
    await _mediator.Send(command);
    return Ok();
}

MediatR管道行为

public class ClientIdBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> 
    where TRequest : IClientIdRequest
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ClientIdBehavior(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        if (!string.IsNullOrEmpty(request.ClientId))
        {
            _httpContextAccessor.HttpContext.Items["ClientId"] = request.ClientId;
        }
        return await next();
    }
}

SaveChangesInterceptor

public class DomainEventInterceptor : SaveChangesInterceptor
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public DomainEventInterceptor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
    {
        var clientId = _httpContextAccessor.HttpContext?.Items["ClientId"] as string;
        if (!string.IsNullOrEmpty(clientId))
        {
            var domainEntities = eventData.Context.ChangeTracker
                .Entries<Entity>()
                .Where(e => e.Entity.DomainEvents.Any())
                .Select(e => e.Entity);

            foreach (var entity in domainEntities)
            {
                foreach (var domainEvent in entity.DomainEvents)
                {
                    ((IDomainEventWithClientId)domainEvent).ClientId = clientId;
                }
            }
        }
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }
}

领域事件处理器

public class ProjectionUpdatedHandler : INotificationHandler<ProjectionUpdatedEvent>
{
    private readonly IHubContext<NotificationHub> _hubContext;

    public ProjectionUpdatedHandler(IHubContext<NotificationHub> hubContext)
    {
        _hubContext = hubContext;
    }

    public async Task Handle(ProjectionUpdatedEvent notification, CancellationToken cancellationToken)
    {
        // 创建/更新投影逻辑...

        if (!string.IsNullOrEmpty(notification.ClientId))
        {
            await _hubContext.Clients.Client(notification.ClientId)
                .SendAsync("ProjectionUpdated", new ProjectionUpdatedDto(), cancellationToken);
        }
    }
}

客户端SignalR订阅

private HubConnection _hubConnection;

protected override async Task OnInitializedAsync()
{
    _hubConnection = new HubConnectionBuilder()
        .WithUrl(NavigationManager.ToAbsoluteUri("/notification-hub"))
        .Build();

    _hubConnection.On<ProjectionUpdatedDto>("ProjectionUpdated", dto =>
    {
        // 处理通知逻辑
        StateHasChanged();
    });

    try
    {
        await _hubConnection.StartAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"连接失败: {ex.Message}");
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 02:57:53