【.NET】ObjectDisposedExceptionを理解する ASP.NET Coreを例に
概要
ObjectDisposedException
が発生して「これ何?」となったので、理解のために記事を書きます。
※ サンプルコード等は、.NET7です。
ObjectDisposedExceptionを起こす
以下のコードを実行すると、ObjectDisposedException
が発生します。
private readonly IUnitOfWork _unitOfWork;
public TestController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
[HttpGet("start-job")]
public ActionResult StartJob([FromServices] ILogger<TestController> logger)
{
_ = Task.Run(() =>
{
try
{
// リクエスト終了後にここが実行されてしまう
var tests = _unitOfWork.TestRepository.GetAll(); // ここで例外が発生
}
catch (Exception ex)
{
logger.LogError(ex, "バックグラウンドタスクで例外が発生しました");
}
});
// レスポンスをクライアントに即座に返す
return Ok("Job started");
}
_unitOfWork.TestRepository.GetAll()
の呼び出し時に、ApplicationDbContext
が破棄されているためにObjectDisposedException
が発生します。
GetAll
の実装は以下のとおりです。
internal class TestRepository : Repository<Test>, ITestRepository
{
public TestRepository(ApplicationDbContext dbContext) :base(dbContext)
{
}
public Test? Get(int id)
{
return _dbContext.Tests.FirstOrDefault(l => l.Id == id);
}
}
発生する例外の詳細は以下のとおりです。
バックグラウンドタスクで例外が発生しました
System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
Object name: 'ApplicationDbContext'.
at Microsoft.EntityFrameworkCore.DbContext.CheckDisposed()
at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
at Microsoft.EntityFrameworkCore.DbContext.Set[TEntity]()
at practiceWebApi.Infrastructure.Repository`1.GetAll() in /Users/******/projects/practiceWebApi/Infrastructure/Repository.cs:line 25
at practiceWebApi.Controllers.TestController.<>c__DisplayClass10_0.<StartJob>b__0() in /Users/******/projects/practiceWebApi/Controllers/TestController.cs:line 258
AddScopedを使用してサービス登録している
Program.cs
では、以下のようにAddScoped
を使用してサービス登録を行っています。
var builder = WebApplication.CreateBuilder(args);
// ...略
builder.Services.AddDbContext<ApplicationDbContext>(option => {
option.UseSqlServer(builder.Configuration.GetConnectionString("DefaultSQLConnection"));
});
// `DbContext`を利用する`UnitOfWork`クラスをDIする
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
ASP.NET Core のDIにおいては、以下の3つのライフサイクルでサービスを登録できます。
- Singleton: アプリケーション全体で1つのインスタンスを使用する(アプリケーションが終了するまで、インスタンスを使い回す)。
- Scoped: リクエストごとにインスタンスを作成する(同じリクエスト内では同じインスタンスを使い、リクエストが終了したらインスタンスを破棄する)。
- Transient: インスタンスが要求されるごとに毎回新しく作成する
このオブジェクトのライフサイクルはスコープ(Scoped
)ですので、HTTPリクエストが終了する時に破棄されています。
なぜObjectDisposedExceptionが起きたのか
なぜObjectDisposedException
が発生したのか、その理由を改めて整理します。
[HttpGet("start-job")]
public ActionResult StartJob([FromServices] ILogger<TestController> logger)
{
// ① 非同期タスクを実行する
_ = Task.Run(() =>
{
try
{
// ④ ③で破棄済みのオブジェクトにアクセスするためにObjectDisposedExceptionが発生
var tests = _unitOfWork.TestRepository.GetAll();
}
catch (Exception ex)
{
logger.LogError(ex, "バックグラウンドタスクで例外が発生しました");
}
});
// ② 非同期タスクの終了を待たずに、即座にリターンする
// ③ HTTPリクエストの終了に伴い、Scopedライフサイクルのオブジェクトが破棄される
return Ok("Job started");
}
流れとしては以下のとおりです。
① Task.Run
で非同期処理が開始される
② ①の非同期処理を待たずに、即リターン
③ HTTPリクエストの終了に伴い、Scopedライフサイクルのオブジェクトが破棄される
④ リクエスト終了後も継続中の非同期処理内で破棄済みのオブジェクトにアクセスして例外発生
非同期処理はHTTPリクエストのスコープ外で動作します。しかし、その非同期処理内で使用するオブジェクトのライフサイクルがスコープのため、リクエスト終了後はそのオブジェクトにアクセスできなくなってしまうということです。
最後に
今回記載したように、HTTPリクエスト内で非同期処理を実行してバックグラウンドで動かしておくような実装は基本的にはNGだと思います。あくまで分かりやすくObjectDisposedException
を起こすサンプルなのでご理解ください。
非同期処理や長時間実行される処理を呼ぶ場合、新たにリクエストを実施し、別のスコープで処理を行う方が良さそうです。
Discussion