Udostępnij przez


Wskazówki dotyczące wstrzykiwania zależności

Ten artykuł zawiera ogólne wytyczne i najlepsze rozwiązania dotyczące implementowania wstrzykiwania zależności (DI) w aplikacjach platformy .NET.

Usługi projektowe do wstrzykiwania zależności

Podczas projektowania usług do wstrzykiwania zależności:

  • Unikaj stanowych, statycznych klas i składowych. Unikaj tworzenia stanu globalnego, projektując aplikacje, które korzystają z usług singletonów.
  • Unikaj, bezpośredniego tworzenia wystąpień klas zależnych w ramach usług. Bezpośrednie tworzenie instancji wiąże kod z konkretną implementacją.
  • Twórz usługi małe, dobrze zaprojektowane i łatwe do testowania.

Jeśli klasa ma wiele wstrzykiwanych zależności, może to być znak, że klasa ma zbyt wiele obowiązków i narusza zasadę pojedynczej odpowiedzialności (SRP). Spróbuj refaktoryzować klasę, przenosząc część swoich obowiązków do nowych klas.

Likwidacja usług

Kontener jest odpowiedzialny za czyszczenie typów, które tworzy, i wywołuje Dispose na wystąpieniach IDisposable. Usługi pobrane z kontenera nigdy nie powinny być likwidowane przez dewelopera. Jeśli typ lub fabryka jest zarejestrowany jako singleton, kontener automatycznie usuwa singleton.

W poniższym przykładzie usługi są tworzone przez kontener usługi i usuwane automatycznie:

namespace ConsoleDisposable.Example;

public sealed class TransientDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}

Poprzedni jednorazowy produkt jest przeznaczony na przelotny okres żywotności.

namespace ConsoleDisposable.Example;

public sealed class ScopedDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}

Poprzedni produkt jednorazowego użytku ma mieć zasięg czasowy.

namespace ConsoleDisposable.Example;

public sealed class SingletonDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}

Poprzedni przedmiot jednorazowego użytku jest przeznaczony do jednorazowego okresu istnienia.

using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();

using IHost host = builder.Build();

ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();

ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();

await host.RunAsync();

static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
    Console.WriteLine($"{scope}...");

    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    _ = provider.GetRequiredService<TransientDisposable>();
    _ = provider.GetRequiredService<ScopedDisposable>();
    _ = provider.GetRequiredService<SingletonDisposable>();
}

Po uruchomieniu konsoli debugowania są wyświetlane następujące przykładowe dane wyjściowe:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

Usługi nieutworowane przez kontener usługi

Spójrzmy na poniższy kod:

// Register example service in IServiceCollection.
builder.Services.AddSingleton(new ExampleService());

W poprzednim kodzie:

  • Wystąpienie ExampleService nie jest tworzone przez kontener usługi.
  • Platforma nie usuwa automatycznie usług.
  • Deweloper jest odpowiedzialny za likwidowanie usług.

Wskazówki dotyczące obsługi IDisposable dla instancji przejściowych i współdzielonych

Przejściowy, ograniczony okres istnienia

Scenariusz

Aplikacja wymaga IDisposable instancji z przejściowym czasem życia dla jednego z następujących scenariuszy:

  • Wystąpienie jest rozwiązane w zakresie głównym (kontener główny).
  • Instancja powinna zostać usunięta zanim zakończy się zakres.

Rozwiązanie

Użyj wzorca fabryki, aby utworzyć wystąpienie poza zakresem nadrzędnym. W takiej sytuacji aplikacja zazwyczaj ma metodę Create , która bezpośrednio wywołuje konstruktor typu końcowego. Jeśli ostateczny typ ma inne zależności, fabryka może:

Udostępniona instancja, ograniczony czas życia

Scenariusz

Aplikacja wymaga współużytkowanego IDisposable wystąpienia w wielu usługach, ale IDisposable wystąpienie powinno mieć ograniczony okres istnienia.

Rozwiązanie

Zarejestruj wystąpienie z okresem istnienia o określonym zakresie. Użyj IServiceScopeFactory.CreateScope do utworzenia nowego IServiceScope. Użyj zakresu IServiceProvider, aby uzyskać wymagane usługi. Usuń zakres, gdy nie jest już potrzebny.

Ogólne IDisposable wytyczne

  • Nie rejestruj IDisposable wystąpień z tymczasową żywotnością. Zamiast tego użyj wzorca fabryki, aby rozwiązaną usługę można było ręcznie zlikwidować, gdy nie jest już używana.
  • Nie usuwaj IDisposable wystąpień z okresem przejściowym lub okresem istnienia w zakresie głównym. Jedynym wyjątkiem jest to, że aplikacja tworzy lub ponownie tworzy i usuwa IServiceProviderelement , ale nie jest to idealny wzorzec.
  • Odbieranie IDisposable zależności za pośrednictwem DI nie wymaga, aby odbiornik zaimplementował samego siebie IDisposable. Odbiorca IDisposable zależności nie powinien wywoływać Dispose tej zależności.
  • Użyj zakresów do kontrolowania okresów istnienia usług. Zakresy nie są hierarchiczne i nie ma specjalnego połączenia między zakresami.

Aby uzyskać więcej informacji na temat oczyszczania zasobów, zobacz Implementowanie Dispose metody lub Implementowanie DisposeAsync metody. Ponadto należy wziąć pod uwagę jednorazowe usługi przejściowe przechwycone przez scenariusz kontenera w odniesieniu do oczyszczania zasobów.

Zastąpienie domyślnego kontenera usług

Wbudowany kontener usługi jest przeznaczony do obsługi potrzeb platformy i większości aplikacji konsumenckich. Zalecamy korzystanie z wbudowanego kontenera, chyba że potrzebujesz określonej funkcji, której nie obsługuje, na przykład:

  • Wstrzykiwanie właściwości
  • Kontenery podrzędne
  • Niestandardowe zarządzanie cyklem życia
  • Func<T> obsługa inicjowania z opóźnieniem
  • Rejestracja oparta na konwencji

Następujące kontenery innych firm mogą być używane z aplikacjami platformy ASP.NET Core:

Bezpieczeństwo wątkowe

Tworzenie usług jednotonowych bezpiecznych wątkowo. Jeśli usługa singleton ma zależność od usługi tymczasowej, usługa tymczasowa może również wymagać bezpieczeństwa wątków w zależności od sposobu jej użycia przez usługę singleton. Metoda fabryczna pojedynczej usługi, taka jak drugi argument AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), nie musi być wątkowo bezpieczna. Podobnie jak konstruktor typu (static), gwarantowane jest wywoływanie tylko raz przez jeden wątek.

Ponadto proces rozpoznawania usług z wbudowanego kontenera iniekcji zależności platformy .NET jest bezpieczny wątkowo.
Po utworzeniu elementu IServiceProvider lub IServiceScope można bezpiecznie rozwiązywać usługi współbieżnie z wielu wątków.

Uwaga / Notatka

Bezpieczeństwo wątków samego kontenera DI gwarantuje, że konstruowanie i rozwiązywanie usług jest bezpieczne. Nie sprawia, że rozpoznane wystąpienia usługi są bezpieczne wątkowo.
Każda usługa (zwłaszcza singletons), która przechowuje współużytkowany stan modyfikowalny, musi zaimplementować własną logikę synchronizacji, jeśli ma być dostępna współbieżnie.

Zalecenia

  • async/await i Task oparte na usługach rozwiązania nie są obsługiwane. Ponieważ język C# nie obsługuje konstruktorów asynchronicznych, użyj metod asynchronicznych po synchronicznym rozwiązaniu usługi.
  • Unikaj przechowywania danych i konfiguracji bezpośrednio w kontenerze usługi. Na przykład koszyk użytkownika nie powinien być zwykle dodawany do kontenera usługi. Konfiguracja powinna używać wzorca opcji. Podobnie należy unikać obiektów "posiadacza danych", które istnieją tylko w celu zezwolenia na dostęp do innego obiektu. Lepiej jest zażądać faktycznego przedmiotu za pośrednictwem DI.
  • Unikaj statycznego dostępu do usług. Na przykład unikaj przechwytywania elementu IApplicationBuilder.ApplicationServices jako pola statycznego lub właściwości do użycia w innym miejscu.
  • Zachowaj szybkość i synchroniczność fabryk DI.
  • Unikaj używania wzorca lokalizatora usług. Na przykład nie należy wywoływać GetService w celu uzyskania wystąpienia usługi, gdy można zamiast tego użyć DI.
  • Unikaj innej odmiany lokalizatora usług polegającej na wstrzykiwaniu fabryki, która rozwiązuje zależności w czasie wykonywania. Obie te praktyki mieszają strategie Odwrócenia Kontroli.
  • Unikaj wywołań do BuildServiceProvider przy konfigurowaniu usług. Wywoływanie BuildServiceProvider zwykle występuje, gdy deweloper chce rozwiązać problem z usługą podczas rejestrowania innej usługi. Zamiast tego należy użyć przeciążenia, które obejmuje IServiceProvider z tego powodu.
  • Przemijające jednorazowe usługi są zarządzane przez pojemnik do usunięcia. Może to przekształcić się w wyciek pamięci, jeśli zostanie rozwiązany z kontenera najwyższego poziomu.
  • Włącz walidację zakresu, żeby upewnić się, że aplikacja nie ma singletonów, które przechwytują usługi o zmiennym zakresie. Aby uzyskać więcej informacji, zobacz Walidacja zakresu.
  • Używaj tylko cyklu życia singleton dla usług ze stanem własnym, który jest kosztowny do utworzenia lub współdzielony globalnie. Unikaj używania pojedynczego okresu istnienia dla usług, które nie mają samego stanu. Większość kontenerów IoC platformy .NET używa "Transient" jako zakresu domyślnego. Rozważania i wady singletonów:
    • Bezpieczeństwo wątków: singleton musi być zaimplementowany w sposób bezpieczny dla wątków.
    • Sprzężenie: Może łączyć niepowiązane żądania.
    • Wyzwania testowe: Współdzielony stan i połączenia mogą utrudnić testowanie jednostkowe.
    • Wpływ na pamięć: singleton może zachować duży graf obiektu w pamięci przez cały okres istnienia aplikacji.
    • Odporność na uszkodzenia: jeśli singleton lub jakakolwiek część drzewa zależności ulegnie awarii, nie może się łatwo odbudować.
    • Ponowne ładowanie konfiguracji: Singletons zazwyczaj nie może obsługiwać "przeładowywania na gorąco" wartości konfiguracji.
    • Wyciek zakresu: Singleton może przypadkowo przechwytywać zależności o zasięgu lub przejściowe, skutecznie przekształcając je w singletony i powodując niezamierzone skutki uboczne.
    • Obciążenie inicjowania: podczas rozpoznawania usługi kontener IoC musi wyszukać instancję singleton. Jeśli jeszcze nie istnieje, musi utworzyć go w bezpieczny wątkowo sposób. Bezstanowa usługa przejściowa jest tania w tworzeniu i niszczeniu.

Podobnie jak w przypadku wszystkich zestawów zaleceń, mogą wystąpić sytuacje, w których wymagane jest ignorowanie rekomendacji. Wyjątki są rzadkie i są głównie specjalnymi przypadkami w ramach samej struktury.

DI to alternatywa dla wzorców dostępu do obiektów statycznych/globalnych. Możesz nie zrealizować korzyści z di, jeśli mieszasz go z dostępem do obiektów statycznych.

Przykładowe antywzorce

Oprócz wytycznych opisanych w tym artykule należy unikać kilku antywłaściwych wzorców. Niektóre z tych antywzorców wynikają z nauki ze środowisk uruchomieniowych.

Ostrzeżenie

Są to przykładowe anty-wzorce. Nie kopiuj kodu, nie używaj tych wzorców i nie unikaj tych wzorców za wszelką cenę.

Jednorazowe usługi przejściowe przechwycone przez kontener

Podczas rejestrowania usług przejściowych , które implementują IDisposableusługę , domyślnie kontener DI przechowuje te odwołania. Nie usuwa ich, dopóki kontener nie zostanie rozwiązany, gdy aplikacja się zatrzyma, jeśli zostały rozwiązane z kontenera, lub dopóki zakres nie zostanie zlikwidowany, jeśli zostały rozwiązane w zakresie. Z poziomu kontenera może dojść do wycieku pamięci.

Antywzorzec: przejściowe zasoby jednorazowe bez utylizacji. Nie kopiuj!

W poprzednim antywzorcu 1000 ExampleDisposable obiektów jest tworzone i zakorzenione. Nie zostaną usunięte, dopóki serviceProvider wystąpienie nie zostanie usunięte.

Aby uzyskać więcej informacji na temat debugowania przecieków pamięci, zobacz Debugowanie przecieku pamięci na platformie .NET.

Asynchroniczne fabryki di mogą powodować zakleszczenia

Termin "fabryki DI" odnosi się do metod przeciążenia, które występują podczas wywoływania Add{LIFETIME}. Istnieją przeciążenia, które przyjmują Func<IServiceProvider, T>, gdzie T to usługa rejestrowana, a parametr nazywa się implementationFactory. Można tę wartość implementationFactory podać jako wyrażenie lambda, funkcję lokalną lub metodę. Jeśli fabryka jest asynchroniczna i używasz Task<TResult>.Result, spowoduje to deadlock.

Antywzór: Zakleszczenie z asynchroniczną fabryką. Nie kopiuj!

W poprzednim kodzie jest podane wyrażenie lambda, implementationFactory w którym treść wywołuje Task<TResult>.Result metodę zwracaną Task<Bar> . Powoduje to zakleszczenie. Metoda GetBarAsync po prostu emuluje asynchroniczne działanie za pomocą metody Task.Delay, a następnie wywołuje GetRequiredService<T>(IServiceProvider).

Antywzorzec: Zakleszczenie spowodowane wewnętrznym problemem asynchronicznej fabryki. Nie kopiuj!

Aby uzyskać więcej informacji na temat asynchronicznych wskazówek, zobacz Programowanie asynchroniczne: ważne informacje i porady. Aby uzyskać więcej informacji na temat debugowania zakleszczeń, przeczytaj Debugowanie zakleszczenia w .NET.

Gdy uruchamiasz ten antywzór i występuje zakleszczenie, możesz wyświetlić dwa wątki oczekujące w oknie Równoległe stosy programu Visual Studio. Aby uzyskać więcej informacji, zobacz Wyświetlanie wątków i zadań w oknie Stosy Równoległe.

Zależność w niewoli

Termin "zależność w niewoli", ukuty przez Marka Seemanna, odnosi się do błędnej konfiguracji okresów istnienia usług, gdzie dłuższa usługa posiada krótszą usługę w niewoli.

Antywzorzec: Zależność zamknięta. Nie kopiuj!

W poprzednim kodzie Foo jest rejestrowany jako pojedynczy i Bar ma zakres — który na powierzchni wydaje się prawidłowy. Należy jednak wziąć pod uwagę implementację elementu Foo.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

Obiekt Foo wymaga obiektu Bar, i ponieważ Foo jest instancją singleton, a Bar ma zakres, jest to błędna konfiguracja. W obecnej formie Foo jest tworzony tylko raz i utrzymuje Bar przez cały czas swojego istnienia, który jest dłuższy niż zamierzony zakres istnienia Bar. Rozważ zweryfikowanie zakresów, przekazując validateScopes: true do BuildServiceProvider(IServiceCollection, Boolean). Podczas sprawdzania poprawności zakresów otrzymasz InvalidOperationException z komunikatem podobnym do "Nie można korzystać z usługi o zakresie 'Bar' w obrębie singletonu 'Foo'.".

Aby uzyskać więcej informacji, zobacz Walidacja zakresu.

Usługa o określonym zakresie jako singleton

W przypadku korzystania z usług o zdefiniowanym zakresie, jeśli nie tworzysz nowego zakresu lub jesteś w istniejącym zakresie, usługa zachowuje się jak jednokrotna.

Antywzór: usługa o określonym zakresie staje się pojedyncza. Nie kopiuj!

W poprzednim kodzie Bar jest pobierany wewnątrz IServiceScope, co jest poprawne. Antywzorem jest pobieranie Bar poza zakresem, a zmienna nazwana jest avoid , aby wskazać, który przykład pobierania jest niepoprawny.

Zobacz też