Dependency Injection контейнер для C# (.NET): полное руководство
Dependency Injection (DI) — это один из ключевых паттернов проектирования в современной разработке на C# и .NET. Он позволяет создавать слабосвязанные, тестируемые и легко поддерживаемые приложения. В этой статье мы подробно рассмотрим, что такое Dependency Injection контейнер, как его использовать в .NET, и приведем практические примеры кода.
Что такое Dependency Injection?
Dependency Injection (внедрение зависимостей) — это паттерн, при котором объект получает свои зависимости извне, а не создает их самостоятельно. Это позволяет:
- Уменьшить связанность между компонентами
- Упростить тестирование (возможность подмены зависимостей моками)
- Повысить гибкость и переиспользуемость кода
- Централизовать управление жизненным циклом объектов
Что такое DI контейнер?
DI контейнер (или IoC контейнер) — это библиотека, которая автоматизирует процесс внедрения зависимостей. Он берет на себя ответственность за создание объектов и управление их временем жизни. В .NET основным DI контейнером является Microsoft.Extensions.DependencyInjection, который встроен в ASP.NET Core и может использоваться в любых .NET приложениях.
Основные понятия DI контейнера
DI контейнер оперирует следующими понятиями:
- Регистрация служб — процесс добавления типов и их реализаций в контейнер
- Разрешение зависимостей — автоматическое создание объектов с их зависимостями
- Время жизни служб — определяет, как долго существует объект (Transient, Scoped, Singleton)
Настройка DI контейнера в .NET
Для начала работы с DI контейнером необходимо установить пакет Microsoft.Extensions.DependencyInjection. Это можно сделать через NuGet:
dotnet add package Microsoft.Extensions.DependencyInjection
После установки создается экземпляр ServiceCollection, в котором регистрируются все зависимости:
using Microsoft.Extensions.DependencyInjection;
// Создаем коллекцию служб
var services = new ServiceCollection();
// Регистрируем службы
services.AddTransient<ILogger, ConsoleLogger>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddSingleton<IConfiguration, AppConfiguration>();
// Строим провайдер служб
var serviceProvider = services.BuildServiceProvider();
Пример использования DI контейнера
Рассмотрим реальный пример: приложение для управления пользователями. Создадим интерфейсы и их реализации:
public interface IUserRepository
{
User GetUser(int id);
}
public interface IEmailService
{
void SendEmail(string to, string subject);
}
public class UserRepository : IUserRepository
{
public User GetUser(int id)
{
// Логика получения пользователя из БД
return new User { Id = id, Name = "John Doe" };
}
}
public class EmailService : IEmailService
{
public void SendEmail(string to, string subject)
{
Console.WriteLine($"Email sent to {to}: {subject}");
}
}
Теперь создадим класс UserService, который использует эти зависимости:
public class UserService
{
private readonly IUserRepository _userRepository;
private readonly IEmailService _emailService;
public UserService(IUserRepository userRepository, IEmailService emailService)
{
_userRepository = userRepository;
_emailService = emailService;
}
public void NotifyUser(int userId)
{
var user = _userRepository.GetUser(userId);
_emailService.SendEmail(user.Email, "Welcome!");
}
}
Регистрируем все в DI контейнере и используем:
var services = new ServiceCollection();
services.AddTransient<IUserRepository, UserRepository>();
services.AddTransient<IEmailService, EmailService>();
services.AddTransient<UserService>();
var serviceProvider = services.BuildServiceProvider();
var userService = serviceProvider.GetService<UserService>();
userService.NotifyUser(1);
Время жизни служб (Lifetimes)
DI контейнер поддерживает три основных времени жизни:
- Transient — новый экземпляр создается каждый раз при запросе. Идеально для легковесных, не имеющих состояния служб.
- Scoped — один экземпляр на область (например, на HTTP-запрос в ASP.NET Core). Подходит для контекстов БД (DbContext).
- Singleton — один экземпляр на все приложение. Используется для кэшей, конфигураций, логгеров.
// Transient: каждый запрос создает новый экземпляр
services.AddTransient<IMyService, MyService>();
// Scoped: один экземпляр на область (например, на запрос)
services.AddScoped<IMyService