.NET Core 6 MVC+SignalR+HostedService与WPF交互优化技术问询
改造方案总览
核心思路是换用「后台任务队列+托管服务消费者」模式,替代原有的自动启动型HostedService,让任务仅在WPF触发时执行;通过SignalR Hub直接作为WPF的触发入口;所有依赖完全通过DI容器管理,彻底摒弃手动实例化的做法。
1. 实现后台任务队列(解决按需触发+DI依赖问题)
先做基础架构:定义任务接口和队列服务,让托管服务只负责监听队列,有任务就执行,没任务就等待。
定义任务接口与队列服务
// 封装长任务的接口 public interface IBackgroundTask { Task ExecuteAsync(CancellationToken cancellationToken); } // 任务队列:负责接收和存储待执行的任务 public interface IBackgroundTaskQueue { void QueueBackgroundTask(Func<CancellationToken, Task> task); Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken); } public class BackgroundTaskQueue : IBackgroundTaskQueue { private readonly Channel<Func<CancellationToken, Task>> _queue; public BackgroundTaskQueue(int capacity = 100) { var options = new BoundedChannelOptions(capacity) { FullMode = BoundedChannelFullMode.Wait }; _queue = Channel.CreateBounded<Func<CancellationToken, Task>>(options); } public void QueueBackgroundTask(Func<CancellationToken, Task> task) { if (task == null) throw new ArgumentNullException(nameof(task)); _queue.Writer.TryWrite(task); } public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken) { return await _queue.Reader.ReadAsync(cancellationToken); } }
实现队列消费型托管服务
这个托管服务启动后只会蹲守队列,不会自动跑长任务,完美符合按需触发的需求:
public class TaskQueueHostedService : BackgroundService { private readonly IBackgroundTaskQueue _taskQueue; private readonly ILogger<TaskQueueHostedService> _logger; private readonly IServiceProvider _serviceProvider; // 用于创建作用域获取Scoped服务 public TaskQueueHostedService(IBackgroundTaskQueue taskQueue, ILogger<TaskQueueHostedService> logger, IServiceProvider serviceProvider) { _taskQueue = taskQueue; _logger = logger; _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("任务队列服务已启动,等待任务触发..."); while (!stoppingToken.IsCancellationRequested) { try { var task = await _taskQueue.DequeueAsync(stoppingToken); // 创建作用域:解决Singleton托管服务不能直接注入Scoped服务(比如DbContext)的问题 using var scope = _serviceProvider.CreateScope(); await task(stoppingToken); } catch (OperationCanceledException) { // 服务停止时忽略 } catch (Exception ex) { _logger.LogError(ex, "执行后台任务出错"); } } _logger.LogInformation("任务队列服务已停止"); } }
2. 封装长任务逻辑(依赖DI注入)
把你的长任务逻辑封装成具体类,所有依赖通过构造函数注入,完全符合DI规范:
public class LongRunningTask : IBackgroundTask { private readonly AppDbContext _dbContext; private readonly IHubContext<ProgressHub> _hubContext; private readonly ICustomService _customService; private readonly string _clientConnectionId; // 给触发任务的WPF客户端单独发进度 // 构造函数注入所有需要的依赖 public LongRunningTask(AppDbContext dbContext, IHubContext<ProgressHub> hubContext, ICustomService customService, string clientConnectionId) { _dbContext = dbContext; _hubContext = hubContext; _customService = customService; _clientConnectionId = clientConnectionId; } public async Task ExecuteAsync(CancellationToken cancellationToken) { try { // 模拟长任务步骤,实际替换成你的业务逻辑 for (int progress = 0; progress <= 100; progress += 10) { if (cancellationToken.IsCancellationRequested) break; // 执行业务操作:调用自定义服务、操作数据库 await _customService.DoWorkAsync(progress, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); // 给触发任务的WPF客户端发送进度 await _hubContext.Clients.Client(_clientConnectionId).SendAsync("ReceiveProgress", progress, cancellationToken); await Task.Delay(500, cancellationToken); } } catch (Exception ex) { // 给客户端发送错误通知 await _hubContext.Clients.Client(_clientConnectionId).SendAsync("TaskFailed", ex.Message, cancellationToken); } } }
3. 配置SignalR Hub作为触发入口
修改SignalR Hub,注入任务队列,当WPF调用Hub方法时,直接把任务加入队列:
public class ProgressHub : Hub { private readonly IBackgroundTaskQueue _taskQueue; private readonly IServiceProvider _serviceProvider; public ProgressHub(IBackgroundTaskQueue taskQueue, IServiceProvider serviceProvider) { _taskQueue = taskQueue; _serviceProvider = serviceProvider; } // WPF客户端调用此方法触发长任务 public async Task StartLongRunningTask() { var clientId = Context.ConnectionId; // 将长任务加入队列,通过作用域获取Scoped依赖 _taskQueue.QueueBackgroundTask(async (cancellationToken) => { using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var hubContext = scope.ServiceProvider.GetRequiredService<IHubContext<ProgressHub>>(); var customService = scope.ServiceProvider.GetRequiredService<ICustomService>(); var task = new LongRunningTask(dbContext, hubContext, customService, clientId); await task.ExecuteAsync(cancellationToken); }); // 通知客户端任务已启动 await Clients.Caller.SendAsync("TaskStarted"); } }
4. MVC项目DI配置(Program.cs)
把所有服务注册到DI容器,确保依赖能正确注入:
var builder = WebApplication.CreateBuilder(args); // 注册DbContext builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); // 注册自定义服务 builder.Services.AddScoped<ICustomService, CustomService>(); // 注册任务队列和托管服务 builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>(); builder.Services.AddHostedService<TaskQueueHostedService>(); // 注册SignalR builder.Services.AddSignalR(); // MVC常规配置 builder.Services.AddControllersWithViews(); var app = builder.Build(); // 中间件配置 if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); // 映射SignalR Hub路由 app.MapHub<ProgressHub>("/progressHub"); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();
5. WPF客户端修改
把原有的API调用逻辑改成SignalR调用,并监听进度更新:
using Microsoft.AspNetCore.SignalR.Client; public class MainViewModel : INotifyPropertyChanged { private HubConnection _hubConnection; private int _progress; public int Progress { get => _progress; set { _progress = value; OnPropertyChanged(); } } public ICommand StartTaskCommand { get; } public MainViewModel() { StartTaskCommand = new RelayCommand(StartTask); _ = InitializeSignalRConnection(); } private async Task InitializeSignalRConnection() { _hubConnection = new HubConnectionBuilder() .WithUrl("https://localhost:5001/progressHub") // 替换成你的MVC项目地址 .Build(); // 监听进度更新 _hubConnection.On<int>("ReceiveProgress", progress => { Progress = progress; }); // 监听任务启动通知 _hubConnection.On("TaskStarted", () => { MessageBox.Show("任务已启动"); }); // 监听任务失败通知 _hubConnection.On<string>("TaskFailed", errorMsg => { MessageBox.Show($"任务失败:{errorMsg}"); }); await _hubConnection.StartAsync(); } private async void StartTask() { if (_hubConnection.State == HubConnectionState.Connected) { await _hubConnection.SendAsync("StartLongRunningTask"); } else { MessageBox.Show("未连接到服务器"); } } // INotifyPropertyChanged实现代码 public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
关键问题解决说明
- DI规范问题:所有依赖(DbContext、SignalR HubContext、自定义服务)都通过DI容器注入,通过
IServiceProvider创建作用域解决Singleton托管服务无法直接使用Scoped服务的问题,彻底告别手动实例化。 - SignalR直接触发:WPF客户端直接调用SignalR Hub的
StartLongRunningTask方法,无需经过API控制器,流程更直接。 - HostedService按需执行:
TaskQueueHostedService启动后仅监听任务队列,只有当WPF触发任务加入队列时才会执行长任务,不会随MVC启动自动运行。
内容的提问来源于stack exchange,提问作者patsy2k
相关产品推荐
相关产品推荐

