ASP.NET Core Dependency Injection Najlepsze praktyki, porady i wskazówki

W tym artykule podzielę się swoimi doświadczeniami i sugestiami dotyczącymi korzystania z wstrzykiwania zależności w aplikacjach ASP.NET Core. Motywy leżące u podstaw tych zasad są;

  • Skuteczne projektowanie usług i ich zależności.
  • Zapobieganie problemom wielowątkowości.
  • Zapobieganie wyciekom pamięci.
  • Zapobieganie potencjalnym błędom.

W tym artykule założono, że znasz już Dependency Injection i ASP.NET Core na poziomie podstawowym. Jeśli nie, przeczytaj najpierw dokumentację wstrzykiwania zależności platformy ASP.NET Core.

Podstawy

Wtrysk Konstruktora

Wstrzykiwanie konstruktora służy do deklarowania i uzyskiwania zależności usługi od konstrukcji usługi. Przykład:

usługa publiczna ProductService
{
    prywatny tylko do odczytu IProductRepository _productRepository;
    public ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Usuń (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService wstrzykuje IProductRepository jako zależność do swojego konstruktora, a następnie używa go w metodzie Delete.

Dobre praktyki:

  • Zdefiniuj jawnie wymagane zależności w konstruktorze usługi. Dlatego usługa nie może zostać zbudowana bez jej zależności.
  • Przypisz wstrzykniętą zależność do pola / właściwości tylko do odczytu (aby zapobiec przypadkowemu przypisaniu do niej innej wartości w metodzie).

Zastrzyk nieruchomości

Standardowy kontener wstrzykiwania zależności programu ASP.NET Core nie obsługuje wstrzykiwania właściwości. Możesz jednak użyć innego pojemnika obsługującego zastrzyk nieruchomości. Przykład:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
przestrzeń nazw MyApp
{
    usługa publiczna ProductService
    {
        public ILogger  Logger {get; zestaw; }
        prywatny tylko do odczytu IProductRepository _productRepository;
        public ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Usuń (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ „Usunięto produkt o id = {id}”);
        }
    }
}

ProductService deklaruje właściwość Logger z ustawiaczem publicznym. Pojemnik do wstrzykiwania zależności może ustawić rejestrator, jeśli jest dostępny (wcześniej zarejestrowany w pojemniku DI).

Dobre praktyki:

  • Używaj wstrzykiwania właściwości tylko dla opcjonalnych zależności. Oznacza to, że Twoja usługa może poprawnie działać bez tych zależności.
  • Jeśli to możliwe, użyj wzorca zerowego obiektu (jak w tym przykładzie). W przeciwnym razie zawsze sprawdzaj, czy nie ma wartości null podczas korzystania z zależności.

Lokalizator usług

Wzorzec lokalizatora usług to kolejny sposób uzyskiwania zależności. Przykład:

usługa publiczna ProductService
{
    prywatny tylko do odczytu IProductRepository _productRepository;
    prywatny tylko do odczytu ILogger  _logger;
    public ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Usuń (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Usunięto produkt o id = {id}");
    }
}

ProductService wstrzykuje IServiceProvider i rozwiązuje zależności za jego pomocą. GetRequiredService zgłasza wyjątek, jeśli żądana zależność nie była wcześniej rejestrowana. Z drugiej strony GetService po prostu zwraca w tym przypadku wartość null.

Po rozwiązaniu usług wewnątrz konstruktora są one zwalniane po zwolnieniu usługi. Tak więc nie obchodzi Cię uwalnianie / usuwanie usług rozwiązanych wewnątrz konstruktora (podobnie jak konstruktor i zastrzyk nieruchomości).

Dobre praktyki:

  • Nie należy używać wzorca lokalizatora usług, jeśli jest to możliwe (jeśli typ usługi jest znany w czasie programowania). Ponieważ sprawia, że ​​zależności są niejawne. Oznacza to, że nie można łatwo zobaczyć zależności podczas tworzenia wystąpienia usługi. Jest to szczególnie ważne w przypadku testów jednostkowych, w których można wyśmiewać niektóre zależności usługi.
  • Jeśli to możliwe, rozwiąż zależności w konstruktorze usług. Rozwiązanie w metodzie serwisowej sprawia, że ​​aplikacja jest bardziej skomplikowana i podatna na błędy. Omówię problemy i rozwiązania w następnych rozdziałach.

Czas życia usługi

Istnieją trzy okresy istnienia usługi w ASP.NET Core Dependency Injection:

  1. Usługi przejściowe są tworzone za każdym razem, gdy są wstrzykiwane lub żądane.
  2. Usługi o określonym zakresie są tworzone według zakresu. W aplikacji internetowej każde żądanie WWW tworzy nowy oddzielny zakres usług. Oznacza to, że usługi o zasięgu są zazwyczaj tworzone na żądanie internetowe.
  3. Usługi singletonowe są tworzone dla kontenerów DI. Ogólnie oznacza to, że są one tworzone tylko raz na aplikację, a następnie wykorzystywane przez cały okres użytkowania aplikacji.

Kontener DI śledzi wszystkie rozwiązane usługi. Usługi są zwalniane i usuwane, gdy kończy się ich okres użytkowania:

  • Jeśli usługa ma zależności, są one również automatycznie zwalniane i usuwane.
  • Jeśli usługa implementuje interfejs IDisposable, metoda Dispose jest automatycznie wywoływana w wersji serwisowej.

Dobre praktyki:

  • Zarejestruj swoje usługi jako przejściowe tam, gdzie to możliwe. Ponieważ łatwo jest zaprojektować usługi przejściowe. Na ogół nie przejmujesz się wielowątkowością i wyciekami pamięci i wiesz, że usługa ma krótki okres użytkowania.
  • Korzystaj ostrożnie z okresu użytkowania usługi o określonym zakresie, ponieważ może to być trudne, jeśli tworzysz zakresy usług podrzędnych lub korzystasz z tych usług z aplikacji innej niż internetowa.
  • Ostrożnie korzystaj z singletonu przez całe życie, ponieważ musisz poradzić sobie z wielowątkowością i potencjalnymi problemami z wyciekiem pamięci.
  • Nie należy polegać na usłudze przejściowej lub o zasięgu z usługi singleton. Ponieważ usługa przejściowa staje się instancją singletonową, gdy wstrzykuje ją usługa singletonowa, co może powodować problemy, jeśli usługa przejściowa nie jest zaprojektowana do obsługi takiego scenariusza. Domyślny kontener DI ASP.NET Core już generuje wyjątki w takich przypadkach.

Rozwiązywanie usług w ciele metody

W niektórych przypadkach może być konieczne rozwiązanie innej usługi w ramach metody usługi. W takich przypadkach należy zwolnić usługę po użyciu. Najlepszym sposobem zapewnienia tego jest utworzenie zakresu usługi. Przykład:

PriceCalculator klasy publicznej
{
    prywatny tylko do odczytu IServiceProvider _serviceProvider;
    public PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Calculate (iloczyn produktu, liczba sztuk,
      Wpisz taxStrategyServiceType)
    {
        using (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var cena = produkt. cena * liczba;
            cena zwrotu + taxStrategy.CalculateTax (cena);
        }
    }
}

PriceCalculator wstrzykuje IServiceProvider do swojego konstruktora i przypisuje go do pola. PriceCalculator używa go następnie w metodzie Calculate, aby utworzyć zakres usługi podrzędnej. Używa scope.ServiceProvider do rozwiązywania usług, zamiast wstrzykiwanej instancji _serviceProvider. W ten sposób wszystkie usługi rozwiązane z zakresu są automatycznie zwalniane / usuwane na końcu instrukcji using.

Dobre praktyki:

  • Jeśli rozwiązujesz usługę w treści metody, zawsze twórz zakres usługi podrzędnej, aby upewnić się, że rozstrzygnięte usługi zostały poprawnie zwolnione.
  • Jeśli metoda pobiera IServiceProvider jako argument, możesz bezpośrednio rozwiązywać z niej usługi, nie dbając o zwolnienie / usunięcie. Za utworzenie / zarządzanie zakresem usługi odpowiada kod wywołujący twoją metodę. Przestrzeganie tej zasady czyni kod czystszym.
  • Nie przechowuj odniesienia do rozwiązanej usługi! W przeciwnym razie może to spowodować wycieki pamięci i dostęp do zbywanej usługi będzie możliwy przy późniejszym użyciu odwołania do obiektu (chyba że rozstrzygnięta usługa jest singleton).

Usługi Singleton

Usługi Singleton są zasadniczo zaprojektowane tak, aby utrzymać stan aplikacji. Pamięć podręczna jest dobrym przykładem stanów aplikacji. Przykład:

FileService klasy publicznej
{
    prywatny tylko do odczytu ConcurrentDictionary  _cache;
    public FileService ()
    {
        _cache = new ConcurrentDictionary  ();
    }
    public byte [] GetFileContent (string filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            zwróć File.ReadAllBytes (filePath);
        });
    }
}

FileService po prostu buforuje zawartość pliku, aby zmniejszyć odczyt dysku. Ta usługa powinna być zarejestrowana jako singleton. W przeciwnym razie buforowanie nie będzie działać zgodnie z oczekiwaniami.

Dobre praktyki:

  • Jeśli usługa utrzymuje stan, powinna uzyskać dostęp do tego stanu w sposób bezpieczny dla wątków. Ponieważ wszystkie żądania jednocześnie korzystają z tej samej instancji usługi. Użyłem ConcurrentDictionary zamiast Dictionary, aby zapewnić bezpieczeństwo wątków.
  • Nie należy korzystać z usług o zasięgu lub przejściowych z usług singleton. Ponieważ usługi przejściowe mogą nie być zaprojektowane jako bezpieczne dla wątków. Jeśli musisz ich użyć, zadbaj o wielowątkowość podczas korzystania z tych usług (na przykład użyj blokady).
  • Wycieki pamięci są zazwyczaj spowodowane przez usługi singleton. Nie są one zwalniane / usuwane do końca aplikacji. Jeśli więc utworzą instancję klas (lub wstrzykną), ale ich nie zwolnią / nie wyrzucą, pozostaną również w pamięci do końca aplikacji. Upewnij się, że je wypuściłeś / pozbyłeś się we właściwym czasie. Zobacz część Usługi rozwiązywania problemów w sekcji Body Method powyżej.
  • Jeśli buforujesz dane (zawartość pliku w tym przykładzie), powinieneś utworzyć mechanizm aktualizujący / unieważniający buforowane dane, gdy zmienia się oryginalne źródło danych (gdy buforowany plik zmienia się na dysku w tym przykładzie).

Zakres usług

Okres istnienia na początku wydaje się dobrym kandydatem do przechowywania danych na żądanie internetowe. Ponieważ program ASP.NET Core tworzy zakres usług dla każdego żądania sieciowego. Jeśli więc zarejestrujesz usługę o zasięgu, możesz ją udostępnić podczas żądania sieciowego. Przykład:

klasa publiczna RequestItemsService
{
    prywatny słownik tylko do odczytu  _items;
    public RequestItemsService ()
    {
        _items = new Dictionary  ();
    }
    public void Set (nazwa ciągu, wartość obiektu)
    {
        _items [nazwa] = wartość;
    }
    obiekt publiczny Get (nazwa ciągu)
    {
        zwróć _items [nazwa];
    }
}

Jeśli zarejestrujesz RequestItemsService jako zakres i wstrzykujesz go do dwóch różnych usług, możesz uzyskać element dodany z innej usługi, ponieważ będą one współużytkować tę samą instancję RequestItemsService. Tego oczekujemy od usług o zasięgu.

Ale .. fakt nie zawsze może być taki. Jeśli utworzysz zakres usługi podrzędnej i rozwiążesz RequestItemsService z zakresu podrzędnego, otrzymasz nową instancję RequestItemsService i nie będzie działać zgodnie z oczekiwaniami. Zatem zakres usług nie zawsze oznacza wystąpienie na żądanie sieciowe.

Możesz pomyśleć, że nie popełniasz tak oczywistego błędu (rozwiązanie zakresu w zasięgu dziecka). Ale to nie jest błąd (bardzo regularne stosowanie) i sprawa może nie być taka prosta. Jeśli między twoimi usługami jest duży wykres zależności, nie możesz wiedzieć, czy ktoś utworzył zakres potomny i rozwiązał usługę, która wstrzykuje inną usługę… która ostatecznie wstrzykuje usługę o zasięgu.

Dobra praktyka:

  • Usługa o zasięgu może być uważana za optymalizację, w której jest wprowadzana przez zbyt wiele usług w żądaniu internetowym. Dlatego wszystkie te usługi wykorzystają pojedyncze wystąpienie usługi podczas tego samego żądania internetowego.
  • Usługi o zasięgu nie muszą być zaprojektowane jako bezpieczne dla wątków. Ponieważ powinny być normalnie używane przez pojedynczy wniosek / wątek. Ale… w takim przypadku nie należy udostępniać zakresów usług między różnymi wątkami!
  • Zachowaj ostrożność, jeśli projektujesz zakresową usługę do udostępniania danych między innymi usługami w żądaniu internetowym (wyjaśnione powyżej). Możesz przechowywać dane żądań internetowych w HttpContext (wstrzykuj IHttpContextAccessor, aby uzyskać do nich dostęp), co jest bezpieczniejszym sposobem na to. Czas życia HttpContext nie jest ograniczony. W rzeczywistości nie jest w ogóle zarejestrowany w DI (dlatego go nie wstrzykujesz, a zamiast tego wstrzykuje IHttpContextAccessor). Implementacja HttpContextAccessor używa AsyncLocal do udostępniania tego samego HttpContext podczas żądania WWW.

Wniosek

Wstrzykiwanie zależności początkowo wydaje się proste w użyciu, ale mogą wystąpić problemy z wielowątkowością i wyciekiem pamięci, jeśli nie przestrzegasz pewnych ścisłych zasad. Podczas tworzenia frameworka Boilerplate ASP.NET podzieliłem się kilkoma dobrymi zasadami opartymi na własnych doświadczeniach.