DI Анти-паттерни та Найкращі Практики
DI Анти-паттерни та Найкращі Практики
Вступ: Чому DI може стати проблемою?
DI — потужний інструмент. Але як і будь-який інструмент, його можна використовувати неправильно. Дивно, що помилки з DI часто роблять досвідчені розробники, особливо при роботі з незнайомим кодом або при перенесенні legacy-рішень.
Ця стаття — збірник найпоширеніших анти-паттернів, кожен з яких пояснений: чому це погано, і як це виправити.
Анти-паттерн 1: Service Locator у бізнес-логіці
Проблема: Впровадження IServiceProvider у бізнес-сервіси та виклик GetRequiredService<T>() всередині методів.
// ❌ Анти-паттерн
public class OrderService
{
private readonly IServiceProvider _sp;
public OrderService(IServiceProvider sp)
{
_sp = sp;
}
public void PlaceOrder(Order order)
{
// Service Locator у бізнес-логіці!
var repo = _sp.GetRequiredService<IOrderRepository>();
var email = _sp.GetRequiredService<IEmailService>();
repo.Save(order);
email.Send(order.CustomerEmail, "...");
}
}
Чому погано:
- Приховані залежності (конструктор не відображає реальних потреб)
- Важко тестувати (треба мокати
IServiceProvider) - Порушення SOLID
// ✅ Правильно: Constructor Injection
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly IEmailService _email;
public OrderService(IOrderRepository repo, IEmailService email)
{
_repo = repo;
_email = email;
}
public void PlaceOrder(Order order)
{
_repo.Save(order);
_email.Send(order.CustomerEmail, "...");
}
}
Анти-паттерн 2: Captive Dependency
Проблема: Singleton тримає посилання на Scoped або Transient сервіс.
// ❌ Анти-паттерн: Singleton захоплює Scoped
services.AddSingleton<BackgroundJobService>();
services.AddScoped<IJobRepository, SqlJobRepository>();
public class BackgroundJobService // Singleton
{
private readonly IJobRepository _repo; // Scoped — CAPTIVE!
public BackgroundJobService(IJobRepository repo)
{
_repo = repo; // Захоплено назавжди з першого scope
}
}
Чому погано:
- Scoped сервіс живе назавжди замість одного scope
- DbContext закривається але посилання лишається →
ObjectDisposedException - Дані "протікають" між HTTP-запитами
// ✅ Правильно: IServiceScopeFactory для Scoped залежностей у Singleton
public class BackgroundJobService // Singleton
{
private readonly IServiceScopeFactory _scopeFactory;
public BackgroundJobService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public void ProcessJobs()
{
using var scope = _scopeFactory.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IJobRepository>();
var jobs = repo.GetPending();
// ...
} // ← repo та DbContext знищуються тут
}
ValidateScopes = true у BuildServiceProvider. У режимі Development в ASP.NET Core — це автоматично. Не вимикайте це!Анти-паттерн 3: Constructor Over-injection
Проблема: Конструктор з 6+ параметрами — ознака порушення SRP.
// ❌ Анти-паттерн: занадто багато залежностей
public class OrderService
{
public OrderService(
IOrderRepository orderRepo,
IProductRepository productRepo,
IUserRepository userRepo,
IEmailService emailService,
ISmsService smsService,
IPushService pushService,
IPaymentGateway payment,
IInventoryService inventory,
IShippingService shipping,
IAuditService audit,
ILogger<OrderService> logger)
{ }
}
Чому погано:
- Клас має занадто багато відповідальностей (порушення SRP)
- Важко тестувати (потрібно 11 моків!)
- Ознака "God Class"
// ✅ Правильно: виділяємо поддомени у окремі сервіси
// Сервіс сповіщень
public class NotificationService
{
public NotificationService(IEmailService email, ISmsService sms, IPushService push) { }
public void NotifyCustomer(Order order) { /* ... */ }
}
// Сервіс обробки оплати і доставки
public class FulfillmentService
{
public FulfillmentService(IPaymentGateway payment, IInventoryService inventory, IShippingService shipping) { }
public bool Fulfill(Order order) { /* ... */ }
}
// Тепер OrderService — тільки оркестрація
public class OrderService
{
public OrderService(
IOrderRepository orderRepo,
NotificationService notifications,
FulfillmentService fulfillment,
IAuditService audit,
ILogger<OrderService> logger)
{ }
}
Анти-паттерн 4: Static Coupling та new у сервісі
Проблема: Прямий виклик new для залежностей або використання статичних методів.
// ❌ Анти-паттерн: new у сервісі
public class OrderService
{
private readonly IOrderRepository _repo;
public OrderService(IOrderRepository repo)
{
_repo = repo;
}
public void PlaceOrder(Order order)
{
// Hard-coded new — порушує DI!
var emailService = new SmtpEmailService("smtp.gmail.com", 587); // Статичний виклик — те саме!
var logger = LoggerFactory.GetLogger<OrderService>();
emailService.Send(order.CustomerEmail, "...");
}
}
// ✅ Правильно: всі залежності через конструктор
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly IEmailService _email; private readonly ILogger<OrderService> _logger;
public OrderService( IOrderRepository repo, IEmailService email, ILogger<OrderService> logger) { _repo = repo; _email = email; _logger = logger; }
public void PlaceOrder(Order order)
{
_email.Send(order.CustomerEmail, "...");
_logger.LogInformation("Order placed: {Id}", order.Id);
}
}
Анти-паттерн 5: Ambient Context (глобальні статичні залежності)
Проблема: Використання static properties або глобальних синглтонів для доступу до "контексту".
// ❌ Анти-паттерн: глобальний статичний контекст
public static class CurrentUser
{
public static string? UserId { get; set; } // Глобальний стан!
public static string? Email { get; set; }
}
// Використання — здається зручним, але...
public class OrderService
{
public void PlaceOrder(Order order)
{
var userId = CurrentUser.UserId; // Звідки це значення в тесті?!
// ...
}
}
// ✅ Правильно: ICurrentUserService через DI
public interface ICurrentUserService
{
string? UserId { get; }
string? Email { get; }
}
public class HttpContextCurrentUserService : ICurrentUserService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextCurrentUserService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string? UserId =>
_httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
public string? Email =>
_httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.Email)?.Value;
}
// Реєстрація
services.AddHttpContextAccessor();
services.AddScoped<ICurrentUserService, HttpContextCurrentUserService>();
// У тестах — легко мокати!
var mockUser = new Mock<ICurrentUserService>();
mockUser.Setup(u => u.UserId).Returns("test-user-id");
Анти-паттерн 6: Реєстрація всього як Singleton
Проблема: Всі сервіси реєструються як Singleton через "продуктивність".
// ❌ Анти-паттерн: всі Singleton
services.AddSingleton<IOrderRepository, SqlOrderRepository>(); // DbContext — НЕ thread-safe!
services.AddSingleton<AppDbContext>(); // ДУЖЕ ПОГАНО!
services.AddSingleton<IShoppingCart, UserShoppingCart>(); // Спільний для всіх!
Наслідки:
AppDbContextяк Singleton → спільний для всіх запитів → race conditions → corrupted dataIShoppingCartяк Singleton → ОДИН кошик для ВСІХ користувачів → дані замовника A у замовника B!
// ✅ Правильно: правильний lifetime для кожного сервісу
services.AddDbContext<AppDbContext>(...); // Автоматично Scoped
services.AddScoped<IOrderRepository, SqlOrderRepository>(); // Scoped (має DbContext)
services.AddScoped<IShoppingCart, UserShoppingCart>(); // Scoped (ізольований до запиту)
services.AddSingleton<ICacheService, MemoryCacheService>(); // Singleton (thread-safe, shared cache)
Анти-паттерн 7: Ін'єкція IConfiguration напряму
Проблема: Inject IConfiguration і читати значення configuration["Key"] у бізнес-класах.
// ❌ Анти-паттерн
public class EmailService
{
private readonly IConfiguration _config; // Пряма залежність від IConfiguration!
public EmailService(IConfiguration configuration)
{
_config = configuration;
}
public void Send(string to, string body)
{
var host = _config["Smtp:Host"]!; // Magic strings!
var port = int.Parse(_config["Smtp:Port"]!); // Може кинути FormatException!
// ...
}
}
Чому погано:
- "Magic strings" — помилки тільки в runtime
- Важко тестувати (треба mock IConfiguration)
- Клас знає про всю конфігурацію, а не тільки про свою частину
// ✅ Правильно: Options Pattern через IOptions<T>
public class SmtpOptions
{
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
// У Program.cs:
services.Configure<SmtpOptions>(configuration.GetSection("Smtp"));
// У сервісі:
public class EmailService
{
private readonly SmtpOptions _options;
// IOptions<T> або IOptionsSnapshot<T> — строго типізований доступ!
public EmailService(IOptions<SmtpOptions> options)
{
_options = options.Value;
}
public void Send(string to, string body)
{
var host = _options.Host; // Типобезпечно! Compile-time перевірка!
var port = _options.Port;
// ...
}
}
// У тестах:
var options = Options.Create(new SmtpOptions { Host = "test-smtp", Port = 465 });
var emailService = new EmailService(options); // Легко!
Найкращі Практики: Загальний Чекліст
Реєстрація
- Організовуйте реєстрації через Extension Methods по модулях
- Використовуйте
TryAdd*у бібліотечному коді - Завжди вмикайте
ValidateOnBuildтаValidateScopes - Реєструйте з найменш привілейованим lifetime (Transient → Scoped → Singleton)
Ін'єкція
- Constructor Injection — за замовчуванням завжди
readonlyдля всіх полів залежностей- Guard clauses (
?? throw new ArgumentNullException) для критичних залежностей - Уникайте
IServiceProviderу бізнес-класах
Lifetimes
- Перевіряйте Captive Dependency перед реєстрацією
- Singleton — тільки thread-safe сервіси без DbContext
- Scoped — для всього пов'язаного з DbContext
- BackgroundService →
IServiceScopeFactoryдля Scoped
Тестування
- Тести не потребують DI-контейнера — передавайте моки напряму
- Перевіряйте коректність реєстрацій інтеграційними тестами
- Використовуйте
WebApplicationFactory<T>для тестів ASP.NET Core
📝 Підсумкове Тестування
Наприкінці курсу перевірте свої знання з усього модулю. Дайте відповідь на наступні питання без допомоги матеріалів, потім перевірте себе:
ValidateScopes = true у BuildServiceProvider. Виправлення: IServiceScopeFactory у Singleton.Type.GetConstructors() та ConstructorInfo.GetParameters() для автоматичного визначення залежностей класу та їх resolve через рекурсивні виклики GetService().GetService<T>(). Обидва знищуються при закритті scope якщо реалізують IDisposable.Корисні Посилання
Service Lifetimes та Scopes
Глибокий розбір трьох часів життя сервісів у DI: Transient, Scoped та Singleton. Як розуміти Scope, IDisposable і прибирання за сервісами. Критичний анти-паттерн Captive Dependency.
Collections (Колекції)
Повний гід по колекціях у .NET — від generic collections до concurrent та immutable типів, з детальним розбором продуктивності та практичних сценаріїв.