Dependency Injection - kukkich/Graphify GitHub Wiki
IoC контейнер - Inversion of control контейнер, можно считать это некоторым объектом, в котором мы перечисляем компоненты нашей программы, а затем он может нам инициализировать любой объект в нашей системе вместе со всеми зависимостями, которые нужны этому объекты и вложенным в него объектам.
Допустим у нас есть такой пример
public class View
{
    private readonly ViewModel _viewModel;
    public View(ViewModel viewModel)
    {
        _viewModel = viewModel;
    }
    
    //...
}
public class ViewModel
{
    private readonly Model _model;
    public ViewModel(Model model)
    {
        _model = model;
    }
    //...
}
public class Model
{
    //...
}Как бы мы руками инициализировали такую систему?
public static void Main()
{
    var model = new Model();
    var viewModel = new ViewModel(model);
    var view = new View(viewModel);
}Получается такая матрешка, но жить можно.
Очевидно что модель тоже должна принимать какие-то зависимости, при чем от всех остальных модулей. Получается примерно такой класс:
public class Model
{
    private readonly IImporter _importer;
    private readonly IExporter _exporter;
    private readonly IGeometryFactory _geometryFactory;
    private readonly IGeometryContext _geometryContext;
    public Model(
        IImporter importer, 
        IExporter exporter, 
        IGeometryFactory geometryFactory, 
        IGeometryContext geometryContext
    )
    {
        _importer = importer;
        _exporter = exporter;
        _geometryFactory = geometryFactory;
        _geometryContext = geometryContext;
    }
}И всю логику создания нам, опять же, надо прописывать в методе Main и вручную передавать все зависимости в конструктор:
public static void Main()
{
    var importer = new Importer();
    var exporter = new Exporter();
    var factory = new GeometryFactory();
    var context = new GeometryContext();
    var model = new Model(importer, exporter, factory, context);
    var viewModel = new ViewModel(model);
    var view = new View(viewModel);
}Допустим, мы захотели добавить логирование в наше приложение, теперь каждый компонент нашей системы должен получать объект логгера в конструкторе:
public class View
{
    private readonly ViewModel _viewModel;
    private readonly ILogger<View> _logger;
    public View(ViewModel viewModel, ILogger<View> logger)
    {
        _viewModel = viewModel;
        _logger = logger;
    }
    
    //...
}
public class ViewModel
{
    private readonly Model _model;
    private readonly ILogger<ViewModel> _logger;
    public ViewModel(Model model, ILogger<ViewModel> logger)
    {
        _model = model;
        _logger = logger;
    }
    //...
}
public class Model
{
    private readonly IImporter _importer;
    private readonly IExporter _exporter;
    private readonly IGeometryFactory _geometryFactory;
    private readonly IGeometryContext _geometryContext;
    private readonly ILogger<Model> _logger;
    public Model(
        IImporter importer, 
        IExporter exporter, 
        IGeometryFactory geometryFactory, 
        IGeometryContext geometryContext,
        ILogger<Model> logger
    )
    {
        _importer = importer;
        _exporter = exporter;
        _geometryFactory = geometryFactory;
        _geometryContext = geometryContext;
        _logger = logger;
    }
    //...
}Понятно, что такая матрешка может разрастаться до бесконечности. Проблема не нова и давно придумано решение - IoC контейнер. Его использование очень просто. Последний пример с логгером разрешался бы вот так:
Регистрация всех зависимостей в контейнере. Другими словами, перечисление всех компонентов нашей системы.
private static void ConfigureServices(IServiceCollection services)
{
    
    Log.Logger = new LoggerConfiguration().CreateLogger();
    services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true));
    services.AddScoped<Model>();
    services.AddScoped<ViewModel>();
    services.AddScoped<View>();
}Запрос к IoC контейнеру на создание объекта View
public static void Main()
{
    var services = new ServiceCollection();
    ConfigureServices(services);
    var provider = services.BuildServiceProvider();
    var view = provider.GetRequiredService<View>();
}При этом будут инициализированы все зависимости всех классов, IoC контейнер создаст их с передаст в нужные конструкторы за нас.
Также хорошей практикой является выделение регистрации компонентов каждого модуля в отдельном методе расширения, при чем этот метод должен быть прописан в самом модуле, где реализованы эти компоненты. Таким образом, ответственность за регистрацию компонентов переходит из нашего метода Main в каждый отдельный модуль, а в методе Main мы будем лишь подключать модули целиком, а не по одному компоненту.
public static class ServicesExtension
{
    public static void AddIO(this IServiceProvider services)
    {
        services.AddScoped<IImporter, Importer>();
        services.AddScoped<IExporter, Exporter>();
    } 
}public static class ServicesExtension
{
    public static void AddGeometry(this IServiceProvider services)
    {
        services.AddScoped<IGeometryFactory, GeometryFactory>();
        services.AddScoped<IGeometryContext, GeometryContext>();
    }
}Регистрация всех зависимостей
private static void ConfigureServices(IServiceCollection services)
{
    
    Log.Logger = new LoggerConfiguration().CreateLogger();
    services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true));
    services.AddScoped<Model>();
    services.AddScoped<ViewModel>();
    services.AddScoped<View>();
    services.AddGeometry();
    services.AddIO();
}Запрос к IoC контейнеру
public static void Main()
{
    var services = new ServiceCollection();
    ConfigureServices(services);
    var provider = services.BuildServiceProvider();
    var view = provider.GetRequiredService<View>();
}Там приводится пример для web приложения, но все правила по работе с IoC контейнером распространяются и на наш случай.