コンテンツにスキップ

DI(依存性の注入)とは?.NETでのサービスの登録と寿命

2026-10-27 更新約7分で読めます

最終確認: 2026-10-27

30秒で復習

  • DI(ディーアイ、Dependency Injection)=クラスが使う部品を、自分で new せずに外から渡してもらう書き方。 日本語では「依存性の注入」。
  • うれしいこと:部品を差し替えやすい(テストでは偽物、本番では本物)、クラス同士の結び付きが弱くなる、作り方と後片付けを1か所にまとめられる。
  • .NET では、Program.cs で builder.Services に登録し、クラスはコンストラクターの引数で受け取る。組み立ては DIコンテナがやってくれる。
  • 登録するときは寿命(ライフタイム)を選ぶ:Transient(毎回新しく)・Scoped(1リクエストに1つ)・Singleton(アプリ全体で1つ)。
  • いちばんの落とし穴は、Singleton の中で Scoped を使うこと(とらわれた依存)。開発環境ではエラーで気づける。
  • 覚え方:「部品は自分で作らず、注文して届けてもらう」

用語の正式名称と読み方

用語 意味
DI Dependency Injection。使う部品(依存)を外から渡す(注入する)こと
依存(dependency) あるクラスが仕事をするのに必要な、ほかのクラス
DIコンテナ 登録された部品を覚えておき、必要なときに組み立てて渡してくれる仕組み。.NET では IServiceProvider
サービス DIコンテナに登録する部品のこと(画面の「サービス」とは別の意味)
ライフタイム 部品をいつ作り、いつまで使い回すか(寿命)
コンストラクター注入 コンストラクターの引数で部品を受け取る、いちばん基本の形

質問:DIとは? なぜ使う?

答え: クラスの中で new して部品を作ると、そのクラスは特定の部品にがっちり結び付く。DI では、使う部品をインターフェースで受け取り、どの実物を渡すかは外(DIコンテナ)が決める。

DIなし:自分で new する DIあり:外から渡してもらう OrderService new SqlOrderRepository(…) 接続文字列もここに書く = この部品にがっちり結び付く 差し替え・テストのたびに書き換え DIコンテナ 本番 → SqlOrderRepository テスト → FakeOrderRepository コンストラクターで渡す OrderService IOrderRepository を受け取るだけ 登録を変えるだけで差し替えられる
// DIなし:OrderService が SqlOrderRepository を自分で作っている
public class OrderService
{
    private readonly SqlOrderRepository _repo = new SqlOrderRepository("Server=…;");
}

// DIあり:使う部品はコンストラクターで受け取る(中身が何かは知らなくてよい)
public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
    public async Task PlaceAsync(Order order)
    {
        await repo.AddAsync(order);
        log.LogInformation("注文 {No} を登録しました", order.No);
    }
}
観点 DIなし(自分で new) DIあり
部品の差し替え クラスのコードを書き換える 登録を1行変えるだけ
テスト 本物のDBにつながってしまう 偽物(モック)を渡せる
接続文字列などの設定 クラスの中に散らばる 登録する場所にまとまる
後片付け(Dispose) 自分で忘れずに呼ぶ DIコンテナが寿命に合わせて呼ぶ

イメージ(たとえ話)

  • DIなし=料理人が毎回、自分で包丁を作ってから料理する。包丁を替えたいときは、料理人を作り直す。
  • DIあり=料理人は「包丁をください」と言うだけ。どの包丁を渡すかは厨房(DIコンテナ)が決める。練習のときは練習用の包丁を渡せる。

質問:.NETではどう書く?

答え: Program.cs で登録し、使う側はコンストラクターで受け取る。

var builder = WebApplication.CreateBuilder(args);

// ① 登録:インターフェース → 実物 と、寿命を決める
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<OrderService>();
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddHttpClient<IPaymentApi, PaymentApi>();   // HttpClient も DI で

var app = builder.Build();

// ② 使う:引数に書くだけで、DIコンテナが組み立てて渡してくれる
app.MapPost("/orders", async (Order order, OrderService service) =>
{
    await service.PlaceAsync(order);
    return Results.Ok();
});
app.Run();
  • コントローラー、Minimal API の引数、ミドルウェア、BackgroundService など、ASP.NET Core のあらゆる場所で受け取れる。
  • ILogger<T> や IConfiguration は、最初から登録されている。

質問:寿命(ライフタイム)はどう選ぶ?

答え: 「いつ作って、いつまで使い回すか」を3つから選ぶ。

起動 終了 リクエスト1 リクエスト2 Singleton アプリで1つ Scoped 1リクエストに1つ Transient 毎回新しく 同じ1つを、起動から終了まで使い回す このリクエストの間は同じ このリクエストの間は同じ 新 新 新 新 新 新
ライフタイム いつ作る? 向いているもの 例
Transient 頼まれるたびに毎回新しく 軽くて、状態を持たないもの 計算だけするクラス
Scoped 1リクエストに1つ(同じリクエストの中では使い回す) リクエストの間だけ状態を持つもの EF Core の DbContext、ログイン中のユーザー情報
Singleton アプリ全体で1つ(起動から終了まで) 重い・共有したいもの。スレッドセーフであること 設定、キャッシュ、IClock

迷ったら、DBにつながるものは Scoped、状態を持たない小さな部品は Transient、全体で共有するものは Singleton。

質問:気をつけることは?

1. とらわれた依存(Singleton の中に Scoped を入れない)

Singleton は1つしか作られないので、その中に入れた Scoped の部品もずっと同じものが使い回される。リクエストごとに新しくなるはずの DbContext が、アプリ全体で共有されてしまい、データが混ざったり、同時に使われてエラーになったりする。

NG:Singleton の中に Scoped OK:使うたびにスコープを作る リクエスト1 リクエスト2 リクエスト3 Singleton (アプリで1つ) DbContext(Scoped) ずっと同じものを共有 データが混ざる・同時に使われてエラー Singleton BackgroundService IServiceScope Factory スコープ+DbContext 1回目・使ったら破棄 スコープ+DbContext 2回目・使ったら破棄 スコープ+DbContext 3回目・使ったら破棄 使うたびに新しいスコープ
  • ASP.NET Core は開発環境ではこの組み合わせをエラーにしてくれる(スコープの検証)。本番だけで動かすと気づけないので、開発環境で一度動かす。
  • Singleton の中で Scoped を使いたいとき(BackgroundService など)は、IServiceScopeFactory でその都度スコープを作って取り出す。
public class NightlyJob(IServiceScopeFactory scopes) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stop)
    {
        using var scope = scopes.CreateScope();                              // 1回分のスコープ
        var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
        await repo.DeleteOldAsync(stop);
    }                                                                         // ここで後片付け
}

2. サービスロケーターにしない

クラスの中で IServiceProvider を受け取って GetService() で何でも取り出すのは、DIに見えてDIではない(サービスロケーター)。何に依存しているかがコンストラクターから見えなくなる。使う部品はコンストラクターに並べる。

3. コンストラクターの引数が多すぎたら、クラスが大きすぎるサイン

引数が5つ、6つと増えてきたら、クラスが仕事を抱えすぎている。分けられないか見直す。

4. 設定は Options パターンで受け取る

builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"));

public class MailSender(IOptions<SmtpOptions> options)
{
    private readonly SmtpOptions _smtp = options.Value;   // appsettings.json の Smtp セクション
}
受け取り方 特徴
IOptions<T> 起動時の値。いちばん基本
IOptionsSnapshot<T> リクエストごとに読み直す(Scoped)
IOptionsMonitor<T> 設定ファイルの変更を検知できる(Singleton でも使える)

5. 同じインターフェースの実物を使い分ける(キー付きサービス)

.NET 8 から、キー(名前)を付けて登録し、受け取るときに選べる。Strategy パターンと相性がよい。

builder.Services.AddKeyedScoped<IShippingFee, NormalFee>("normal");
builder.Services.AddKeyedScoped<IShippingFee, MemberFee>("member");

public class Checkout([FromKeyedServices("member")] IShippingFee fee) { /* … */ }

質問:テストではどう役立つ?

答え: 本物の代わりに偽物(テストダブル)を渡せるので、DBや外部APIなしでクラスだけを確かめられる。

[Fact]
public async Task 注文を登録したら_ログが出る()
{
    var repo = new FakeOrderRepository();                         // メモリに保存するだけの偽物
    var service = new OrderService(repo, NullLogger<OrderService>.Instance);

    await service.PlaceAsync(new Order("A-1001"));

    Assert.Single(repo.Saved);
}

具体例

やりたいこと DIでの書き方
本番はSQL Server、テストはメモリ IOrderRepository を登録し分ける
外部APIを呼ぶ AddHttpClient<IPaymentApi, PaymentApi>()(HttpClient の作り方も任せる)
現在時刻をテストで固定したい IClock(または .NET 8 の TimeProvider)を DI で受け取る
ログを足したい 元のクラスを Decorator で包んで登録する
夜間バッチで DB を使う BackgroundService + IServiceScopeFactory

関連メモ

確認したいこと

  • クラスの中で new している部品のうち、差し替えたい・テストで偽物にしたいものはどれか
  • Singleton に登録したクラスが、Scoped の部品(DbContext など)を受け取っていないか
  • IServiceProvider から直接取り出している場所(サービスロケーター)はないか
  • 設定値を、IConfiguration["..."] の文字列で直接読んでいる場所はないか(Options にできるか)

参考資料