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

ASP.NET Core WebAPI中按请求范围执行托管服务及定时后台服务问题

嘿,针对你在ASP.NET Core WebApi里要按请求范围执行托管服务的需求,结合你已经有一个每30秒运行的Singleton后台服务的情况,我给你分两种常见场景来提供解决方案,都是项目里验证过的实用方法:

场景1:让现有定时后台服务在请求范围内执行逻辑

默认的IHostedService是Singleton生命周期,没法直接注入请求范围(Scoped)的服务。要让你的30秒定时任务能在请求上下文里跑,我们可以借助IServiceScopeFactory手动创建服务范围,在这个范围内使用Scoped服务,相当于模拟一次请求的生命周期。

第一步:改造你的SimpleService

给它注入IServiceScopeFactory,然后在定时执行的逻辑里创建服务范围:

public class SimpleService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<SimpleService> _logger;

    // 构造函数注入需要的服务
    public SimpleService(IServiceScopeFactory scopeFactory, ILogger<SimpleService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("定时任务启动,创建请求范围...");
            
            // 创建服务范围,这就相当于模拟了一个请求上下文
            using var scope = _scopeFactory.CreateScope();
            // 从范围里获取你需要的Scoped服务
            var scopedBusinessService = scope.ServiceProvider.GetRequiredService<IScopedBusinessService>();
            
            // 在这里执行你的业务逻辑,完全在请求范围内
            await scopedBusinessService.DoRequestScopedWorkAsync();

            _logger.LogInformation("定时任务执行完成,销毁请求范围");
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

第二步:注册你的Scoped服务

别忘了在Startup.cs的ConfigureServices里把你的请求范围服务注册进去:

services.AddScoped<IScopedBusinessService, ScopedBusinessService>();

这样每次定时任务跑的时候,都会生成一个独立的请求范围,Scoped服务在这个范围内的生命周期和真实请求完全一致,不会有Singleton服务的线程安全问题。


场景2:针对每个HTTP请求触发后台任务

如果你的需求是每进来一个请求,就触发一个后台任务执行(比如异步处理请求的后续逻辑,不阻塞请求响应),那可以用「任务队列 + 后台服务消费」的模式,同样能保证任务在请求范围内执行。

第一步:先搞个任务队列

用来存放每个请求要执行的后台任务:

public interface IBackgroundTaskQueue
{
    // 加入任务到队列
    void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem);
    // 从队列取出任务
    Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken stoppingToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<IServiceProvider, CancellationToken, Task>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // 设置队列容量,避免请求太多撑爆内存
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<IServiceProvider, CancellationToken, Task>>(options);
    }

    public void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem)
    {
        if (workItem == null)
            throw new ArgumentNullException(nameof(workItem));

        _queue.Writer.TryWrite(workItem);
    }

    public async Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken stoppingToken)
    {
        return await _queue.Reader.ReadAsync(stoppingToken);
    }
}

第二步:写个消费队列的后台服务

这个服务会一直监听队列,有任务就拿出来执行:

public class QueueConsumerHostedService : BackgroundService
{
    private readonly ILogger<QueueConsumerHostedService> _logger;
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly IServiceScopeFactory _scopeFactory;

    public QueueConsumerHostedService(IBackgroundTaskQueue taskQueue, IServiceScopeFactory scopeFactory, ILogger<QueueConsumerHostedService> logger)
    {
        _taskQueue = taskQueue;
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("队列消费服务启动,开始监听任务队列...");

        while (!stoppingToken.IsCancellationRequested)
        {
            // 从队列取任务
            var workItem = await _taskQueue.DequeueAsync(stoppingToken);

            try
            {
                // 创建请求范围,执行任务
                using var scope = _scopeFactory.CreateScope();
                await workItem(scope.ServiceProvider, stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "执行后台任务时出错了");
            }
        }
    }
}

第三步:把这些服务注册到容器

在Startup.cs的ConfigureServices里加这些代码:

// 注册任务队列,设置容量为100(根据你的请求量调整)
services.AddSingleton<IBackgroundTaskQueue>(_ => new BackgroundTaskQueue(capacity: 100));
// 注册队列消费的后台服务
services.AddHostedService<QueueConsumerHostedService>();
// 注册你的Scoped业务服务
services.AddScoped<IScopedBusinessService, ScopedBusinessService>();

第四步:加个中间件触发请求任务

写个中间件,每个请求进来时就把任务扔进队列:

public class RequestTriggeredTaskMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IBackgroundTaskQueue _taskQueue;

    public RequestTriggeredTaskMiddleware(RequestDelegate next, IBackgroundTaskQueue taskQueue)
    {
        _next = next;
        _taskQueue = taskQueue;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 把请求相关的后台任务加入队列
        _taskQueue.QueueBackgroundWorkItem(async (sp, ct) =>
        {
            var scopedService = sp.GetRequiredService<IScopedBusinessService>();
            // 还能获取请求的信息,比如路径、参数
            var requestPath = context.Request.Path;
            await scopedService.DoWorkForRequestAsync(requestPath, ct);
        });

        // 继续处理请求,不阻塞响应
        await _next(context);
    }
}

// 写个扩展方法方便注册中间件
public static class RequestTriggeredTaskMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTriggeredBackgroundTasks(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestTriggeredTaskMiddleware>();
    }
}

最后在Startup.cs的Configure方法里注册这个中间件:

app.UseRequestTriggeredBackgroundTasks();

这样每个请求进来时,都会异步触发一个后台任务,任务在独立的请求范围内执行,既能使用Scoped服务,又不会拖慢请求的响应速度。


几个要注意的点

  • 服务范围的释放:用using var scope创建的范围会在代码块结束时自动释放,一定要这么写,避免Scoped服务没被回收导致内存泄漏。
  • 取消令牌的传递:把stoppingToken传给所有异步方法,这样应用关闭时任务能及时终止,不会出现僵尸任务。
  • 队列容量调整:如果你的请求量很大,记得调大队列的容量,或者改成非阻塞的模式,避免请求因为队列满而卡住。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 07:49:47