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!
🔍 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