C# Web開發教程(十一)后臺主動服務
托管服務(HostedService,也稱為"后臺服務")
-
托管服務,這個翻譯是不準確的,我覺得應該翻譯成主動服務:服務器自己主動發起的服務(任務)[相對于客戶端發起請求,服務端才響應]- 它是一種在應用啟動后自動運行、無需外部觸發的服務
-
使用場景
- 代碼運行在后臺,比如服務器啟動的時候在后臺預先加載數據到緩存,每天凌晨3點把數據導出到備份數據庫,或者每隔5秒鐘在兩張表之間同步一次數據。
- 定時任務
- 代碼實現流程
- 實現IHostedService接口(用起來麻煩),一般編寫從BackgroundService繼承的類(用起來簡單).
- 注冊服務: services.AddHostedService<DemoBgService>();
- 注意事項
- 一旦托管服務的代碼有錯,整個項目就無法啟動
- 可以把HostOptions.BackgroundServiceExceptionBehavior設置為Ignore,程序會忽略異常,而不是停止程序.
- 不過不推薦這么搞,因為“異常應該被妥善的處理,而不是被忽略”.
- 要在ExecuteAsync方法中把代碼用try....catch包裹起來,當發生異常的時候,記錄日志中或發警報等.
- 而在.net6.0之前的版本中,托管代碼就算有異常,項目也可以啟動起來(舊的設計其實不好,所以新的版本修復了)
// HostedServiceDemo1.cs
namespace WebApplicationAboutJWTConfigRun
{
public class HostedServiceDemo1 : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("HostedService1啟動");
await Task.Delay(3000);
string txt = await File.ReadAllTextAsync("d:/text.txt");
Console.WriteLine("文件讀取中...");
await Task.Delay(10000);
Console.WriteLine(txt);
Console.WriteLine("HostedService1啟動任務結束!");
}
}
}
// Program.cs
......
builder.Services.AddSwaggerGen(c =>
{
......
});
// 主動服務(注冊)
builder.Services.AddHostedService<HostedServiceDemo1>();
- 故意觸發異常的效果
namespace WebApplicationAboutJWTConfigRun
{
public class HostedServiceDemo1 : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
Console.WriteLine("HostedService1啟動");
await Task.Delay(3000);
string txt = await File.ReadAllTextAsync("d:/text.ext"); // 故意寫錯
Console.WriteLine("文件讀取中...");
await Task.Delay(10000);
Console.WriteLine(txt);
Console.WriteLine("HostedService1啟動任務結束!");
}
catch(Exception Ex)
{
Console.WriteLine("啟動代碼異常" + Ex);
}
}
}
}
- 測試效果: 項目正常跑起來了,日志記錄異常
......
啟動代碼異常System.IO.FileNotFoundException: Could not find file 'd:\text.ext'.
File name: 'd:\text.ext'
......
- 托管服務中使用DI(依賴注入(DI)限制)
- 托管服務是以單例的生命周期注冊到依賴注入容器中的。因此不能注入生命周期為范圍或者瞬態的服務(不能直接注入 Scoped 或 Transient 服務(如 DbContext))。
比如注入FCore的上下文的話,程序就會拋出異常。
- 可以通過構造方法注入一個IServiceScopeFactory服務,它可以用來創建一個IServiceScope對象,這樣我們
就可以通過IServiceScope來創建短生命周期的服務了(記得在Dispose中釋放IServiceScope)。
異常實例演示
// HostedServiceDemo2.cs
namespace WebApplicationAboutJWTConfigRun
{
public class HostedServiceDemo2
{
public int Add(int a,int b)
{
return a + b;
}
}
}
// Program.cs
......
// 主動服務
builder.Services.AddHostedService<HostedServiceDemo1>();
// 注入生命周期為范圍或者瞬態的服務
builder.Services.AddScoped<HostedServiceDemo2>();
// HostedServiceDemo1.cs 測試
namespace WebApplicationAboutJWTConfigRun
{
public class HostedServiceDemo1 : BackgroundService
{
private readonly HostedServiceDemo2 hostedServiceDemo2;
// 依賴注入
public HostedServiceDemo1(HostedServiceDemo2 hostedServiceDemo2)
{
this.hostedServiceDemo2 = hostedServiceDemo2;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
Console.WriteLine("HostedService1啟動");
// 執行邏輯,這里會觸發異常
Console.WriteLine("執行HostedService2服務"+hostedServiceDemo2.Add(1,1));
......
}
catch(Exception Ex)
{
Console.WriteLine("啟動代碼異常" + Ex);
}
}
}
}
- 現在,修復上面的異常
namespace WebApplicationAboutJWTConfigRun
{
public class HostedServiceDemo1 : BackgroundService
{
//private readonly HostedServiceDemo2 hostedServiceDemo2;
//public HostedServiceDemo1(HostedServiceDemo2 hostedServiceDemo2)
//{
// this.hostedServiceDemo2 = hostedServiceDemo2;
//}
// 聲明 IServiceScope
private IServiceScope serviceScope;
// 依賴注入
public HostedServiceDemo1(IServiceScopeFactory serviceScopeFactory)
{
this.serviceScope = serviceScopeFactory.CreateScope();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
var testService = serviceScope.ServiceProvider.GetRequiredService<HostedServiceDemo2>();
Console.WriteLine("HostedService1啟動");
Console.WriteLine("執行HostedService2服務"+ testService.Add(1,1));
await Task.Delay(3000);
string txt = await File.ReadAllTextAsync("d:/text.txt");
Console.WriteLine("文件讀取中...");
await Task.Delay(10000);
Console.WriteLine(txt);
Console.WriteLine("HostedService1啟動任務結束!");
}
catch(Exception Ex)
{
Console.WriteLine("啟動代碼異常" + Ex);
}
}
public override void Dispose()
{
this.serviceScope.Dispose();
base.Dispose();
}
}
}
? 總結要點
| 要點 | 說明 |
|---|---|
| 用途 | 后臺任務、定時任務、數據同步等 |
| 實現方式 | 繼承 BackgroundService |
| 注冊方式 | AddHostedService() |
| 異常處理 | 必須用 try-catch 包裹 |
| DI 限制 | 不能直接注入 Scoped/Transient 服務 |
| 解決方案 | 使用 IServiceScopeFactory 創建作用域 |
連接數據庫示例: 使用托管服務(BackgroundService)實現的定時數據導出任務
- 安裝工具包
<Project Sdk="Microsoft.NET.Sdk.Web">
......
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
// MyUser.cs
using Microsoft.AspNetCore.Identity;
namespace WebApplicationAboutJWTConfigRun
{
public class MyUser:IdentityUser<long>
{
public string? WeiXinAccount { get; set; }
}
}
// MyRole.cs
using Microsoft.AspNetCore.Identity;
namespace WebApplicationAboutJWTConfigRun
{
public class MyRole:IdentityRole<long>
{
}
}
// MyDbContext.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace WebApplicationAboutJWTConfigRun
{
public class MyDbContext : IdentityDbContext<MyUser,MyRole,long>
{
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
{
}
}
}
// Program.cs
......
builder.Services.AddDbContext<MyDbContext>(opt =>
{
opt.UseSqlServer("Server=.;Database=idtest2;Trusted_Connection=True;");
});
- 作遷移和更新db
// ScheduledService.cs
using Microsoft.EntityFrameworkCore;
namespace WebApplicationAboutJWTConfigRun
{
public class ScheduledService : BackgroundService
{
// 解決單例服務無法直接使用Scoped服務的問題
private readonly IServiceScope serviceScope;
public ScheduledService(IServiceScopeFactory serviceScopeFactory)
{
// 創建獨立的作用域來獲取DbContext
this.serviceScope = serviceScopeFactory.CreateScope();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
var dbCtx = serviceScope.ServiceProvider.GetRequiredService<MyDbContext>();
// 檢查取消令牌:stoppingToken.IsCancellationRequested - 應用關閉時自動停止
while (!stoppingToken.IsCancellationRequested)
{
long c = await dbCtx.Users.LongCountAsync();
await File.WriteAllTextAsync("d:/text.txt",c.ToString());
await Task.Delay(5000);
}
Console.WriteLine("導出成功" + DateTime.Now);
}
catch (Exception ex)
{
Console.WriteLine($"出錯了: {ex.Message}, 堆棧跟蹤: {ex.StackTrace}");
}
}
public override void Dispose()
{
// 手動釋放作用域:避免內存泄漏
this.serviceScope.Dispose();
// 調用基類Dispose:確保BackgroundService正確清理
base.Dispose();
}
}
}
// Program.cs
......
// 主動服務
builder.Services.AddHostedService<ScheduledService>();
builder.Services.AddDbContext<MyDbContext>(opt =>
{
......
}

浙公網安備 33010602011771號