.NET 6新极简托管模型下自托管Web API集成问题咨询
问题背景
现有基于.NET 6开发的后台服务,核心逻辑为继承BackgroundService的Worker类,初始实现代码如下:
public class Worker : BackgroundService { private readonly ILogger<Worker> _logger; private readonly IService _service; public Worker(ILogger<Worker> logger, IService service) { _logger = logger; _service = service; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _service.StartAsync().ConfigureAwait(false); while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); await Task.Delay(1000, stoppingToken).ConfigureAwait(false); } } public override async Task StopAsync(CancellationToken stoppingToken) { await _service.StopAsync().ConfigureAwait(false); await base.StopAsync(stoppingToken).ConfigureAwait(false); } }
需求为在该服务内托管带控制器的.NET Core Web API,最初尝试在ExecuteAsync方法中直接添加WebApplication启动代码,但始终无法正常运行,尝试的启动代码如下:
var builder = WebApplication.CreateBuilder(); builder.RegisterWeb(); _app = builder.Build(); _app.ConfigureWebApplication(builder.Environment); _app.Run();
使用的两个自定义扩展方法为常规Web API配置实现,代码如下:
public static void ConfigureWebApplication(this WebApplication app, IWebHostEnvironment environment) { if (environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "E.WebApi v1")); } app.UseHttpsRedirection(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } public static void RegisterWeb(this WebApplicationBuilder builder) { builder.Services.AddControllers(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "E.WebApi", Version = "v1" }); }); builder.WebHost.UseUrls("http://localhost:7153/"); }
问题根因
直接在ExecuteAsync中调用同步方法_app.Run()会阻塞当前异步执行流,导致后续后台循环逻辑完全无法执行;同时手动构建的WebApplication生命周期没有和后台服务的停止令牌绑定,服务终止时无法触发Web主机的优雅停机、资源释放逻辑,两个独立构建的主机生命周期割裂是运行异常的核心原因。不需要引入Katana,.NET 6 原生就支持在Worker Service中托管Web API,以下是两种可直接落地的方案。
实现方案
方案1:启动阶段集成Web主机(官方推荐)
不需要在Worker类内部手动构建WebApplication,直接在Program.cs启动阶段将Web API和后台服务统一注册到通用主机,由.NET主机统一管理两者的生命周期,实现最简单、稳定性最高。
- 首先补全框架引用:如果项目是从Worker Service模板创建的,默认未携带ASP.NET Core框架,需要修改csproj文件,添加如下节点:
<ItemGroup> <FrameworkReference Include="Microsoft.AspNetCore.App" /> </ItemGroup>
- 修改Program.cs启动代码,使用
WebApplication.CreateBuilder构建主机,同时注册后台服务和Web API相关功能:
var builder = WebApplication.CreateBuilder(args); // 注册原有后台Worker服务 builder.Services.AddHostedService<Worker>(); // 注册原有业务服务 builder.Services.AddSingleton<IService, YourServiceImplementation>(); // 复用已写好的扩展方法注册Web API服务 builder.RegisterWeb(); var app = builder.Build(); // 复用已写好的扩展方法配置Web中间件管道 app.ConfigureWebApplication(app.Environment); // 启动主机,会并行运行Web API和后台Worker任务 app.Run();
该方案下不需要修改原有Worker类的任何业务逻辑,主机启动时会自动执行ExecuteAsync中的后台循环,收到停止信号时会同时触发Web API优雅停机和Worker的停止逻辑,完全不需要手动处理生命周期绑定。
方案2:Worker内部控制Web应用启动(特殊场景使用)
如果因为特殊业务逻辑必须在Worker类内部控制Web应用的启动时机,不要使用同步的_app.Run()方法,改用RunAsync绑定服务停止令牌,同时将Web应用启动作为独立异步任务执行,避免阻塞后台循环。
修改后的Worker代码如下:
public class Worker : BackgroundService { private readonly ILogger<Worker> _logger; private readonly IService _service; private WebApplication? _app; public Worker(ILogger<Worker> logger, IService service) { _logger = logger; _service = service; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _service.StartAsync().ConfigureAwait(false); // 构建Web主机,复用当前应用的配置和路径 var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = AppContext.BaseDirectory, EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") }); // 注意:如果需要复用Worker已注入的服务实例,需要手动将服务同步到builder.Services集合,避免重复构建实例 builder.RegisterWeb(); _app = builder.Build(); _app.ConfigureWebApplication(builder.Environment); // 异步启动Web主机,传入停止令牌,服务停止时自动触发Web主机停机,不要await该任务避免阻塞 var webRunTask = _app.RunAsync(stoppingToken); // 原有后台循环逻辑 while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); await Task.Delay(1000, stoppingToken).ConfigureAwait(false); } // 等待Web主机完成停机流程 await webRunTask.ConfigureAwait(false); } public override async Task StopAsync(CancellationToken stoppingToken) { // 优先停止Web主机 if (_app != null) { await _app.StopAsync(stoppingToken).ConfigureAwait(false); await _app.DisposeAsync().ConfigureAwait(false); } await _service.StopAsync().ConfigureAwait(false); await base.StopAsync(stoppingToken).ConfigureAwait(false); } }
注意:该方案需要手动处理服务注册同步、配置复用、生命周期绑定逻辑,容易出现服务实例重复、配置不生效的问题,非特殊场景优先选择方案1。
内容的提问来源于stack exchange,提问作者user1175338

