Ответ
При реализации простого 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), которые уже решают сложные проблемы: циклические зависимости, валидацию графа, производительность и интеграцию с различными фреймворками. Самописный контейнер оправдан только для очень специфических учебных или изолированных задач.