C#MiddleSystem design
Объясните концепцию dependency injection и как она реализуется в C#.
DI в .NET — встроенный IoC-контейнер (Microsoft.Extensions.DependencyInjection) с тремя lifetime: Transient (новый каждый раз), Scoped (один на HTTP-запрос), Singleton (один на приложение). Главная ловушка — captive dependency: Singleton, захвативший Scoped-сервис, ломает его жизненный цикл.
Dependency Injection в C# и контейнер .NET
Dependency Injection (DI) — паттерн, при котором зависимости объекта передаются снаружи, а не создаются внутри. В .NET это реализует встроенный IoC-контейнер из пакета Microsoft.Extensions.DependencyInjection, который регистрирует сервисы и разрешает их через конструкторы.
Регистрация сервисов
// Program.cs (ASP.NET Core)
var builder = WebApplication.CreateBuilder(args);
// Transient: новый экземпляр на каждый запрос зависимости
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
// Scoped: один экземпляр на HTTP-запрос
builder.Services.AddScoped<IJobRepository, JobRepository>();
builder.Services.AddScoped<AppDbContext>(); // EF Core DbContext — всегда Scoped
// Singleton: один экземпляр на время жизни приложения
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
// Фабричная регистрация
builder.Services.AddTransient<ISearchClient>(sp =>
{
var cfg = sp.GetRequiredService<IConfiguration>();
return new MeilisearchClient(cfg["Search:Url"]!, cfg["Search:ApiKey"]!);
});
Внедрение через конструктор
public class JobRepository : IJobRepository
{
private readonly AppDbContext _db;
private readonly ILogger<JobRepository> _logger;
// DI-контейнер разрешает зависимости автоматически
public JobRepository(AppDbContext db, ILogger<JobRepository> logger)
{
_db = db;
_logger = logger;
}
public async Task<Job?> GetByIdAsync(Guid id, CancellationToken ct)
{
_logger.LogDebug("Fetching job {Id}", id);
return await _db.Jobs
.AsNoTracking()
.FirstOrDefaultAsync(j => j.Id == id, ct);
}
}
Жизненные циклы и когда что использовать
- Transient — лёгкие stateless-сервисы без shared state (валидаторы, mappers). Создаётся и уничтожается на каждом resolve.
- Scoped — сервисы, привязанные к единице работы: HTTP-запрос, Background Job.
DbContextвсегда Scoped — один контекст на запрос гарантирует целостность change tracker. - Singleton — дорогие в создании или разделяемые объекты: HTTP-клиенты (через
IHttpClientFactory), кеш, конфиги. Должны быть thread-safe.
Keyed services (ASP.NET Core 8+)
// Регистрация нескольких реализаций одного интерфейса с ключом
builder.Services.AddKeyedSingleton<IPaymentGateway, StripeGateway>("stripe");
builder.Services.AddKeyedSingleton<IPaymentGateway, YooKassaGateway>("yookassa");
// Получение по ключу
public class PaymentService(
[FromKeyedServices("stripe")] IPaymentGateway stripe,
[FromKeyedServices("yookassa")] IPaymentGateway yookassa)
{ ... }
Подводные камни
- Captive dependency (захваченная зависимость). Singleton, захвативший Scoped-зависимость — самая частая ошибка. Singleton живёт вечно и держит Scoped-объект, который должен умирать на каждом запросе. При включённой валидации (
ValidateOnBuild = true) контейнер бросит исключение на старте. - Resolver в конструкторе. Не используйте
IServiceProvider.GetService<T>()внутри конструкторов — это Service Locator антипаттерн. Внедряйте зависимости явно. - DbContext в Singleton.
DbContextне thread-safe; регистрация как Singleton приведёт к concurrency-исключениям. Всегда Scoped. - IHttpClientFactory vs new HttpClient. Создание
new HttpClient()в Singleton исчерпывает сокеты (socket exhaustion). ИспользуйтеIHttpClientFactoryс именованными клиентами. - Dispose и Scoped. Контейнер вызывает
Disposeна Scoped/Transient-сервисах в конце scope. Если зависимость создана вне контейнера и передана черезAddSingleton(instance)— контейнер её не Dispose-ит. - Открытые дженерики.
services.AddTransient(typeof(IRepository<>), typeof(Repository<>))— регистрация открытого дженерика. Не все контейнеры поддерживают это одинаково.
Common mistakes
- Путать dependency injection и контейнер сервисов .NET с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы C#: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет dependency injection и контейнер сервисов .NET на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.