【C# DIコンテナ入門】Microsoft.Extensions.DependencyInjectionの基本と使い方

| 17 min read
Author: yoshihiro-tamori yoshihiro-tamoriの画像

C#から7年ほど遠ざかり、久々にデベロッパーサイト向けにC#をやり出しました。そこでふと疑問が出てきました。最近のC#ではDIコンテナはどんなのがあるんだろうと。

以前やっていたときは、Castle WindsorやUnity(ゲーム制作ツールのUnityとは別物)、Seasarなどがありました(実は.NET用のSeasarなんてものがかつては存在しました)。

Castle Windsorのページ
https://www.castleproject.org/projects/windsor/

.NET CoreになってからMicrosoft製のDIコンテナも登場しているようです。他にはAUTOFACというものやNinjectというものもあるようです。

AUTOFACのページ
https://autofac.org/

Ninjectのページ
http://www.ninject.org/

色々ありますが、Microsoft製のものが一番とっつきやすいかなと思って使ってみたところ、本当にとっつきやすかったです。

そのためこの記事ではMicrosoft製のMicrosoft.Extensions.DependencyInjectionの使い方について、サンプルコード付きで解説します。

DIコンテナとは

#

あらためてDIコンテナとは何かについて確認します。

DI(Dependency Injection)コンテナはオブジェクトの生成、ライフサイクルの管理、依存関係の注入を自動化するライブラリです。コードを疎結合化し、修正やテストをしやすくします。

DIコンテナを使うメリットは主に以下です。

  • 仕様変更などが発生しても、修正の手間を減らせる。
  • テスト時にはテスト用のクラス(モックと呼ばれる)に差し替えることで、テストをしやすくできる。
  • どの部品が他のどの部品を必要としているかが分かりやすいため、システムの構造が見通しやすい。

例えば以下のサンプルコードのように、オブジェクトの生成がハードコードされているとします。

public class Sample
{
    private readonly SampleWriter _sampleWriter = new();

    protected override SampleResult ExecuteSample()
    {
        return _sampleWriter.Write($"Execute sample at: {DateTimeOffset.Now}");
    }
}

シンプルなコードなので気にならないと思いますが、SampleWriterクラスを別のクラスで置き換えることを考えてみましょう。

するとSampleWriterクラスのオブジェクトを使っている個所を見直さなければいけなくなります(見直す範囲は仕様次第ですが)。

そこで登場するのがDIコンテナです。DIコンテナは例えるなら「必要なオブジェクトをまとめて提供してくれる万能な倉庫」です。

このインターフェイスにはこのクラスを代入してくださいという設定をDIコンテナに教えます。するとDIコンテナはその設定に基づいて必要なオブジェクトを自動的に作成し、渡してくれます。

例えば先ほどのサンプルコードは次のように修正できます。

public class Sample()
{
    public Sample(ISampleWriter sampleWriter)
    {
        _sampleWriter = sampleWriter;
    }

    private readonly ISampleWriter _sampleWriter;

    protected override SampleResult ExecuteSample()
    {
        return _sampleWriter.Write($"Execute sample at: {DateTimeOffset.Now}");
    }
}

コンストラクタの引数としてDIコンテナからオブジェクトを受け取ります。そしてSampleWriteの型をインターフェイスとしています。

SampleWriterクラスを別のクラスで置き換えるにしても、インターフェイスを使ってISampleWriterとしているので、DIコンテナの設定だけ変えればよくなります。

Microsoft.Extensions.DependencyInjectionとは

#

Microsoft.Extensions.DependencyInjectionはMicrosoft製のDIコンテナです。NuGetからインストールするだけですぐ使えます。またMicrosoft公式の記事も充実しています。

必要十分な機能を備えており、軽量でシンプルです。そして何より実際にやってみてコーディングが簡単でした。プロジェクト作成時に自動生成されるProgram.csに少し追加するだけなのです。

Microsoft.Extensions.DependencyInjectionはC#で使うには導入のハードルが低いDIコンテナと言ってよいでしょう。

WebAppSample
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddTransient<ISampleProc, SamplePoc>();

var app = builder.Build();

主要な概念

#

サービス

#

サービスは依存関係として注入するインスタンスのことです。

例えばIsampleProcというインターフェイスにSampleProcというクラスのインスタンスを注入するよう設定した場合、SampleProcというクラスのインスタンスがサービスに該当します。

サービスには以下の3つのライフサイクルがあります。

種類 概要
Transient 一時的という意味。
サービスが要求されるたびに、新しいインスタンスが生成される。
一時的な操作を行うサービスや状態を持つべきでないサービスに適している。
Scoped 特定のスコープ内でインスタンスが1つだけ生成される。
例えばWebアプリにおけるHTTPリクエスト(リクエストスコープ)や
アプリケーション全体(アプリケーションスコープ)。
Singleton アプリケーション全体でインスタンスが1つだけ生成される。

コンテナ

#

ISampleProcが要求されたらSampleProcのインスタンスを渡すというインターフェイスとオブジェクトの紐付けや、そのスコープを登録しておくものがコンテナです。

サービスプロバイダー

#

サービスプロバイダーはコンテナに登録された内容に基づいて依存関係の解決を行います。

あるクラスのオブジェクトが生成されるとき、そのクラスに依存関係の注入が必要なインターフェイスがあったら、オブジェクトを生成して注入します。

言葉だと抽象的なので、コードで見てみましょう。

DiSampleというクラスのオブジェクトをサービスプロバイダーが生成するケースを考えます。

このクラスにはISampleProcがあります。サービスプロバイダーがDiSampleを生成したとき、コンストラクタにISampleProcがあるのを見てSampleProcも生成してくれるのです。

DiSample
public class DiSample
{
    private ISampleProc _sampleProc;

    // コンストラクタでインジェクション
    public DiSample(ISampleProc sampleProc)
    {
        _sampleProc = sampleProc;
    }
}

さらにサービスプロバイダーは依存関係を連鎖解決してくれます。SampleProcIDbConnectionがある場合を考えてみましょう。

DiSample
public class SampleProc
{
    private IDbConnection _dbConnection;

    // コンストラクタでインジェクション
    public SampleProc(IDbConnection dbConnection)
    {
        _dbConnection = dbConnection;
    }
}

サービスプロバイダーがDiSampleを生成すると、先ほど書いた通りISampleProcがあるのを見てSampleProcも生成が必要だと判断します。

すると次はSampleProcIDbConnectionがあるのを見て、DbConnectionの生成が必要だと判断します。

こうしてサービスプロバイダーは連鎖解決してオブジェクトを生成してくれます。なんて便利なのでしょう。

サービスの登録方法

#

AddSingleton

#

ライフサイクルをSingletonにしてサービスを登録するには、下記のように記述します。

SingletonSample
services.AddSingleton<ISampleProc, SampleProc>();

AddScoped

#

ライフサイクルをScopedにしてサービスを登録するには、下記のように記述します。

ScopedSample
services.AddScoped<ISampleProc, SampleProc>();

AddTransient

#

ライフサイクルをTransientにしてサービスを登録するには、下記のように記述します。

TransientSample
services.AddTransient<ISampleProc, SampleProc>();

複数のオブジェクトを登録する方法

#

Microsoft.Extensions.DependencyInjectionは1つのインターフェイスに対して複数のオブジェクトを登録できます。その場合は後から追加した設定で上書きされ、最後に追加された設定が使われます。

ただしIEnumerable<{SERVICE}>を使って解決すれば、登録したオブジェクトすべてを生成できます。

サンプルコードを見てみましょう。まずはISampleProcに注入するオブジェクトを2つ登録します。

DiEnumerable
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddTransient<ISampleProc, SampleProc>();
builder.Services.AddTransient<ISampleProc, SampleProcess>();

var app = builder.Build();
ResolveSample
public class ResolveSample
{
    public ResolveSample(ISampleProc sampleProc)
    {
        // この場合はSampleProcessのオブジェクトが渡される
    }
}
ResolveSampleEnumerable
public class ResolveSample
{
    public ResolveSample(IEnumerable<ISampleProc> sampleProcs)
    {
        // この場合はIEnumerableにSampleProcとSampleProcessのオブジェクトが入って渡される
        // つまり値が2つあるコレクションとして渡される
    }
}

サンプルコードで実践しつつ解説

#

サービスとして登録するインターフェイスとクラス

#

まずはサービスとして登録するインターフェイスとクラスのサンプルコードを提示します。

1つのインターフェイスに対して、インジェクションするオブジェクトのクラスを変えることで、Hello WorldとMorning Worldの表示を切り替えます。またIEnumerableを使って1つのインターフェイスに複数のクラスを登録し、利用するサンプルも掲載します。

以下はHello WorldとMorning Worldを表示するためのインターフェイスとクラスのサンプルコードです。namespaceDIConsoleAppになっていますが、プロジェクト名やディレクトリ名に合わせてください。

IMessageCreator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIConsoleApp
{
    public interface IMessageCreator
    {
        string CreateMessage();
    }
}
MessageCreatorHello.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIConsoleApp
{
    internal class MessageCreatorHello : IMessageCreator
    {
        public string CreateMessage()
        {
            return "Hello, World!";
        }
    }
}
MessageCreatorMorning.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIConsoleApp
{
    internal class MessageCreatorMorning : IMessageCreator
    {
        public string CreateMessage()
        {
            return "Morning, World!";
        }
    }
}

続いてこれらのクラスをコンストラクタからインジェクションするサンプルコードを掲載します。

ISampleProc.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIConsoleApp
{
    internal interface ISampleProc
    {
        void DisplayMessage();
    }
}
SampleProc.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIConsoleApp
{
    internal class SampleProc : ISampleProc
    {
        private IMessageCreator _messageCreator;

        public SampleProc(IMessageCreator messageCreator)
        {
            _messageCreator = messageCreator;
        }

        public void DisplayMessage()
        {
            Console.WriteLine(_messageCreator.CreateMessage());
        }
    }
}

こちらは1つのインターフェイスに複数のクラスが登録されている場合に、複数のクラスのオブジェクトを取得するサンプルコードです。

SampleProcEnumerable.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIConsoleApp
{
    internal class SampleProcEnumerable : ISampleProc
    {
        private IMessageCreator _messageCreator;

        public SampleProcEnumerable(IEnumerable<IMessageCreator> messageCreators)
        {
            _messageCreator = messageCreators.ToArray()[0];
        }

        public void DisplayMessage()
        {
            Console.WriteLine(_messageCreator.CreateMessage());
        }
    }
}

コンソールアプリ

#

まずはコンソールアプリでDIを試してみましょう。理由はシンプルなものから見ていった方が理解しやすいからです。

コンソールアプリプロジェクトを作ってください。そしてインターフェイスやクラスを作成し、先ほど掲載したサンプルコードをコピペしてください。

それができたらProgram.csにDI設定を記述します。

Program.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DIConsoleApp;

// ビルダーの作成
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// サービスの登録
builder.Services.AddTransient<ISampleProc, SampleProc>();
builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>();

// ホストの構築
IHost host = builder.Build();

// サービスの取得と使用
ISampleProc sampleProc = host.Services.GetRequiredService<ISampleProc>();
sampleProc.DisplayMessage();

サービス登録の個所を以下のように、IEnumerableを使わない方のクラスにして実行してみましょう。

Program.cs(一部抜粋)
// サービスの登録
builder.Services.AddTransient<ISampleProc, SampleProc>();
//builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>();
//builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>();

コンソールにHello Worldが表示されればOKです。

これを以下のようにMorningWorld用のクラスに変えて実行してみましょう。

Program.cs(一部抜粋)
// サービスの登録
builder.Services.AddTransient<ISampleProc, SampleProc>();
//builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>();
//builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>();

今度はMorning Worldが表示されます。

それではHello WorldのクラスもMorning Worldのクラスも両方とも登録してみましょう。

Program.cs(一部抜粋)
// サービスの登録
builder.Services.AddTransient<ISampleProc, SampleProc>();
//builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>();

この場合は後勝ちとなってMorning Worldが表示されます。

その次はIEnumerableを試してみましょう。コードを次のように変えて実行します。

Program.cs(一部抜粋)
// サービスの登録
//builder.Services.AddTransient<ISampleProc, SampleProc>();
builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>();

SampleProcEnumerableでは、以下のようにインデックスが0のオブジェクトを使うようになっています。そのため先に登録されたMessageCreatorHelloのオブジェクトがインジェクションされます。インデックスを1にすれば2番目に登録されたクラスのオブジェクトがインジェクションされます。

SampleProcEnumerable.cs(一部抜粋)
public SampleProcEnumerable(IEnumerable<IMessageCreator> messageCreators)
{
    _messageCreator = messageCreators.ToArray()[0];
}

Webアプリ

#

WebアプリのDI設定

今度はWebアプリで試してみましょう。やっぱり現実的にはWebアプリのプロジェクトが多いでしょうから、Webアプリでの使い方を知っておきたいところです。

この記事ではRazorページを使って解説していきます。Razorとは何かについてはこちらの記事を参照してください。

C#とRazorで始める効率的なWeb開発!サンプルコード付きで徹底解説

Razorページアプリプロジェクトを作ってください。そしてインターフェイスやクラスを作成し、先ほど掲載したサンプルコードをコピペしてください。

そしたらProgram.csを開いてみてください。次のようになっています。

Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

なんとRazorページアプリプロジェクトのProgram.csには、最初からMicrosoft.Extensions.DependencyInjectionで使うビルダーが記述されているのです。RazorページがDI設定同様にサービスとして登録されているのです。

ここがMicrosoft.Extensions.DependencyInjectionの導入のしやすさなのでしょう。仕組みがRazorページのようなC#でよく使う技術と共通化されているわけですね。

Razorページの登録前にDI設定を記述します。サンプルコードは以下です。コメントで「サービスの登録」と記述した個所が該当します。

Program.cs
using DIConsoleApp;

var builder = WebApplication.CreateBuilder(args);

// サービスの登録
builder.Services.AddTransient<ISampleProc, SampleProc>();
builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>();
builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>();

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Webアプリの動作確認

Webアプリの場合は確認用の画面を作る必要もあります。Indexを修正し、SampleProcSampleProcEnumerableというRazorページを作ってください。ページ遷移のイメージはこの画像のようになります。

Webアプリのサンプルのページ遷移
Webアプリのサンプルのページ遷移

サンプルコードを掲載します。まずはIndex.cshtmlに以下のようにアンカータグを2ページ分追加します。

Index.cshtml
<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
    <p><a href="/SampleProc">SampleProc</a></p>
    <p><a href="/SampleProcEnumerable">SampleProcEnumerable</a></p>
</div>

そしたらSampleProc(後勝ち用のページ)とSampleProcEnumerableIEnumerable用のページ)を作ります。まずはSampleProcのサンプルコードを掲載します。

SampleProc.cshtml
@page
@model DIWebApp.Pages.SampleProcModel
@{
}

<h2>SampleProc</h2>
<p>@Model.DisplayMessage()</p>
SampleProc.cshtml.cs
using DIWebApp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace DIWebApp.Pages
{
    public class SampleProcModel : PageModel
    {
        private IMessageCreator _messageCreator;

        public SampleProcModel(IMessageCreator messageCreator)
        {
            _messageCreator = messageCreator;
        }

        public string DisplayMessage()
        {
            return _messageCreator.CreateMessage();
        }

        public void OnGet()
        {
        }
    }
}

続いてSampleProcEnumerableIEnumerableを使うページ)のサンプルコードを掲載します。

SampleProcEnumerable.cshtml
@page
@model DIWebApp.Pages.SampleProcEnumerableModel
@{
}

<h2>SampleProcEnumerable</h2>
<p>@Model.DisplayMessage()</p>
SampleProcEnumerable.cshtml.cs
using DIWebApp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace DIWebApp.Pages
{
    public class SampleProcEnumerableModel : PageModel
    {
        private IMessageCreator _messageCreator;

        public SampleProcEnumerableModel(IEnumerable<IMessageCreator> messageCreatora)
        {
            _messageCreator = messageCreatora.ToArray()[0];
        }

        public string DisplayMessage()
        {
            return _messageCreator.CreateMessage();
        }

        public void OnGet()
        {
        }
    }
}

デバッグ起動すると最初にIndex画面が表示されます。

そしたらSampleProcSampleProcEnumerableにアクセスして、先ほどのコンソールアプリ同様に、後勝ちであることやIEnumerableについて実行して確認してみてください。

おわりに

#

私はC#にはLINQやRazorなどとても便利な技術があるのに、DIコンテナはいまいちだなぁと感じていました。

しかし今回Microsoft.Extensions.DependencyInjectionを使ってみて、C#にも簡単に扱えるDIコンテナがあるんだと知りました。

かつて私がCastle Windsorを使ったときは、.NET MVCで今のProgram.csに該当するクラスにもっと複雑なコードを書いていました。そしてXMLに冗長なDI設定を書いていました。

それと比べるとMicrosoft.Extensions.DependencyInjectionは書くべき個所が明確ですし、書き方も簡単ですね。これなら導入のハードルは低いです。

もしC#で開発する際のDIコンテナに迷っているようでしたら、Microsoft.Extensions.DependencyInjectionを使ってみてはいかがでしょうか。その際にこの記事が参考になれば幸いです。

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。