ASP.NET Core の DI(Dependency Injection)で Singleton / Scoped / Transient を選ぶとき、「いつ作られて、いつ破棄されるか」がわからないとハマりやすい。
この記事では、各スコープのライフサイクルとWeb API での実務的な使い分けをざっくり整理する。
IServiceScopeFactory で都度スコープを切るCreateScope() するスコープ 寿命(ざっくり) 典型例
────────────────────────────────────────────────────────────
Transient 注入・GetService の都度 1 個 軽いサービス、Mapper
Scoped 1 スコープ内で 1 個 DbContext、UnitOfWork
Singleton アプリ全体で 1 個 設定、MemoryCache、HttpClientFactory
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddSingleton<IClock, SystemClock>();
省略時: Add<T>() は Transient。AddDbContext は Scoped(プール利用時も Scoped)。
ASP.NET Core の HTTP リクエストでは、だいたい次の流れ。
リクエスト開始
└─ Request Scope 作成
├─ Scoped サービスをこの Scope 用に生成・共有
├─ Controller / Minimal API が DI で解決
└─ Transient は依存解決のたびに new
リクエスト終了
└─ Scope 破棄 → Scoped の Dispose / AsyncDispose
Minimal API でも Controller でも、Middleware パイプライン上では同じ「リクエストスコープ」の考え方。
向いているもの おすすめスコープ
──────────────────────────────────────────────────
EF Core DbContext Scoped
リポジトリ(DbContext 利用) Scoped
HTTP リクエスト単位の状態 Scoped
軽い変換・送信(状態なし) Transient
バリデーション(状態なし) Transient
アプリ設定・Feature Flag Singleton
IMemoryCache / 共有クライアント Singleton(Factory 経由)
ログ(ILogger<T>) フレームワークが適切に管理
迷ったら: DB 触る → Scoped、リクエストをまたぐ状態 → Singleton は慎重に、特に理由なし → Transient か Scoped。
長い寿命のサービスが、短い寿命のサービスをフィールドで保持してはいけない。Singleton → Scoped 注入が典型。
// NG: DbContext は Scoped なのに Singleton が保持
public class BadCacheService
{
public BadCacheService(AppDbContext db) { ... }
}
builder.Services.AddSingleton<BadCacheService>();
起動時に検証が効く場合もあるが、ObjectDisposedException や別リクエストのデータ混在につながる。
public class GoodWorker
{
private readonly IServiceScopeFactory _scopeFactory;
public GoodWorker(IServiceScopeFactory scopeFactory)
=> _scopeFactory = scopeFactory;
public async Task RunAsync(CancellationToken ct)
{
await using var scope = _scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// この Scope 内だけ DbContext を使う
}
}
Singleton / HostedService から Scoped を使うときの定石。
Singleton → Singleton OK
Singleton → Transient OK(都度 new される)
Scoped → Scoped OK(同一 Scope 内)
Scoped → Transient OK
Scoped → Singleton OK
Singleton → Scoped NG(Captive Dependency)
Singleton → DbContext NG
IDisposable / IAsyncDisposable を実装したサービスは、スコープ終了時に破棄されるIServiceScope 内で使うDbContext は Scoped なので、リクエスト終了で接続が返るawait using var scope = ...CreateAsyncScope() でスコープ破棄を明示すると安全。
HostedService はSingleton 相当。HTTP リクエストスコープは自動では作られない。
public class OrderSyncWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopes;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await using var scope = _scopes.CreateAsyncScope();
var repo = scope.ServiceProvider
.GetRequiredService<IOrderRepository>();
await repo.SyncPendingAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
IServiceProvider を Singleton に注入して GetRequiredService<DbContext>() 直叩きは NGIServiceScopeFactory.CreateScope() / CreateAsyncScope()WebApplicationFactory 内で Scope を切るServiceProvider から手動 Scopeawait using (var scope = scopeFactory.CreateAsyncScope())
{
var sp = scope.ServiceProvider;
var a = sp.GetRequiredService<IMyScoped>();
var b = sp.GetRequiredService<IMyScoped>();
// a と b は同一インスタンス(この Scope 内)
}
ValidateScopes)開発環境では builder.Host.UseDefaultServiceProvider(o => o.ValidateScopes = true) を有効にしておくと、Captive Dependency に早く気づける(本番相当の Host 設定はプロジェクトに合わせる)。
設計時に「このサービスは何リクエスト / 何秒生きるか」を一言メモしておくと、スコープ選びで迷いにくい。