C# / .NET/03core11 min

Dependency Injection in .NET

.NET has a built-in Dependency Injection container — we don't need external libraries. DI inverts dependencies: a class declares what it needs, and the container supplies it. The result: testable, loosely coupled, maintainable code.

Constructor injection

The standard is constructor injection. Dependencies are explicit and immutable.

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;
    }
}

ProfessNet standard: we inject dependencies through the constructor and keep them in readonly fields. We do not use a service locator (manual provider.GetService<T>()) in business logic.

Registering services

We register services in Program.cs, programming to an interface.

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

Lifetimes — choose deliberately

LifetimeOne instance perFor what
Singletonthe whole applicationstateless services, cache, configuration
Scopeda single HTTP requestdomain services, DbContext
Transienteach resolutionlightweight, stateless helpers

ProfessNet standard: services that depend on the database are Scoped (like DbContext). The most common mistake is injecting a Scoped service into a Singleton — the container catches this at startup (ValidateScopes), so do not ignore that exception.

Captive dependency — a trap

A Singleton holding a Scoped service "captures" that dependency for the entire lifetime of the application.

// bad — Singleton holds a Scoped DbContext
public sealed class CacheService           // registered as Singleton
{
    public CacheService(AppDbContext db) { } // DbContext is Scoped!
}

The fix: in the Singleton, obtain the Scoped service via IServiceScopeFactory.

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

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

DI and testability

Thanks to interfaces, in tests we substitute a mock for the real implementation.

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

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

Tip: register ILogger<T> automatically (AddLogging), and in tests use NullLogger<T>.Instance — there's no need to mock the logger.


Constructor injection, programming to an interface, a deliberate choice of lifetime. The built-in .NET container is entirely sufficient, and the code gains testability and clean boundaries between layers.