Asp.Net Core Blazor&SignalR 后端消息推送
目录
前言
为了能够将后台Job的进度同步前端,借助SignalR和BackgroundJob很方便完成同步。Blazor三种模式下,都能很方便的完成,并且无需引入js包,写前端代码,很是方便。
Blazor Server(WebApp)模式
新建项目,选择Blazor WebApp Server模式
 增加Hub,连接前端和后端通信
增加Hub,连接前端和后端通信
public class MessageHub : Hub
{
    public async Task SendMessage(string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", message);
    }
}
program.cs中增加端点
// Configure SignalR endpoint
app.MapHub<MessageHub>("/messageHub");
Blazor Server基于SignalR,所以不需要如下SignalR注册
builder.Services.AddSignalR();
添加Nuget包,该包用于前端页面处理SignalR。
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.16" />
</ItemGroup>
新建Blazor页,需要注意请求路径需要与端点路径保持一致。
@page "/message"
@using Microsoft.AspNetCore.SignalR.Client
@implements IAsyncDisposable
@rendermode InteractiveServer
@inject NavigationManager NavigationManager
<PageTitle>Background Messages</PageTitle>
<h3>Background Messages</h3>
<div class="message-container">
    @foreach (var message in messages)
    {
        <div class="message">
            <span class="message-time">@message.TimeStamp.ToString("HH:mm:ss")</span>
            <span class="message-content">@message.Content</span>
        </div>
    }
</div>
<style>
    .message-container {
        max-height: 500px;
        overflow-y: auto;
        border: 1px solid #ddd;
        padding: 10px;
        margin-top: 20px;
    }
    .message {
        padding: 8px;
        margin-bottom: 8px;
        background-color: #f8f9fa;
        border-radius: 4px;
    }
    .message-time {
        color: #666;
        margin-right: 10px;
    }
    .message-content {
        color: #333;
    }
</style>
@code {
    private HubConnection? hubConnection;
    private List<MessageModel> messages = new();
    private const int MaxMessages = 100;
    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/messageHub"))
            .WithAutomaticReconnect()
            .Build();
        hubConnection.On<string>("ReceiveMessage", (message) =>
        {
            messages.Insert(0, new MessageModel { Content = message, TimeStamp = DateTime.Now });
            if (messages.Count > MaxMessages)
            {
                messages.RemoveAt(messages.Count - 1);
            }
            InvokeAsync(StateHasChanged);
        });
        await hubConnection.StartAsync();
    }
    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
    private class MessageModel
    {
        public string Content { get; set; } = string.Empty;
        public DateTime TimeStamp { get; set; }
    }
}
如此,前后端通信即可配置完毕,接下来增加BackgroundJob,其内部发送消息,从而推送到前端展示。新增BackgroundJob
public class BackgroundJobService : BackgroundService
{
    private readonly ILogger<BackgroundJobService> _logger;
    private readonly IHubContext<MessageHub> _hubContext;
    public BackgroundJobService(
        ILogger<BackgroundJobService> logger,
        IHubContext<MessageHub> hubContext)
    {
        _logger = logger;
        _hubContext = hubContext;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Background job is running at: {time}", DateTimeOffset.Now);
                
                // 发送消息到所有连接的客户端
                var message = $"Background service message at {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
                await _hubContext.Clients.All.SendAsync("ReceiveMessage", message);
                
                await Task.Delay(TimeSpan.FromSeconds(20), stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error occurred while executing background job");
            }
        }
    }
}
program.cs中注册BackgorundService。
// 注册后台服务
builder.Services.AddHostedService<BackgroundJobService>();
启动运行,如此即可同步后端消息,推送到前端展示。

Blazor WebAssembly(WebApp)模式
新建项目,选择Blazor WebApp WebAssembly模式
 Demo结构如下
Demo结构如下
 在主Host中增加Hub
在主Host中增加Hub
public class MessageHub : Hub
{
    public async Task SendMessage(string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", message);
    }
}
增加BackgroundJob
public class BackgroundJobService : BackgroundService
{
    private readonly ILogger<BackgroundJobService> _logger;
    private readonly IHubContext<MessageHub> _hubContext;
    public BackgroundJobService(
        ILogger<BackgroundJobService> logger,
        IHubContext<MessageHub> hubContext)
    {
        _logger = logger;
        _hubContext = hubContext;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Background job is running at: {time}", DateTimeOffset.Now);
                // 发送消息到所有连接的客户端
                var message = $"Background service message at {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
                await _hubContext.Clients.All.SendAsync("ReceiveMessage", message);
                await Task.Delay(TimeSpan.FromSeconds(20), stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error occurred while executing background job");
            }
        }
    }
}
在Program.cs中注册Job、注册SignalR服务、配置端点
// 注册后台服务
builder.Services.AddHostedService<BackgroundJobService>();
// 注册 SignalR 服务
builder.Services.AddSignalR();
app.MapHub<MessageHub>("/messagehub");
在Client中增加SignalR的Nuget包
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.16" />
增加Blazor页面
@page "/message"
@rendermode InteractiveWebAssembly
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IAsyncDisposable
<PageTitle>Background Messages</PageTitle>
<h3>Background Messages</h3>
<div class="message-container">
    @foreach (var message in messages)
    {
        <div class="message">
            <span class="message-time">@message.TimeStamp.ToString("HH:mm:ss")</span>
            <span class="message-content">@message.Content</span>
        </div>
    }
</div>
<style>
    .message-container {
        max-height: 500px;
        overflow-y: auto;
        border: 1px solid #ddd;
        padding: 10px;
        margin-top: 20px;
    }
    .message {
        padding: 8px;
        margin-bottom: 8px;
        background-color: #f8f9fa;
        border-radius: 4px;
    }
    .message-time {
        color: #666;
        margin-right: 10px;
    }
    .message-content {
        color: #333;
    }
</style>
@code {
    private HubConnection? hubConnection;
    private List<MessageModel> messages = new();
    private const int MaxMessages = 100;
    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/messageHub"))
            .WithAutomaticReconnect()
            .Build();
        hubConnection.On<string>("ReceiveMessage", (message) =>
        {
            messages.Insert(0, new MessageModel { Content = message, TimeStamp = DateTime.Now });
            if (messages.Count > MaxMessages)
            {
                messages.RemoveAt(messages.Count - 1);
            }
            InvokeAsync(StateHasChanged);
        });
        await hubConnection.StartAsync();
    }
    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
    private class MessageModel
    {
        public string Content { get; set; } = string.Empty;
        public DateTime TimeStamp { get; set; }
    }
}
启动运行后,同样能够看到后端推送到前端消息

Blazor WebAssembly(Standalone)模式
与前两者模式不同,Blazor WebAssembly通常需要额外的后端配合。
新建Blazor WebAssembly(非Blazor WebApp中的WebAssembly)。
 新建WebApi,用来模拟后端消息同步给前端
新建WebApi,用来模拟后端消息同步给前端
 Demo结构如下
Demo结构如下
 同样与Blazor Server过程相同,增加Hub到Api项目中
同样与Blazor Server过程相同,增加Hub到Api项目中
public class MessageHub : Hub
{
    public async Task SendMessage(string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", message);
    }
}
增加BackgroundJob到Api项目中
public class BackgroundJobService : BackgroundService
{
    private readonly ILogger<BackgroundJobService> _logger;
    private readonly IHubContext<MessageHub> _hubContext;
    public BackgroundJobService(
        ILogger<BackgroundJobService> logger,
        IHubContext<MessageHub> hubContext)
    {
        _logger = logger;
        _hubContext = hubContext;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Background job is running at: {time}", DateTimeOffset.Now);
                
                // 发送消息到所有连接的客户端
                var message = $"Background service message at {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
                await _hubContext.Clients.All.SendAsync("ReceiveMessage", message);
                
                await Task.Delay(TimeSpan.FromSeconds(20), stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error occurred while executing background job");
            }
        }
    }
}
在program.cs中注册Job、注册SignalR、配置跨域和设置端点等。
// 添加CORS服务,允许所有来源
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder.AllowAnyOrigin()
               .AllowAnyMethod()
               .AllowAnyHeader();
    });
});
// 注册后台服务
builder.Services.AddHostedService<BackgroundJobService>();
// 注册 SignalR 服务
builder.Services.AddSignalR();
//...
// 启用CORS中间件
app.UseCors();
// 配置 SignalR 端点
app.MapHub<MessageHub>("/messageHub");
在App项目中,安装SignalR的Nuget包
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.16" />
增加Blazor页面
@page "/message"
@using Microsoft.AspNetCore.SignalR.Client
@implements IAsyncDisposable
<PageTitle>Background Messages</PageTitle>
<h3>Background Messages</h3>
<div class="message-container">
    @foreach (var message in messages)
    {
        <div class="message">
            <span class="message-time">@message.TimeStamp.ToString("HH:mm:ss")</span>
            <span class="message-content">@message.Content</span>
        </div>
    }
</div>
<style>
    .message-container {
        max-height: 500px;
        overflow-y: auto;
        border: 1px solid #ddd;
        padding: 10px;
        margin-top: 20px;
    }
    .message {
        padding: 8px;
        margin-bottom: 8px;
        background-color: #f8f9fa;
        border-radius: 4px;
    }
    .message-time {
        color: #666;
        margin-right: 10px;
    }
    .message-content {
        color: #333;
    }
</style>
@code {
    private HubConnection? hubConnection;
    private List<MessageModel> messages = new();
    private const int MaxMessages = 100;
    protected override async Task OnInitializedAsync()
    {
        // Api项目地址
        hubConnection = new HubConnectionBuilder()
            .WithUrl(new Uri("https://localhost:7012/messageHub"))
            .WithAutomaticReconnect()
            .Build();
        hubConnection.On<string>("ReceiveMessage", (message) =>
        {
            messages.Insert(0, new MessageModel { Content = message, TimeStamp = DateTime.Now });
            if (messages.Count > MaxMessages)
            {
                messages.RemoveAt(messages.Count - 1);
            }
            InvokeAsync(StateHasChanged);
        });
        await hubConnection.StartAsync();
    }
    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
    private class MessageModel
    {
        public string Content { get; set; } = string.Empty;
        public DateTime TimeStamp { get; set; }
    }
}
启动项目,后端Api项目Job发送消息到Hub,前端App项目监听消息并展示。
 需要注意这种方式下,请求地址和之前两种模式下不同,原有模式下使用NavigationManager拿到当前服务所在地址。当前服务前后端非一个服务,地址也不同。
需要注意这种方式下,请求地址和之前两种模式下不同,原有模式下使用NavigationManager拿到当前服务所在地址。当前服务前后端非一个服务,地址也不同。
总结
三种模式下,完成后端通信到前端及其方便,如此一来,完成服务端到浏览器的一些通知整个过程非常容易上手接入。
参考
2025-04-12,望技术有成后能回来看见自己的脚步。