依存性注入について理解する

完了

ASP.NET Core アプリでは、複数のコンポーネント間で同じサービスにアクセスする必要がある場合がよくあります。 たとえば、いくつかのコンポーネントは、データベースからデータをフェッチするサービスにアクセスする必要がある場合があります。 ASP.NET Core では、組み込みの依存性注入 (DI) コンテナーを使用して、アプリが使用するサービスを管理します。

依存性注入と制御の反転 (IoC)

依存性注入パターンは、制御の反転 (IoC) の一種です。 依存性注入パターンでは、コンポーネントは依存関係を自ら作成するのではなく、外部ソースから受け取ります。 このパターンによりコードが依存関係から切り離され、コードのテストや保守が容易になります。

次の Program.cs ファイルについて考えてみましょう。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using MyApp.Services;

var builder = WebApplication.CreateBuilder(args);
    
builder.Services.AddSingleton<PersonService>();
var app = builder.Build();

app.MapGet("/", 
    (PersonService personService) => 
    {
        return $"Hello, {personService.GetPersonName()}!";
    }
);
    
app.Run();

そして、次の PersonService.cs ファイル:

namespace MyApp.Services;

public class PersonService
{
    public string GetPersonName()
    {
        return "John Doe";
    }
}

コードを理解するには、強調表示された app.MapGet コードから始めます。 このコードは、ルート URL (/) に対する HTTP GET 要求を、あいさつメッセージを返すデリゲートにマッピングします。 デリゲートのシグネチャにより、PersonService という名前の personService パラメーターが定義されます。 アプリが実行され、クライアントがルート URL を要求すると、デリゲート内のコードは、応答メッセージに含めるテキストを取得する サービスPersonServiceします。

デリゲートはどこで PersonService サービスを取得しますか? サービス コンテナーによって暗黙的に提供されます。 強調表示されている builder.Services.AddSingleton<PersonService>() 行は、アプリの起動時に PersonService クラスの新しいインスタンスを作成し、そのインスタンスを必要とするコンポーネントに提供するようにサービス コンテナーに指示します。

PersonService サービスを必要とするコンポーネントは、デリゲートのシグネチャで PersonService 型のパラメーターを宣言できます。 コンポーネントが作成されると、サービス コンテナーは自動的に PersonService クラスのインスタンスを提供します。 デリゲートは PersonService インスタンス自体を作成するのではなく、サービス コンテナーが提供するインスタンスを使用するだけです。

インターフェイスと依存性注入

特定のサービス実装への依存を避けるために、代わりに特定のインターフェイス用にサービスを構成し、そのインターフェイスのみに依存することができます。 この方法ではサービス実装を柔軟に入れ替えることができるため、コードがよりテストしやすくなり、保守もしやすくなります。

PersonService クラスのインターフェイスについて考えてみましょう。

public interface IPersonService
{
    string GetPersonName();
}

このインターフェイスは GetPersonName を返す単一のメソッド string を定義します。 この PersonService クラスによって、IPersonService インターフェイスが実装されます。

internal sealed class PersonService : IPersonService
{
    public string GetPersonName()
    {
        return "John Doe";
    }
}

PersonService クラスを直接登録する代わりに、IPersonService インターフェースの実装として登録することができます。

var builder = WebApplication.CreateBuilder(args);
    
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();

app.MapGet("/", 
    (IPersonService personService) => 
    {
        return $"Hello, {personService.GetPersonName()}!";
    }
);
    
app.Run();

この例 Program.cs 前の例とは 2 つの点で異なります。

  • PersonService インスタンスは、( クラスを直接登録するのではなく) IPersonService インターフェイスのPersonServiceとして登録されます。
  • デリゲートのシグネチャでは、IPersonService パラメータではなく PersonService パラメータが必要になりました。

アプリが実行され、クライアントがルート URL を要求すると、PersonService クラスは IPersonService インターフェイスの実装として登録されているため、サービス コンテナーはそのクラスのインスタンスを提供します。

ヒント

IPersonService をコントラクトと考えてください。 実装に 必要 なメソッドとプロパティを定義します。 デリゲートは IPersonService のインスタンスを必要とします。 基になる実装についてはまったく重要ではなく、インスタンスにコントラクトで定義されたメソッドとプロパティが含まれているかどうかが重要です。

依存性注入を使用したテスト

インターフェイスを使用すると、コンポーネントを分離して簡単にテストできます。 テスト目的で IPersonService インターフェイスのモック実装を作成できます。 テストにモック実装を登録すると、サービス コンテナーはテスト対象のコンポーネントにモック実装を提供します。

たとえば、ハードコーディングされた文字列を返す代わりに、GetPersonName クラスの PersonService メソッドがデータベースから名前をフェッチするとします。 IPersonService インターフェイスに依存するコンポーネントをテストするには、ハードコーディングされた文字列を返す IPersonService インターフェイスのモック実装を作成します。 テスト対象のコンポーネントは、実際の実装とモック実装の違いを把握していません。

また、アプリがあいさつメッセージを返す API エンドポイントをマッピングするとします。 エンドポイントは、あいさつするユーザーの名前を取得する IPersonService インターフェイスに依存します。 IPersonService サービスを登録し、API エンドポイントをマッピングするコードは次のようになります。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IPersonService, PersonService>();

var app = builder.Build();

app.MapGet("/", (IPersonService personService) =>
{
    return $"Hello, {personService.GetPersonName()}!";
});

app.Run();

これは IPersonService を使用した前の例と似ています。 デリゲートはサービス コンテナーが提供する IPersonService パラメーターが必要です。 前述したように、このインターフェイスを実装する PersonService が、データベースからあいさつするユーザーの名前をフェッチするとします。

次に、同じ API エンドポイントをテストする次の XUnit テストを考えてみましょう。

ヒント

XUnit や Moq に詳しくなくても心配しないでください。 単体テストの記述は、このモジュールの範囲外です。 この例は、依存性注入がテストでどのように使用されるかを示しています。

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using MyWebApp;
using System.Net;

public class GreetingApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public GreetingApiTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task GetGreeting_ReturnsExpectedGreeting()
    {
        //Arrange
        var mockPersonService = new Mock<IPersonService>();
        mockPersonService.Setup(service => service.GetPersonName()).Returns("Jane Doe");

        var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton(mockPersonService.Object);
            });
        }).CreateClient();

        // Act
        var response = await client.GetAsync("/");
        var responseString = await response.Content.ReadAsStringAsync();

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("Hello, Jane Doe!", responseString);
    }
}

前のテスト:

  • ハードコーディングされた文字列を返す IPersonService インターフェイスのモック実装を作成します。
  • モック実装をサービス コンテナーに登録します。
  • API エンドポイントに要求を行う HTTP クライアントを作成します。
  • API エンドポイントからの応答が期待どおりであることを確認します。

このテストでは、PersonService クラスがあいさつするユーザーの名前をどのように取得するかは重要ではありません。 重要なのは、あいさつメッセージに名前が含まれていることだけです。 このテストでは IPersonService インターフェイスのモック実装を使用して、テスト対象のコンポーネントをサービスの実際の実装から分離します。