Приложение демонстрирует модульную архитектуру с поддержкой динамической загрузки модулей, управления зависимостями между ними и внедрения зависимостей через DI-контейнер. Ядро системы не знает о конкретных модулях и работает только с контрактами, что позволяет добавлять новые модули без изменения логики запуска.
cd csharp
dotnet run --project src/Pr2.ModulesAndDi/Pr2.ModulesAndDi.csprojФайлы: Core/IAppModule.cs, Core/IAppAction.cs, Core/ModuleCatalog.cs, Core/ModuleLoadException.cs
Ответственность:
- Определяет контракты для модулей (
IAppModule,IAppAction) - Обнаруживает модули через рефлексию
- Загружает внешние модули из папки
modules - Строит граф зависимостей и вычисляет порядок загрузки (топологическая сортировка)
- Проверяет корректность конфигурации (отсутствующие модули, циклические зависимости)
- НЕ знает о конкретных модулях и их реализации
Почему ядро не нужно менять при добавлении модуля:
- Ядро работает только с интерфейсами
IAppModuleиIAppAction - Обнаружение модулей происходит автоматически через рефлексию
- Порядок загрузки вычисляется динамически на основе зависимостей
- Регистрация сервисов делегируется самим модулям
Ответственность:
- Реализуют бизнес-логику
- Объявляют свои зависимости декларативно
- Регистрируют свои сервисы в DI-контейнере
- Взаимодействуют друг с другом только через DI-контейнер
Добавление нового модуля:
- Создать класс, реализующий
IAppModule - Указать имя и зависимости
- Зарегистрировать сервисы в
RegisterServices() - Добавить имя модуля в
appsettings.json
Ядро автоматически:
- Обнаружит новый модуль
- Проверит его зависимости
- Загрузит в правильном порядке
- Вызовет его методы инициализации
Ответственность:
- Читает конфигурацию из
appsettings.json - Использует ядро для обнаружения и загрузки модулей
- Создает DI-контейнер
- Запускает действия модулей
public interface IAppModule
{
string Name { get; } // Имя модуля
IReadOnlyCollection<string> Requires { get; } // Зависимости
void RegisterServices(IServiceCollection services); // Регистрация сервисов
Task InitializeAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken); // Инициализация
}Из текущей сборки (через рефлексию):
var discovered = ModuleCatalog.DiscoverFromAssembly(Assembly.GetExecutingAssembly());Из внешней папки modules (загрузка DLL):
var modulesPath = Path.Combine(AppContext.BaseDirectory, "modules");
var externalModules = ModuleCatalog.DiscoverFromDirectory(modulesPath);Приложение автоматически находит все классы, реализующие IAppModule, и создает их экземпляры.
Алгоритм Кана (топологическая сортировка):
- Строит граф зависимостей между модулями
- Вычисляет корректный порядок загрузки
- Обнаруживает циклические зависимости
- Проверяет наличие всех требуемых модулей
Пример порядка загрузки:
Core → Logging → Validation → Export → Report
Unhandled exception. Pr2.ModulesAndDi.Core.ModuleLoadException:
Модуль не найден, имя модуля NonExistent
Unhandled exception. Pr2.ModulesAndDi.Core.ModuleLoadException:
Не хватает модуля для зависимости, модуль Logging требует Core
Unhandled exception. Pr2.ModulesAndDi.Core.ModuleLoadException:
Обнаружена циклическая зависимость модулей, проблемные модули TestCycleA, TestCycleB
Все зависимости регистрируются через Microsoft.Extensions.DependencyInjection:
- Модули регистрируют свои сервисы в методе
RegisterServices() - Сервисы внедряются через конструкторы
- Поддерживаются разные времена жизни: Singleton, Transient, Scoped
Модуль Lifetime демонстрирует разницу между Singleton и Transient:
=== Демонстрация времен жизни ===
Singleton #1: 66d0c3e9-3f36-4dfb-8978-ef9e7447ce68
Singleton #2: 66d0c3e9-3f36-4dfb-8978-ef9e7447ce68
Singleton одинаковые: True
Transient #1: baac0481-3d73-470d-a47c-a3b394dc3c4d
Transient #2: 528a9381-6a6d-44d0-a22b-110ba0364ada
Transient разные: True
Singleton - один экземпляр на всё приложение (ID одинаковый)
Transient - новый экземпляр при каждом запросе (ID разные)
Зависимости: нет
Сервисы:
IClock/SystemClock- получение текущего времениIStorage/InMemoryStorage- хранилище данных в памяти
Назначение: Базовый модуль, предоставляющий инфраструктурные сервисы для других модулей.
Зависимости: Core
Сервисы:
ILoggerчерезMicrosoft.Extensions.Logging- Консольный логгер
Действие: Выводит тестовое сообщение в журнал.
Зависимости: Core
Действие:
- Проверяет правила валидации данных (минимальная длина строки)
- Добавляет валидированные данные в хранилище
Зависимости: Core, Validation
Действие:
- Читает все данные из хранилища
- Экспортирует их в файл
export.txt
Зависимости: Core, Export
Действие:
- Формирует отчет с текущим временем
- Выводит количество записей в хранилище
Зависимости: Core
Сервисы:
SingletonService(Singleton)TransientService(Transient)
Действие: Демонстрирует разницу между временами жизни объектов.
{
"Modules": [
"Core",
"Logging",
"Validation",
"Export",
"Report",
"Lifetime"
]
}Проект включает несколько готовых конфигураций:
- appsettings.scenario1.json - минимальный набор (Core, Logging, Validation)
- appsettings.scenario2.json - модули с экспортом (Core, Validation, Export)
- appsettings.scenario3.json - все модули включая Lifetime
- appsettings.error-missing.json - демонстрация ошибки отсутствующего модуля
- appsettings.error-dependency.json - демонстрация ошибки отсутствующей зависимости
cd csharp
.\run-scenarios.ps1cd csharp
chmod +x run-scenarios.sh
./run-scenarios.shСкрипт автоматически запустит все сценарии и покажет результаты работы приложения с разными конфигурациями.
cd csharp
dotnet testФайл: tests/Pr2.ModulesAndDi.Tests/ModuleCatalogTests.cs
-
Порядок_запуска_учитывает_зависимости
- Проверяет корректную топологическую сортировку
- Тест: A → B → C загружаются именно в этом порядке
-
Отсутствующий_модуль_даёт_понятную_ошибку
- Проверяет выброс
ModuleLoadException - Проверяет наличие понятного сообщения "Модуль не найден"
- Проверяет выброс
-
Цикл_зависимостей_обнаруживается
- Проверяет обнаружение циклических зависимостей (A → B → A)
- Проверяет наличие понятного сообщения "циклическая"
-
Внедрение_зависимостей_работает
- Проверяет, что сервисы действительно внедряются через DI-контейнер
- Проверяет, что модуль может получить зарегистрированный сервис
Приложение поддерживает загрузку модулей из внешних DLL файлов.
- Создайте новый проект библиотеки классов:
dotnet new classlib -n MyExternalModule- Добавьте ссылку на основной проект:
dotnet add reference ../Pr2.ModulesAndDi/Pr2.ModulesAndDi.csproj- Реализуйте
IAppModule:
public sealed class MyExternalModule : IAppModule
{
public string Name => "MyExternal";
public IReadOnlyCollection<string> Requires => new[] { "Core" };
public void RegisterServices(IServiceCollection services)
{
services.AddSingleton<IAppAction, MyAction>();
}
public Task InitializeAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
=> Task.CompletedTask;
}- Скомпилируйте и скопируйте DLL в папку
modules:
dotnet build
mkdir -p ../Pr2.ModulesAndDi/bin/Debug/net8.0/modules
cp bin/Debug/net8.0/MyExternalModule.dll ../Pr2.ModulesAndDi/bin/Debug/net8.0/modules/- Добавьте модуль в
appsettings.json:
{
"Modules": ["Core", "MyExternal"]
}Приложение автоматически обнаружит и загрузит внешний модуль.
✅ 1. Контракт модуля - имя, зависимости, регистрация служб, инициализация
✅ 2. Чтение списка модулей из файла настроек (appsettings.json)
✅ 2. Загрузка из каталога modules - поддержка внешних DLL
✅ 3. Построение порядка запуска с учетом зависимостей (топологическая сортировка)
✅ 4. Обработка отсутствующего модуля с понятным сообщением
✅ 5. Обработка циклических зависимостей с понятным сообщением
✅ 6. Регистрация через DI-контейнер (Microsoft.Extensions.DependencyInjection)
✅ 7. Модули выполняют действия - видимое изменение поведения программы
✅ 1. Проверки корректного порядка на нескольких наборах зависимостей
✅ 2. Проверка ошибки отсутствующего модуля с понятным сообщением
✅ 3. Проверка ошибки циклических зависимостей с понятным сообщением
✅ 4. Проверка внедрения зависимостей через контейнер (не вручную)
✅ Загрузка модулей из отдельной папки как внешних библиотек
✅ Демонстрация времен жизни объектов (Singleton vs Transient) с доказательством через журнал
- .NET 8.0
- Microsoft.Extensions.DependencyInjection - DI-контейнер
- Microsoft.Extensions.Configuration - конфигурация
- Microsoft.Extensions.Logging - журналирование
- xUnit - тестирование
- System.Reflection - обнаружение модулей
- System.Runtime.Loader - загрузка внешних сборок
csharp/
├── src/
│ └── Pr2.ModulesAndDi/
│ ├── Core/ # Ядро системы
│ │ ├── IAppModule.cs # Контракт модуля
│ │ ├── IAppAction.cs # Контракт действия
│ │ ├── ModuleCatalog.cs # Каталог модулей
│ │ └── ModuleLoadException.cs
│ ├── Modules/ # Реализованные модули
│ │ ├── CoreModule.cs
│ │ ├── LoggingModule.cs
│ │ ├── ValidationModule.cs
│ │ ├── ExportModule.cs
│ │ ├── ReportModule.cs
│ │ └── LifetimeModule.cs
│ ├── Services/ # Сервисы
│ │ ├── IClock.cs
│ │ ├── SystemClock.cs
│ │ ├── IStorage.cs
│ │ ├── InMemoryStorage.cs
│ │ ├── ILifetimeDemo.cs
│ │ ├── SingletonService.cs
│ │ └── TransientService.cs
│ ├── Program.cs # Главная программа
│ ├── appsettings.json # Основная конфигурация
│ ├── appsettings.scenario*.json # Демо-сценарии
│ └── appsettings.error-*.json # Сценарии ошибок
├── tests/
│ └── Pr2.ModulesAndDi.Tests/
│ └── ModuleCatalogTests.cs # Тесты
├── run-scenarios.ps1 # Скрипт демонстрации (Windows)
└── run-scenarios.sh # Скрипт демонстрации (Linux/macOS)
Запуск действий модулей
Действие Проверка журнала событий
Действие Проверка правил данных
Действие Демонстрация времен жизни объектов
=== Демонстрация времен жизни ===
info: Pr2.ModulesAndDi.Modules.LoggingModule.LoggingAction[0]
Сообщение из модуля журналирования
Singleton #1: 66d0c3e9-3f36-4dfb-8978-ef9e7447ce68
Singleton #2: 66d0c3e9-3f36-4dfb-8978-ef9e7447ce68
Singleton одинаковые: True
Transient #1: baac0481-3d73-470d-a47c-a3b394dc3c4d
Transient #2: 528a9381-6a6d-44d0-a22b-110ba0364ada
Transient разные: True
Действие Экспорт данных в файл
Действие Формирование отчёта
Отчёт сформирован, время 5/11/2026 9:39:12 PM +03:00, записей 1
Unhandled exception. Pr2.ModulesAndDi.Core.ModuleLoadException:
Модуль не найден, имя модуля NonExistent
at Pr2.ModulesAndDi.Core.ModuleCatalog.BuildExecutionOrder(...)
Unhandled exception. Pr2.ModulesAndDi.Core.ModuleLoadException:
Не хватает модуля для зависимости, модуль Logging требует Core
at Pr2.ModulesAndDi.Core.ModuleCatalog.BuildExecutionOrder(...)
- ✅ Исходный код - весь проект в папке
csharp/ - ✅ Инструкция запуска - этот README.md
- ✅ Набор проверок - тесты в
tests/Pr2.ModulesAndDi.Tests/ - ✅ Демонстрационные сценарии - файлы
appsettings.scenario*.jsonи скриптыrun-scenarios.* - ✅ Пояснение архитектуры - раздел "Архитектура решения" в этом документе
Проект демонстрирует полноценную модульную архитектуру с:
- Четким разделением ответственности между ядром и модулями
- Автоматическим обнаружением модулей
- Управлением зависимостями через топологическую сортировку
- Внедрением зависимостей через DI-контейнер
- Поддержкой внешних модулей
- Демонстрацией разных времен жизни объектов
- Полным набором тестов и демонстрационных сценариев
Ключевое преимущество: Добавление нового модуля не требует изменения ядра или логики запуска - достаточно создать класс, реализующий IAppModule, и добавить его имя в конфигурацию.