Udostępnij za pośrednictwem


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 w aplikacjach .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 konstruktora 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.
  • Nie usuwaj IDisposable wystąpień z okresem przejściowym lub okresem istnienia w zakresie głównym. Jedynym wyjątkiem jest, gdy aplikacja tworzy/odtwarza i usuwa IServiceProvider, co jednak nie jest idealnym wzorcem.
  • 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
  • Iniekcja oparta na nazwie (dotyczy tylko .NET 7 i wcześniejszych wersji. Aby uzyskać więcej informacji, zobacz Usługi kluczy (Keyed services).)
  • 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 przejściowej, usługa przejściowa może również wymagać bezpieczeństwa względem wątków, w zależności od sposobu jej użycia przez 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.

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.
  • Należy unikać kolejnej odmiany lokalizatora usług, która polega na wstrzykiwaniu fabryki rozwiązującej 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.

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

DI to alternatywa dla wzorców dostępu do obiektów statycznych/globalnych. Jeśli połączysz go z dostępem do obiektów statycznych, możesz nie być w stanie zrealizować korzyści z di.

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 unikaj tych wzorców za wszelką cenę.

Jednorazowe usługi przejściowe przechwycone przez kontener

Podczas rejestrowania usług przejściowych, które implementują IDisposable, domyślnie kontener DI przechowuje te odwołania i nie usuwa ich, aż do momentu usunięcia kontenera, gdy aplikacja jest zatrzymana, jeśli zostały rozwiązane z kontenera, lub dopóki zakres nie zostanie usunięty, jeśli zostały rozwiązane z zakresu. Może to przekształcić się w przeciek pamięci, jeśli zostanie rozwiązany z poziomu kontenera.

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 przyjmujące Func<IServiceProvider, T>, gdzie T to usługa rejestrowana, a parametr nosi nazwę 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 zakleszczenie.

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" został ukuty przez Marka Seemanna i odnosi się do błędnej konfiguracji okresów istnienia usług, gdzie dłużej działająca usługa przywiązuje do siebie krócej działającą usługę.

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, a ponieważ Foo jest pojedynczy, a Bar ma zakres - jest to błędna konfiguracja. W aktualnym stanie, Foo zostanie utworzone tylko raz i będzie utrzymywać Bar przez cały swój okres istnienia, który będzie dłuższy niż zamierzony okres istnienia Bar. Należy rozważyć weryfikację zakresów, przekazując validateScopes: true do BuildServiceProvider(IServiceCollection, Boolean). Po zweryfikowaniu zakresów otrzymasz InvalidOperationException z komunikatem podobnym do "Nie można skonsumować usługi z zakresem 'Bar' z 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 określonym zakresie, jeśli nie tworzysz zakresu lub w istniejącym zakresie — usługa staje się pojedyncza.

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ż