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 Dispose IDisposable 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:
- Odbierz element IServiceProvider w konstruktorze.
- Użyj ActivatorUtilities.CreateInstance polecenia , aby utworzyć wystąpienie poza kontenerem, używając kontenera dla jego zależności.
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 iTask
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 obejmujeIServiceProvider
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.
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.
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).
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.
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.
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.