一. 託管服務
1. 簡介
使用背景:代碼運行在後臺。比如服務器啓動的時候在後臺預先加載數據到緩存,再比如定時任務凌晨1點需要遍歷數據庫修改狀態等等。
注意:
常駐後臺的託管服務並不需要特殊的技術,我們只要while (!stoppingToken.IsCancellationRequested) 讓ExecuteAsync中的代碼一直執行不結束就行了, 但是不能部署在IIS上。
因爲如果掛在IIS上,閒置超時20分鐘,是指20分鐘內沒有任何請求進行訪問,如果有請求則這個閒置超時時間會重新計算。如果場景是定時任務,且期間沒有請求,該方案不適合,
因爲IIS會回收它,這一點類似Quartz.Net 部署在IIS上是一個道理的(可以用控制檯方案解決 或 其它部署方案解決)。
2.核心說明
(1). 託管服務實現IHostedService接口,但我們通常用BackgroundService這個基類來做
3. 託管服務的異常處理
(1).從.NET 6開始,當託管服務中發生未處理異常的時候,程序就會自動停止並退出(之前程序不會停止)。可以把HostOptions.BackgroundServiceExceptionBehavior設置爲Ignore,程序會忽略異常,而不是停止程序。 不過推薦採用默認的設置,因爲“異常應該被妥善的處理,而不是被忽略”。
(2).通常建議在ExecuteAsync方法中把代碼用try-catch包裹起來,當發生異常的時候,記錄日誌中或發警報等。
(3). 代碼實操:
A. 後臺常駐服務,通過 while (!stoppingToken.IsCancellationRequested) 來判斷
B. 業務代碼要try-catch包裹
C. 通過 await base.StopAsync(stoppingToken); 停止後臺服務
D. 服務註冊 builder.Services.AddHostedService<TestBackService1>(); 【這是單例模式的注入】
後臺服務代碼
public class TestBackService1 : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("----------後臺任務開啓-------------------");
while (!stoppingToken.IsCancellationRequested)
{
try
{
//模擬實際業務,輸出當前時間
Console.WriteLine($"當前時間爲:{DateTime.Now}");
//等待5s
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
//測試報錯
//int.Parse("fdgdfg");
}
catch (Exception ex)
{
Console.WriteLine($"出錯了:{ex.Message}");
//根據實際情況決定是否停止後臺任務
await base.StopAsync(stoppingToken);
}
}
}
}
註冊服務
//註冊後臺服務
builder.Services.AddHostedService<TestBackService1>();
運行結果
4. 託管服務中的DI
(1). 託管服務是以單例的生命週期註冊到依賴注入容器中的。因此不能注入請求內單例或者瞬時的服務。比如注入EF Core的上下文的話(默認是請求內單例的),程序就會拋出異常。
(2). 解決方案
創建一個IServiceScope對象,這樣我們就可以通過IServiceScope來創建所需聲明週期的服務即可
這裏通常有兩種寫法,
要麼直接在ExecuteAsync中的using中構建出來所需聲明週期的服務 【詳見代碼版本2】
要麼在構造函數中創建出來所需聲明週期的服務(需要dispose一下) 【詳見代碼版本3】
(3). 代碼實操
A.新建EF上下午MyDBContext, 然後 builder.Services.AddScoped<MyDbContext>(); 請求內單例的
B.在後臺服務中TestBackServiceDI中注入MyDBContext。 運行:直接報錯,錯誤內容如下圖,大體意思:不同生命週期的內容不能相互注入使用 【詳見版本1代碼】
代碼分享:
public class TestBackServiceDI : BackgroundService
{
#region 版本1--構造函數注入MyDbContext【報錯】
private readonly MyDbContext dbContext;
public TestBackServiceDI(MyDbContext dbContext)
{
this.dbContext = dbContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("----------後臺任務開啓-------------------");
while (!stoppingToken.IsCancellationRequested)
{
try
{
//調用EF上下午
string result = dbContext.GetMsg();
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"出錯了:{ex.Message}");
//根據實際情況決定是否停止後臺任務
await base.StopAsync(stoppingToken);
}
}
}
#endregion
}
報錯:
C.注入IServiceProvider service
D.在ExecuteAsync中通過using+ serviceScope.ServiceProvider.GetRequiredService<MyDbContext>(); 創建dbContext即可,運行代碼,直接輸出Hello world 【詳見代碼版本2】
E.詳見代碼版本3
解決方案1代碼 【推薦】
public class TestBackServiceDI : BackgroundService
{
#region 版本2-通過service.CreateScope()創建
private readonly IServiceProvider service;
public TestBackServiceDI(IServiceProvider service)
{
this.service = service;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("----------後臺任務開啓-------------------");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using (IServiceScope serviceScope = service.CreateScope())
{
var dbContext = serviceScope.ServiceProvider.GetRequiredService<MyDbContext>();
//調用EF上下午
string result = dbContext.GetMsg();
Console.WriteLine(result);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
catch (Exception ex)
{
Console.WriteLine($"出錯了:{ex.Message}");
//根據實際情況決定是否停止後臺任務
await base.StopAsync(stoppingToken);
}
}
}
#endregion
}
解決方案2代碼
public class TestBackServiceDI : BackgroundService
{
#region 版本3-通過構造函數中創建符合生命期的EF上下文
private readonly IServiceScope serviceScope;
private readonly MyDbContext dbContext;
public TestBackServiceDI(IServiceProvider service)
{
this.serviceScope = service.CreateScope();
this.dbContext = serviceScope.ServiceProvider.GetRequiredService<MyDbContext>();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("----------後臺任務開啓-------------------");
while (!stoppingToken.IsCancellationRequested)
{
try
{
//調用EF上下午
string result = dbContext.GetMsg();
Console.WriteLine(result);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
catch (Exception ex)
{
Console.WriteLine($"出錯了:{ex.Message}");
//根據實際情況決定是否停止後臺任務
await base.StopAsync(stoppingToken);
}
}
}
public override void Dispose()
{
base.Dispose();
serviceScope.Dispose();
}
#endregion
}
二. 內置數據校驗
三. FluentValidition
四. 程序發佈部署
!
- 作 者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲 明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。