ServiceCollection, ServiceProvider を利用したシンプルなDI (Dependency Injection) のコードを記述する - C#

ServiceCollection, ServiceProvider を利用したシンプルなDI (Dependency Injection) のコードを記述します。

概要

C#で ServiceCollection, ServiceProvider を利用したシンプルな DI (Dependency Injection) のコードを記述して動作を確認します。

事前準備

プロジェクトの作成

プロジェクトを作成します。今回は、コードを非常に単純にするため、コンソールアプリケーションを作成します。

Microsoft.Extensions.DependencyInjection のインストール

プロジェクトに Microsoft.Extensions.DependencyInjection のパッケージをインストールします。 NuGet パッケージマネージャーでインストールするか、または、パッケージマネージャーコンソールで次のコマンドを実行します。
Install-Package Microsoft.Extensions.DependencyInjection 

プログラム : オブジェクトの挿入をせずにインスタンスのみを作成する

はじめにオブジェクトの注入が無く、(オブジェクトが挿入されるクラスの)インスタンスのみを生成するプログラムを記述します。

コード

注入するオブジェクト(サービス)のクラスです。
MyService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DISimple
{
  public interface IMyService
  {
    string GetName(int code);
  }

  public class MyService : IMyService
  {
    public string GetName(int code)
    {
      switch (code) {
        case 0:
          return "ぺんぎんクッキー";
        case 1:
          return "らくだキャラメル";
        case 2:
          return "しろくまアイス";
        case 3:
          return "あひるタルト";
        default:
          return "(Balnk)";
      }
    }
  }
}

オブジェクト(サービス)が注入されるクラスです。今回は何も注入しない状態で実装します。
MyClass.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DISimple
{
  public class MyClass
  {
    public MyClass()
    {
    }

    public string GetData()
    {
      return "出力:";
    }
  }
}

コンソールアプリケーションのメインのProgram.cs です。
Program.cs
using System;
using Microsoft.Extensions.DependencyInjection;

namespace DISimple
{
  class Program
  {
    static void Main(string[] args)
    {
      //ServiceCollection, ServiceProvider 作成
      ServiceCollection services = new ServiceCollection();
      ConfigureServices(services);
      ServiceProvider serviceProvider = services.BuildServiceProvider();

      //インスタンス生成
      MyClass mc = (MyClass)serviceProvider.GetRequiredService<MyClass>();
      string result = mc.GetData();

      Console.WriteLine(result);
    }

    private static void ConfigureServices(ServiceCollection services)
    {
      services.AddSingleton<MyClass>();
      services.AddScoped<IMyService, MyService>();
    }
  }
}

解説

MyService.cs

他のオブジェクトに注入するオブジェクトのクラスです。DIでは注入するオブジェクトを「サービス」という名称で呼ぶことが一般的です。
サービスは 抽象クラスを実装します。今回の例では、IMyService が抽象クラスになっています。抽象クラスで GetName メソッドの抽象メソッドを定義しています。
サービスクラス(MyService)は、抽象クラス(IMyService)を継承します。抽象メソッドのGetNameメソッドの具体的な実装を記述します。今回の例では、 引数のコードの数値に応じて、文字列を返すメソッドを実装しています。

MyClass.cs

オブジェクト(サービス)が注入されるクラスです。今回は注入の無い状態でコードを記述しています。
GetData() メソッドで文字列を返す処理を記述しています。

Program.cs

ServiceCollection, ServiceProvider を作成し、MyClassをインスタンス化する処理を記述しています。

以下のコードで、ServiceCollection, ServiceProvider を作成しています。
ServiceCollection オブジェクトを作成し、作成したオブジェクトの設定をしたのちに、BuildServiceProvider() メソッドを呼び出し、ServiceProvider を作成します。
      ServiceCollection services = new ServiceCollection();
      ConfigureServices(services);
      ServiceProvider serviceProvider = services.BuildServiceProvider();

ConfigureServices を別のメソッドにする必要はありませんが、わかりやすさのために別のメソッドにしています。
ServiceCollectionにMyClassを追加し、サービスクラスのMyServiceも追加します。
    private static void ConfigureServices(ServiceCollection services)
    {
      services.AddSingleton<MyClass>();
      services.AddScoped<IMyService, MyService>();
    }

MyClassのインスタンス生成は次のコードで実行します。
      MyClass mc = (MyClass)serviceProvider.GetRequiredService<MyClass>();

作成されたMyClassのインスタンスオブジェクトのGetData() メソッドを呼び出し、戻り値をコンソールに表示します。
      string result = mc.GetData();

今回は何もオブジェクトを注入していないため、MyClass.cs に記述した「出力:」のみが画面に表示される動作になります。

実行結果

コンソールウィンドウが表示され、「出力:」 の文字が表示されます。

プログラム : オブジェクトの注入をする

オブジェクトの注入をするようコードを変更します。

コード


MyService.cs は先ほどのプログラムから変更はありません。
MyService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DISimpleInjection
{
  public interface IMyService
  {
    string GetName(int code);
  }

  public class MyService : IMyService
  {
    public string GetName(int code)
    {
      switch (code) {
        case 0:
          return "ぺんぎんクッキー";
        case 1:
          return "らくだキャラメル";
        case 2:
          return "しろくまアイス";
        case 3:
          return "あひるタルト";
        default:
          return "(Balnk)";
      }
    }
  }
}

MyClass.cs はコンストラクタの引数を一つ増やし、IMyService を受け取るよう記述します。
コンストラクタに渡されたIMyServiceオブジェクトをクラスのプライベート変数(_srv)に代入して保持します。 GetDataメソッドで、受け取ったIMyServiceオブジェクトのGetName() メソッドを呼び出し、結果をGetDataの出力として返します。
MyClass.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DISimpleInjection
{
  public class MyClass
  {
    private IMyService _srv;
    public MyClass(IMyService srv)
    {
      _srv = srv;
    }

    public string GetData()
    {
      return "出力:" + _srv.GetName(2);
    }
  }
}

Proigram.cs は先ほどのプログラムから変更はありません。
Program.cs
using System;
using Microsoft.Extensions.DependencyInjection;

namespace DISimpleInjection
{
  class Program
  {
    static void Main(string[] args)
    {
      //ServiceCollection, ServiceProvider 作成
      ServiceCollection services = new ServiceCollection();
      ConfigureServices(services);
      ServiceProvider serviceProvider = services.BuildServiceProvider();

      //インスタンス生成
      MyClass mc = (MyClass)serviceProvider.GetRequiredService<MyClass>();
      string result = mc.GetData();

      Console.WriteLine(result);
    }

    private static void ConfigureServices(ServiceCollection services)
    {
      services.AddSingleton<MyClass>();
      services.AddScoped<IMyService, MyService>();
    }
  }
}

解説

コンストラクタにIMyServiceのパラメータを追加します。
先ほどはパラメータが無い状態でしたが、パラメーターを増やしてコンパイルしてもビルドが通ります。 従来のC#のプログラムではインスタンスオブジェクトを作成する際に new 演算子を利用するため、コンストラクタのパラメータが変更されると ビルドエラーになりますが、DIを利用して、new 演算子を使わずにインスタンスを生成しているためビルドエラーにはなりません。
コンストラクタに渡された、IMyService オブジェクトをクラスのプライベートのメンバ変数に代入してオブジェクトの参照を保持します。
    private IMyService _srv;
    public MyClass(IMyService srv)
    {
      _srv = srv;
    }

GetDataメソッドで、IMyServiceのGetName() メソッドを呼び出し、戻り値をDatDataメソッドの戻り値として返す処理を実装します。
    public string GetData()
    {
      return "出力:" + _srv.GetName(2);
    }

実行結果

プロジェクトを実行すると、「出力:しろくまアイス」の結果が表示されます。 MyClass に MyService が注入され、MyClassからのGetNameのメソッド呼び出しが動作していることがわかります。

プログラム : オブジェクトに複数のサービスを注入する

サービスの数を増やして、複数のサービスを注入するコードを記述します。

コード

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

namespace DISimpleMultiInjection
{
  public interface IMyService1
  {
    string GetName(int code);
  }

  public class MyService1: IMyService1
  {
    public string GetName(int code)
    {
      switch (code) {
        case 0:
          return "ぺんぎんクッキー";
        case 1:
          return "らくだキャラメル";
        case 2:
          return "しろくまアイス";
        case 3:
          return "あひるタルト";
        default:
          return "(Balnk)";
      }
    }
  }
}
MyService2.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DISimpleMultiInjection
{
  public interface IMyService2
  {
    public int GetCount(int code);
  }

  public class MyService2: IMyService2
  {
    public int GetCount(int code)
    {
      switch (code) {
        case 0:
          return 15;
        case 1:
          return 8;
        case 2:
          return 2;
        case 3:
          return 4;
        default:
          return 0;
      }
    }
  }
}
MyService3.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DISimpleMultiInjection
{
  public interface IMyService3
  {
    int GetPrice(int code);
  }

  public class MyService3: IMyService3
  {
    public int GetPrice(int code)
    {
      switch (code) {
        case 0:
          return 280;
        case 1:
          return 460;
        case 2:
          return 380;
        case 3:
          return 520;
        default:
          return 0;
      }
    }
  }
}
MyClass1.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DISimpleMultiInjection
{
  public class MyClass1
  {
    private IMyService1 _srv1;
    private IMyService2 _srv2;
    private IMyService3 _srv3;

    public MyClass1(IMyService2 srv2, IMyService1 srv1, IMyService3 srv3)
    {
      _srv1 = srv1;
      _srv2 = srv2;
      _srv3 = srv3;
    }

    public string GetData()
    {
      return "出力:" + _srv1.GetName(2) + " / " + _srv2.GetCount(2).ToString() + " / " + _srv3.GetPrice(2).ToString();
    }
  }
}
MyClass2.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DISimpleMultiInjection
{
  public class MyClass2
  {
    private IMyService3 _srv3;

    public MyClass2(IMyService3 srv3)
    {
      _srv3 = srv3;
    }

    public string GetData()
    {
      return "出力:" + _srv3.GetPrice(0).ToString();
    }

  }
}
Program.cs
using System;
using Microsoft.Extensions.DependencyInjection;

namespace DISimpleMultiInjection
{
  class Program
  {
    static void Main(string[] args)
    {
      //ServiceCollection, ServiceProvider 作成
      ServiceCollection services = new ServiceCollection();
      ConfigureServices(services);
      ServiceProvider serviceProvider = services.BuildServiceProvider();

      //インスタンス生成
      MyClass1 mc1 = (MyClass1)serviceProvider.GetRequiredService<MyClass1>();
      string result1 = mc1.GetData();
      Console.WriteLine(result1);

      MyClass2 mc2 = (MyClass2)serviceProvider.GetRequiredService<MyClass2>();
      string result2 = mc2.GetData();
      Console.WriteLine(result2);

    }

    private static void ConfigureServices(ServiceCollection services)
    {
      services.AddSingleton<MyClass1>();
      services.AddSingleton<MyClass2>();
      services.AddScoped<IMyService1, MyService1>();
      services.AddScoped<IMyService2, MyService2>();
      services.AddScoped<IMyService3, MyService3>();
    }
  }
}

解説

MyService1.cs

先のプログラムの MyService.cs とほぼ同じコードです。抽象クラス、IMyServiceを定義し、GetName メソッドの抽象メソッドを定義しています。
クラスの実装部分ではGetNameメソッドの引数の値に応じて文字列を返す処理を実装しています」。

MyService2.cs

MyService1.cs と同様の実装です。抽象クラスで GetCount メソッドを定義し実装クラスでメソッドの実装をしています。引数の数値に対応して数値を返すコードを実装しています。

MyService3.cs

MyService1.cs と同様の実装です。抽象クラスで GetPrice メソッドを定義し実装クラスでメソッドの実装をしています。引数の数値に対応して数値を返すコードを実装しています。

MyClass1.cs

オブジェクトを注入されるクラスです。
今回はコンストラクタは下記のコードになっています。IMyService1 IMyService2 IMyService3 3つをクラスに注入しています。
    private IMyService1 _srv1;
    private IMyService2 _srv2;
    private IMyService3 _srv3;

    public MyClass1(IMyService2 srv2, IMyService1 srv1, IMyService3 srv3)
    {
      _srv1 = srv1;
      _srv2 = srv2;
      _srv3 = srv3;
    }

GetDataメソッドでは3つのサービスクラスのメソッドを呼び出してメソッドの戻り値の文字列を作成しています。
    public string GetData()
    {
      return "出力:" + _srv1.GetName(2) + " / " + _srv2.GetCount(2).ToString() + " / " + _srv3.GetPrice(2).ToString();
    }

MyClass2.cs

MyClass2もオブジェクトを注入されるクラスですが、コンストラクタの引数は1つで、IMyService3のみを注入します。
    private IMyService3 _srv3;

    public MyClass2(IMyService3 srv3)
    {
      _srv3 = srv3;
    }

GetDataメソッドでは MyService3 クラスのメソッドを呼び出して戻り値の文字列を作成しています。
    public string GetData()
    {
      return "出力:" + _srv3.GetPrice(0).ToString();
    }

Program.cs

Program.csも先のコードと同じですが、ConfigureServices メソッドでサービスクラスとして3つのクラスを追加しています。
    private static void ConfigureServices(ServiceCollection services)
    {
      services.AddSingleton<MyClass1>();
      services.AddSingleton<MyClass2>();
      services.AddScoped<IMyService1, MyService1>();
      services.AddScoped<IMyService2, MyService2>();
      services.AddScoped<IMyService3, MyService3>();
    }

実行結果

プロジェクトを実行します。
1行目に「出力:しろくまアイス / 2 / 380」の文字列が表示されます。この文字列が MyClass1 のGetData() メソッドの戻り値の文字列です。 MyClassに注入された、MyService1, MyService2, MyService3 のメソッドが呼び出さていることがわかります。
2行目は、「出力:280」の文字列が表示されます。こちらはMyClass2 のGetData()メソッドの戻散の文字列です。MyClass2 には MyService3 が注入され、 MyClass3のメソッド呼び出しが動作していることがわかります。

DIの簡単な理解

細かい挙動を省いて、おおよそのDIの動作の理解としては、DIで管理されているクラスは、コンストラクタのパラメーターを自由に変更できます。
今回の例では MyClass1 のコンストラクタは、下記の、IMyservice1, IMyService2, IMyService3 の3つのパラメータを受け取る記述でした。 順番が、IMyService2, IMyService1, IMyService3 となっていますが、順番は自由に変更でき、どのような順番でも動作します。
public MyClass1(IMyService2 srv2, IMyService1 srv1, IMyService3 srv3)
{
}

以下の記述でも同じ動作になります。
public MyClass1(IMyService3 srv3, IMyService2 srv2, IMyService1 srv1)
{
}

また、サービスが3つ必要でない場合は、必要なサービスのみをパラメータに記述しても動作します。
以下の記述でもコンパイルは通ります。
public MyClass1(IMyService1 srv1, IMyService2 srv2)
{
}

必要なオブジェクトがあれば、コンストラクタにパラメーターを記述するだけで、オブジェクトのインスタンス生成時に 必要なオブジェクトが自動的に渡される挙動になります。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2021-09-24
iPentec all rights reserverd.