C# / .NET/03core11 min

Dependency Injection w .NET

.NET ma wbudowany kontener Dependency Injection — nie potrzebujemy zewnętrznych bibliotek. DI odwraca zależności: klasa deklaruje, czego potrzebuje, a kontener to dostarcza. Efekt: kod testowalny, luźno powiązany i łatwy w utrzymaniu.

Wstrzykiwanie przez konstruktor

Standardem jest constructor injection. Zależności są jawne i niezmienne.

public sealed class ProbeService : IProbeService
{
    private readonly IProbeRepository _repository;
    private readonly ILogger<ProbeService> _logger;

    public ProbeService(IProbeRepository repository, ILogger<ProbeService> logger)
    {
        _repository = repository;
        _logger = logger;
    }
}

Standard ProfessNet: zależności wstrzykujemy przez konstruktor i trzymamy w polach readonly. Nie używamy service locator (ręcznego provider.GetService<T>()) w logice biznesowej.

Rejestracja usług

Usługi rejestrujemy w Program.cs, programując pod interfejs.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IProbeService, ProbeService>();
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddTransient<IReportBuilder, ReportBuilder>();
builder.Services.AddHttpClient<IBackendClient, BackendClient>();

var app = builder.Build();

Cykle życia — wybierz świadomie

Cykl życiaJedna instancja naDla czego
Singletoncałą aplikacjębezstanowe usługi, cache, konfiguracja
Scopedjedno żądanie HTTPserwisy domenowe, DbContext
Transientkażde rozwiązanielekkie, bezstanowe obiekty pomocnicze

Standard ProfessNet: serwisy zależne od bazy są Scoped (jak DbContext). Najczęstszy błąd to wstrzyknięcie Scoped do Singleton — kontener to wyłapie przy starcie (ValidateScopes), więc nie ignoruj tego wyjątku.

Captive dependency — pułapka

Singleton trzymający Scoped „uwięzi" tę zależność na całe życie aplikacji.

// źle — Singleton trzyma Scoped DbContext
public sealed class CacheService           // zarejestrowany jako Singleton
{
    public CacheService(AppDbContext db) { } // DbContext jest Scoped!
}

Rozwiązanie: w Singletonie pobierz Scoped przez IServiceScopeFactory.

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

public async Task RefreshAsync()
{
    using var scope = _scopeFactory.CreateScope();
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    // ...
}

DI a testowalność

Dzięki interfejsom w testach podstawiamy mock zamiast prawdziwej implementacji.

var repo = new Mock<IProbeRepository>();
repo.Setup(r => r.GetHostsAsync("corp", default))
    .ReturnsAsync(new[] { "dc01" });

var service = new ProbeService(repo.Object, NullLogger<ProbeService>.Instance);

Wskazówka: rejestruj ILogger<T> automatycznie (AddLogging), a w testach używaj NullLogger<T>.Instance — nie trzeba mockować logera.


Constructor injection, programowanie pod interfejs, świadomy wybór cyklu życia. Wbudowany kontener .NET w zupełności wystarcza, a kod zyskuje testowalność i czyste granice między warstwami.