Dependency Injection в ASP.NET Core: принципы и практика

Введение: 

В мире enterprise-разработки на C# Dependency Injection давно перестал быть опциональным паттерном — это обязательный стандарт построения масштабируемых и поддерживаемых приложений. ASP.NET Core с самого начала был спроектирован с DI в ядре фреймворка, что кардинально отличает его от классической ASP.NET.

Почему DI стал критически важным?

  • Сложность современных систем: микросервисная архитектура требует четкого разделения ответственности

  • Тестируемость: только DI позволяет легко заменять реализации на моки в unit-тестах

  • Гибкость: замена компонентов без переписывания кода всей системы

  • Производительность: правильное управление временем жизни объектов снижает нагрузку на память

Что делает DI в ASP.NET Core особенным?

  • Встроенный контейнер: не требуются сторонние библиотеки для базовых сценариев

  • Интеграция со всеми компонентами: от контроллеров до middleware и фоновых служб

  • Глубокая оптимизация: разработан specifically для высоконагруженных web-приложений

  • Простота и мощность: минимальный код для старта, но неограниченные возможности для роста


1. Базовая регистрация сервисов

1.1. Регистрация в Startup.cs

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<IUserService, UserService>();
services.AddSingleton<ILogger, FileLogger>();
services.AddTransient<IEmailSender, EmailSender>();
}

1.2. Внедрение в контроллер

[ApiController]
[Route(«api/users»)]
public class UserController : ControllerBase
{
private readonly IUserService _userService;

public UserController(IUserService userService)
{
_userService = userService;
}

[HttpGet(«{id}»)]
public async Task<IActionResult> GetUser(int id)
{
var user = await _userService.GetUserAsync(id);
return Ok(user);
}
}

2. Время жизни сервисов

2.1. Transient — новая копия каждый раз

services.AddTransient<ITransientService, TransientService>();

2.2. Scoped — одна копия на запрос

services.AddScoped<IScopedService, ScopedService>();

2.3. Singleton — одна копия на всё приложение

services.AddSingleton<ISingletonService, SingletonService>();

3. Фабрики и опции

3.1. Фабрика сервисов

services.AddTransient<IService>(provider =>
{
var config = provider.GetService<IConfiguration>();
return new Service(config.GetValue<string>(«ConnectionString»));
});

3.2. Конфигурация Options

services.Configure<DatabaseOptions>(Configuration.GetSection(«Database»));

3.3. Внедрение IOptions

public class UserService
{
private readonly DatabaseOptions _options;

public UserService(IOptions<DatabaseOptions> options)
{
_options = options.Value;
}
}

4. Интеграция с Entity Framework

4.1. Регистрация DbContext

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString(«Default»)));

4.2. Использование в репозитории

public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _context;

public UserRepository(ApplicationDbContext context)
{
_context = context;
}

public async Task<User> GetUserAsync(int id)
{
return await _context.Users.FindAsync(id);
}
}

5. Ошибки и лучшие практики

5.1. Captive Dependency — неправильное время жизни

// ОШИБКА: Singleton зависит от Scoped
services.AddSingleton<ISingletonService>(provider =>
{
var scopedService = provider.GetService<IScopedService>(); // Захват Scoped в Singleton!
return new SingletonService(scopedService);
});

5.2. Правильное решение

services.AddSingleton<ISingletonService>(provider =>
{
using var scope = provider.CreateScope();
var scopedService = scope.ServiceProvider.GetService<IScopedService>();
return new SingletonService(scopedService);
});

5.3. Внедрение в Middleware

public class CustomMiddleware
{
private readonly RequestDelegate _next;
private readonly IService _service;

public CustomMiddleware(RequestDelegate next, IService service)
{
_next = next;
_service = service;
}

public async Task InvokeAsync(HttpContext context)
{
await _service.DoSomethingAsync();
await _next(context);
}
}

6. Продвинутые сценарии

6.1. Generic-сервисы

services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

6.2. Группировка регистраций

services.AddInfrastructureServices()
.AddApplicationServices()
.AddDomainServices();

6.3. Валидация зависимостей

var serviceProvider = services.BuildServiceProvider();
serviceProvider.ValidateScopes(); // Проверка в Development


Заключение

Dependency Injection в ASP.NET Core — это не просто технология, а философия построения современных приложений. Мы рассмотрели ключевые аспекты:

  1. Упрощение архитектуры через инверсию управления и уменьшение связанности компонентов

  2. Контроль времени жизни объектов (Transient, Scoped, Singleton) для оптимизации памяти

  3. Глубокая интеграция с Entity Framework, Middleware и другими компонентами фреймворка

  4. Повышение тестируемости за счет легкого внедрения моков и заглушек

  5. Избежание распространенных ошибок (Captive Dependency, неправильная регистрация)

Освоив DI, вы не просто научились использовать контейнер зависимостей — вы начали мыслить в терминах слабосвязанной архитектуры, что является обязательным навыком для современного enterprise-разработчика.


Для дальнейшего изучения

  1. Сторонние контейнеры — Autofac, DryIoc и их преимущества перед встроенным

  2. Паттерн Decorator — для кеширования, логирования и cross-cutting concerns

  3. Source Generators — автоматическая регистрация сервисов

  4. Integration with Hosted Services — правильное внедрение в фоновые задачи

  5. Dynamic factories — создание объектов с runtime-параметрами

  6. Validation scopes — диагностика проблем с временем жизни

  7. Performance optimization — сравнение различных подходов к регистрации