使用ASP.NET Core托管服务运行后台任务

原文:Use ASP.NET Core hosted services to run a background task (roundthecode.com)

翻译:HHaoWang

托管服务在ASP.NET Core 3.1中被引入,是一种运行后台任务的绝佳方式。

托管服务(Hosted services)可以在ASP.NET Core web应用中运行,这意味着如果我们需要在后台更新一些可能影响所有用户的东西,托管服务就非常有用了。

此外,托管服务还可以通过使用Worker Service模板单独运行,Worker Service模板就是为了运行后台任务而设计的,并且还能够作为Windows服务来运行(译者注:实际上还可以作为Linux服务运行)。

在这篇文章里我们将会探讨如何添加一个托管服务到一个ASP.NET Core web应用中去。首先我们会讨论如何创建一个托管服务,其次会讨论如何将其添加到IServiceCollection实例中,最后我们会讨论如何在托管服务中使用依赖注入。

创建托管服务

有很多种方式创建一个托管服务。

继承IHostedService接口

第一种方式,我们可以通过继承IHostedService接口创建一个服务类,因此必须要在服务类中实现StartAsyncStopAsync方法。

using Microsoft.Extensions.Hosting;
 
namespace RoundTheCode.HostedServiceExample
{
    public class MyHostedService : IHostedService
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
 
        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

StartAsync方法中,我们可以开始我们需要做的任务,必须要注意的一点是,我们必须使用Task.Run将我们要做的任务作为单独的一个Task启动。

如果我们将一个无限持续的任务直接在StartAsync方法中运行的话,StartAsync方法将永远无法结束,而ASP.NET Core web应用只有在StartAsync方法完成并返回后才能开始提供正常的Web服务。

因此我们可以在StartAsync中运行一个独立的任务避免阻塞ASP.NET Core web应用启动:

using Microsoft.Extensions.Hosting;
 
namespace RoundTheCode.HostedServiceExample
{
    public class MyHostedService : IHostedService
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            Task.Run(async () =>
            {
                while (!cancellationToken.IsCancellationRequested)
                {
                    await Task.Delay(new TimeSpan(0, 0, 5)); // 5 second delay
                }
            });
 
            return Task.CompletedTask;
        }
 
        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

继承BackgroundService类

一个替代方案是继承BackgroundService类。BackgroundService类是一个抽象类,它也继承了IHostedService接口。

采用这种方式的好处之一就是我们不必继承StartAsync方法和StopAsync方法,并且我们还是可以在需要的时候重写这两个方法。

所有要怎么样在这个类中执行我们需要执行的任务呢?BackgroundService类中有一个抽象方法ExecuteAsync,在继承的时候必须要实现该方法,而我们需要执行的任务就可以在这个方法中执行。

using Microsoft.Extensions.Hosting;
 
namespace RoundTheCode.HostedServiceExample
{
    public class MyBackgroundService : BackgroundService
    {
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            return Task.CompletedTask;
        }
    }
}

和在StartAsync方法中执行任务不一样的是,我们不必再将需要执行的任务作为单独的任务执行以避免阻塞web服务启动了,我们可以通过async关键字将ExecuteAsync方法作为异步方法实现,并可以直接在其中执行需要执行的任务了。

using Microsoft.Extensions.Hosting;
 
namespace RoundTheCode.HostedServiceExample
{
    public class MyBackgroundService : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await Task.CompletedTask;
        }
    }
}

将托管服务添加到IServiceCollection

在配置服务的时候,IServiceCollection接口有一个扩展方法AddHostedService,该扩展方法接收一个继承自IHostedService接口的类,因此我们在添加托管服务的时候无需特意指定。

假如你的Web应用基于.NET 5或更低版本的话,那么会有一个Startup.cs文件,在它的ConfigureServices方法中,可以使用传递进来的IServiceCollection类型参数添加托管服务。

namespace RoundTheCode.HostedServiceExample
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
 
        public IConfiguration Configuration { get; }
 
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add dependency injection.
            ...
            // 可以在此处添加托管服务
            services.AddHostedService<MyBackgroundService>();
 
            ...
        }
 
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            ...
        }
    }
}

如果你的Web应用程序正在使用.NET 6并且是使用.NET 6模板进行创建的,可以在Program.cs文件中添加托管服务,例如:

var builder = WebApplication.CreateBuilder(args);
 
// Add services to the container.
builder.Services.AddHostedService<MyBackgroundService();
 
var app = builder.Build();
 
...
 
app.Run();

无论是哪种模板,托管服务最终都是以同样的方式运作的。

如何在托管服务中使用依赖注入

托管服务会以单例服务生存期(Singleton Service Lifetime)注入到DI容器中,因此它只能使用以单例服务生存期或暂时服务生存期(Transient Service Lifetime)注入的类。

如果我们将一个作用域服务生存期(Scoped Service Lifetime)的类注入到托管服务中的话,被注入的类无法确定它应该属于哪个作用域,因此会在托管服务启动的时候抛出一个运行时错误。

但是,我们可以在运行后台任务的时候通过创建自己的作用域来使用作用域服务生存期服务(Scoped Service)。

首先我们需要将IServiceProvider实例注入到托管服务中,在该接口中,可以CreateScope方法创建一个新的作用域实例,之后,我们就可以使用我们的新的作用域实例解析一个作用域服务生存期服务到我们的后台任务中了。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
 
namespace RoundTheCode.HostedServiceExample
{
    public class MyBackgroundService : BackgroundService
    {
        private readonly IServiceProvider _serviceProvider;
        public MyBackgroundService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
 
        protected override async Task ExecuteAsync(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                using (var scope = _serviceProvider.CreateScope())
                {
                    var myScopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
                }
 
                await Task.Delay(new TimeSpan(0, 1, 0));
            }
        }
    }
}

托管服务是如何工作的?

(译者注:本部分是原文作者在一个ASP.NET Core web应用中实现托管服务的视频,原视频地址:Use ASP.NET Core hosted services to run a background task(Youtube)

下载基于.NET 6的托管服务示例代码:ASP.NET Core hosted service in .NET 6 (roundthecode.com)

Worker Service模板

我们应该在一个ASP.NET Core web应用程序中使用后台服务么?如果我们需要向网站所有用户提供数据,这么做没什么问题,然而当后台任务比较占用性能时,就可能会影响到网站的稳定性了。

一个更好的替代方案是使用Worker Service模板,它是一个单独的后台服务应用,并且可以作为Windows服务使用(译者注:或Linux服务)。

留下评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code