DI(依存性の注入)とは?.NETでのサービスの登録と寿命¶
最終確認: 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なし: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つから選ぶ。
| ライフタイム | いつ作る? | 向いているもの | 例 |
|---|---|---|---|
| Transient | 頼まれるたびに毎回新しく | 軽くて、状態を持たないもの | 計算だけするクラス |
| Scoped | 1リクエストに1つ(同じリクエストの中では使い回す) | リクエストの間だけ状態を持つもの | EF Core の DbContext、ログイン中のユーザー情報 |
| Singleton | アプリ全体で1つ(起動から終了まで) | 重い・共有したいもの。スレッドセーフであること | 設定、キャッシュ、IClock |
迷ったら、DBにつながるものは Scoped、状態を持たない小さな部品は Transient、全体で共有するものは Singleton。
質問:気をつけることは?¶
1. とらわれた依存(Singleton の中に Scoped を入れない)¶
Singleton は1つしか作られないので、その中に入れた Scoped の部品もずっと同じものが使い回される。リクエストごとに新しくなるはずの DbContext が、アプリ全体で共有されてしまい、データが混ざったり、同時に使われてエラーになったりする。
- 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 にできるか)