Какие методы будешь использовать для реализации DI-контейнера

«Какие методы будешь использовать для реализации DI-контейнера» — вопрос из категории Архитектура, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

При реализации простого DI-контейнера "с нуля" я бы сосредоточился на следующих ключевых методах и принципах:

1. Регистрация сервисов (Registration)

  • Register<TService, TImplementation>() — регистрация сопоставления типа интерфейса/абстракции с конкретной реализацией.
  • RegisterSingleton<TService>(TService instance) — регистрация готового экземпляра как синглтона.
  • RegisterInstance<TService>(Func<IServiceProvider, TService> factory) — регистрация фабрики для создания экземпляра.

2. Разрешение зависимостей (Resolution)

  • TService GetService<TService>() — основной метод для получения экземпляра сервиса.
  • object GetService(Type serviceType) — нетипизированная версия для динамических сценариев.
  • Внутри этого метода контейнер должен рекурсивно обходить граф зависимостей, создавая все необходимые экземпляры.

3. Управление жизненным циклом (Lifetime Management)

Контейнер должен поддерживать три основных скоупа:

  • Transient — новый экземпляр при каждом запросе.
  • Scoped — один экземпляр на область видимости (например, на web-запрос). Требует реализации IServiceScope.
  • Singleton — один экземпляр на весь жизненный цикл контейнера.

Пример упрощённой реализации

public class SimpleContainer
{
    private readonly Dictionary<Type, ServiceDescriptor> _descriptors = new();

    public void RegisterTransient<TService, TImplementation>() where TImplementation : TService
    {
        _descriptors[typeof(TService)] = new ServiceDescriptor(typeof(TImplementation), ServiceLifetime.Transient);
    }

    public TService GetService<TService>()
    {
        return (TService)GetService(typeof(TService));
    }

    private object GetService(Type serviceType)
    {
        if (!_descriptors.TryGetValue(serviceType, out var descriptor))
            throw new InvalidOperationException($"Service {serviceType.Name} is not registered.");

        // Для синглтона может потребоваться кэширование созданного экземпляра
        var implementationType = descriptor.ImplementationType;
        var constructor = implementationType.GetConstructors().First();
        var parameters = constructor.GetParameters()
                                    .Select(p => GetService(p.ParameterType)) // Рекурсивное разрешение
                                    .ToArray();

        return constructor.Invoke(parameters);
    }
}

public class ServiceDescriptor
{
    public Type ImplementationType { get; }
    public ServiceLifetime Lifetime { get; }
    // ... конструктор и свойства
}

Важное замечание: В реальных проектах почти всегда лучше использовать проверенные библиотеки (Microsoft.Extensions.DependencyInjection, Autofac, Ninject), которые уже решают сложные проблемы: циклические зависимости, валидацию графа, производительность и интеграцию с различными фреймворками. Самописный контейнер оправдан только для очень специфических учебных или изолированных задач.