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żywamyservice locator(ręcznegoprovider.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 życia | Jedna instancja na | Dla czego |
|---|---|---|
Singleton | całą aplikację | bezstanowe usługi, cache, konfiguracja |
Scoped | jedno żądanie HTTP | serwisy domenowe, DbContext |
Transient | każde rozwiązanie | lekkie, bezstanowe obiekty pomocnicze |
Standard ProfessNet: serwisy zależne od bazy są
Scoped(jakDbContext). Najczęstszy błąd to wstrzyknięcieScopeddoSingleton— 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żywajNullLogger<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.