Transient czy scoped w .NET? Praktyczny wybór cyklu życia usług

Radosław Krajewski 19 maja 2026
Cykl życia domeny: rejestracja, aktywna (365 dni), kwarantanna (koszt wzrasta), wolna. Transient vs scoped – domena wraca do puli wolnych.

Spis treści

Wybór między cyklem życia transient i scoped wpływa na to, ile instancji usług powstaje, jak długo żyją i czy mogą bezpiecznie przechowywać stan. W artykule porównuję oba podejścia w .NET, pokazuję praktyczne przykłady rejestracji, typowe błędy oraz sytuacje, w których jedno rozwiązanie wyraźnie wygrywa z drugim.

Najważniejsze zasady wyboru cyklu życia usług

  • Transient tworzy nową instancję przy każdym pobraniu usługi z kontenera.
  • Scoped używa jednej instancji w obrębie konkretnego zakresu, najczęściej jednego żądania HTTP.
  • DbContext zwykle powinien być rejestrowany jako scoped, ponieważ reprezentuje jednostkę pracy.
  • Transient sprawdza się przy usługach bezstanowych i lekkich.
  • Scoped jest właściwy, gdy kilka komponentów musi współdzielić stan podczas jednej operacji.
  • Nie należy wstrzykiwać usługi scoped bezpośrednio do singletona.

Diagram porównuje lifetimów w .NET: transient (nowa instancja za każdym razem), scoped (jedna instancja na żądanie) i singleton (jedna instancja dla całej aplikacji).

Transient vs scoped w praktyce .NET

Wbudowany kontener dependency injection w .NET obsługuje kilka cykli życia usług. W tym porównaniu najważniejsze są dwa z nich: transient, czyli nowa instancja przy każdym żądaniu, oraz scoped, czyli jedna instancja przypisana do konkretnego zakresu.

W aplikacji ASP.NET Core zakresem jest zwykle pojedyncze żądanie HTTP. Jeżeli trzy kontrolery lub serwisy w ramach tego samego żądania pobiorą usługę scoped, otrzymają ten sam obiekt. Usługa transient może w tej samej sytuacji zostać utworzona kilka razy.

Kryterium Transient Scoped
Tworzenie instancji Przy każdym pobraniu z kontenera Raz na zakres
Typowy zakres w ASP.NET Core Pojedyncze żądanie lub konkretna zależność Jedno żądanie HTTP
Współdzielenie stanu Nie Tak, w obrębie zakresu
Typowe zastosowanie Lekkie usługi bezstanowe Jednostka pracy, kontekst operacji
Ryzyko Nadmierna liczba instancji Przypadkowe przechowywanie stanu zbyt długo

Najważniejszy szczegół bywa pomijany. Transient nie oznacza, że obiekt powstaje przy każdym wywołaniu metody. Jeśli usługa transient zostanie wstrzyknięta do usługi scoped, jej instancja powstanie podczas tworzenia usługi scoped i będzie używana przez cały czas życia tej zależności.

Kiedy wybrać transient

Po transient sięgam wtedy, gdy usługa jest bezstanowa, szybko się tworzy i nie musi być współdzielona z innymi komponentami. Dobrym przykładem jest formatter, walidator, mapper albo prosty kalkulator reguł biznesowych.

builder.Services.AddTransient();
builder.Services.AddTransient();
Każda instancja może mieć własne zależności i lokalne dane, ale po zakończeniu operacji nie powinna być potrzebna innej części aplikacji. Dzięki temu kod jest łatwiejszy do testowania, a usługa nie zaczyna przypadkowo pełnić roli magazynu wspólnego stanu.

Przykłady dobrych zastosowań

  • Walidatory, które analizują przekazany model i zwracają wynik.
  • Formatery generujące tekst, dokument lub komunikat.
  • Mappery przekształcające encje na modele API.
  • Strategie biznesowe, które nie przechowują danych między wywołaniami.
  • Lekkie adaptery, których utworzenie nie wymaga kosztownych zasobów.

Nie traktowałbym jednak transient jako domyślnego wyboru dla każdej małej klasy. Jeśli obiekt otwiera połączenie, tworzy ciężki graf zależności albo ma współdzielić kontekst operacji, częste tworzenie nowych instancji może utrudnić kontrolę zasobów.

W praktyce sam koszt konstrukcji rzadko powinien decydować o wyborze. Najpierw patrzę na semantykę stanu i odpowiedzialność usługi, a dopiero później na mikrooptymalizacje. Jeżeli wydajność ma znaczenie, mierzę ją profilerem zamiast zgadywać.

Kiedy scoped jest lepszym wyborem

Scoped pasuje do usług, które powinny istnieć przez czas trwania jednej spójnej operacji. W aplikacji webowej najczęściej oznacza to jedną instancję na żądanie HTTP. Wszystkie komponenty obsługujące to żądanie mogą wtedy korzystać z tego samego kontekstu.

builder.Services.AddScoped();
builder.Services.AddScoped();

Najbardziej znanym przykładem jest Entity Framework Core DbContext. Kontekst śledzi encje, przechowuje zmiany i zwykle zapisuje je jako jedną jednostkę pracy. Utworzenie kilku niezależnych kontekstów w obrębie jednej operacji może sprawić, że komponenty nie zobaczą tych samych zmian albo wykonają kilka niepotrzebnych transakcji.

Co faktycznie daje jedna instancja

Załóżmy, że kontroler wywołuje serwis zamówień, a ten korzysta z repozytorium i kontekstu bazy danych. Jeśli wszystkie zależności są scoped, mogą używać tego samego kontekstu operacji. To upraszcza śledzenie zmian i pozwala zachować spójność procesu.

Scoped nie oznacza jednak automatycznie „bezpieczny współbieżnie”. Jeżeli w jednym żądaniu uruchomisz równoległe zadania korzystające z tej samej instancji DbContext, nadal możesz doprowadzić do błędów. Czas życia obiektu i jego bezpieczeństwo wielowątkowe to dwie różne właściwości.

Najważniejsze różnice w realnej architekturze

Wybór cyklu życia staje się prostszy, gdy spojrzymy na sposób użycia usługi, a nie tylko na jej nazwę. Poniższe zestawienie pokazuje typowe decyzje architektoniczne.

Przypadek Lepszy wybór Dlaczego
Walidacja pojedynczego modelu Transient Brak potrzeby współdzielenia stanu
Formatter odpowiedzi API Transient Usługa wykonuje krótką, niezależną operację
DbContext Scoped Wspólny kontekst i jednostka pracy
Kontekst zalogowanego żądania Scoped Wiele komponentów może odczytać te same dane
Serwis używany przez background worker Scoped w ręcznie utworzonym zakresie Worker nie ma automatycznie zakresu HTTP
Usługa z dużym stanem współdzielonym globalnie Najczęściej singleton Transient i scoped nie rozwiązują potrzeby globalnego współdzielenia

Warto też pamiętać, że cykl życia zależności może ograniczać cykl życia usługi nadrzędnej. Usługa transient może korzystać ze scoped, jeśli sama jest tworzona wewnątrz aktywnego zakresu. Problem pojawia się wtedy, gdy krócej żyjąca zależność zostaje przechwycona przez obiekt żyjący znacznie dłużej.

Typowe błędy przy transient i scoped

Wstrzykiwanie scoped do singletona

To jeden z najczęstszych błędów. Singleton żyje przez cały czas działania aplikacji, więc nie może bezpośrednio przechowywać zależności scoped, która powinna kończyć życie po zakończeniu zakresu.

public class CacheRefreshService
{
    private readonly AppDbContext _db;

    public CacheRefreshService(AppDbContext db)
    {
        _db = db;
    }
}

Jeśli CacheRefreshService jest singletonem, a AppDbContext ma cykl scoped, powstaje tak zwana captured dependency. W zależności od konfiguracji aplikacja zgłosi błąd podczas budowania kontenera albo przechowa zależność dłużej, niż zakładano.

Włączona walidacja zakresów pomaga wykrywać takie problemy wcześniej. Nie wyłączałbym jej tylko po to, aby aplikacja „przestała narzekać”. Komunikat kontenera często wskazuje realny błąd architektoniczny.

Pobieranie scoped w background service

Hosted service i worker nie obsługują żądania HTTP, dlatego nie mają automatycznego zakresu. Jeśli potrzebują DbContext albo innej usługi scoped, trzeba utworzyć zakres przez IServiceScopeFactory.

public sealed class ImportWorker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public ImportWorker(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        using IServiceScope scope = _scopeFactory.CreateScope();

        var importer = scope.ServiceProvider
            .GetRequiredService();

        await importer.RunAsync(stoppingToken);
    }
}

Zakres powinien obejmować konkretną partię pracy, a nie cały czas działania workera. To zapewnia prawidłowe zwalnianie zasobów i ogranicza ryzyko pozostawienia starego stanu.

Przeczytaj również: Architektura monolityczna w .NET - kiedy ma sens?

Traktowanie scoped jak cache

Scoped współdzieli instancję tylko wewnątrz zakresu. Nie powinien zastępować cache aplikacyjnego ani mechanizmu komunikacji między żądaniami. Dane przechowywane w takiej usłudze znikną wraz z końcem zakresu, a w przypadku aplikacji internetowej zwykle po zakończeniu żądania.

Jak podejmuję decyzję przy projektowaniu usługi

Zaczynam od trzech prostych pytań. Czy usługa ma przechowywać stan? Czy kilka komponentów w ramach jednej operacji musi zobaczyć tę samą instancję? Czy tworzenie obiektu wiąże się z zasobem, który powinien być zwolniony na końcu zakresu?

  • Jeżeli odpowiedzi brzmią „nie”, wybieram zwykle transient.
  • Jeżeli wspólny stan ma obowiązywać w jednej operacji, wybieram scoped.
  • Jeżeli usługa korzysta z DbContext, sprawdzam, czy cały graf zależności respektuje jego zakres.
  • Jeżeli kod działa w workerze, tworzę zakres ręcznie i jasno określam jego granice.
  • Jeżeli usługa ma być singletonem, upewniam się, że wszystkie jej zależności też mogą bezpiecznie żyć tak długo.

Pomaga mi także prześledzenie grafu zależności. Sama rejestracja AddTransient lub AddScoped mówi tylko, jak kontener tworzy konkretny typ. Nie mówi jeszcze, jak długo ten typ będzie faktycznie używany przez inne obiekty.

Granica zakresu zależy od hosta. W ASP.NET Core jest nią zazwyczaj żądanie HTTP, ale w innych modelach aplikacji może to być ręcznie utworzona operacja, obwód Blazor Server albo dłużej żyjący kontekst aplikacji. Dlatego nie przenoszę reguły „scoped oznacza request” bez sprawdzenia konkretnego środowiska.

Prosta reguła, która sprawdza się w większości projektów

Jeśli usługa wykonuje krótką, niezależną operację i nie przechowuje stanu, rejestruję ją jako transient. Jeśli ma reprezentować kontekst jednej operacji, współdzielić stan między komponentami albo pracować razem z DbContext, wybieram scoped.

Największe problemy nie wynikają zwykle z samego wyboru jednej z tych opcji, lecz z nieświadomego mieszania ich zakresów. Gdy jasno określam, gdzie zaczyna się i kończy operacja, a potem sprawdzam cały graf zależności, różnica między tymi cyklami życia staje się praktyczna, a nie tylko teoretyczna.

FAQ - Najczęstsze pytania

Transient sprawdza się przy lekkich usługach bezstanowych, takich jak walidatory, formatery, mappery i strategie biznesowe. Kontener tworzy nową instancję przy każdym pobraniu usługi, ale jeśli transient zostanie wstrzyknięty do usługi scoped, jego instancja będzie używana przez czas życia tej zależności scoped.

DbContext reprezentuje jednostkę pracy, śledzi encje i przechowuje zmiany, dlatego jedna instancja na zakres, najczęściej żądanie HTTP, pomaga zachować spójność operacji. Scoped nie zapewnia jednak bezpieczeństwa współbieżnego, więc nie należy wykonywać równoległych zadań korzystających z tego samego DbContext.

Worker nie ma automatycznie utworzonego zakresu, dlatego należy wstrzyknąć IServiceScopeFactory i wywołać CreateScope(). W utworzonym zakresie można pobrać wymaganą usługę scoped, wykonać konkretną partię pracy, a następnie zakończyć zakres, aby zasoby zostały prawidłowo zwolnione.

Powstaje captured dependency, ponieważ singleton może przechowywać usługę scoped znacznie dłużej, niż przewidziano. Kontener może zgłosić błąd podczas budowania aplikacji albo utrzymywać zależność poza jej właściwym zakresem, dlatego walidacji zakresów nie należy wyłączać tylko po to, by ukryć problem.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

singleton
wstrzykiwanie zależności
transient
scoped
dbcontext
Autor Radosław Krajewski
Radosław Krajewski
Nazywam się Radosław Krajewski i od 6 lat zgłębiam tajniki programowania .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z tymi technologiami zaczęła się od fascynacji tym, jak złożone problemy można rozwiązywać za pomocą kodu i innowacyjnych narzędzi. Staram się przekazywać tę wiedzę w sposób zrozumiały, dzieląc się swoimi doświadczeniami i spostrzeżeniami na kursdotnet.pl. W moich artykułach skupiam się na praktycznych aspektach, porównuję różne rozwiązania i analizuję najnowsze trendy, aby dostarczyć Wam rzetelne i aktualne informacje, które pomogą Wam rozwijać się w tej dynamicznie zmieniającej się dziedzinie.

Udostępnij artykuł

Napisz komentarz