Contents

.Net 開發非同步執行 ServiceProvider 寫入資料庫失敗問題

最近在專案中遇到一個有趣又棘手的問題:當我需要儲存多筆 NText 資料時,因為單次寫入會花上 3 秒以上,為了提升 API 回應速度,決定先回傳 response,再用非同步方式將資料寫入資料庫。沒想到偶爾(極低機率)會寫入失敗,而且 log 也沒有任何錯誤訊息。這到底是什麼原因?

操作流程與問題重現

  1. 先用 Task.Run 包裝資料庫寫入邏輯,讓 API 可以先回應。
  2. 非同步執行時偶爾會失敗,且沒有任何錯誤訊息。
  3. 嘗試加上延遲(例如 await Task.Delay(10000);)後,錯誤就 100% 發生。

範例程式碼

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var _ = Task.Run(async () =>
{
    await Task.Delay(10000);
    try
    {
        // ... DB儲存處理
    }
    catch(Exception e)
    {
        Console.WriteLine("exception:" + e.ToString());
    }
    finally
    {
        Console.WriteLine("finished");
    }
});
小提示
記得在 Task 內部加上 try-catch,否則例外不會被正確記錄到 log!

參考:如何正確捕捉 Task 例外 | .NET 臉笑維 - 點部落

錯誤訊息與踩雷心得

當我加上延遲後,終於看到錯誤訊息:

1
2
3
4
5
6
exception:System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'IServiceProvider'.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ThrowHelper.ThrowObjectDisposedException()
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.CreateScope(IServiceProvider provider)

這代表什麼?原來 response 回傳後,IServiceProvider 已經被釋放,導致非同步任務無法再使用原本的 ServiceProvider。

解決方案:使用 IServiceScopeFactory

要在非同步背景下正確存取 DI 服務,必須改用 IServiceScopeFactory 產生新的 scope。這樣就算原本的 ServiceProvider 被釋放,新的 scope 依然可以正常運作。

修正版程式碼

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class A
{
    private readonly IServiceScopeFactory _serviceScopeFactory;
    
    public A(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }
    
    public void TestMethod()
    {
        _ = Task.Run(async () =>
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
                // 資料庫處理
            }
        });
    }
}
小提示
IServiceScopeFactory 幾乎可以解決所有這類非同步背景下的 DI 問題,記得不要直接傳遞 ServiceProvider!

詳細可參考:解决ASP.NET Core在Task中使用IServiceProvider的问题 - yi念之间 - 博客园 備份圖

感言

這次經驗讓我再次體會到 ASP.NET Core DI 生命週期的重要性。非同步背景下千萬不要直接用原本的 ServiceProvider,否則踩雷機率極高。建議大家遇到類似需求時,務必改用 IServiceScopeFactory,讓每個非同步任務都能有自己的 DI scope,資料庫寫入就能穩定又安全!