.NET Tutorial
Junior5 мин чтенияВопрос 3 из 19

Как работает Dependency Injection встроенный в ASP.NET Core?

Без DI и с DI

C#
// Без DI: класс сам создаёт зависимости — жёсткая связь, нельзя подменить в тестах
public class OrderService
{
    private readonly SqlOrderRepository _repo = new SqlOrderRepository("Server=...");
    private readonly SmtpEmailSender _email = new SmtpEmailSender();
}

// С DI: класс объявляет, что ему нужно, а кто и как это создаёт — не его забота
public class OrderService(IOrderRepository repo, IEmailSender email, ILogger<OrderService> log)
{
    public async Task PlaceAsync(Order order, CancellationToken ct)
    {
        await repo.AddAsync(order, ct);
        await email.SendAsync(order.CustomerEmail, "Заказ принят", ct);
        log.LogInformation("Order {Id} placed", order.Id);
    }
}

Как контейнер строит граф

OrdersController

IOrderService → OrderService
(Scoped)

IOrderRepository → EfOrderRepository
(Scoped)

IEmailSender → SmtpEmailSender
(Singleton)

ILogger<OrderService>
(Singleton)

AppDbContext
(Scoped)

DbContextOptions
(Singleton)

IOptions<SmtpSettings>
(Singleton)

OrdersController

IOrderService → OrderService
(Scoped)

IOrderRepository → EfOrderRepository
(Scoped)

IEmailSender → SmtpEmailSender
(Singleton)

ILogger<OrderService>
(Singleton)

AppDbContext
(Scoped)

DbContextOptions
(Singleton)

IOptions<SmtpSettings>
(Singleton)

Разрешение зависимостей для OrdersController
Scope запросаIServiceProviderIServiceCollectionProgram.csпроверка графа (вDevelopment)пришёлHTTP-запросзапрос завершёнAddScoped, AddSingleton,AddTransientbuilder.Build()CreateScope()GetService(OrdersCont-roller)рекурсивно создатьзависимости поконструкторамDispose() —освободить Scoped иTransient IDisposable
Scope запросаIServiceProviderIServiceCollectionProgram.csпроверка графа (вDevelopment)пришёлHTTP-запросзапрос завершёнAddScoped, AddSingleton,AddTransientbuilder.Build()CreateScope()GetService(OrdersCont-roller)рекурсивно создатьзависимости поконструкторамDispose() —освободить Scoped иTransient IDisposable
Два этапа: регистрация и разрешение

Регистрация

C#
var builder = WebApplication.CreateBuilder(args);

// Базовые варианты
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddTransient<IPasswordHasher, BcryptHasher>();

// Фабрика — когда нужна логика создания
builder.Services.AddSingleton<IStorage>(sp =>
{
    var cfg = sp.GetRequiredService<IOptions<StorageOptions>>().Value;
    return cfg.UseS3 ? new S3Storage(cfg) : new LocalStorage(cfg.Path);
});

// Готовый экземпляр
builder.Services.AddSingleton(TimeProvider.System);

// Open generics: IRepository<User> → Repository<User> и т.д.
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

// Несколько реализаций одного интерфейса
builder.Services.AddScoped<INotifier, EmailNotifier>();
builder.Services.AddScoped<INotifier, SmsNotifier>();
// IEnumerable<INotifier> получит обе; INotifier — последнюю зарегистрированную

// TryAdd — не регистрировать, если уже есть (для библиотек)
builder.Services.TryAddSingleton<IClock, SystemClock>();

// Keyed services (.NET 8)
builder.Services.AddKeyedSingleton<IPaymentGateway, PaymeGateway>("payme");
builder.Services.AddKeyedSingleton<IPaymentGateway, ClickGateway>("click");

public class CheckoutService([FromKeyedServices("payme")] IPaymentGateway gateway) { }

Способы получения зависимостей

Способ Где Комментарий
Конструктор Везде Основной способ, зависимости явные
Параметр действия [FromServices] Контроллер Если сервис нужен только одному действию
Параметр обработчика Minimal API Minimal API Резолвится автоматически
Параметр InvokeAsync Middleware Для scoped-сервисов в middleware
IServiceProvider.GetRequiredService Фабрики, инфраструктура Service Locator — избегать в бизнес-коде
IServiceScopeFactory.CreateScope() Singleton, BackgroundService Для работы со scoped-сервисами вне запроса
Scoped-сервис из фоновой службы
public class CleanupWorker(IServiceScopeFactory scopes) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using (var scope = scopes.CreateScope())
            {
                var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                await db.Sessions.Where(s => s.ExpiresAt < DateTime.UtcNow).ExecuteDeleteAsync(ct);
            }
            await Task.Delay(TimeSpan.FromMinutes(10), ct);
        }
    }
}

Выбор конструктора и проверки

  • Контейнер выбирает конструктор с наибольшим числом параметров, которые он может разрешить. Неоднозначность → исключение. Лучше иметь один публичный конструктор.
  • ValidateScopes (включено в Development) — ошибка при внедрении Scoped в Singleton.
  • ValidateOnBuild — проверка, что все зарегистрированные сервисы можно создать, при старте, а не при первом запросе.
C#
builder.Host.UseDefaultServiceProvider(o =>
{
    o.ValidateScopes = true;
    o.ValidateOnBuild = true;
});

Ограничения встроенного контейнера

Встроенный Microsoft.Extensions.DependencyInjection намеренно простой. Нет: внедрения в свойства, декораторов «из коробки», перехватчиков (AOP), регистрации по соглашениям (сканирования сборок), дочерних контейнеров.

Решения:

  • Scrutor — сканирование сборок (services.Scan(...)) и декораторы (services.Decorate<IOrderService, CachedOrderService>()).
  • Сторонние контейнеры (Autofac, DryIoc) через UseServiceProviderFactory.
Декоратор вручную
builder.Services.AddScoped<OrderService>();
builder.Services.AddScoped<IOrderService>(sp =>
    new LoggingOrderService(sp.GetRequiredService<OrderService>(), sp.GetRequiredService<ILogger<LoggingOrderService>>()));

Анти-паттерны

  • Слишком много зависимостей в конструкторе (7+) — сигнал нарушения SRP; класс делает слишком много.
  • Captive dependency — Scoped/Transient внутри Singleton «застревает» навсегда (подробно в следующем вопросе).
  • Регистрация конкретных классов вместо абстракций там, где нужна подмена — затрудняет тестирование.
  • Логика в конструкторе (I/O, запросы к БД) — замедляет разрешение графа и превращает ошибки конфигурации в ошибки DI.

Вопросы на засыпку

Чем IoC отличается от DI?

Inversion of Control — общий принцип: не ваш код управляет потоком/созданием объектов, а фреймворк. DI — конкретная техника реализации IoC для зависимостей: объекты получают зависимости извне. Service Locator — другая реализация IoC.

Что будет, если зарегистрировать один интерфейс дважды?

При внедрении IService получите последнюю регистрацию. При внедрении IEnumerable<IService> — все в порядке регистрации. TryAdd* не добавит, если сервис уже есть; TryAddEnumerable не добавит дубликат той же реализации.

Вызывает ли контейнер Dispose у объектов, которые вы передали экземпляром?

Нет. AddSingleton(new MyService()) — контейнер не создавал объект и не владеет им, поэтому не освобождает. Объекты, созданные контейнером (в том числе через фабрику), освобождаются.

Как работает внедрение в Minimal API без атрибутов?

Генератор (или рефлексия при старте) определяет источник каждого параметра: маршрут, query, тело, заголовки, специальные типы (HttpContext, CancellationToken) или DI — если тип зарегистрирован в контейнере (IServiceProviderIsService).

Итог

Встроенный DI в ASP.NET Core — простой и быстрый контейнер: регистрируем в IServiceCollection, получаем через конструктор, контейнер строит граф и управляет временем жизни и освобождением. Держите зависимости явными, конструкторы — лёгкими, а проверку графа — включённой.