Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

Dependency Injection to fundamentalny pattern w nowoczesnym C#! W 2015 roku używałeś third-party kontenerów (Autofac, Ninject). W 2026 roku masz Microsoft.Extensions.DependencyInjection built-in w .NET - szybki, prosty, wystarczający dla 95% scenariuszy!

📅 Timeline - DI w .NET
  • 2010-2015 - Third-party: Autofac, Ninject, StructureMap
  • ASP.NET Core 1.0 (2016) - 🔥 Built-in DI container!
  • .NET Core 2.0+ (2017+) - Microsoft.Extensions.DependencyInjection
  • .NET 6+ (2021+) - Improvements, keyed services (.NET 8)
🔍 Dependency Injection = Inversion of Control (IoC)

Problem: Klasa tworzy swoje dependencies → tight coupling
Rozwiązanie: Ktoś DAJE klasie dependencies → loose coupling
Korzyści: Testable, flexible, maintainable code! ✨

Problem - bez Dependency Injection

Tight coupling - zły przykład

// ❌ BEZ Dependency Injection - tight coupling
public class OrderService
{
    // Tworzy własne dependencies - tight coupling!
    private readonly SqlServerRepository _repository = new SqlServerRepository();
    private readonly EmailService _emailService = new EmailService();
    private readonly Logger _logger = new Logger();
    
    public void ProcessOrder(Order order)
    {
        _logger.Log("Processing order");
        _repository.Save(order);
        _emailService.SendConfirmation(order.CustomerEmail);
    }
}

// Problemy:
// ❌ Nie można testować (nie możesz zamockować dependencies)
// ❌ Związany z konkretną implementacją (SqlServerRepository, nie może być MySQL)
// ❌ Nie można zmienić loggera bez zmiany kodu
// ❌ Trudny w utrzymaniu
// ❌ Każda instancja OrderService tworzy NOWE dependencies (memory overhead)

Manual dependency injection - lepsze, ale verbose

// Lepiej - ale nadal ręczne tworzenie dependencies
public class OrderService
{
    private readonly IRepository _repository;
    private readonly IEmailService _emailService;
    private readonly ILogger _logger;
    
    // Constructor przyjmuje dependencies
    public OrderService(IRepository repository, IEmailService emailService, ILogger logger)
    {
        _repository = repository;
        _emailService = emailService;
        _logger = logger;
    }
    
    public void ProcessOrder(Order order)
    {
        _logger.Log("Processing order");
        _repository.Save(order);
        _emailService.SendConfirmation(order.CustomerEmail);
    }
}

// Użycie - musisz ręcznie tworzyć wszystko!
var logger = new ConsoleLogger();
var repository = new SqlServerRepository(connectionString);
var emailService = new EmailService(smtpSettings);

var orderService = new OrderService(repository, emailService, logger);
orderService.ProcessOrder(order);

// Lepsze, ale:
// ❌ Verbose - musisz ręcznie tworzyć wszystko
// ❌ Kolejność konstruktorów - musisz znać zależności
// ❌ Jeśli EmailService potrzebuje ILogger - musisz to wiedzieć i stworzyć
Dependency Injection Container - rozwiązanie!

Microsoft.Extensions.DependencyInjection

🎉 ASP.NET Core - Built-in DI Container

Container automatycznie tworzy obiekty z dependencies! Rejestrujesz typy, container zarządza tworzeniem! ✨

// Setup DI container
var builder = WebApplication.CreateBuilder(args);

// Rejestracja services w container
builder.Services.AddTransient<IRepository, SqlServerRepository>();
builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddSingleton<ILogger, ConsoleLogger>();
builder.Services.AddTransient<OrderService>();

var app = builder.Build();

// Użycie - container AUTOMATYCZNIE tworzy dependencies!
app.MapPost("/orders", (OrderService orderService, Order order) =>
{
    // Container AUTOMATIC tworzy:
    // 1. ConsoleLogger
    // 2. SqlServerRepository
    // 3. EmailService
    // 4. OrderService z wszystkimi dependencies
    // Ty nie musisz nic robić ręcznie! ✨
    
    orderService.ProcessOrder(order);
    return Results.Ok();
});

app.Run();

Jak to działa?

// Container workflow:

// 1. Rejestracja (configuration)
services.AddTransient<IRepository, SqlServerRepository>();
//                     ↑ interface    ↑ implementation

// 2. Resolution (runtime)
// Gdy prosisz o OrderService:
var orderService = serviceProvider.GetRequiredService<OrderService>();

// Container:
// a) Patrzy na constructor OrderService
//    → widzi: IRepository, IEmailService, ILogger
// b) Dla każdego dependency:
//    → sprawdza co jest zarejestrowane dla tego interfejsu
//    → tworzy instancję (lub bierze istniejącą jeśli Singleton)
// c) Tworzy OrderService z wszystkimi dependencies
// d) Zwraca gotowy obiekt!

// Ty nie musisz znać dependency graph - container wie! ✨
Service Lifetimes - Transient, Scoped, Singleton

Transient - nowa instancja za każdym razem

// Transient - ZAWSZE tworzy NOWĄ instancję
services.AddTransient<IEmailService, EmailService>();

// Za każdym request o IEmailService → NOWY obiekt
var email1 = serviceProvider.GetService<IEmailService>();
var email2 = serviceProvider.GetService<IEmailService>();

Console.WriteLine(email1 == email2);  // FALSE - różne instancje! ✨

// Użycie:
// ✅ Stateless services (nie przechowują state)
// ✅ Lightweight objects
// ✅ Services które nie mogą być shared

// Przykład:
public class EmailService : IEmailService
{
    // Stateless - można tworzyć wiele instancji
    public void Send(string to, string message)
    {
        // Send email
    }
}

Scoped - jedna instancja per scope (request)

// Scoped - JEDNA instancja PER SCOPE
services.AddScoped<IRepository, SqlServerRepository>();

// W ASP.NET Core: scope = HTTP request
// Wszystkie serwisy w ramach JEDNEGO requesta dostają TĄ SAMĄ instancję

// Request 1:
// OrderService i ProductService dostają TĄ SAMĄ instancję IRepository ✨
// [Request ends] → instancja jest disposed

// Request 2:
// NOWA instancja IRepository (nowy scope)

// Użycie:
// ✅ Database contexts (DbContext)
// ✅ Unit of Work pattern
// ✅ Services które powinny być shared w ramach requesta

// Przykład:
public class MyDbContext : DbContext
{
    // Jeden DbContext per request - ChangeTracker działa poprawnie
}

services.AddScoped<MyDbContext>();

Singleton - jedna instancja dla całej aplikacji

// Singleton - JEDNA instancja dla CAŁEJ aplikacji
services.AddSingleton<ILogger, FileLogger>();

// ZAWSZE ta sama instancja
var logger1 = serviceProvider.GetService<ILogger>();
var logger2 = serviceProvider.GetService<ILogger>();

Console.WriteLine(logger1 == logger2);  // TRUE - ta sama instancja! ✨

// Użycie:
// ✅ Thread-safe stateless services
// ✅ Configuration objects
// ✅ Caches
// ✅ Expensive to create objects

// Przykład:
public class AppSettings
{
    public string ConnectionString { get; set; }
    public string ApiKey { get; set; }
}

// Load once, share everywhere
var settings = new AppSettings { /* ... */ };
services.AddSingleton(settings);
Lifetime Kiedy tworzona Kiedy używać
Transient Za każdym razem gdy requested Stateless services, lightweight
Scoped Raz per scope (HTTP request) DbContext, Unit of Work
Singleton Raz per aplikację Config, cache, thread-safe services
⚠️ Captive Dependency - pułapka!

NIE wstrzykuj Scoped service do Singleton!

// ❌ BŁĄD - Captive Dependency
services.AddSingleton<MySingleton>();  // Singleton
services.AddScoped<MyDbContext>();     // Scoped

public class MySingleton
{
    public MySingleton(MyDbContext db)  // ❌ BŁĄD!
    {
        // Singleton trzyma Scoped dependency
        // DbContext będzie żył całą aplikację zamiast per-request!
    }
}

// ✅ Poprawne:
// Singleton może mieć Singleton dependencies
// Scoped może mieć Scoped lub Transient
// Transient może mieć wszystkie
Constructor Injection - standard pattern

Constructor injection - recommended

// ✅ Constructor injection - RECOMMENDED pattern
public class OrderService
{
    private readonly IRepository _repository;
    private readonly IEmailService _emailService;
    private readonly ILogger<OrderService> _logger;
    
    // Dependencies przez constructor
    public OrderService(
        IRepository repository,
        IEmailService emailService,
        ILogger<OrderService> logger)
    {
        _repository = repository;
        _emailService = emailService;
        _logger = logger;
    }
    
    public void ProcessOrder(Order order)
    {
        _logger.LogInformation("Processing order {OrderId}", order.Id);
        _repository.Save(order);
        _emailService.SendConfirmation(order.CustomerEmail);
    }
}

// Zalety constructor injection:
// ✅ Dependencies są JAWNE (widoczne w sygnaturze)
// ✅ Immutable - nie można zmienić po utworzeniu
// ✅ Łatwe do testowania (mockujesz w teście)
// ✅ Container automatycznie resolve

Testing z DI

// Testing - easy mocking dzięki DI!
public class OrderServiceTests
{
    [Fact]
    public void ProcessOrder_SavesToRepository()
    {
        // Arrange - mock dependencies
        var mockRepository = new Mock<IRepository>();
        var mockEmailService = new Mock<IEmailService>();
        var mockLogger = new Mock<ILogger<OrderService>>();
        
        var orderService = new OrderService(
            mockRepository.Object,
            mockEmailService.Object,
            mockLogger.Object
        );
        
        var order = new Order { Id = 1, CustomerEmail = "test@example.com" };
        
        // Act
        orderService.ProcessOrder(order);
        
        // Assert
        mockRepository.Verify(r => r.Save(order), Times.Once);
        mockEmailService.Verify(e => e.SendConfirmation("test@example.com"), Times.Once);
    }
}

// Łatwe testowanie dzięki interfaces i DI! ✨

Multiple implementations

// Multiple implementations tego samego interface
services.AddTransient<INotificationService, EmailNotificationService>();
services.AddTransient<INotificationService, SmsNotificationService>();
services.AddTransient<INotificationService, PushNotificationService>();

// Inject IEnumerable<INotificationService> - wszystkie implementacje!
public class NotificationManager
{
    private readonly IEnumerable<INotificationService> _notifiers;
    
    public NotificationManager(IEnumerable<INotificationService> notifiers)
    {
        _notifiers = notifiers;  // Wszystkie 3 implementacje! ✨
    }
    
    public async Task NotifyAllAsync(string message)
    {
        foreach (var notifier in _notifiers)
        {
            await notifier.SendAsync(message);
        }
        // Wysyła Email + SMS + Push!
    }
}
Practical Patterns

Factory pattern z DI

// Factory pattern - dynamiczne tworzenie objects
public interface IReportGeneratorFactory
{
    IReportGenerator Create(ReportType type);
}

public class ReportGeneratorFactory : IReportGeneratorFactory
{
    private readonly IServiceProvider _serviceProvider;
    
    public ReportGeneratorFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public IReportGenerator Create(ReportType type)
    {
        return type switch
        {
            ReportType.Pdf => _serviceProvider.GetRequiredService<PdfReportGenerator>(),
            ReportType.Excel => _serviceProvider.GetRequiredService<ExcelReportGenerator>(),
            ReportType.Html => _serviceProvider.GetRequiredService<HtmlReportGenerator>(),
            _ => throw new ArgumentException("Unknown report type")
        };
    }
}

// Rejestracja
services.AddTransient<PdfReportGenerator>();
services.AddTransient<ExcelReportGenerator>();
services.AddTransient<HtmlReportGenerator>();
services.AddSingleton<IReportGeneratorFactory, ReportGeneratorFactory>();

// Użycie
public class ReportService
{
    private readonly IReportGeneratorFactory _factory;
    
    public ReportService(IReportGeneratorFactory factory)
    {
        _factory = factory;
    }
    
    public byte[] GenerateReport(ReportType type, ReportData data)
    {
        var generator = _factory.Create(type);  // Dynamiczne tworzenie!
        return generator.Generate(data);
    }
}

Options pattern

// Options pattern - strongly-typed configuration
public class EmailSettings
{
    public string SmtpServer { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}

// appsettings.json
// {
//   "EmailSettings": {
//     "SmtpServer": "smtp.gmail.com",
//     "Port": 587,
//     "Username": "user@example.com",
//     "Password": "secret"
//   }
// }

// Rejestracja
builder.Services.Configure<EmailSettings>(
    builder.Configuration.GetSection("EmailSettings")
);

// Użycie - inject IOptions<T>
public class EmailService
{
    private readonly EmailSettings _settings;
    
    public EmailService(IOptions<EmailSettings> options)
    {
        _settings = options.Value;  // Strongly-typed config! ✨
    }
    
    public void SendEmail(string to, string message)
    {
        // Use _settings.SmtpServer, _settings.Port, etc.
    }
}

Keyed services (.NET 8+)

// .NET 8 - Keyed services (multiple implementations z kluczem)
services.AddKeyedTransient<IPaymentProcessor, StripePaymentProcessor>("stripe");
services.AddKeyedTransient<IPaymentProcessor, PayPalPaymentProcessor>("paypal");
services.AddKeyedTransient<IPaymentProcessor, BankTransferProcessor>("bank");

// Inject z [FromKeyedServices]
public class PaymentService
{
    private readonly IPaymentProcessor _stripeProcessor;
    private readonly IPaymentProcessor _paypalProcessor;
    
    public PaymentService(
        [FromKeyedServices("stripe")] IPaymentProcessor stripeProcessor,
        [FromKeyedServices("paypal")] IPaymentProcessor paypalProcessor)
    {
        _stripeProcessor = stripeProcessor;
        _paypalProcessor = paypalProcessor;
    }
    
    public async Task ProcessPaymentAsync(string provider, decimal amount)
    {
        var processor = provider switch
        {
            "stripe" => _stripeProcessor,
            "paypal" => _paypalProcessor,
            _ => throw new ArgumentException("Unknown provider")
        };
        
        await processor.ProcessAsync(amount);
    }
}

Decorator pattern

// Decorator pattern - wrap service z dodatkową funkcjonalnością
public interface IOrderService
{
    Task ProcessOrderAsync(Order order);
}

public class OrderService : IOrderService
{
    public async Task ProcessOrderAsync(Order order)
    {
        // Core logic
        await SaveToDatabase(order);
    }
}

// Decorator - dodaje logging
public class LoggingOrderServiceDecorator : IOrderService
{
    private readonly IOrderService _inner;
    private readonly ILogger _logger;
    
    public LoggingOrderServiceDecorator(IOrderService inner, ILogger logger)
    {
        _inner = inner;
        _logger = logger;
    }
    
    public async Task ProcessOrderAsync(Order order)
    {
        _logger.LogInformation("Processing order {OrderId}", order.Id);
        
        try
        {
            await _inner.ProcessOrderAsync(order);
            _logger.LogInformation("Order {OrderId} processed successfully", order.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing order {OrderId}", order.Id);
            throw;
        }
    }
}

// Rejestracja z Scrutor library
services.AddTransient<OrderService>();
services.Decorate<IOrderService, LoggingOrderServiceDecorator>();

// Lub manual:
services.AddTransient<OrderService>();
services.AddTransient<IOrderService>(provider =>
{
    var inner = provider.GetRequiredService<OrderService>();
    var logger = provider.GetRequiredService<ILogger>();
    return new LoggingOrderServiceDecorator(inner, logger);
});

Background services

// Background service z DI
public class EmailBackgroundService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<EmailBackgroundService> _logger;
    
    public EmailBackgroundService(
        IServiceProvider serviceProvider,
        ILogger<EmailBackgroundService> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Create scope - ważne dla Scoped services!
            using (var scope = _serviceProvider.CreateScope())
            {
                var emailQueue = scope.ServiceProvider.GetRequiredService<IEmailQueue>();
                var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
                
                var email = await emailQueue.DequeueAsync(stoppingToken);
                if (email != null)
                {
                    await emailService.SendAsync(email);
                }
            }
            
            await Task.Delay(1000, stoppingToken);
        }
    }
}

// Rejestracja
services.AddHostedService<EmailBackgroundService>();
Podsumowanie

  • Problem bez DI - tight coupling, trudne testowanie
  • DI Container - automatyczne tworzenie dependencies
  • Microsoft.Extensions.DependencyInjection - built-in w .NET
  • Service lifetimes:
    • Transient - nowa instancja zawsze
    • Scoped - jedna per request
    • Singleton - jedna per aplikację
  • Constructor injection - recommended pattern
  • Practical patterns: Factory, Options, Keyed services, Decorator

Następny wpis: Testowanie - Unit testy w C#!