第40章:DI(依存の差し込み)で依存向きを守る🪄🔌✨
この章のゴールはこれだよ〜!🎯💕
- **Dependency Rule(依存は内側へ)**を、実行時の配線(DI)で守れるようになる🧭✨
- Core(Entities/UseCases)に new を持ち込まず、外側で組み立てできるようになる🧼🏗️
- **よくあるDI事故(ライフタイム・循環参照・登録漏れ)**を避けられるようになる🧯😵💫➡️😌
※2026年1月時点では .NET 10 / ASP.NET Core 10 の更新が出ているので、コードもその流儀(いまの主流の書き方)でいくね💎✨ (マイクロソフトサポート)
1) そもそもDIって何がうれしいの?🥰🧩

クリーンアーキで一番大事なのは 「内側が外側を知らない」 だったよね?(Dependency Rule)🧠➡️⭕ でも実際にはアプリを動かすには、UseCaseがDBや外部APIを呼ぶ必要がある…🤔💦
そこで登場するのが DI(Dependency Injection)!🎉
- Core側:
interface(ポート)だけを知ってる🧼 - 外側:その
interfaceを実装したクラス(アダプター)を作る🔧 - 起動時(最外周):DIで「このinterfaceはこの実装を使ってね」って**合体(配線)**する🪄
ASP.NET Coreには 組み込みのDIコンテナ(IServiceProvider) があって、 サービス登録→コンストラクタへ注入、までを面倒みてくれるよ✨ (Microsoft Learn)
2) “DIの型” はこれだけ覚えればOK👌💕
DIをクリーンアーキ的に言うと、基本はこの4点セットだよ📦✨
- Coreに interface(ポート)を置く🧼
- 外側に実装(アダプター)を置く🔧
- 起動時に登録(Composition Root)する🧵(※次章で深掘りするよ!)
- 使う側はコンストラクタで受け取る🎁(newしない!)
3) ハンズオン:Repository差し替えをDIでやってみよ〜!🧪🎮
題材:IMemoRepository を InMemory版 と DB版 で差し替えできるようにする💡✨
(ポイント:Core側はどっちでも動くこと!)
3-1) Core(UseCases)に「ポート(interface)」を置く🧼🔌
// Core/UseCases/Ports/IMemoRepository.cs
public interface IMemoRepository
{
Task SaveAsync(Memo memo, CancellationToken ct);
Task<Memo?> FindByIdAsync(Guid id, CancellationToken ct);
}
// Core/Entities/Memo.cs(超ざっくり)
public sealed class Memo
{
public Guid Id { get; }
public string Title { get; private set; }
public Memo(Guid id, string title)
{
if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("タイトルは必須だよ");
Id = id;
Title = title;
}
}
ここが超重要💖:Coreは「保存できる」ことしか知らない。 DBとかEF Coreとか、そういう単語すら出てこないのが勝ち🏆✨
3-2) UseCaseは「interface」をコンストラクタで受け取る🎁✨
// Core/UseCases/CreateMemo/CreateMemoInteractor.cs
public sealed class CreateMemoInteractor
{
private readonly IMemoRepository _repo;
public CreateMemoInteractor(IMemoRepository repo) // ← DIで入る
{
_repo = repo;
}
public async Task<Guid> HandleAsync(string title, CancellationToken ct)
{
var memo = new Memo(Guid.NewGuid(), title);
await _repo.SaveAsync(memo, ct);
return memo.Id;
}
}
**newしてない!**えらい!🥳🎉 これで 依存方向は内向きのままで、実行時に差し込めるようになるよ🪄
3-3) 外側(Adapters/Infrastructure)で実装を書く🔧🗄️
InMemory実装(まずはこれでOK!)🧠💾
// Adapters/Persistence/InMemoryMemoRepository.cs
public sealed class InMemoryMemoRepository : IMemoRepository
{
private readonly Dictionary<Guid, Memo> _store = new();
public Task SaveAsync(Memo memo, CancellationToken ct)
{
_store[memo.Id] = memo;
return Task.CompletedTask;
}
public Task<Memo?> FindByIdAsync(Guid id, CancellationToken ct)
{
_store.TryGetValue(id, out var memo);
return Task.FromResult(memo);
}
}
3-4) 最外周(起動時)でDI登録する🪄🧵
ASP.NET Coreはサービス登録→注入をやってくれるよ✨ (Microsoft Learn)
// Web/Program.cs
var builder = WebApplication.CreateBuilder(args);
// UseCase(アプリの手順書)もDIで作る
builder.Services.AddScoped<CreateMemoInteractor>();
// Repositoryは「interface → 実装」を登録する
builder.Services.AddSingleton<IMemoRepository, InMemoryMemoRepository>();
var app = builder.Build();
app.MapPost("/memos", async (string title, CreateMemoInteractor usecase, CancellationToken ct) =>
{
var id = await usecase.HandleAsync(title, ct);
return Results.Ok(new { id });
});
app.Run();
これで、エンドポイントが CreateMemoInteractor を受け取るとき、
DIが勝手に IMemoRepository まで芋づる式に解決してくれるよ〜!🍠✨
4) ライフタイム(Singleton / Scoped / Transient)で事故らないコツ🧯😵💫
DIには寿命(ライフタイム)があって、ここでミスると爆発する💥 Microsoftのガイドでも スコープ検証(ValidateScopes) や Singletonの扱い に注意が書かれてるよ📌 (Microsoft Learn)
ざっくり指針はこれ👇💕
- Singleton:アプリ全体で1個。状態持つなら慎重に🧊
- Scoped:Webなら「1リクエスト=1スコープ」感覚で使いやすい🧃
- Transient:毎回新規。軽いやつ向き🫧
💣ありがち事故:
- SingletonがScoped(例:DbContext)を抱え込む → 例外で死ぬ or バグる😇 → 対策:スコープ検証をON(次のコード)+設計見直し✨ (Microsoft Learn)
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true;
options.ValidateOnBuild = true;
});
5) 実装の差し替え:開発はInMemory、本番はDB🪄🏗️
「差し替えたい」のがDIの一番おいしい所だよね🍰💕
if (builder.Environment.IsDevelopment())
{
builder.Services.AddSingleton<IMemoRepository, InMemoryMemoRepository>();
}
else
{
builder.Services.AddScoped<IMemoRepository, EfMemoRepository>(); // 例:DB版
}
✅Coreは一切変更なし! 外側の配線だけ変えて挙動が変わるのがクリーンアーキの気持ちよさ🥰✨
6) “複数実装” を同時に使いたい時:Keyed Services🔑✨
最近のASP.NET Coreでは Keyed services が使えるよ!
AddKeyedSingleton/AddKeyedScoped/AddKeyedTransient で「キー付き登録」して、
取り出すときは [FromKeyedServices] を使うスタイルが公式に説明されてるよ🔑📌 (Microsoft Learn)
例:同じ IMemoRepository でも「高速版」「監査ログ版」を使い分けたい…みたいなときに便利💕
builder.Services.AddKeyedScoped<IMemoRepository, InMemoryMemoRepository>("fast");
builder.Services.AddKeyedScoped<IMemoRepository, AuditingMemoRepository>("audit");
app.MapGet("/memos/{id:guid}", async (
Guid id,
[FromKeyedServices("fast")] IMemoRepository repo,
CancellationToken ct) =>
{
var memo = await repo.FindByIdAsync(id, ct);
return memo is null ? Results.NotFound() : Results.Ok(memo);
});
※ただし!クリーンアーキ的には 「どれを使うかの判断」は できるだけ外側(起動時 or adapter) に寄せるのが安定だよ〜🧠💕
7) 外部HTTP連携は HttpClientFactory が王道🌍📡✨
外部API呼び出しをDIで扱うなら、IHttpClientFactory を使うのが基本だよ☕✨
公式ドキュメントでも、IHttpClientFactory の登録と利点がまとまってる🧾 (Microsoft Learn)
(クリーンアーキ的には)
- Core:
IWeatherClientみたいな ポート - 外側:
WeatherClient(HttpClient使う実装) - DI:Typed clientとして登録 って分けると超きれい🥰
8) よくあるDIエラーと即死回避💥➡️🧯
✅「登録してないよ」系
- 例外:
No service for type ... has been registered - 原因:
AddScoped<Interface, Impl>()を忘れてる😇 - 対策:起動時登録を1か所に集約(次章のComposition Root)🧵✨
✅ 循環参照(AがB、BがA)
- 原因:設計的に責務が絡まってるサイン🌀
- 対策:ポートを切る/依存を片方に寄せる/ユースケース分割🔪✨
✅ ライフタイム事故(Singleton ← Scoped)
- 対策:
ValidateScopesON + Singletonを減らす(ガイドでも注意あり)📌 (Microsoft Learn)
9) Copilot / Codexに手伝わせるときのコツ🤖💕
DIは「配線が多くてミスりやすい」から、AIがめっちゃ役立つよ✨ おすすめプロンプト👇
- 「このSolution構成で、Coreが外側参照しないDI登録を提案して」🧼➡️🪄
- 「この登録、ライフタイム不整合ない?(SingletonがScoped掴んでない?)」🧯
- 「
IMemoRepositoryの InMemory/Ef を差し替える設計、クリーンアーキ的に変な所ある?」🩺✨
最後に人間が “依存向き” と “責務” だけチェックすればOK🙆♀️💕
10) ミニ課題🎮📘✨
課題1:差し替え成功チェック✅
- InMemory登録→DB登録に変えても、Core/UseCasesは一切変更しないで動かしてみてね🪄
課題2:ライフタイム事故をわざと起こす💥(学習用)
- RepositoryをSingletonにして、内部でScopedなものを掴む設定を想像してみて
- 何が危ないか、言葉で説明できたら勝ち🏆✨(スコープ検証の意味が腹落ちするよ) (Microsoft Learn)
課題3:Keyed servicesで2実装を並走🔑
fastとauditを登録して、エンドポイントで使い分けてみよ〜! (Microsoft Learn)
章まとめ📝💖
- DIは 依存方向を守ったまま、実行時に「外側の実装」を内側へ差し込む魔法🪄✨ (Microsoft Learn)
- Coreは interfaceだけ、外側は 実装、最外周は **登録(配線)**🔌
- ライフタイムと登録漏れが事故ポイント!
ValidateScopesなどのガイドも活用しよ🧯 (Microsoft Learn) - 最新のASP.NET Coreには Keyed services もあるから、複数実装の扱いもやりやすいよ🔑✨ (Microsoft Learn)
次の第41章は、この章で出てきた「登録(配線)」を 散らかさずに1か所へ集約する(Composition Root) をガッツリやるよ〜🧵💖