ASP.NET Core Dependency Injectionのベストプラクティス、ヒントとコツ

この記事では、ASP.NET Coreアプリケーションでの依存性注入の使用に関する経験と提案を共有します。これらの原則の背後にある動機は次のとおりです。

  • サービスとその依存関係を効果的に設計します。
  • マルチスレッドの問題を防ぐ。
  • メモリーリークの防止。
  • 潜在的なバグの防止。

この記事は、基本レベルのDependency InjectionとASP.NET Coreに既に精通していることを前提としています。そうでない場合は、最初にASP.NET Core Dependency Injectionのドキュメントをお読みください。

基礎

コンストラクター注入

コンストラクター注入は、サービスの構築におけるサービスの依存関係を宣言および取得するために使用されます。例:

パブリッククラスProductService
{
    プライベート読み取り専用IProductRepository _productRepository;
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Delete(int id)
    {
        _productRepository.Delete(id);
    }
}

ProductServiceは、IProductRepositoryをコンストラクターの依存関係として注入し、Deleteメソッド内で使用します。

良い習慣:

  • サービスコンストラクターで必要な依存関係を明示的に定義します。したがって、サービスは依存関係なしでは構築できません。
  • 注入された依存関係を読み取り専用のフィールド/プロパティに割り当てます(メソッド内で誤って別の値を割り当てることを防ぐため)。

プロパティインジェクション

ASP.NET Coreの標準の依存関係注入コンテナーは、プロパティ注入をサポートしていません。ただし、プロパティインジェクションをサポートする別のコンテナを使用できます。例:

Microsoft.Extensions.Loggingを使用します。
Microsoft.Extensions.Logging.Abstractionsを使用します。
名前空間MyApp
{
    パブリッククラスProductService
    {
        public ILogger  Logger {get;セットする; }
        プライベート読み取り専用IProductRepository _productRepository;
        public ProductService(IProductRepository productRepository)
        {
            _productRepository = productRepository;
            ロガー= NullLogger  .Instance;
        }
        public void Delete(int id)
        {
            _productRepository.Delete(id);
            Logger.LogInformation(
                $ "id = {id}"の製品を削除しました ");
        }
    }
}

ProductServiceは、パブリックセッターでLoggerプロパティを宣言しています。依存性注入コンテナーは、ロガーが使用可能な場合にロガーを設定できます(以前にDIコンテナーに登録されていました)。

良い習慣:

  • オプションの依存関係にのみプロパティインジェクションを使用します。つまり、これらの依存関係が提供されなくても、サービスは適切に機能します。
  • 可能であれば、(この例のように)Null Object Patternを使用します。それ以外の場合は、依存関係の使用中は常にnullを確認してください。

サービスロケーター

サービスロケーターパターンは、依存関係を取得する別の方法です。例:

パブリッククラスProductService
{
    プライベート読み取り専用IProductRepository _productRepository;
    プライベート読み取り専用ILogger  _logger;
    パブリックProductService(IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService ();
        _logger = serviceProvider
          .GetService >()??
            NullLogger  .Instance;
    }
    public void Delete(int id)
    {
        _productRepository.Delete(id);
        _logger.LogInformation($ "id = {id}の製品を削除しました");
    }
}

ProductServiceはIServiceProviderを注入し、それを使用して依存関係を解決しています。要求された依存関係が以前に登録されていなかった場合、GetRequiredServiceは例外をスローします。一方、その場合、GetServiceは単にnullを返します。

コンストラクター内でサービスを解決すると、サービスがリリースされたときにサービスがリリースされます。そのため、コンストラクター内で解決されたサービスの解放/破棄については気にしません(コンストラクターやプロパティインジェクションと同様)。

良い習慣:

  • 可能な限りサービスロケーターパターンを使用しないでください(開発時にサービスタイプがわかっている場合)。依存関係を暗黙的にするためです。つまり、サービスのインスタンスの作成中に依存関係を簡単に確認することはできません。これは、サービスの依存関係をモックしたい場合がある単体テストでは特に重要です。
  • 可能であれば、サービスコンストラクターの依存関係を解決します。サービスメソッドで解決すると、アプリケーションがより複雑になり、エラーが発生しやすくなります。次のセクションで問題と解決策を説明します。

耐用年数

ASP.NET Core Dependency Injectionには3つのサービスライフタイムがあります。

  1. 一時的なサービスは、挿入または要求されるたびに作成されます。
  2. スコープサービスはスコープごとに作成されます。 Webアプリケーションでは、すべてのWeb要求が新しい分離されたサービススコープを作成します。つまり、スコープ付きサービスは通常、Webリクエストごとに作成されます。
  3. シングルトンサービスは、DIコンテナごとに作成されます。これは通常、アプリケーションごとに1回だけ作成され、アプリケーションのライフタイム全体で使用されることを意味します。

DIコンテナは、解決されたすべてのサービスを追跡します。サービスは、その有効期間が終了するとリリースおよび破棄されます。

  • サービスに依存関係がある場合、それらも自動的に解放および破棄されます。
  • サービスがIDisposableインターフェイスを実装している場合、サービスのリリース時にDisposeメソッドが自動的に呼び出されます。

良い習慣:

  • 可能な限り一時的にサービスを登録します。一時的なサービスを設計するのは簡単だからです。通常、マルチスレッドやメモリリークは気にせず、サービスの寿命が短いことを知っています。
  • スコープ付きサービスライフタイムは慎重に使用してください。子サービススコープを作成したり、これらのサービスを非Webアプリケーションから使用したりする場合には注意が必要になる可能性があるためです。
  • マルチスレッドと潜在的なメモリリークの問題に対処する必要があるため、シングルトンライフタイムを慎重に使用してください。
  • シングルトンサービスの一時的なサービスまたはスコープサービスに依存しないでください。一時サービスは、シングルトンサービスがそれを挿入するとシングルトンインスタンスになるため、一時サービスがそのようなシナリオをサポートするように設計されていない場合、問題が発生する可能性があるためです。このような場合、ASP.NET CoreのデフォルトのDIコンテナはすでに例外をスローしています。

メソッド本体のサービスを解決する

場合によっては、サービスのメソッドで別のサービスを解決する必要があります。そのような場合は、使用後にサービスをリリースするようにしてください。それを確実にする最良の方法は、サービススコープを作成することです。例:

パブリッククラスPriceCalculator
{
    private readonly IServiceProvider _serviceProvider;
    public PriceCalculator(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Calculate(製品積、intカウント、
      タイプtaxStrategyServiceType)
    {
        使用(var scope = _serviceProvider.CreateScope())
        {
            var taxStrategy =(ITaxStrategy)scope.ServiceProvider
              .GetRequiredService(taxStrategyServiceType);
            var price = product.Price * count;
            返品価格+ taxStrategy.CalculateTax(price);
        }
    }
}

PriceCalculatorは、コンストラクターにIServiceProviderを挿入し、フィールドに割り当てます。 PriceCalculatorは、Calculateメソッド内でそれを使用して、子サービススコープを作成します。挿入された_serviceProviderインスタンスの代わりに、scope.ServiceProviderを使用してサービスを解決します。したがって、スコープから解決されたすべてのサービスは、usingステートメントの最後に自動的に解放/破棄されます。

良い習慣:

  • メソッド本体でサービスを解決する場合は、必ず子サービススコープを作成して、解決されたサービスが適切にリリースされるようにします。
  • メソッドが引数としてIServiceProviderを取得する場合、解放/破棄を気にせずに、そこからサービスを直接解決できます。サービススコープの作成/管理は、メソッドを呼び出すコードの責任です。この原則に従うと、コードがよりきれいになります。
  • 解決されたサービスへの参照を保持しないでください!そうしないと、メモリリークが発生する可能性があり、オブジェクト参照を後で使用するときに破棄されたサービスにアクセスします(解決されたサービスがシングルトンでない場合)。

シングルトンサービス

シングルトンサービスは通常、アプリケーションの状態を保持するように設計されています。キャッシュは、アプリケーションの状態の良い例です。例:

パブリッククラスFileService
{
    private readonly ConcurrentDictionary  _cache;
    public FileService()
    {
        _cache = new ConcurrentDictionary ();
    }
    public byte [] GetFileContent(string filePath)
    {
        return _cache.GetOrAdd(filePath、_ =>
        {
            return File.ReadAllBytes(filePath);
        });
    }
}

FileServiceは単にファイルの内容をキャッシュして、ディスクの読み取りを減らします。このサービスはシングルトンとして登録する必要があります。そうしないと、キャッシュが期待どおりに機能しません。

良い習慣:

  • サービスが状態を保持している場合、スレッドセーフな方法でその状態にアクセスする必要があります。すべてのリクエストがサービスの同じインスタンスを同時に使用するためです。スレッドの安全性を確保するために、辞書の代わりにConcurrentDictionaryを使用しました。
  • シングルトンサービスのスコープサービスまたは一時サービスを使用しないでください。なぜなら、一時的なサービスはスレッドセーフになるように設計されていないかもしれないからです。これらのサービスを使用する必要がある場合は、これらのサービスの使用中にマルチスレッドを処理します(たとえば、ロックを使用します)。
  • メモリリークは通常、シングルトンサービスが原因です。それらは、アプリケーションの終了まで解放/破棄されません。したがって、クラスをインスタンス化(またはインジェクト)しても、それらを解放/破棄しない場合は、アプリケーションの終了までメモリに残ります。適切なタイミングでリリース/廃棄してください。上記のメソッド本体セクションのサービスの解決を参照してください。
  • データ(この例ではファイルの内容)をキャッシュする場合、元のデータソースが変更されたとき(この例ではディスク上のキャッシュファイルが変更されたとき)にキャッシュデータを更新/無効にするメカニズムを作成する必要があります。

対象サービス

スコープライフタイムは、最初にWebリクエストデータごとに保存するのに適していると思われます。 ASP.NET CoreはWeb要求ごとにサービススコープを作成するためです。そのため、サービスをスコープとして登録すると、Webリクエスト中にサービスを共有できます。例:

パブリッククラスRequestItemsService
{
    private readonly Dictionary  _items;
    パブリックRequestItemsService()
    {
        _items = new Dictionary ();
    }
    public void Set(文字列名、オブジェクト値)
    {
        _items [name] = value;
    }
    パブリックオブジェクトGet(文字列名)
    {
        return _items [name];
    }
}

RequestItemsServiceをスコープとして登録し、2つの異なるサービスに注入すると、同じRequestItemsServiceインスタンスを共有するため、別のサービスから追加されたアイテムを取得できます。それは、スコープサービスに期待することです。

しかし..事実は常にそのようなものではないかもしれません。子サービススコープを作成し、子スコープからRequestItemsServiceを解決すると、RequestItemsServiceの新しいインスタンスが取得され、期待どおりに機能しなくなります。そのため、スコープサービスは、Webリクエストごとのインスタンスを常に意味するわけではありません。

あなたはそのような明らかな間違いを犯さないと考えるかもしれません(子スコープ内のスコープを解決する)。しかし、これは間違いではなく(非常に規則的な使用法)、ケースはそれほど単純ではないかもしれません。サービス間に大きな依存関係グラフがある場合、誰かが子スコープを作成し、最終的にスコープ付きサービスを注入する別のサービスを注入するサービスを解決したかどうかはわかりません。

いい練習:

  • スコープ付きサービスは、Webリクエストに含まれるサービスが多すぎるために最適化されると考えることができます。したがって、これらのサービスはすべて、同じWebリクエスト中にサービスの単一のインスタンスを使用します。
  • スコープサービスは、スレッドセーフとして設計する必要はありません。なぜなら、それらは通常、単一のWebリクエスト/スレッドによって使用されるべきだからです。しかし…その場合、異なるスレッド間でサービススコープを共有しないでください!
  • Webリクエストの他のサービス間でデータを共有するようにスコープサービスを設計する場合は注意してください(上記で説明)。より安全な方法であるHttpContext内にWebリクエストごとのデータを保存できます(アクセスするにはIHttpContextAccessorを挿入します)。 HttpContextの有効期間はスコープされていません。実際には、DIにはまったく登録されていません(だから、注入せず、代わりにIHttpContextAccessorを注入します)。 HttpContextAccessor実装は、AsyncLocalを使用して、Webリクエスト中に同じHttpContextを共有します。

結論

依存性注入は、最初は簡単に使用できるように見えますが、厳密な原則に従わないと、マルチスレッドとメモリリークの問題が発生する可能性があります。 ASP.NET Boilerplateフレームワークの開発中に、私自身の経験に基づいていくつかの良い原則を共有しました。