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 do korzystania z usług jednotonowych.
  • Unikaj bezpośredniego tworzenia wystąpień klas zależnych w ramach usług. Bezpośrednie tworzenie wystąpienia łączy kod z określoną implementacją.
  • Umożliwianie usługom małych, dobrze ocenianych i łatwych 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ę o pojedynczej odpowiedzialności (SRP). Spróbuj refaktoryzować klasę, przenosząc część swoich obowiązków do nowych klas.

Usuwanie usług

Kontener jest odpowiedzialny za czyszczenie tworzonych typów i wywoływanie DisposeIDisposable wystąpień. Usługi rozwiązane z kontenera nigdy nie powinny być usuwane przez dewelopera. Jeśli typ lub fabryka jest rejestrowana jako pojedyncza, kontener automatycznie usuwa pojedynczyton.

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()");
}

Poprzedniego jednorazowego użytku ma mieć okres przejściowy.

namespace ConsoleDisposable.Example;

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

Poprzedni jednorazowy ma mieć okres istnienia o określonym zakresie.

namespace ConsoleDisposable.Example;

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

Poprzednie jednorazowe jest przeznaczone do jednego 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());

Powyższy kod:

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

Wskazówki dotyczące interfejsu IDisposable dla wystąpień przejściowych i udostępnionych

Przejściowy, ograniczony okres istnienia

Scenariusz

Aplikacja wymaga IDisposable wystąpienia z przejściowym okresem istnienia dla jednego z następujących scenariuszy:

  • Wystąpienie jest rozpoznawane w zakresie głównym (kontener główny).
  • Wystąpienie powinno zostać usunięte przed zakończeniem zakresu.

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:

Wystąpienie udostępnione, ograniczony okres istnienia

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 polecenia , aby utworzyć nowy IServiceScopeelement . Użyj zakresu IServiceProvider , aby uzyskać wymagane usługi. Usuwaj zakres, gdy nie jest już potrzebny.

Ogólne IDisposable wytyczne

  • Nie rejestruj IDisposable wystąpień w okresie przejściowym. 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 utworzenie/ponowne utworzenie aplikacji i usunięcie IServiceProviderelementu , ale nie jest to idealny wzorzec.
  • Odbieranie IDisposable zależności za pośrednictwem di nie wymaga, aby odbiornik zaimplementował IDisposable się. 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.

Domyślne zastąpienie kontenera usługi

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 (tylko na platformie .NET 7 i starszych wersjach. Aby uzyskać więcej informacji, zobacz Keyed services (Usługi kluczy).
  • Kontenery podrzędne
  • Niestandardowe zarządzanie okresem istnienia
  • 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 pojedyncza usługa ma zależność od usługi przejściowej, usługa przejściowa może również wymagać bezpieczeństwa wątków w zależności od sposobu jej użycia przez pojedynczy element.

Metoda fabryki pojedynczej usługi, taka jak drugi argument AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), nie musi być bezpieczna wątkowo. Podobnie jak konstruktor typu (static), gwarantowane jest wywoływanie tylko raz przez jeden wątek.

Zalecenia

  • async/await rozpoznawanie i Task rozpoznawanie usług opartych na usłudze nie jest obsługiwane. Ponieważ język C# nie obsługuje konstruktorów asynchronicznych, użyj metod asynchronicznych po synchronicznym rozpoznawaniu 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ć rzeczywistego elementu 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 szybkie i synchroniczne fabryki 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.
  • Kolejna odmiana lokalizatora usług, aby uniknąć, to wstrzykiwanie fabryki, która rozwiązuje zależności w czasie wykonywania. Obie te praktyki mieszają inwersję strategii kontroli .
  • Unikaj wywołań podczas BuildServiceProvider konfigurowania 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.
  • Jednorazowe usługi przejściowe są przechwytywane przez pojemnik do dyspozycji. Może to przekształcić się w wyciek pamięci, jeśli zostanie rozwiązany z kontenera najwyższego poziomu.
  • Włącz walidację zakresu, aby upewnić się, że aplikacja nie ma pojedynczych dysków, które przechwytują usługi o określonym 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 wzorce antywłaściwe

Oprócz wytycznych opisanych w tym artykule należy unikać kilku antywłaściwych wzorców. Niektóre z tych wzorców anty-wzorce są uczenie się od samych ś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 będzie przechowywać te odwołania, a nie Dispose() do momentu usunięcia kontenera, gdy aplikacja zostanie zatrzymana, jeśli zostały rozpoznane z kontenera, lub dopóki zakres nie zostanie usunięty, jeśli został rozwiązany z zakresu. Może to przekształcić się w przeciek pamięci, jeśli zostanie rozwiązany z poziomu kontenera.

Anti-pattern: Transient disposables without dispose. Do not copy!

W poprzednim antywzór 1000 ExampleDisposable obiektów jest tworzone i rooted. 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 istnieją podczas wywoływania Add{LIFETIME}. Istnieją przeciążenia akceptujące lokalizację Func<IServiceProvider, T> , w której T jest zarejestrowana usługa, a parametr ma nazwę implementationFactory. Można implementationFactory go podać jako wyrażenie lambda, funkcję lokalną lub metodę. Jeśli fabryka jest asynchroniczna i używasz Task<TResult>.Resultmetody , spowoduje to zakleszczenie.

Anti-pattern: Deadlock with async factory. Do not copy!

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 operację pracy asynchronicznej za pomocą Task.Delaymetody , a następnie wywołuje metodę GetRequiredService<T>(IServiceProvider).

Anti-pattern: Deadlock with async factory inner issue. Do not copy!

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ń, zobacz Debugowanie zakleszczenia na platformie .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, w których dłuższe usługi posiadają krótszy niewoli usług.

Anti-pattern: Captive dependency. Do not copy!

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, a ponieważ Foo jest pojedynczym obiektem Bar i Bar ma zakres — jest to błędna konfiguracja. Tak jak to jest, Foo zostanie utworzone tylko raz, i będzie trzymać się Bar na jego okres istnienia, który jest dłuższy niż zamierzony okres istnienia Bar. Należy rozważyć weryfikację zakresów, przekazując validateScopes: true element do elementu BuildServiceProvider(IServiceCollection, Boolean). Po zweryfikowaniu zakresów otrzymasz InvalidOperationException komunikat podobny do komunikatu "Nie można używać usługi o określonym zakresie "Bar" z pojedynczego elementu "Foo".

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

Usługa o określonym zakresie jako pojedyncza

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.

Anti-pattern: Scoped service becomes singleton. Do not copy!

W poprzednim kodzie Bar jest pobierany w obiekcie IServiceScope, który jest poprawny. Antywzór to pobieranie poza Bar zakresem, a zmienna ma nazwę avoid , aby pokazać, które przykładowe pobieranie jest niepoprawne.

Zobacz też