C#入門編です。今回はC#における依存性注入(DI: Dependency Injection)について解説します。

「依存性注入」という言葉、聞いたことはあるけど難しそう…と感じていませんか?

本記事は以下の方に役立つ内容となっています。

  • 依存性注入の本質を理解したい方
  • DIコンテナの基本的な使い方を学びたい方
  • モダンなC#アプリ開発の基盤を身につけたい方

DIは現代のC#開発(特に業務アプリ)においてほぼ必須の技術です。ASP.NET Core、Blazor、MAUIなど、主要なフレームワークはすべてDIを前提に設計されています。

今回は設定管理に続き、C#モダン開発における以下の構成要素の1つであるDIについて解説します。

  • 設定管理:appsettings.jsonとIConfigurationで設定を外部化
  • DI(依存性注入):依存関係を外から渡す設計手法
    ↑今回紹介!
  • ログ:ILoggerによる構造化ログ
  • Host:設定・DI・ログを統合するアプリの骨格
プロ太

DIは考え方自体はシンプルです。記事後半では、簡単なDIコンテナを作る演習も行います。一緒に学んでいきましょう!

演習コードをGitHubで公開してます。

講義1:依存性注入(DI)とは

依存性注入の本質

まず、依存性注入(DI)の本質を理解しましょう。実は非常にシンプルな考え方です。

Before:依存をクラス内部で生成する場合

以下のコードを見てみましょう。DataServiceApiClientを内部で直接生成しています。



// APIクライアント
public class ApiClient
{
    private readonly string _baseUrl;
    private readonly int _timeoutSeconds;

    public ApiClient(string baseUrl, int timeoutSeconds)
    {
        _baseUrl = baseUrl;
        _timeoutSeconds = timeoutSeconds;
    }

    public string GetData()
        => $"[{_baseUrl}] からデータ取得(タイムアウト: {_timeoutSeconds}秒)";
}

// データサービス
public class DataService
{
    private readonly ApiClient _client;

    public DataService()
    {
        // 内部で依存を生成(ハードコード)
        _client = new ApiClient("https://api.example.com", 30);
    }

    public void Execute()
    {
        var data = _client.GetData();
        // 実際のアプリでは、取得したデータを加工・保存・表示などする
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 取得結果: {data}");
    }
}

これらを利用する側は、以下のようなコードになります。

var service = new DataService();
service.Execute();

この書き方には以下の根本的な問題があります。

責務が混ざっている:DataServiceが自分の仕事(データ処理)以外に、APIクライアントの生成や設定の決定まで担当してしまっている。

つまり、「DataServiceがApiClientの生成方法まで知る必要がある」ということです。その結果、以下のような様々な問題が発生します。

  • テストしにくい:本番APIに接続せずにテストしたくても、ApiClientを差し替えられない
  • 再利用しにくい:異なるAPI設定でDataServiceを使い回せない
プロ太

次に、依存性注入の考え方で、どのようにこの問題が解決するかをみてみましょう!

After:依存を外から渡す場合

依存性注入(DI)を適用すると、以下のようになります。

// APIクライアント(変更なし)
public class ApiClient
{
    ...
}

// データサービス(依存を外から受け取る)
public class DataService
{
    private readonly ApiClient _client;

    // コンストラクタで依存を受け取る
    public DataService(ApiClient client)
    {
        _client = client;
    }

    public void Execute()
    {
        var data = _client.GetData();
        // 実際のアプリでは、取得したデータを加工・保存・表示などする
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 取得結果: {data}");
    }
}

これらを利用する側では、以下のように依存を組み立てて渡します。

// 本番用の設定でApiClientを作成
var client = new ApiClient("https://api.example.com", 30);
var service = new DataService(client);
service.Execute();

これですと例えば開発用に設定を変えたい場合も簡単です。

// 開発用に設定を変えたい場合も、DataServiceのコードは変更不要!
var devClient = new ApiClient("https://dev-api.example.com", 60);
var devService = new DataService(devClient);
devService.Execute();

これが「依存性注入(DI)」です。

プロ美

え、これだけ?コンストラクタで受け取るようにしただけじゃん!

プロ太

その通りです!DIの本質は「依存を外から渡す」というシンプルな考え方です。(コンストラクタを使うのは、その渡し方の一例です)

オブジェクト指向やインターフェイスなどとは独立した概念なんですよ。

ここまでで、「依存をどう注入するか」というDIの考え方は掴めたと思います。

配線とコンポジションルート

DIで「依存を外から渡す」ことができるようになると、次に必ず直面するのが、

「この依存関係を、アプリ全体のどこで・どう組み立てるべきか?」という設計の問題です。その答えを与えてくれるの以下の2つの概念です。

  • 配線:依存関係を組み立てること。「AにはBを渡す」という設定
  • コンポジションルート:配線を行う場所。プログラムのエントリーポイント付近に集約

配線はプログラムの一箇所(コンポジションルート)にまとめるのが良い設計です。あちこちでnewしていると、依存関係が把握しにくくなるためです。

例えば、ASP.NET Core (Blazorなど)では、Program.csがコンポジションルートです。(具体的には、後述するDIコンテナの仕組みを使って配線を行っています)

プロ美

あ、Program.csで「builder.Services.AddXxx()」とかってたくさん書いてあったけど、あれが配線だったんだね!

インターフェイスを使った依存性注入

DIはインターフェイスなしでも成立しますが、インターフェイスと組み合わせるとさらに強力になります。

先ほどの例では、DataServiceは具体的なApiClientクラスに依存していました。これをインターフェイスに依存するように変えてみます。

// インターフェイスを定義
public interface IApiClient
{
    string GetData();
}

// 本番用の実装
public class ApiClient : IApiClient
{
    private readonly string _baseUrl;
    private readonly int _timeoutSeconds;

    public ApiClient(string baseUrl, int timeoutSeconds)
    {
        _baseUrl = baseUrl;
        _timeoutSeconds = timeoutSeconds;
    }

    public string GetData()
        => $"[{_baseUrl}] からデータ取得(タイムアウト: {_timeoutSeconds}秒)";
}

// テスト用のモック実装
public class MockApiClient : IApiClient
{
    public string GetData() => "モックデータ";
}

// DataServiceはインターフェイスに依存
public class DataService
{
    private readonly IApiClient _client;

    public DataService(IApiClient client)
    {
        _client = client;
    }

    public void Execute()
    {
        var data = _client.GetData();
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 取得結果: {data}");
    }
}

利用側では、状況に応じて実装を切り替えられます。

// 本番用の設定でApiClientを作成
var service = new DataService(new ApiClient("https://api.example.com", 30));
service.Execute();

// テスト時(本番APIに接続せずにテストできる)
var testService = new DataService(new MockApiClient());
testService.Execute();
プロ美

なるほど、実装自体をまるごと差し替えて配線しているんだね!

プロ太

そうです。これはポリモーフィズムの活用ですね。DIとポリモーフィズムは相性がとても良いので、実務ではよく併用されます。

インターフェイスやポリモーフィズムについては以下の記事も参考にしてください。

C#入門編(10)オブジェクト指向とは?「ポリモーフィズム(多態性)」 ~条件分岐を使わず型に応じた振る舞いをさせる~ C#入門編です。今回は、「ポリモーフィズム(多態性)」について解説します。 ポリモーフィズムとは、あるインスタンスメソッドを呼び...
C#入門編(12)オブジェクト指向とは?「インターフェイス」 ~さまざまなクラスを一貫した方法でJSON出力する~ C#入門編です。今回は「インターフェイス」について解説します。 インターフェイスとは、関連性のないクラス間で共通の振る舞いを定義...

講義2:DIコンテナとは?

手動の配線は大変

これまで見てきたDIは、手動で配線を行っていました。

var client = new ApiClient("https://api.example.com", 30);
var service = new DataService(client);

しかし、実際のアプリでは依存関係が複雑なので、配線も大変です。

// 依存関係が増えると大変...
var logger = new Logger();
var config = new Configuration();
var httpClient = new HttpClient();
var apiClient = new ApiClient(config.BaseUrl, config.Timeout, httpClient, logger);
var cache = new CacheService(logger);
var dataService = new DataService(apiClient, cache, logger);
var controller = new DataController(dataService, logger);
// ... まだまだ続く

これを自動化してくれるのがDIコンテナです。

DIコンテナのコンセプト

DIコンテナは、依存関係の「登録」と「解決」を自動化する仕組みです。

  • 登録:「この型が必要になったら、こうやって作る」というルールを登録
  • 解決:登録されたルールに基づいて、必要な依存を自動で組み立てて提供

以下のようなイメージです。

プロ美

「DataServiceのインスタンスが欲しい!」っていうと、自動で配線(組み立て)済みのインスタンスを作ってくれるんだね!

ライフサイクル(ライフタイム)

DIコンテナでは、インスタンスのライフサイクル(生存期間)を指定できます。

ライフサイクル説明用途例
Singletonアプリ全体で 1 つを共有設定値の保持、メモリキャッシュ、HTTPクライアント
Scopedスコープ内(例:Web の 1 リクエスト)で共有DbContext、Unit of Work、リクエスト単位のサービス
Transient要求されるたびに新規作成軽量な処理サービス(変換/検証/計算など)、毎回作り直して問題ない処理

基本的には短命(Transient)から始めて、必要に応じてSingleton/Scopedに寄せるのが安全です。

例えば、HTTPクライアントであれば、接続を都度作ると、ソケット枯渇(ポート不足)のリスクがあるためSingletonにする、といった判断をします。

プロ太

フレームワーク(例:ASP.NET Core)を使う場合、そのフレームワークの作法もあるため、その場合はそちらに従うとよいでしょう。

DIコンテナにおける、ライフサイクルのより詳しいガイドラインはこちらを参考にしてください。

演習:DIコンテナを使ってみよう

.NET標準のDIコンテナ「Microsoft.Extensions.DependencyInjection」を使って、先ほどの例を書き直してみましょう。

手順1:プロジェクト作成とパッケージ追加

Visual Studioでコンソールアプリのプロジェクトを作成します。プロジェクト名は「DiSample」としました。

以下のパッケージをインストールします。

  • Microsoft.Extensions.DependencyInjection

NuGetパッケージのインストール方法は以下を参考にしてください。(dotnet package addコマンドを使う方法もあります)

C#入門編(18)NuGetパッケージの使い方 ~CSVファイルを読み込む~【Visual Studio+nuget】 C#入門編です。今回はNuGetパッケージの使い方を紹介します。 実践的なアプリ開発では、他人が作った既存部品をいかにうまく使う...

手順2:インターフェイスとクラスの定義

インターフェイス・クラス定義のコードを以下のように追加します。

コードはそれぞれ以下のようになります。(講義で説明したものと同じです)

//===IApiClient.cs===
namespace DISample;

public interface IApiClient
{
    string GetData();
}

//===ApiClient.cs===
namespace DISample;

public class ApiClient : IApiClient
{
    private readonly string _baseUrl;
    private readonly int _timeoutSeconds;

    public ApiClient(string baseUrl, int timeoutSeconds)
    {
        _baseUrl = baseUrl;
        _timeoutSeconds = timeoutSeconds;
    }

    public string GetData()
        => $"[{_baseUrl}] からデータ取得(タイムアウト: {_timeoutSeconds}秒)";
}

//===MockApiClient.cs===
namespace DISample;

public class MockApiClient : IApiClient
{
    public string GetData() => "モックデータ";
}


//===DataService.cs===
namespace DISample;

public class DataService
{
    private readonly IApiClient _client;

    public DataService(IApiClient client)
    {
        _client = client;
    }

    public void Execute()
    {
        var data = _client.GetData();
        // 実際のアプリでは、取得したデータを加工・保存・表示などする
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 取得結果: {data}");
    }
}

手順3:Program.csでDIコンテナの構築と実行

Program.csではDIコンテナへサービスを登録します。ここがコンポジションルートであり、配線を行っている部分です。

そして、作成したDIコンテナからDataServiceを受け取り(解決し)、実際に使っています。

using Microsoft.Extensions.DependencyInjection;
using DISample;

// DIコンテナの設定
var services = new ServiceCollection();

// サービスの登録
// ここでMockApiClientに変更すれば、実装を簡単に差し替えられる
services.AddSingleton<IApiClient>(sp =>
    new ApiClient("https://api.example.com", 30));

services.AddTransient<DataService>();

// サービスプロバイダーの構築
using var provider = services.BuildServiceProvider();

// サービスの取得と実行
var dataService = provider.GetRequiredService<DataService>();
dataService.Execute();
プロ太

ApiClientのついては、HTTPリクエスト処理の接続を管理すると想定し、Singletonとして作成しています。

DIコンテナはIDisposableなクラスなので適切にリソース解放を行う(usingを使う)ようにします。リソース管理の考え方は以下も参考にしてください。

C#入門編(22)ファイル読み書きとリソース管理(using/Dispose) C#入門編です。今回は「C#におけるファイル操作」と「リソース管理」について解説します。 設定ファイルの読み込み、ログの記録、デ...

アプリを実行

アプリを実行すると、例えば以下のように表示されます。

[12:52:17] 取得結果: [https://api.example.com] からデータ取得(タイムアウト: 30秒)
プロ美

DIコンテナで作成したサービスが動いたね!

プロ太

これで、DI、配線、コンポジションルート、DIコンテナの基本についてはばっちりですね!

Mockへ配線を切り替えてみたり、それぞれのサービスのライフサイクルを変えてみるなど、ぜひ色々と試してみてください!

今回、DIの基本に集中するため、APIの設定値(ベースURL等)についてはハードコーディングしています。

前回学習したIConfigurationと、今回のDIコンテナを組み合わせるとよりスマートに実装できるのですが、これについてはHostの説明の時に解説します。

まとめ

今回は依存性注入(DI)について学びました。

DIの本質は「依存を外から渡す」というシンプルな考え方です。

クラス内部でnewを使って依存を生成するのではなく、コンストラクタで依存を受け取るようにすることで、テストしやすく、設定を変えやすいコードになります。

さらにインターフェイスと組み合わせることで、実装の差し替えも可能になります。

依存を外から渡せるようになると、次は依存関係を組み立てる配線(Wiring)が必要になります。この配線を一箇所にまとめる場所がコンポジションルートです。

実際のアプリでは依存関係が複雑になるため、手動配線は大変です。DIコンテナを使えば、依存の登録情報をもとに自動で解決してくれます。

演習では、標準のDIコンテナの使い方を学びました。これはASP.NET Core、Blazor、MAUIなど主要フレームワークで採用されています。

次回は、DIとも密接に関わる「ロギング」について解説します。

プロ太

DIを理解することで、モダンなC#フレームワークの設計がよく分かるようになります。引き続き一緒に学んでいきましょう!

ABOUT ME
プロ太
ソフトウェア開発を楽しく、効率的に行う方法を追求しています。 開発者の視点から技術的課題に向き合い、「純粋な技術的興味に基づく探求」と「実践的な課題解決」という二つの柱を両輪として活動しています。