次の方法で共有


.NET の依存関係の挿入

.NET では、 依存関係挿入 (DI) ソフトウェア設計パターンがサポートされています。これは、クラスとその依存関係 の間で制御の反転 (IoC) を実現するための手法です。 .NET での依存関係の挿入は、構成、ログ、オプション パターンと共に、フレームワークの組み込み部分です。

"依存関係" とは、他のオブジェクトが依存するオブジェクトのことです。 次の MessageWriter クラスには、他のクラスが依存する可能性がある Write メソッドがあります。

public class MessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}

クラスは、MessageWriter メソッドを使用するWrite クラスのインスタンスを作成できます。 次の例では、MessageWriter クラスは クラスのWorkerです。

public class Worker : BackgroundService
{
    private readonly MessageWriter _messageWriter = new();

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

この場合、 Worker クラスは MessageWriter クラスを作成し、直接依存します。 このようなハードコーディングされた依存関係は問題があり、次の理由から避ける必要があります。

  • MessageWriterを別の実装に置き換えるには、Worker クラスを変更する必要があります。
  • MessageWriterに依存関係がある場合は、Worker クラスもそれらを構成する必要があります。 複数のクラスが MessageWriter に依存している大規模なプロジェクトでは、構成コードがアプリ全体に分散するようになります。
  • このような実装では、単体テストを行うことが困難です。 アプリはモックまたはスタブの MessageWriter クラスを使用する必要がありますが、この方法では不可能です。

概念

依存関係の挿入は、次の方法でハードコーディングされた依存関係の問題に対処します。

  • 依存関係の実装を抽象化するための、インターフェイスまたは基底クラスの使用。

  • サービス コンテナーへの依存関係の登録。

    .NET には、組み込みのサービス コンテナー IServiceProvider が用意されています。 通常、サービスは、アプリの起動時に登録され、IServiceCollection に追加されます。 すべてのサービスが追加されたら、 BuildServiceProvider を使用してサービス コンテナーを作成します。

  • サービスが使用されているクラスのコンストラクターへのサービスの挿入。

    依存関係のインスタンスの作成、およびインスタンスが不要になったときの廃棄の役割を、フレームワークが担当します。

ヒント

依存関係の挿入の用語では、 サービス は通常、 IMessageWriter サービスなどの他のオブジェクトにサービスを提供するオブジェクトです。 サービスは Web サービスに関連していませんが、Web サービスを使用する場合があります。

たとえば、 IMessageWriter インターフェイスが Write メソッドを定義しているとします。 このインターフェイスは、前に示した具象型 ( MessageWriter) によって実装されます。 次のサンプル コードでは、 IMessageWriter サービスを具象型の MessageWriterに登録します。 AddSingleton メソッドは、シングルトンの有効期間でサービスを登録します。つまり、アプリがシャットダウンされるまで破棄されません。

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();

using IHost host = builder.Build();

host.Run();

// <SnippetMW>
public class MessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}
// </SnippetMW>

// <SnippetIMW>
public interface IMessageWriter
{
    void Write(string message);
}
// </SnippetIMW>

// <SnippetWorker>
public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

// </SnippetWorker>

前のコード例では、強調表示された行は次のとおりです。

  • ホスト アプリ ビルダー インスタンスを作成します。
  • Workerホストされたサービスとして登録し、IMessageWriter インターフェイスをシングルトン サービスとして登録し、MessageWriter クラスの対応する実装を使用してサービスを構成します。
  • ホストをビルドして実行します。

ホストには、依存関係挿入サービス プロバイダーが含まれています。 また、Worker のインスタンスを自動的に作成し、対応する IMessageWriter 実装を引数として提供するために必要なその他すべての関連サービスが含まれています。

DI パターンを使用すると、ワーカー サービスは具象型の MessageWriterを使用せず、実装する IMessageWriter インターフェイスのみを使用します。 この設計により、ワーカー サービスを変更することなく、ワーカー サービスが使用する実装を簡単に変更できます。 ワーカー サービスでは、MessageWriterされません。 DI コンテナーによってインスタンスが作成されます。

次に、MessageWriterを使用する型でを切り替えたいとします。 コンストラクターで要求することで、LoggingMessageWriterに依存するクラス ILogger<TCategoryName>を作成します。

public class LoggingMessageWriter(
    ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
    public void Write(string message) =>
        logger.LogInformation("Info: {Msg}", message);
}

MessageWriterからLoggingMessageWriterに切り替えるには、AddSingletonへの呼び出しを更新して、この新しいIMessageWriter実装を登録するだけです。

builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();

ヒント

コンテナーはILogger<TCategoryName>を利用してを解決します。これにより、すべての (ジェネリック) 構築型を登録する必要がなくなります。

コンストラクターの挿入の動作

サービスは、 IServiceProvider (組み込みのサービス コンテナー) または ActivatorUtilitiesを使用して解決できます。 ActivatorUtilities では、コンテナーに登録されていないオブジェクトが作成され、一部のフレームワーク機能で使用されます。

コンストラクターは、依存関係の挿入によって提供されない引数を受け取ることができますが、引数は既定値を割り当てる必要があります。

サービスをIServiceProviderまたはActivatorUtilitiesで解決する場合は、コンストラクターの挿入にパブリックコンストラクターが必要です。

ActivatorUtilitiesがサービスを解決する場合、コンストラクターの挿入では、適用可能なコンストラクターが 1 つだけ存在する必要があります。 コンストラクターのオーバーロードはサポートされていますが、依存関係の挿入によってすべての引数を設定できるオーバーロードは 1 つしか存在できません。

コンストラクターの選択規則

型で複数のコンストラクターが定義されている場合、サービス プロバイダーにはどのコンストラクターを使うかを決定するためのロジックがあります。 型が DI 解決可能であるパラメーターを一番多く持つコンストラクターが選ばれます。 次のサービス例を考えてみましょう。

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // ...
    }

    public ExampleService(ServiceA serviceA, ServiceB serviceB)
    {
        // ...
    }
}

前のコードでは、ログ記録が追加され、サービス プロバイダーから解決できるが、 ServiceA 型と ServiceB 型は解決できないと仮定します。 ILogger<ExampleService> パラメーターを持つコンストラクターは、ExampleService インスタンスを解決します。 より多くのパラメーターを定義するコンストラクターがある場合でも、 ServiceA 型と ServiceB 型は DI 解決できません。

コンストラクターを検出するときにあいまいさがある場合は、例外がスローされます。 次の C# のサービス例を考えてみます。

Warnung

このExampleServiceコードは、DIで解決可能な型パラメーターが曖昧な場合に例外をスローします。 これを行わないでください。これは、"あいまいな DI 解決可能な型" の意味を示すことを目的としています。

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // ...
    }

    public ExampleService(IOptions<ExampleOptions> options)
    {
        // ...
    }
}

前の例では、3 つのコンストラクターがあります。 最初のコンストラクターにはパラメーターがなく、サービス プロバイダーからのサービスを必要としません。 ログ記録とオプションの両方が DI コンテナーに追加されており、DI 解決可能なサービスであることを想定してください。 DI コンテナーは、 ExampleService 型の解決を試みると、2 つのコンストラクターがあいまいであるため、例外をスローします。

代わりに、両方の DI 解決可能な型を受け入れるコンストラクターを定義することで、あいまいさを回避します。

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(
        ILogger<ExampleService> logger,
        IOptions<ExampleOptions> options)
    {
        // ...
    }
}

スコープの検証

スコープ付きサービス は、サービスを作成したコンテナーによって破棄されます。 スコープ付きサービスがルート コンテナーに作成された場合、サービスの有効期間は実質的に シングルトン に昇格されます。これは、アプリのシャットダウン時にのみルート コンテナーによって破棄されるためです。 BuildServiceProvider が呼び出されると、サービス スコープの検証がこれらの状況をキャッチします。

アプリが開発環境で実行され、 CreateApplicationBuilder を呼び出してホストをビルドすると、既定のサービス プロバイダーは次のことを確認するためのチェックを実行します。

  • スコープ付きサービスが、ルート サービス プロバイダーによって解決されない。
  • スコープ付きサービスが、シングルトンに挿入されない。

スコープのシナリオ

IServiceScopeFactory は常にシングルトンとして登録されますが、IServiceProvider は包含クラスの有効期間に基づいて変化する可能性があります。 たとえば、スコープからサービスを解決し、それらのサービスのいずれかが IServiceProviderを受け取る場合、それはスコープ付きインスタンスです。

IHostedServiceなど、BackgroundServiceの実装内でスコープ サービスを実現するには、コンストラクターの挿入を介してサービスの依存関係を挿入しないでください。 代わりに、IServiceScopeFactory を挿入し、スコープを作成し、そしてそのスコープから依存関係を解決して適切なサービス有効期間を使用します。

namespace WorkerScope.Example;

public sealed class Worker(
    ILogger<Worker> logger,
    IServiceScopeFactory serviceScopeFactory)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using (IServiceScope scope = serviceScopeFactory.CreateScope())
            {
                try
                {
                    logger.LogInformation(
                        "Starting scoped work, provider hash: {hash}.",
                        scope.ServiceProvider.GetHashCode());

                    var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
                    var next = await store.GetNextAsync();
                    logger.LogInformation("{next}", next);

                    var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
                    await processor.ProcessAsync(next);
                    logger.LogInformation("Processing {name}.", next.Name);

                    var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
                    await relay.RelayAsync(next);
                    logger.LogInformation("Processed results have been relayed.");

                    var marked = await store.MarkAsync(next);
                    logger.LogInformation("Marked as processed: {next}", marked);
                }
                finally
                {
                    logger.LogInformation(
                        "Finished scoped work, provider hash: {hash}.{nl}",
                        scope.ServiceProvider.GetHashCode(), Environment.NewLine);
                }
            }
        }
    }
}

前のコードでは、アプリが実行されている間、バックグラウンド サービスは次のようになります。

  • IServiceScopeFactory に依存する。
  • 他のサービスを解決するための IServiceScope を作成します。
  • 利用に向けてスコープ付のサービスを解決する。
  • オブジェクトを処理してから、それらを中継し、最後に処理済みとしてマークする。

サンプル ソース コードから、スコープ付きサービスの有効期間が IHostedService の実装にどのような恩恵をもたらすかを確認できます。

キー付きサービス

サービスを登録し、キーに基づいて検索を実行できます。 言い換えると、複数のサービスを異なるキーに登録し、このキーを参照に使用できます。

たとえば、インターフェイス IMessageWriter の異なる実装 (MemoryMessageWriterQueueMessageWriter) がある場合を考えましょう。

これらのサービスは、次のようにパラメーターとしてキーをサポートするサービス登録メソッド (前述) のオーバーロードを使用して登録できます。

services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");

keystringに限定されません。 keyは、型がobjectを正しく実装している限り、任意のEqualsにすることができます。

IMessageWriter を使用するクラスのコンストラクターで、次のように FromKeyedServicesAttribute を追加して解決するサービスのキーを指定します。

public class ExampleService
{
    public ExampleService(
        [FromKeyedServices("queue")] IMessageWriter writer)
    {
        // Omitted for brevity...
    }
}

KeyedService.AnyKey プロパティ

KeyedService.AnyKey プロパティは、キー付きサービスを操作するための特別なキーを提供します。 任意のキーに一致するフォールバックとして KeyedService.AnyKey を使用してサービスを登録できます。 これは、明示的な登録がないキーの既定の実装を提供する場合に便利です。

var services = new ServiceCollection();

// Register a fallback cache for any key.
services.AddKeyedSingleton<ICache>(KeyedService.AnyKey, (sp, key) =>
{
    // Create a cache instance based on the key.
    return new DefaultCache(key?.ToString() ?? "unknown");
});

// Register a specific cache for the "premium" key.
services.AddKeyedSingleton<ICache>("premium", new PremiumCache());

var provider = services.BuildServiceProvider();

// Requesting with "premium" key returns PremiumCache.
var premiumCache = provider.GetKeyedService<ICache>("premium");
Console.WriteLine($"Premium key: {premiumCache}");

// Requesting with any other key uses the AnyKey fallback.
var basicCache = provider.GetKeyedService<ICache>("basic");
Console.WriteLine($"Basic key: {basicCache}");

var standardCache = provider.GetKeyedService<ICache>("standard");
Console.WriteLine($"Standard key: {standardCache}");

前の例の場合:

  • キーICacheを使用して"premium"を要求すると、PremiumCache インスタンスが返されます。
  • 他のキー (ICache"basic"など) で"standard"を要求すると、DefaultCache フォールバックを使用して新しいAnyKeyが作成されます。

Important

.NET 10以降、GetKeyedService()KeyedService.AnyKey と共に呼び出すと、InvalidOperationException がスローされます。これは、AnyKey がクエリキーではなく登録フォールバックとして意図されているためです。 詳細については、「 GetKeyedService() と GetKeyedServices() with AnyKey の問題を修正する」を参照してください。

こちらも参照ください