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

.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));
    }
}

关键问题解决说明

  1. DI规范问题:所有依赖(DbContext、SignalR HubContext、自定义服务)都通过DI容器注入,通过IServiceProvider创建作用域解决Singleton托管服务无法直接使用Scoped服务的问题,彻底告别手动实例化。
  2. SignalR直接触发:WPF客户端直接调用SignalR Hub的StartLongRunningTask方法,无需经过API控制器,流程更直接。
  3. HostedService按需执行:TaskQueueHostedService启动后仅监听任务队列,只有当WPF触发任务加入队列时才会执行长任务,不会随MVC启动自动运行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 03:07:27