← Назад к вопросам

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

2.2 Middle🔥 172 комментариев
#Dependency Injection и IoC

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Подходы к реализации DI-контейнера

При реализации собственного DI-контейнера (контейнера внедрения зависимостей) я бы использовал комбинацию классических шаблонов проектирования и современных практик, характерных для .NET. Вот ключевые методы и подходы, которые я бы применил:

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

Основной метод — предоставление API для регистрации сервисов с их жизненным циклом. Реализую интерфейс наподобие IServiceCollection:

public interface IServiceContainer
{
    void RegisterSingleton<TService, TImplementation>() where TImplementation : TService;
    void RegisterScoped<TService, TImplementation>() where TImplementation : TService;
    void RegisterTransient<TService, TImplementation>() where TImplementation : TService;
    void RegisterInstance<TService>(TService instance);
}

Для хранения регистраций использую словарь:

private readonly Dictionary<Type, ServiceDescriptor> _descriptors = new();

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

Ключевой метод — рекурсивное создание объектов с учетом их зависимостей. Использую рефлексию для анализа конструкторов:

public object GetService(Type serviceType)
{
    if (!_descriptors.ContainsKey(serviceType))
    {
        throw new InvalidOperationException($"Service {serviceType.Name} not registered");
    }
    
    var descriptor = _descriptors[serviceType];
    return CreateInstance(descriptor);
}

private object CreateInstance(ServiceDescriptor descriptor)
{
    // Если уже есть экземпляр (для Singleton), возвращаем его
    if (descriptor.Instance != null && descriptor.Lifetime == ServiceLifetime.Singleton)
    {
        return descriptor.Instance;
    }
    
    // Получаем конструктор с наибольшим количеством параметров
    var constructor = descriptor.ImplementationType
        .GetConstructors()
        .OrderByDescending(c => c.GetParameters().Length)
        .First();
    
    // Рекурсивно разрешаем зависимости конструктора
    var parameters = constructor.GetParameters()
        .Select(p => GetService(p.ParameterType))
        .ToArray();
    
    var instance = Activator.CreateInstance(descriptor.ImplementationType, parameters);
    
    // Сохраняем экземпляр для Singleton
    if (descriptor.Lifetime == ServiceLifetime.Singleton)
    {
        descriptor.Instance = instance;
    }
    
    return instance;
}

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

Реализую три основных типа жизненного цикла:

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

Для этого создам перечисление и класс-дескриптор:

public enum ServiceLifetime
{
    Singleton,
    Scoped,
    Transient
}

public class ServiceDescriptor
{
    public Type ServiceType { get; }
    public Type ImplementationType { get; }
    public ServiceLifetime Lifetime { get; }
    public object Instance { get; set; }
    
    public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime)
    {
        ServiceType = serviceType;
        ImplementationType = implementationType;
        Lifetime = lifetime;
    }
}

4. Поддержка областей видимости (Scopes)

Для поддержки Scoped зависимостей реализую паттерн "Composite Root":

public interface IServiceScope : IDisposable
{
    IServiceProvider ServiceProvider { get; }
}

public interface IServiceScopeFactory
{
    IServiceScope CreateScope();
}

Каждая область будет иметь свой словарь экземпляров Scoped-сервисов.

5. Валидация графа зависимостей

Важный метод — проверка циклических зависимостей:

private void ValidateCircularDependencies(Type serviceType, HashSet<Type> visited)
{
    if (visited.Contains(serviceType))
    {
        throw new InvalidOperationException($"Circular dependency detected for {serviceType.Name}");
    }
    
    visited.Add(serviceType);
    
    var descriptor = _descriptors[serviceType];
    var constructor = descriptor.ImplementationType
        .GetConstructors()
        .First();
    
    foreach (var param in constructor.GetParameters())
    {
        ValidateCircularDependencies(param.ParameterType, new HashSet<Type>(visited));
    }
}

6. Оптимизация производительности

Для production-реализации добавлю:

  • Кэширование скомпилированных конструкторов с использованием Expression Trees
  • Ленивое создание экземпляров
  • Потокобезопасность для Singleton-сервисов

Пример с Expression Trees:

private delegate object ObjectFactory(IServiceProvider provider);
private readonly Dictionary<Type, ObjectFactory> _factories = new();

private ObjectFactory CompileFactory(Type implementationType)
{
    var constructor = implementationType.GetConstructors().First();
    var parameters = constructor.GetParameters();
    
    var providerParam = Expression.Parameter(typeof(IServiceProvider), "provider");
    var arguments = parameters.Select(p =>
        Expression.Convert(
            Expression.Call(
                providerParam,
                typeof(IServiceProvider).GetMethod("GetService"),
                Expression.Constant(p.ParameterType)),
            p.ParameterType));
    
    var newExpression = Expression.New(constructor, arguments);
    var lambda = Expression.Lambda<ObjectFactory>(
        Expression.Convert(newExpression, typeof(object)),
        providerParam);
    
    return lambda.Compile();
}

7. Расширяемость

Добавлю поддержку:

  • Фабричных методов для создания экземпляров
  • Делегатов как реализаций сервисов
  • Открытых generic-типов

Ключевые принципы реализации:

  • Инверсия управления — контейнер управляет созданием объектов
  • Единая ответственность — каждый класс решает одну задачу
  • Открытость/закрытость — контейнер можно расширять, не меняя основной код
  • Производительность — минимизация использования рефлексии в runtime

Эта реализация будет покрывать 90% случаев использования, сохраняя при этом простоту и производительность. Для enterprise-решений я бы рекомендовал использовать готовые контейнеры (Autofac, DryIoc, встроенный в .NET), так как они имеют лучшую оптимизацию и больше возможностей.

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