From 1b5076a607e4213a925815a99286c5b158a4e2ed Mon Sep 17 00:00:00 2001 From: Mlavrukk Date: Mon, 23 Mar 2026 14:20:37 +0400 Subject: [PATCH 1/3] lab1 --- Client.Wasm/Components/StudentCard.razor | 8 +- Client.Wasm/Properties/launchSettings.json | 6 +- Client.Wasm/wwwroot/appsettings.json | 2 +- CloudDevelopment.sln | 18 +++ README.md | 151 ++++-------------- VehicleVault.Api/Entities/Vehicle.cs | 57 +++++++ VehicleVault.Api/Program.cs | 31 ++++ .../Properties/launchSettings.json | 38 +++++ .../Services/IVehicleCacheService.cs | 14 ++ .../Services/IVehicleGeneratorService.cs | 14 ++ .../Services/VehicleCacheService.cs | 82 ++++++++++ .../Services/VehicleGeneratorService.cs | 30 ++++ VehicleVault.Api/VehicleVault.Api.csproj | 18 +++ VehicleVault.Api/appsettings.Development.json | 8 + VehicleVault.Api/appsettings.json | 18 +++ VehicleVault/VehicleVault.AppHost/AppHost.cs | 13 ++ .../Properties/launchSettings.json | 29 ++++ .../VehicleVault.AppHost.csproj | 23 +++ .../appsettings.Development.json | 8 + .../VehicleVault.AppHost/appsettings.json | 9 ++ .../Extensions.cs | 127 +++++++++++++++ .../VehicleVault.ServiceDefaults.csproj | 22 +++ 22 files changed, 601 insertions(+), 125 deletions(-) create mode 100644 VehicleVault.Api/Entities/Vehicle.cs create mode 100644 VehicleVault.Api/Program.cs create mode 100644 VehicleVault.Api/Properties/launchSettings.json create mode 100644 VehicleVault.Api/Services/IVehicleCacheService.cs create mode 100644 VehicleVault.Api/Services/IVehicleGeneratorService.cs create mode 100644 VehicleVault.Api/Services/VehicleCacheService.cs create mode 100644 VehicleVault.Api/Services/VehicleGeneratorService.cs create mode 100644 VehicleVault.Api/VehicleVault.Api.csproj create mode 100644 VehicleVault.Api/appsettings.Development.json create mode 100644 VehicleVault.Api/appsettings.json create mode 100644 VehicleVault/VehicleVault.AppHost/AppHost.cs create mode 100644 VehicleVault/VehicleVault.AppHost/Properties/launchSettings.json create mode 100644 VehicleVault/VehicleVault.AppHost/VehicleVault.AppHost.csproj create mode 100644 VehicleVault/VehicleVault.AppHost/appsettings.Development.json create mode 100644 VehicleVault/VehicleVault.AppHost/appsettings.json create mode 100644 VehicleVault/VehicleVault.ServiceDefaults/Extensions.cs create mode 100644 VehicleVault/VehicleVault.ServiceDefaults/VehicleVault.ServiceDefaults.csproj diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..10ea29e1 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,10 +4,10 @@ - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №1 "Кэширование" + Вариант №27 "Транспортное средство" + Выполнила Лаврук Маргарита 6512 + Ссылка на форк diff --git a/Client.Wasm/Properties/launchSettings.json b/Client.Wasm/Properties/launchSettings.json index 0d824ea7..60120ec3 100644 --- a/Client.Wasm/Properties/launchSettings.json +++ b/Client.Wasm/Properties/launchSettings.json @@ -12,7 +12,7 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5127", "environmentVariables": { @@ -22,7 +22,7 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:7282;http://localhost:5127", "environmentVariables": { @@ -31,7 +31,7 @@ }, "IIS Express": { "commandName": "IISExpress", - "launchBrowser": true, + "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..28598706 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "https://localhost:7283/api/vehicle" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..5d0b1423 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -5,6 +5,12 @@ VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.AppHost", "VehicleVault\VehicleVault.AppHost\VehicleVault.AppHost.csproj", "{675E2979-6231-45C9-A79D-E8D1BE095D7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.ServiceDefaults", "VehicleVault\VehicleVault.ServiceDefaults\VehicleVault.ServiceDefaults.csproj", "{2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.Api", "VehicleVault.Api\VehicleVault.Api.csproj", "{EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +21,18 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Release|Any CPU.Build.0 = Release|Any CPU + {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Release|Any CPU.Build.0 = Release|Any CPU + {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index dcaa5eb7..8e632c21 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,45 @@ -# Современные технологии разработки программного обеспечения -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1an43o-iqlq4V_kDtkr_y7DC221hY9qdhGPrpII27sH8/edit?usp=sharing) +# Лабораторная работа №1 «Кэширование» -## Задание -### Цель -Реализация проекта микросервисного бекенда. +## Описание -### Задачи -* Реализация межсервисной коммуникации, -* Изучение работы с брокерами сообщений, -* Изучение архитектурных паттернов, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Интеграционное тестирование. +Реализация сервиса генерации данных о транспортных средствах с кэшированием ответов в Redis. +Оркестрация проектов при помощи .NET Aspire. -### Лабораторные работы -
-1. «Кэширование» - Реализация сервиса генерации контрактов, кэширование его ответов -
- -В рамках первой лабораторной работы необходимо: -* Реализовать сервис генерации контрактов на основе Bogus, -* Реализовать кеширование при помощи IDistributedCache и Redis, -* Реализовать структурное логирование сервиса генерации, -* Настроить оркестрацию Aspire. - -
-
-2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы -
- -В рамках второй лабораторной работы необходимо: -* Настроить оркестрацию на запуск нескольких реплик сервиса генерации, -* Реализовать апи гейтвей на основе Ocelot, -* Имплементировать алгоритм балансировки нагрузки согласно варианту. +## Технологии -
-
-
-3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда -
+- .NET 8 +- .NET Aspire 9.5 +- Bogus — генерация фейковых данных +- Redis — распределённое кэширование (`IDistributedCache`) +- Blazor WebAssembly — клиентское приложение +- Blazorise — UI-компоненты +- OpenTelemetry — структурное логирование и трассировка -В рамках третьей лабораторной работы необходимо: -* Добавить в оркестрацию объектное хранилище, -* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище, -* Реализовать отправку генерируемых данных в файловый сервис посредством брокера, -* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе. +## Характеристики генерируемого объекта -
-
-
-4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud -
- -В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы: -* Клиент - в хостинг через отдельный бакет Object Storage, -* Сервис генерации - в Cloud Function, -* Апи гейтвей - в Serverless Integration как API Gateway, -* Брокер сообщений - в Message Queue, -* Файловый сервис - в Cloud Function, -* Объектное хранилище - в отдельный бакет Object Storage, +| № | Характеристика | Тип данных | Источник Bogus | +|---|---|---|---| +| 1 | Идентификатор в системе | `int` | Параметр запроса | +| 2 | VIN-номер | `string` | `Vehicle.Vin()` | +| 3 | Производитель | `string` | `Vehicle.Manufacturer()` | +| 4 | Модель | `string` | `Vehicle.Model()` | +| 5 | Год выпуска | `int` | `Random.Int(1960, текущий год)` | +| 6 | Тип корпуса | `string` | `Vehicle.Type()` | +| 7 | Тип топлива | `string` | `Vehicle.Fuel()` | +| 8 | Цвет корпуса | `string` | `Commerce.Color()` | +| 9 | Пробег | `double` | `Random.Double(0, 1000000)` | +| 10 | Дата последнего техобслуживания | `DateOnly` | Между годом выпуска и текущей датой | -
-
+### Кэширование -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview). -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus). -* Реализация тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. +- Ключ кэша: `vehicle:{id}` +- Время жизни: настраивается в `appsettings.json` (`Cache:ExpirationMinutes`, по умолчанию 5 минут) +- При ошибке чтения/записи кэша — логируется warning, запрос обрабатывается без кэша -**Факультативно**: -* Перенос бекенда на облачную инфраструктуру Yandex Cloud +## API -Внимательно прочитайте [дискуссии](https://github.com/itsecd/cloud-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. - -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма -Современные_технологии_разработки_ПО_drawio -
- -## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1WGmLYwffTTaAj4TgFCk5bUyW3XKbFMiBm-DHZrfFWr4/edit?usp=sharing) -[Список предметных областей и алгоритмов балансировки](https://docs.google.com/document/d/1PLn2lKe4swIdJDZhwBYzxqFSu0AbY2MFY1SUPkIKOM4/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. -Не укладываетесь в дедлайн - получаете минимально возможный балл. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соответствующим разделом дискуссий](https://github.com/itsecd/cloud-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/cloud-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/cloud-development/discussions/categories/ideas). +``` +GET /api/vehicle?id={id} +``` +Возвращает JSON с данными транспортного средства. При повторном запросе с тем же `id` — данные берутся из кэша Redis. diff --git a/VehicleVault.Api/Entities/Vehicle.cs b/VehicleVault.Api/Entities/Vehicle.cs new file mode 100644 index 00000000..f04911c8 --- /dev/null +++ b/VehicleVault.Api/Entities/Vehicle.cs @@ -0,0 +1,57 @@ +namespace VehicleVault.Api.Entities; + +/// +/// Транспортное средство +/// +public class Vehicle +{ + /// + /// Идентификатор в системе + /// + public int SystemId { get; set; } + + /// + /// VIN-номер + /// + public required string Vin { get; set; } + + /// + /// Производитель + /// + public required string Manufacturer { get; set; } + + /// + /// Модель + /// + public required string Model { get; set; } + + /// + /// Год выпуска + /// + public int Year { get; set; } + + /// + /// Тип корпуса + /// + public required string BodyType { get; set; } + + /// + /// Тип топлива + /// + public required string FuelType { get; set; } + + /// + /// Цвет корпуса + /// + public required string BodyColor { get; set; } + + /// + /// Пробег + /// + public double Mileage { get; set; } + + /// + /// Дата последнего техобслуживания + /// + public DateOnly LastServiceDate { get; set; } +} diff --git a/VehicleVault.Api/Program.cs b/VehicleVault.Api/Program.cs new file mode 100644 index 00000000..9b4f3d2c --- /dev/null +++ b/VehicleVault.Api/Program.cs @@ -0,0 +1,31 @@ +using VehicleVault.Api.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("cache"); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.WithOrigins(allowedOrigins) + .AllowAnyHeader() + .WithMethods("GET"); + }); +}); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); +app.UseCors(); + +app.MapGet("api/vehicle", async (int id, IVehicleCacheService vehicleService) => + await vehicleService.GetOrGenerate(id)); + +app.Run(); diff --git a/VehicleVault.Api/Properties/launchSettings.json b/VehicleVault.Api/Properties/launchSettings.json new file mode 100644 index 00000000..304fa877 --- /dev/null +++ b/VehicleVault.Api/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:12998", + "sslPort": 44340 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5260", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7283;http://localhost:5260", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/VehicleVault.Api/Services/IVehicleCacheService.cs b/VehicleVault.Api/Services/IVehicleCacheService.cs new file mode 100644 index 00000000..2c234b63 --- /dev/null +++ b/VehicleVault.Api/Services/IVehicleCacheService.cs @@ -0,0 +1,14 @@ +using VehicleVault.Api.Entities; + +namespace VehicleVault.Api.Services; + +/// +/// Сервис транспортных средств с кэшированием +/// +public interface IVehicleCacheService +{ + /// + /// Получение или генерация транспортного средства по идентификатору + /// + public Task GetOrGenerate(int id); +} diff --git a/VehicleVault.Api/Services/IVehicleGeneratorService.cs b/VehicleVault.Api/Services/IVehicleGeneratorService.cs new file mode 100644 index 00000000..b8964bc9 --- /dev/null +++ b/VehicleVault.Api/Services/IVehicleGeneratorService.cs @@ -0,0 +1,14 @@ +using VehicleVault.Api.Entities; + +namespace VehicleVault.Api.Services; + +/// +/// Сервис генерации данных транспортного средства +/// +public interface IVehicleGeneratorService +{ + /// + /// Генерация транспортного средства по идентификатору + /// + public Vehicle Generate(int id); +} diff --git a/VehicleVault.Api/Services/VehicleCacheService.cs b/VehicleVault.Api/Services/VehicleCacheService.cs new file mode 100644 index 00000000..9ea3f825 --- /dev/null +++ b/VehicleVault.Api/Services/VehicleCacheService.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; +using VehicleVault.Api.Entities; + +namespace VehicleVault.Api.Services; + +/// +/// Сервис транспортных средств с кэшированием +/// +/// Генератор транспортных средств +/// Распределённый кэш +/// Конфигурация приложения +/// Логгер +public class VehicleCacheService( + IVehicleGeneratorService vehicleGenerator, + IDistributedCache distributedCache, + IConfiguration appConfiguration, + ILogger logger) : IVehicleCacheService +{ + private readonly TimeSpan _entryLifetime = TimeSpan.FromMinutes(appConfiguration.GetValue("Cache:ExpirationMinutes", 5)); + + /// + public async Task GetOrGenerate(int id) + { + var key = $"vehicle:{id}"; + + var existing = await TryGetFromCache(key); + if (existing is not null) + return existing; + + logger.LogInformation("Cache miss for id {Id}, generating new vehicle", id); + var result = vehicleGenerator.Generate(id); + await TrySaveToCache(key, result); + + return result; + } + + /// + /// Попытка получить транспортное средство из кэша + /// + /// Ключ записи + /// Транспортное средство или null + private async Task TryGetFromCache(string key) + { + try + { + var data = await distributedCache.GetStringAsync(key); + if (data is null) + return null; + + logger.LogInformation("Cache hit for key {Key}", key); + return JsonSerializer.Deserialize(data); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to retrieve from cache by key {Key}", key); + return null; + } + } + + /// + /// Попытка сохранить транспортное средство в кэш + /// + /// Ключ записи + /// Транспортное средство + private async Task TrySaveToCache(string key, Vehicle vehicle) + { + try + { + var serialized = JsonSerializer.Serialize(vehicle); + await distributedCache.SetStringAsync(key, serialized, new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _entryLifetime + }); + logger.LogInformation("Saved to cache with key {Key}", key); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to save to cache by key {Key}", key); + } + } +} diff --git a/VehicleVault.Api/Services/VehicleGeneratorService.cs b/VehicleVault.Api/Services/VehicleGeneratorService.cs new file mode 100644 index 00000000..2781c834 --- /dev/null +++ b/VehicleVault.Api/Services/VehicleGeneratorService.cs @@ -0,0 +1,30 @@ +using Bogus; +using VehicleVault.Api.Entities; + +namespace VehicleVault.Api.Services; + +/// +/// Реализация сервиса генерации данных транспортного средства +/// +public class VehicleGeneratorService : IVehicleGeneratorService +{ + private readonly Faker _faker = new Faker() + .RuleFor(v => v.Vin, f => f.Vehicle.Vin()) + .RuleFor(v => v.Manufacturer, f => f.Vehicle.Manufacturer()) + .RuleFor(v => v.Model, f => f.Vehicle.Model()) + .RuleFor(v => v.Year, f => f.Random.Int(1960, DateTime.Now.Year)) + .RuleFor(v => v.BodyType, f => f.Vehicle.Type()) + .RuleFor(v => v.FuelType, f => f.Vehicle.Fuel()) + .RuleFor(v => v.BodyColor, f => f.Commerce.Color()) + .RuleFor(v => v.Mileage, f => Math.Round(f.Random.Double(0, 1000000), 3)) + .RuleFor(v => v.LastServiceDate, (f, v) => + DateOnly.FromDateTime(f.Date.Between(new DateTime(v.Year, 1, 1), DateTime.Now))); + + /// + public Vehicle Generate(int id) + { + var vehicle = _faker.Generate(); + vehicle.SystemId = id; + return vehicle; + } +} diff --git a/VehicleVault.Api/VehicleVault.Api.csproj b/VehicleVault.Api/VehicleVault.Api.csproj new file mode 100644 index 00000000..8dcb8dac --- /dev/null +++ b/VehicleVault.Api/VehicleVault.Api.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/VehicleVault.Api/appsettings.Development.json b/VehicleVault.Api/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/VehicleVault.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/VehicleVault.Api/appsettings.json b/VehicleVault.Api/appsettings.json new file mode 100644 index 00000000..e7350acf --- /dev/null +++ b/VehicleVault.Api/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Cache": { + "ExpirationMinutes": 5 + }, + "Cors": { + "AllowedOrigins": [ + "http://localhost:5127", + "https://localhost:7282" + ] + } +} diff --git a/VehicleVault/VehicleVault.AppHost/AppHost.cs b/VehicleVault/VehicleVault.AppHost/AppHost.cs new file mode 100644 index 00000000..a4202cd9 --- /dev/null +++ b/VehicleVault/VehicleVault.AppHost/AppHost.cs @@ -0,0 +1,13 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("cache") + .WithRedisInsight(); + +var api = builder.AddProject("vehiclevault-api") + .WithReference(cache) + .WaitFor(cache); + +builder.AddProject("client-wasm") + .WaitFor(api); + +builder.Build().Run(); diff --git a/VehicleVault/VehicleVault.AppHost/Properties/launchSettings.json b/VehicleVault/VehicleVault.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..e61ad6be --- /dev/null +++ b/VehicleVault/VehicleVault.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17060;http://localhost:15002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21017", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22019" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19170", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20000" + } + } + } +} diff --git a/VehicleVault/VehicleVault.AppHost/VehicleVault.AppHost.csproj b/VehicleVault/VehicleVault.AppHost/VehicleVault.AppHost.csproj new file mode 100644 index 00000000..b73474a2 --- /dev/null +++ b/VehicleVault/VehicleVault.AppHost/VehicleVault.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net8.0 + enable + enable + e8350e86-dfa1-4c37-9efc-069d9ff7570f + + + + + + + + + + + + + diff --git a/VehicleVault/VehicleVault.AppHost/appsettings.Development.json b/VehicleVault/VehicleVault.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/VehicleVault/VehicleVault.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/VehicleVault/VehicleVault.AppHost/appsettings.json b/VehicleVault/VehicleVault.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/VehicleVault/VehicleVault.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/VehicleVault/VehicleVault.ServiceDefaults/Extensions.cs b/VehicleVault/VehicleVault.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..b72c8753 --- /dev/null +++ b/VehicleVault/VehicleVault.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/VehicleVault/VehicleVault.ServiceDefaults/VehicleVault.ServiceDefaults.csproj b/VehicleVault/VehicleVault.ServiceDefaults/VehicleVault.ServiceDefaults.csproj new file mode 100644 index 00000000..1b6e209a --- /dev/null +++ b/VehicleVault/VehicleVault.ServiceDefaults/VehicleVault.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + From deee993d61f4cc393f0fba8a3b3ae3b556fcc66f Mon Sep 17 00:00:00 2001 From: Mlavrukk Date: Sun, 12 Apr 2026 22:15:15 +0400 Subject: [PATCH 2/3] lab2 --- Api.Gateway/Api.Gateway.csproj | 17 +++ .../LoadBalancers/QueryBasedLoadBalancer.cs | 42 ++++++ Api.Gateway/Program.cs | 33 +++++ Api.Gateway/Properties/launchSettings.json | 38 ++++++ Api.Gateway/appsettings.Development.json | 8 ++ Api.Gateway/appsettings.json | 13 ++ Api.Gateway/ocelot.json | 20 +++ Client.Wasm/Components/StudentCard.razor | 8 +- Client.Wasm/wwwroot/appsettings.json | 2 +- CloudDevelopment.sln | 42 +++--- ProjectGenerator.Api/Program.cs | 18 +++ .../ProjectGenerator.Api.csproj | 23 ++++ .../Properties/launchSettings.json | 38 ++++++ .../Services/ISoftwareProjectGenerator.cs | 14 ++ .../Services/ISoftwareProjectService.cs | 14 ++ .../Services/SoftwareProjectGenerator.cs | 62 +++++++++ .../Services/SoftwareProjectService.cs | 90 +++++++++++++ .../appsettings.Development.json | 8 ++ ProjectGenerator.Api/appsettings.json | 10 ++ .../Models/SoftwareProject.cs | 57 ++++++++ .../ProjectGenerator.Domain.csproj | 13 ++ .../ProjectGenerator.AppHost/AppHost.cs | 20 +++ .../ProjectGenerator.AppHost.csproj | 24 ++++ .../Properties/launchSettings.json | 29 ++++ .../appsettings.Development.json | 8 ++ .../ProjectGenerator.AppHost/appsettings.json | 9 ++ .../Extensions.cs | 127 ++++++++++++++++++ .../ProjectGenerator.ServiceDefaults.csproj | 22 +++ README.md | 71 +++++----- 29 files changed, 828 insertions(+), 52 deletions(-) create mode 100644 Api.Gateway/Api.Gateway.csproj create mode 100644 Api.Gateway/LoadBalancers/QueryBasedLoadBalancer.cs create mode 100644 Api.Gateway/Program.cs create mode 100644 Api.Gateway/Properties/launchSettings.json create mode 100644 Api.Gateway/appsettings.Development.json create mode 100644 Api.Gateway/appsettings.json create mode 100644 Api.Gateway/ocelot.json create mode 100644 ProjectGenerator.Api/Program.cs create mode 100644 ProjectGenerator.Api/ProjectGenerator.Api.csproj create mode 100644 ProjectGenerator.Api/Properties/launchSettings.json create mode 100644 ProjectGenerator.Api/Services/ISoftwareProjectGenerator.cs create mode 100644 ProjectGenerator.Api/Services/ISoftwareProjectService.cs create mode 100644 ProjectGenerator.Api/Services/SoftwareProjectGenerator.cs create mode 100644 ProjectGenerator.Api/Services/SoftwareProjectService.cs create mode 100644 ProjectGenerator.Api/appsettings.Development.json create mode 100644 ProjectGenerator.Api/appsettings.json create mode 100644 ProjectGenerator.Domain/Models/SoftwareProject.cs create mode 100644 ProjectGenerator.Domain/ProjectGenerator.Domain.csproj create mode 100644 ProjectGenerator/ProjectGenerator.AppHost/AppHost.cs create mode 100644 ProjectGenerator/ProjectGenerator.AppHost/ProjectGenerator.AppHost.csproj create mode 100644 ProjectGenerator/ProjectGenerator.AppHost/Properties/launchSettings.json create mode 100644 ProjectGenerator/ProjectGenerator.AppHost/appsettings.Development.json create mode 100644 ProjectGenerator/ProjectGenerator.AppHost/appsettings.json create mode 100644 ProjectGenerator/ProjectGenerator.ServiceDefaults/Extensions.cs create mode 100644 ProjectGenerator/ProjectGenerator.ServiceDefaults/ProjectGenerator.ServiceDefaults.csproj diff --git a/Api.Gateway/Api.Gateway.csproj b/Api.Gateway/Api.Gateway.csproj new file mode 100644 index 00000000..0b6b959f --- /dev/null +++ b/Api.Gateway/Api.Gateway.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Api.Gateway/LoadBalancers/QueryBasedLoadBalancer.cs b/Api.Gateway/LoadBalancers/QueryBasedLoadBalancer.cs new file mode 100644 index 00000000..c5646c7c --- /dev/null +++ b/Api.Gateway/LoadBalancers/QueryBasedLoadBalancer.cs @@ -0,0 +1,42 @@ +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Api.Gateway.LoadBalancers; + + +/// +/// Балансировщик нагрузки на основе параметра запроса id. +/// +/// +/// Алгоритм: индекс реплики = id % количество_реплик. +/// Это гарантирует, что один и тот же идентификатор всегда попадает +/// на одну и ту же реплику (sticky routing по id). +/// +/// +/// Делегат, возвращающий список доступных реплик сервиса. +/// Вызывается при каждом запросе, чтобы учитывать динамически +/// изменяющийся пул экземпляров. +/// +public class QueryBasedLoadBalancer(Func>> serviceProviderFactory) : ILoadBalancer +{ + public string Type => nameof(QueryBasedLoadBalancer); + + public async Task> LeaseAsync(HttpContext context) + { + var services = await serviceProviderFactory(); + + if (services.Count == 0) + throw new InvalidOperationException("No available downstream services"); + + if (!context.Request.Query.TryGetValue("id", out var idValue) || + !int.TryParse(idValue, out var id)) + throw new InvalidOperationException("Query parameter 'id' is missing or invalid"); + + var index = Math.Abs(id) % services.Count; + + return new OkResponse(services[index].HostAndPort); + } + + public void Release(ServiceHostAndPort hostAndPort) { } +} \ No newline at end of file diff --git a/Api.Gateway/Program.cs b/Api.Gateway/Program.cs new file mode 100644 index 00000000..c74a8bee --- /dev/null +++ b/Api.Gateway/Program.cs @@ -0,0 +1,33 @@ +using Api.Gateway.LoadBalancers; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.Services.AddServiceDiscovery(); +builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); +builder.Services.AddOcelot() + .AddCustomLoadBalancer((_, _, provider) => new(provider.GetAsync)); + + +var trustedUrls = builder.Configuration.GetSection("CorsOrigins").Get() ?? []; +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.WithOrigins(trustedUrls) + .WithMethods("GET") + .WithHeaders("Content-Type"); + }); +}); + +var app = builder.Build(); + +app.UseCors(); + +app.MapDefaultEndpoints(); + +await app.UseOcelot(); + +app.Run(); diff --git a/Api.Gateway/Properties/launchSettings.json b/Api.Gateway/Properties/launchSettings.json new file mode 100644 index 00000000..82a900e4 --- /dev/null +++ b/Api.Gateway/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:62081", + "sslPort": 44391 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5200", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7190;http://localhost:5200", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Api.Gateway/appsettings.Development.json b/Api.Gateway/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Api.Gateway/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Api.Gateway/appsettings.json b/Api.Gateway/appsettings.json new file mode 100644 index 00000000..c6c9e512 --- /dev/null +++ b/Api.Gateway/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CorsOrigins": [ + "http://localhost:5127", + "https://localhost:7282" + ] +} diff --git a/Api.Gateway/ocelot.json b/Api.Gateway/ocelot.json new file mode 100644 index 00000000..59e0a2b2 --- /dev/null +++ b/Api.Gateway/ocelot.json @@ -0,0 +1,20 @@ +{ + "Routes": [ + { + "UpstreamPathTemplate": "/generate", + "UpstreamHttpMethod": [ "GET" ], + "DownstreamPathTemplate": "/api/generate", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 5000 }, + { "Host": "localhost", "Port": 5001 }, + { "Host": "localhost", "Port": 5002 }, + { "Host": "localhost", "Port": 5003 }, + { "Host": "localhost", "Port": 5004 } + ], + "LoadBalancerOptions": { + "Type": "QueryBasedLoadBalancer" + } + } + ] +} diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 10ea29e1..dbe1ac9e 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,10 +4,10 @@ - Номер №1 "Кэширование" - Вариант №27 "Транспортное средство" - Выполнила Лаврук Маргарита 6512 - Ссылка на форк + Номер №2 "Балансировка нагрузки" + Вариант №46 "Программный проект" + Выполнена Митеревым Дмитрием 6513 + Ссылка на форк diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index 28598706..9f75e8ad 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "https://localhost:7283/api/vehicle" + "BaseAddress": "https://localhost:7190/generate" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index 5d0b1423..403883e1 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -5,11 +5,15 @@ VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.AppHost", "VehicleVault\VehicleVault.AppHost\VehicleVault.AppHost.csproj", "{675E2979-6231-45C9-A79D-E8D1BE095D7E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectGenerator.AppHost", "ProjectGenerator\ProjectGenerator.AppHost\ProjectGenerator.AppHost.csproj", "{2BD86B8F-8507-43F0-B17F-9607B7352733}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.ServiceDefaults", "VehicleVault\VehicleVault.ServiceDefaults\VehicleVault.ServiceDefaults.csproj", "{2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectGenerator.ServiceDefaults", "ProjectGenerator\ProjectGenerator.ServiceDefaults\ProjectGenerator.ServiceDefaults.csproj", "{8F9985A2-9D06-28F9-3428-8039644EF545}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.Api", "VehicleVault.Api\VehicleVault.Api.csproj", "{EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectGenerator.Api", "ProjectGenerator.Api\ProjectGenerator.Api.csproj", "{AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectGenerator.Domain", "ProjectGenerator.Domain\ProjectGenerator.Domain.csproj", "{67E00678-695C-407F-B401-27C9EC3527B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Gateway", "Api.Gateway\Api.Gateway.csproj", "{C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,18 +25,26 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU - {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Release|Any CPU.Build.0 = Release|Any CPU - {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Release|Any CPU.Build.0 = Release|Any CPU - {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Release|Any CPU.Build.0 = Release|Any CPU + {2BD86B8F-8507-43F0-B17F-9607B7352733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BD86B8F-8507-43F0-B17F-9607B7352733}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BD86B8F-8507-43F0-B17F-9607B7352733}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BD86B8F-8507-43F0-B17F-9607B7352733}.Release|Any CPU.Build.0 = Release|Any CPU + {8F9985A2-9D06-28F9-3428-8039644EF545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F9985A2-9D06-28F9-3428-8039644EF545}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F9985A2-9D06-28F9-3428-8039644EF545}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F9985A2-9D06-28F9-3428-8039644EF545}.Release|Any CPU.Build.0 = Release|Any CPU + {AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}.Release|Any CPU.Build.0 = Release|Any CPU + {67E00678-695C-407F-B401-27C9EC3527B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67E00678-695C-407F-B401-27C9EC3527B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67E00678-695C-407F-B401-27C9EC3527B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67E00678-695C-407F-B401-27C9EC3527B1}.Release|Any CPU.Build.0 = Release|Any CPU + {C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ProjectGenerator.Api/Program.cs b/ProjectGenerator.Api/Program.cs new file mode 100644 index 00000000..f09a9815 --- /dev/null +++ b/ProjectGenerator.Api/Program.cs @@ -0,0 +1,18 @@ +using ProjectGenerator.Api.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("cache"); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +app.MapGet("/api/generate", async (int id, ISoftwareProjectService service) => + await service.GetOrGenerate(id)); + +app.Run(); diff --git a/ProjectGenerator.Api/ProjectGenerator.Api.csproj b/ProjectGenerator.Api/ProjectGenerator.Api.csproj new file mode 100644 index 00000000..3c878a17 --- /dev/null +++ b/ProjectGenerator.Api/ProjectGenerator.Api.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/ProjectGenerator.Api/Properties/launchSettings.json b/ProjectGenerator.Api/Properties/launchSettings.json new file mode 100644 index 00000000..553903db --- /dev/null +++ b/ProjectGenerator.Api/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:10098", + "sslPort": 44392 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7046;http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ProjectGenerator.Api/Services/ISoftwareProjectGenerator.cs b/ProjectGenerator.Api/Services/ISoftwareProjectGenerator.cs new file mode 100644 index 00000000..d5845502 --- /dev/null +++ b/ProjectGenerator.Api/Services/ISoftwareProjectGenerator.cs @@ -0,0 +1,14 @@ +using ProjectGenerator.Domain.Models; + +namespace ProjectGenerator.Api.Services; + +/// +/// Интерфейс генератора программных проектов +/// +public interface ISoftwareProjectGenerator +{ + /// + /// Генерирует программный проект с указанным идентификатором + /// + public SoftwareProject Generate(int id); +} diff --git a/ProjectGenerator.Api/Services/ISoftwareProjectService.cs b/ProjectGenerator.Api/Services/ISoftwareProjectService.cs new file mode 100644 index 00000000..26644fbc --- /dev/null +++ b/ProjectGenerator.Api/Services/ISoftwareProjectService.cs @@ -0,0 +1,14 @@ +using ProjectGenerator.Domain.Models; + +namespace ProjectGenerator.Api.Services; + +/// +/// Интерфейс сервиса программных проектов +/// +public interface ISoftwareProjectService +{ + /// + /// Получает программный проект по идентификатору из кэша или генерирует новый + /// + public Task GetOrGenerate(int id); +} diff --git a/ProjectGenerator.Api/Services/SoftwareProjectGenerator.cs b/ProjectGenerator.Api/Services/SoftwareProjectGenerator.cs new file mode 100644 index 00000000..fd38e8ab --- /dev/null +++ b/ProjectGenerator.Api/Services/SoftwareProjectGenerator.cs @@ -0,0 +1,62 @@ +using Bogus; +using Bogus.DataSets; +using ProjectGenerator.Domain.Models; + +namespace ProjectGenerator.Api.Services; + +/// +/// Генератор программных проектов на основе Bogus +/// +public class SoftwareProjectGenerator : ISoftwareProjectGenerator +{ + private readonly Faker _faker = new Faker("ru") + .RuleFor(p => p.Id, _ => 0) + .RuleFor(p => p.ProjectName, f => + $"{f.Commerce.ProductName()} {f.Hacker.Adjective()} {f.Finance.AccountName()} {f.Lorem.Word()}") + .RuleFor(p => p.Customer, f => f.Company.CompanyName()) + .RuleFor(p => p.ProjectManager, f => + { + var gender = f.PickRandom(); + return + $"{f.Name.LastName(gender)} {f.Name.FirstName(gender)} " + + $"{GeneratePatronymic(f.Name.FirstName(Name.Gender.Male), gender == Name.Gender.Female)}"; + }) + .RuleFor(p => p.StartDate, f => + DateOnly.FromDateTime(f.Date.Past(2))) + .RuleFor(p => p.PlannedEndDate, (f, p) => + p.StartDate.AddDays(f.Random.Int(30, 365))) + .RuleFor(p => p.ActualEndDate, (f, p) => + f.Random.Bool(0.3f) + ? p.StartDate.AddDays(f.Random.Int(10, 400)) + : null) + .RuleFor(p => p.Budget, f => + Math.Round(f.Finance.Amount(100_000, 100_000_000), 2)) + .RuleFor(p => p.ActualCosts, (f, p) => + Math.Round(p.Budget * f.Random.Decimal(0.01m, 1.2m), 2)) + .RuleFor(p => p.CompletionPercentage, (f, p) => + p.ActualEndDate.HasValue ? 100 : f.Random.Int(0, 99)); + + /// + /// Генерация отчества на основе мужского имени + /// + /// Мужское имя + /// Признак женского пола + /// Отчество + private static string GeneratePatronymic(string maleName, bool isFemale) => maleName switch + { + _ when maleName.EndsWith("ий") => maleName[..^2] + (isFemale ? "ьевна" : "ьевич"), + _ when maleName.EndsWith('ь') => maleName[..^1] + (isFemale ? "евна" : "евич"), + _ when maleName.EndsWith('й') => maleName[..^1] + (isFemale ? "евна" : "евич"), + _ when maleName.EndsWith('а') || maleName.EndsWith('я') => maleName[..^1] + (isFemale ? "ична" : "ич"), + _ => maleName + (isFemale ? "овна" : "ович") + }; + + /// + public SoftwareProject Generate(int id) + { + var project = _faker.Generate(); + project.Id = id; + return project; + } + +} diff --git a/ProjectGenerator.Api/Services/SoftwareProjectService.cs b/ProjectGenerator.Api/Services/SoftwareProjectService.cs new file mode 100644 index 00000000..02b23937 --- /dev/null +++ b/ProjectGenerator.Api/Services/SoftwareProjectService.cs @@ -0,0 +1,90 @@ +using System.Text.Json; +using Microsoft.Extensions.Caching.Distributed; +using ProjectGenerator.Domain.Models; + +namespace ProjectGenerator.Api.Services; + +/// +/// Сервис программных проектов с кэшированием +/// +/// Генератор программных проектов +/// Распределённый кэш +/// Конфигурация приложения +/// Логгер +public class SoftwareProjectService( + ISoftwareProjectGenerator generator, + IDistributedCache cache, + IConfiguration configuration, + ILogger logger) : ISoftwareProjectService +{ + private const string CacheKeyPrefix = "software-project"; + private readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(configuration.GetValue("CacheTtlMinutes", 15)); + + /// + public async Task GetOrGenerate(int id) + { + var cacheKey = $"{CacheKeyPrefix}:{id}"; + + var cached = await GetFromCache(cacheKey); + if (cached is not null) + { + return cached; + } + + logger.LogInformation("Cache miss for id {Id}, generating new data", id); + + var project = generator.Generate(id); + + await SetToCache(cacheKey, project); + + return project; + } + + /// + /// Получение программного проекта из кэша + /// + /// Ключ кэша + /// Программный проект или null + private async Task GetFromCache(string cacheKey) + { + try + { + var cached = await cache.GetStringAsync(cacheKey); + if (cached is null) + { + return null; + } + + logger.LogInformation("Cache hit for key {CacheKey}", cacheKey); + return JsonSerializer.Deserialize(cached); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to read from cache for key {CacheKey}", cacheKey); + return null; + } + } + + /// + /// Сохранение программного проекта в кэш + /// + /// Ключ кэша + /// Программный проект + private async Task SetToCache(string cacheKey, SoftwareProject project) + { + try + { + var json = JsonSerializer.Serialize(project); + await cache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheTtl + }); + + logger.LogInformation("Cached data for key {CacheKey}", cacheKey); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to write to cache for key {CacheKey}", cacheKey); + } + } +} diff --git a/ProjectGenerator.Api/appsettings.Development.json b/ProjectGenerator.Api/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ProjectGenerator.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ProjectGenerator.Api/appsettings.json b/ProjectGenerator.Api/appsettings.json new file mode 100644 index 00000000..2d053cce --- /dev/null +++ b/ProjectGenerator.Api/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CacheTtlMinutes": 15 +} diff --git a/ProjectGenerator.Domain/Models/SoftwareProject.cs b/ProjectGenerator.Domain/Models/SoftwareProject.cs new file mode 100644 index 00000000..23c86aea --- /dev/null +++ b/ProjectGenerator.Domain/Models/SoftwareProject.cs @@ -0,0 +1,57 @@ +namespace ProjectGenerator.Domain.Models; + +/// +/// Программный проект +/// +public sealed class SoftwareProject +{ + /// + /// Идентификатор в системе + /// + public int Id { get; set; } + + /// + /// Название проекта + /// + public required string ProjectName { get; init; } + + /// + /// Заказчик проекта + /// + public required string Customer { get; init; } + + /// + /// Менеджер проекта + /// + public required string ProjectManager { get; init; } + + /// + /// Дата начала + /// + public DateOnly StartDate { get; init; } + + /// + /// Плановая дата завершения + /// + public DateOnly PlannedEndDate { get; init; } + + /// + /// Фактическая дата завершения + /// + public DateOnly? ActualEndDate { get; init; } + + /// + /// Бюджет + /// + public decimal Budget { get; init; } + + /// + /// Фактические затраты + /// + public decimal ActualCosts { get; init; } + + /// + /// Процент выполнения + /// + public int CompletionPercentage { get; init; } +} diff --git a/ProjectGenerator.Domain/ProjectGenerator.Domain.csproj b/ProjectGenerator.Domain/ProjectGenerator.Domain.csproj new file mode 100644 index 00000000..c60e6513 --- /dev/null +++ b/ProjectGenerator.Domain/ProjectGenerator.Domain.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/ProjectGenerator/ProjectGenerator.AppHost/AppHost.cs b/ProjectGenerator/ProjectGenerator.AppHost/AppHost.cs new file mode 100644 index 00000000..d43a3743 --- /dev/null +++ b/ProjectGenerator/ProjectGenerator.AppHost/AppHost.cs @@ -0,0 +1,20 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("cache") + .WithRedisInsight(); + +var apiGateway = builder.AddProject("api-gateway"); + +for (var i = 0; i < 5; i++) +{ + var generationApi = builder.AddProject($"generation-api-{i}", launchProfileName: null) + .WithHttpsEndpoint(5000 + i) + .WithReference(cache) + .WaitFor(cache); + apiGateway.WaitFor(generationApi); +} + +builder.AddProject("client-wasm") + .WaitFor(apiGateway); + +builder.Build().Run(); diff --git a/ProjectGenerator/ProjectGenerator.AppHost/ProjectGenerator.AppHost.csproj b/ProjectGenerator/ProjectGenerator.AppHost/ProjectGenerator.AppHost.csproj new file mode 100644 index 00000000..0d95d202 --- /dev/null +++ b/ProjectGenerator/ProjectGenerator.AppHost/ProjectGenerator.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + net8.0 + enable + enable + 5f0fcf55-56c6-4723-953b-9e24de107d8f + + + + + + + + + + + + + + diff --git a/ProjectGenerator/ProjectGenerator.AppHost/Properties/launchSettings.json b/ProjectGenerator/ProjectGenerator.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..475d465e --- /dev/null +++ b/ProjectGenerator/ProjectGenerator.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17246;http://localhost:15108", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21233", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22087" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15108", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19053", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20103" + } + } + } +} diff --git a/ProjectGenerator/ProjectGenerator.AppHost/appsettings.Development.json b/ProjectGenerator/ProjectGenerator.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ProjectGenerator/ProjectGenerator.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ProjectGenerator/ProjectGenerator.AppHost/appsettings.json b/ProjectGenerator/ProjectGenerator.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/ProjectGenerator/ProjectGenerator.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/ProjectGenerator/ProjectGenerator.ServiceDefaults/Extensions.cs b/ProjectGenerator/ProjectGenerator.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..b72c8753 --- /dev/null +++ b/ProjectGenerator/ProjectGenerator.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/ProjectGenerator/ProjectGenerator.ServiceDefaults/ProjectGenerator.ServiceDefaults.csproj b/ProjectGenerator/ProjectGenerator.ServiceDefaults/ProjectGenerator.ServiceDefaults.csproj new file mode 100644 index 00000000..1b6e209a --- /dev/null +++ b/ProjectGenerator/ProjectGenerator.ServiceDefaults/ProjectGenerator.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 8e632c21..3effe569 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,52 @@ -# Лабораторная работа №1 «Кэширование» +# Лабораторная работа №2 — Балансировка нагрузки ## Описание -Реализация сервиса генерации данных о транспортных средствах с кэшированием ответов в Redis. -Оркестрация проектов при помощи .NET Aspire. +Реализация API Gateway с балансировкой нагрузки на основе параметра запроса (Query Based Load Balancing) по алгоритму: `индекс_реплики = id % количество_реплик`. -## Технологии +## Стек технологий - .NET 8 -- .NET Aspire 9.5 -- Bogus — генерация фейковых данных -- Redis — распределённое кэширование (`IDistributedCache`) +- .NET Aspire — оркестрация и запуск нескольких реплик сервиса генерации +- Ocelot — API Gateway с кастомным балансировщиком нагрузки + +## Оркестрация реплик + +В `AppHost.cs` запускаются 5 реплик сервиса генерации (`generation-api-0` … `generation-api-4`) на портах 5000–5004: + +# Лабораторная работа №1 — Кэширование + +## Описание + +Реализация сервиса генерации контрактов предметной области «Программный проект» с кэшированием ответов через Redis. + +## Стек технологий + +- .NET 8 +- .NET Aspire — оркестрация сервисов +- Bogus — генерация тестовых данных +- Redis — распределённый кэш (IDistributedCache) - Blazor WebAssembly — клиентское приложение -- Blazorise — UI-компоненты -- OpenTelemetry — структурное логирование и трассировка - -## Характеристики генерируемого объекта - -| № | Характеристика | Тип данных | Источник Bogus | -|---|---|---|---| -| 1 | Идентификатор в системе | `int` | Параметр запроса | -| 2 | VIN-номер | `string` | `Vehicle.Vin()` | -| 3 | Производитель | `string` | `Vehicle.Manufacturer()` | -| 4 | Модель | `string` | `Vehicle.Model()` | -| 5 | Год выпуска | `int` | `Random.Int(1960, текущий год)` | -| 6 | Тип корпуса | `string` | `Vehicle.Type()` | -| 7 | Тип топлива | `string` | `Vehicle.Fuel()` | -| 8 | Цвет корпуса | `string` | `Commerce.Color()` | -| 9 | Пробег | `double` | `Random.Double(0, 1000000)` | -| 10 | Дата последнего техобслуживания | `DateOnly` | Между годом выпуска и текущей датой | - -### Кэширование - -- Ключ кэша: `vehicle:{id}` -- Время жизни: настраивается в `appsettings.json` (`Cache:ExpirationMinutes`, по умолчанию 5 минут) -- При ошибке чтения/записи кэша — логируется warning, запрос обрабатывается без кэша + +## Предметная область — Программный проект + +| № | Характеристика | Тип данных | +|---|---|---| +| 1 | Идентификатор в системе | int | +| 2 | Название проекта | string | +| 3 | Заказчик проекта | string | +| 4 | Менеджер проекта | string | +| 5 | Дата начала | DateOnly | +| 6 | Плановая дата завершения | DateOnly | +| 7 | Фактическая дата завершения | DateOnly? | +| 8 | Бюджет | decimal | +| 9 | Фактические затраты | decimal | +| 10 | Процент выполнения | int | ## API ``` -GET /api/vehicle?id={id} +GET /generate/?id={id} ``` -Возвращает JSON с данными транспортного средства. При повторном запросе с тем же `id` — данные берутся из кэша Redis. +Возвращает JSON с программным проектом. При повторном запросе с тем же id данные берутся из кэша Redis (TTL — 15 минут). From 421e1bb15c7738f9a332372b62e826bd970b6873 Mon Sep 17 00:00:00 2001 From: Mlavrukk Date: Fri, 17 Apr 2026 20:38:50 +0400 Subject: [PATCH 3/3] remade2 --- Api.Gateway/Api.Gateway.csproj | 4 +- .../LoadBalancers/QueryBasedLoadBalancer.cs | 42 ------ Api.Gateway/Program.cs | 13 +- Api.Gateway/Properties/launchSettings.json | 8 +- Api.Gateway/WeightedRoundRobin.cs | 52 +++++++ Api.Gateway/appsettings.json | 13 +- Api.Gateway/ocelot.json | 16 +-- Client.Wasm/Components/StudentCard.razor | 6 +- Client.Wasm/wwwroot/appsettings.json | 2 +- CloudDevelopment.sln | 36 +++-- ProjectGenerator.Api/Program.cs | 18 --- .../ProjectGenerator.Api.csproj | 23 ---- .../Properties/launchSettings.json | 38 ------ .../Services/ISoftwareProjectGenerator.cs | 14 -- .../Services/ISoftwareProjectService.cs | 14 -- .../Services/SoftwareProjectGenerator.cs | 62 --------- .../Services/SoftwareProjectService.cs | 90 ------------- .../appsettings.Development.json | 8 -- ProjectGenerator.Api/appsettings.json | 10 -- .../Models/SoftwareProject.cs | 57 -------- .../ProjectGenerator.Domain.csproj | 13 -- .../ProjectGenerator.AppHost/AppHost.cs | 20 --- .../ProjectGenerator.AppHost.csproj | 24 ---- .../Properties/launchSettings.json | 29 ---- .../appsettings.Development.json | 8 -- .../ProjectGenerator.AppHost/appsettings.json | 9 -- .../Extensions.cs | 127 ------------------ .../ProjectGenerator.ServiceDefaults.csproj | 22 --- README.md | 89 +++++++----- VehicleVault.Api/Program.cs | 13 -- VehicleVault.Api/appsettings.json | 6 - VehicleVault/VehicleVault.AppHost/AppHost.cs | 14 +- .../VehicleVault.AppHost.csproj | 3 +- 33 files changed, 168 insertions(+), 735 deletions(-) delete mode 100644 Api.Gateway/LoadBalancers/QueryBasedLoadBalancer.cs create mode 100644 Api.Gateway/WeightedRoundRobin.cs delete mode 100644 ProjectGenerator.Api/Program.cs delete mode 100644 ProjectGenerator.Api/ProjectGenerator.Api.csproj delete mode 100644 ProjectGenerator.Api/Properties/launchSettings.json delete mode 100644 ProjectGenerator.Api/Services/ISoftwareProjectGenerator.cs delete mode 100644 ProjectGenerator.Api/Services/ISoftwareProjectService.cs delete mode 100644 ProjectGenerator.Api/Services/SoftwareProjectGenerator.cs delete mode 100644 ProjectGenerator.Api/Services/SoftwareProjectService.cs delete mode 100644 ProjectGenerator.Api/appsettings.Development.json delete mode 100644 ProjectGenerator.Api/appsettings.json delete mode 100644 ProjectGenerator.Domain/Models/SoftwareProject.cs delete mode 100644 ProjectGenerator.Domain/ProjectGenerator.Domain.csproj delete mode 100644 ProjectGenerator/ProjectGenerator.AppHost/AppHost.cs delete mode 100644 ProjectGenerator/ProjectGenerator.AppHost/ProjectGenerator.AppHost.csproj delete mode 100644 ProjectGenerator/ProjectGenerator.AppHost/Properties/launchSettings.json delete mode 100644 ProjectGenerator/ProjectGenerator.AppHost/appsettings.Development.json delete mode 100644 ProjectGenerator/ProjectGenerator.AppHost/appsettings.json delete mode 100644 ProjectGenerator/ProjectGenerator.ServiceDefaults/Extensions.cs delete mode 100644 ProjectGenerator/ProjectGenerator.ServiceDefaults/ProjectGenerator.ServiceDefaults.csproj diff --git a/Api.Gateway/Api.Gateway.csproj b/Api.Gateway/Api.Gateway.csproj index 0b6b959f..1bf624ba 100644 --- a/Api.Gateway/Api.Gateway.csproj +++ b/Api.Gateway/Api.Gateway.csproj @@ -9,9 +9,9 @@ - + - + diff --git a/Api.Gateway/LoadBalancers/QueryBasedLoadBalancer.cs b/Api.Gateway/LoadBalancers/QueryBasedLoadBalancer.cs deleted file mode 100644 index c5646c7c..00000000 --- a/Api.Gateway/LoadBalancers/QueryBasedLoadBalancer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Ocelot.LoadBalancer.Interfaces; -using Ocelot.Responses; -using Ocelot.Values; - -namespace Api.Gateway.LoadBalancers; - - -/// -/// Балансировщик нагрузки на основе параметра запроса id. -/// -/// -/// Алгоритм: индекс реплики = id % количество_реплик. -/// Это гарантирует, что один и тот же идентификатор всегда попадает -/// на одну и ту же реплику (sticky routing по id). -/// -/// -/// Делегат, возвращающий список доступных реплик сервиса. -/// Вызывается при каждом запросе, чтобы учитывать динамически -/// изменяющийся пул экземпляров. -/// -public class QueryBasedLoadBalancer(Func>> serviceProviderFactory) : ILoadBalancer -{ - public string Type => nameof(QueryBasedLoadBalancer); - - public async Task> LeaseAsync(HttpContext context) - { - var services = await serviceProviderFactory(); - - if (services.Count == 0) - throw new InvalidOperationException("No available downstream services"); - - if (!context.Request.Query.TryGetValue("id", out var idValue) || - !int.TryParse(idValue, out var id)) - throw new InvalidOperationException("Query parameter 'id' is missing or invalid"); - - var index = Math.Abs(id) % services.Count; - - return new OkResponse(services[index].HostAndPort); - } - - public void Release(ServiceHostAndPort hostAndPort) { } -} \ No newline at end of file diff --git a/Api.Gateway/Program.cs b/Api.Gateway/Program.cs index c74a8bee..e45ec601 100644 --- a/Api.Gateway/Program.cs +++ b/Api.Gateway/Program.cs @@ -1,4 +1,4 @@ -using Api.Gateway.LoadBalancers; +using Api.Gateway; using Ocelot.DependencyInjection; using Ocelot.Middleware; @@ -8,17 +8,18 @@ builder.Services.AddServiceDiscovery(); builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); builder.Services.AddOcelot() - .AddCustomLoadBalancer((_, _, provider) => new(provider.GetAsync)); + .AddCustomLoadBalancer((sp, _, provider) => + new WeightedRoundRobin(provider.GetAsync, sp.GetRequiredService())); +var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; -var trustedUrls = builder.Configuration.GetSection("CorsOrigins").Get() ?? []; builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { - policy.WithOrigins(trustedUrls) - .WithMethods("GET") - .WithHeaders("Content-Type"); + policy.WithOrigins(allowedOrigins) + .AllowAnyHeader() + .WithMethods("GET"); }); }); diff --git a/Api.Gateway/Properties/launchSettings.json b/Api.Gateway/Properties/launchSettings.json index 82a900e4..7ad1b007 100644 --- a/Api.Gateway/Properties/launchSettings.json +++ b/Api.Gateway/Properties/launchSettings.json @@ -4,8 +4,8 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:62081", - "sslPort": 44391 + "applicationUrl": "http://localhost:6371", + "sslPort": 44374 } }, "profiles": { @@ -13,7 +13,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5200", + "applicationUrl": "http://localhost:5116", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -22,7 +22,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7190;http://localhost:5200", + "applicationUrl": "https://localhost:7145;http://localhost:5116", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Api.Gateway/WeightedRoundRobin.cs b/Api.Gateway/WeightedRoundRobin.cs new file mode 100644 index 00000000..9006a6fc --- /dev/null +++ b/Api.Gateway/WeightedRoundRobin.cs @@ -0,0 +1,52 @@ +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Api.Gateway; + +/// +/// Балансировщик нагрузки «Взвешенная карусель» (Weighted Round Robin). +/// Реплики перебираются циклически; каждая обрабатывает ровно W запросов подряд, +/// где W — её вес из секции LoadBalancer:Weights в конфигурации (0-based). +/// +/// Делегат для получения списка доступных реплик сервиса. +/// Конфигурация приложения с секцией LoadBalancer:Weights. +public class WeightedRoundRobin( + Func>> services, + IConfiguration configuration) : ILoadBalancer +{ + public string Type => nameof(WeightedRoundRobin); + + private readonly int[] _weights = configuration.GetSection("LoadBalancer:Weights").Get() ?? []; + private readonly object _lock = new(); + private int _index; + private int _used; + + public async Task> LeaseAsync(HttpContext context) + { + var pool = await services(); + + if (pool.Count == 0) + throw new InvalidOperationException("No available downstream services"); + + lock (_lock) + { + if (_index >= pool.Count) _index = 0; + + if (_used >= Weight(_index)) + { + _index = (_index + 1) % pool.Count; + _used = 0; + } + + _used++; + return new OkResponse(pool[_index].HostAndPort); + } + } + + public void Release(ServiceHostAndPort hostAndPort) { } + + /// Возвращает вес реплики по индексу. Если не задан или не положителен — возвращает 1. + /// Индекс реплики. + private int Weight(int i) => i < _weights.Length && _weights[i] > 0 ? _weights[i] : 1; +} diff --git a/Api.Gateway/appsettings.json b/Api.Gateway/appsettings.json index c6c9e512..ba471f37 100644 --- a/Api.Gateway/appsettings.json +++ b/Api.Gateway/appsettings.json @@ -6,8 +6,13 @@ } }, "AllowedHosts": "*", - "CorsOrigins": [ - "http://localhost:5127", - "https://localhost:7282" - ] + "Cors": { + "AllowedOrigins": [ + "http://localhost:5127", + "https://localhost:7282" + ] + }, + "LoadBalancer": { + "Weights": [ 3, 2, 1, 1, 1 ] + } } diff --git a/Api.Gateway/ocelot.json b/Api.Gateway/ocelot.json index 59e0a2b2..09386663 100644 --- a/Api.Gateway/ocelot.json +++ b/Api.Gateway/ocelot.json @@ -1,19 +1,19 @@ { "Routes": [ { - "UpstreamPathTemplate": "/generate", + "UpstreamPathTemplate": "/vehicle", "UpstreamHttpMethod": [ "GET" ], - "DownstreamPathTemplate": "/api/generate", + "DownstreamPathTemplate": "/api/vehicle", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ - { "Host": "localhost", "Port": 5000 }, - { "Host": "localhost", "Port": 5001 }, - { "Host": "localhost", "Port": 5002 }, - { "Host": "localhost", "Port": 5003 }, - { "Host": "localhost", "Port": 5004 } + { "Host": "localhost", "Port": 8000 }, + { "Host": "localhost", "Port": 8001 }, + { "Host": "localhost", "Port": 8002 }, + { "Host": "localhost", "Port": 8003 }, + { "Host": "localhost", "Port": 8004 } ], "LoadBalancerOptions": { - "Type": "QueryBasedLoadBalancer" + "Type": "WeightedRoundRobin" } } ] diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index dbe1ac9e..b6a3ab87 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -5,9 +5,9 @@ Номер №2 "Балансировка нагрузки" - Вариант №46 "Программный проект" - Выполнена Митеревым Дмитрием 6513 - Ссылка на форк + Вариант №27 "Транспортное средство" + Выполнила Лаврук Маргарита 6512 + Ссылка на форк diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index 9f75e8ad..b6183a9a 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "https://localhost:7190/generate" + "BaseAddress": "https://localhost:7145/vehicle" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index 403883e1..1521a7f9 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -5,13 +5,11 @@ VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectGenerator.AppHost", "ProjectGenerator\ProjectGenerator.AppHost\ProjectGenerator.AppHost.csproj", "{2BD86B8F-8507-43F0-B17F-9607B7352733}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.AppHost", "VehicleVault\VehicleVault.AppHost\VehicleVault.AppHost.csproj", "{675E2979-6231-45C9-A79D-E8D1BE095D7E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectGenerator.ServiceDefaults", "ProjectGenerator\ProjectGenerator.ServiceDefaults\ProjectGenerator.ServiceDefaults.csproj", "{8F9985A2-9D06-28F9-3428-8039644EF545}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.ServiceDefaults", "VehicleVault\VehicleVault.ServiceDefaults\VehicleVault.ServiceDefaults.csproj", "{2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectGenerator.Api", "ProjectGenerator.Api\ProjectGenerator.Api.csproj", "{AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectGenerator.Domain", "ProjectGenerator.Domain\ProjectGenerator.Domain.csproj", "{67E00678-695C-407F-B401-27C9EC3527B1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleVault.Api", "VehicleVault.Api\VehicleVault.Api.csproj", "{EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Gateway", "Api.Gateway\Api.Gateway.csproj", "{C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}" EndProject @@ -25,22 +23,18 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU - {2BD86B8F-8507-43F0-B17F-9607B7352733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2BD86B8F-8507-43F0-B17F-9607B7352733}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BD86B8F-8507-43F0-B17F-9607B7352733}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2BD86B8F-8507-43F0-B17F-9607B7352733}.Release|Any CPU.Build.0 = Release|Any CPU - {8F9985A2-9D06-28F9-3428-8039644EF545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F9985A2-9D06-28F9-3428-8039644EF545}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F9985A2-9D06-28F9-3428-8039644EF545}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F9985A2-9D06-28F9-3428-8039644EF545}.Release|Any CPU.Build.0 = Release|Any CPU - {AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AAEE18D8-2A8E-7273-1128-2FA0B1FA40F9}.Release|Any CPU.Build.0 = Release|Any CPU - {67E00678-695C-407F-B401-27C9EC3527B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {67E00678-695C-407F-B401-27C9EC3527B1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {67E00678-695C-407F-B401-27C9EC3527B1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {67E00678-695C-407F-B401-27C9EC3527B1}.Release|Any CPU.Build.0 = Release|Any CPU + {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {675E2979-6231-45C9-A79D-E8D1BE095D7E}.Release|Any CPU.Build.0 = Release|Any CPU + {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2691D15C-C371-2A92-5FA1-3FA8EDE4BFA9}.Release|Any CPU.Build.0 = Release|Any CPU + {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEC635D9-1FAB-BCC6-17E0-3EE52C2D262C}.Release|Any CPU.Build.0 = Release|Any CPU {C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C99E72F4-9BA7-7D56-C88E-FB28534EFCB6}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/ProjectGenerator.Api/Program.cs b/ProjectGenerator.Api/Program.cs deleted file mode 100644 index f09a9815..00000000 --- a/ProjectGenerator.Api/Program.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ProjectGenerator.Api.Services; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); -builder.AddRedisDistributedCache("cache"); - -builder.Services.AddSingleton(); -builder.Services.AddScoped(); - -var app = builder.Build(); - -app.MapDefaultEndpoints(); - -app.MapGet("/api/generate", async (int id, ISoftwareProjectService service) => - await service.GetOrGenerate(id)); - -app.Run(); diff --git a/ProjectGenerator.Api/ProjectGenerator.Api.csproj b/ProjectGenerator.Api/ProjectGenerator.Api.csproj deleted file mode 100644 index 3c878a17..00000000 --- a/ProjectGenerator.Api/ProjectGenerator.Api.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - - - - - - diff --git a/ProjectGenerator.Api/Properties/launchSettings.json b/ProjectGenerator.Api/Properties/launchSettings.json deleted file mode 100644 index 553903db..00000000 --- a/ProjectGenerator.Api/Properties/launchSettings.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:10098", - "sslPort": 44392 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5283", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7046;http://localhost:5283", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/ProjectGenerator.Api/Services/ISoftwareProjectGenerator.cs b/ProjectGenerator.Api/Services/ISoftwareProjectGenerator.cs deleted file mode 100644 index d5845502..00000000 --- a/ProjectGenerator.Api/Services/ISoftwareProjectGenerator.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ProjectGenerator.Domain.Models; - -namespace ProjectGenerator.Api.Services; - -/// -/// Интерфейс генератора программных проектов -/// -public interface ISoftwareProjectGenerator -{ - /// - /// Генерирует программный проект с указанным идентификатором - /// - public SoftwareProject Generate(int id); -} diff --git a/ProjectGenerator.Api/Services/ISoftwareProjectService.cs b/ProjectGenerator.Api/Services/ISoftwareProjectService.cs deleted file mode 100644 index 26644fbc..00000000 --- a/ProjectGenerator.Api/Services/ISoftwareProjectService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ProjectGenerator.Domain.Models; - -namespace ProjectGenerator.Api.Services; - -/// -/// Интерфейс сервиса программных проектов -/// -public interface ISoftwareProjectService -{ - /// - /// Получает программный проект по идентификатору из кэша или генерирует новый - /// - public Task GetOrGenerate(int id); -} diff --git a/ProjectGenerator.Api/Services/SoftwareProjectGenerator.cs b/ProjectGenerator.Api/Services/SoftwareProjectGenerator.cs deleted file mode 100644 index fd38e8ab..00000000 --- a/ProjectGenerator.Api/Services/SoftwareProjectGenerator.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Bogus; -using Bogus.DataSets; -using ProjectGenerator.Domain.Models; - -namespace ProjectGenerator.Api.Services; - -/// -/// Генератор программных проектов на основе Bogus -/// -public class SoftwareProjectGenerator : ISoftwareProjectGenerator -{ - private readonly Faker _faker = new Faker("ru") - .RuleFor(p => p.Id, _ => 0) - .RuleFor(p => p.ProjectName, f => - $"{f.Commerce.ProductName()} {f.Hacker.Adjective()} {f.Finance.AccountName()} {f.Lorem.Word()}") - .RuleFor(p => p.Customer, f => f.Company.CompanyName()) - .RuleFor(p => p.ProjectManager, f => - { - var gender = f.PickRandom(); - return - $"{f.Name.LastName(gender)} {f.Name.FirstName(gender)} " + - $"{GeneratePatronymic(f.Name.FirstName(Name.Gender.Male), gender == Name.Gender.Female)}"; - }) - .RuleFor(p => p.StartDate, f => - DateOnly.FromDateTime(f.Date.Past(2))) - .RuleFor(p => p.PlannedEndDate, (f, p) => - p.StartDate.AddDays(f.Random.Int(30, 365))) - .RuleFor(p => p.ActualEndDate, (f, p) => - f.Random.Bool(0.3f) - ? p.StartDate.AddDays(f.Random.Int(10, 400)) - : null) - .RuleFor(p => p.Budget, f => - Math.Round(f.Finance.Amount(100_000, 100_000_000), 2)) - .RuleFor(p => p.ActualCosts, (f, p) => - Math.Round(p.Budget * f.Random.Decimal(0.01m, 1.2m), 2)) - .RuleFor(p => p.CompletionPercentage, (f, p) => - p.ActualEndDate.HasValue ? 100 : f.Random.Int(0, 99)); - - /// - /// Генерация отчества на основе мужского имени - /// - /// Мужское имя - /// Признак женского пола - /// Отчество - private static string GeneratePatronymic(string maleName, bool isFemale) => maleName switch - { - _ when maleName.EndsWith("ий") => maleName[..^2] + (isFemale ? "ьевна" : "ьевич"), - _ when maleName.EndsWith('ь') => maleName[..^1] + (isFemale ? "евна" : "евич"), - _ when maleName.EndsWith('й') => maleName[..^1] + (isFemale ? "евна" : "евич"), - _ when maleName.EndsWith('а') || maleName.EndsWith('я') => maleName[..^1] + (isFemale ? "ична" : "ич"), - _ => maleName + (isFemale ? "овна" : "ович") - }; - - /// - public SoftwareProject Generate(int id) - { - var project = _faker.Generate(); - project.Id = id; - return project; - } - -} diff --git a/ProjectGenerator.Api/Services/SoftwareProjectService.cs b/ProjectGenerator.Api/Services/SoftwareProjectService.cs deleted file mode 100644 index 02b23937..00000000 --- a/ProjectGenerator.Api/Services/SoftwareProjectService.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Text.Json; -using Microsoft.Extensions.Caching.Distributed; -using ProjectGenerator.Domain.Models; - -namespace ProjectGenerator.Api.Services; - -/// -/// Сервис программных проектов с кэшированием -/// -/// Генератор программных проектов -/// Распределённый кэш -/// Конфигурация приложения -/// Логгер -public class SoftwareProjectService( - ISoftwareProjectGenerator generator, - IDistributedCache cache, - IConfiguration configuration, - ILogger logger) : ISoftwareProjectService -{ - private const string CacheKeyPrefix = "software-project"; - private readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(configuration.GetValue("CacheTtlMinutes", 15)); - - /// - public async Task GetOrGenerate(int id) - { - var cacheKey = $"{CacheKeyPrefix}:{id}"; - - var cached = await GetFromCache(cacheKey); - if (cached is not null) - { - return cached; - } - - logger.LogInformation("Cache miss for id {Id}, generating new data", id); - - var project = generator.Generate(id); - - await SetToCache(cacheKey, project); - - return project; - } - - /// - /// Получение программного проекта из кэша - /// - /// Ключ кэша - /// Программный проект или null - private async Task GetFromCache(string cacheKey) - { - try - { - var cached = await cache.GetStringAsync(cacheKey); - if (cached is null) - { - return null; - } - - logger.LogInformation("Cache hit for key {CacheKey}", cacheKey); - return JsonSerializer.Deserialize(cached); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to read from cache for key {CacheKey}", cacheKey); - return null; - } - } - - /// - /// Сохранение программного проекта в кэш - /// - /// Ключ кэша - /// Программный проект - private async Task SetToCache(string cacheKey, SoftwareProject project) - { - try - { - var json = JsonSerializer.Serialize(project); - await cache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = _cacheTtl - }); - - logger.LogInformation("Cached data for key {CacheKey}", cacheKey); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to write to cache for key {CacheKey}", cacheKey); - } - } -} diff --git a/ProjectGenerator.Api/appsettings.Development.json b/ProjectGenerator.Api/appsettings.Development.json deleted file mode 100644 index 0c208ae9..00000000 --- a/ProjectGenerator.Api/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/ProjectGenerator.Api/appsettings.json b/ProjectGenerator.Api/appsettings.json deleted file mode 100644 index 2d053cce..00000000 --- a/ProjectGenerator.Api/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "CacheTtlMinutes": 15 -} diff --git a/ProjectGenerator.Domain/Models/SoftwareProject.cs b/ProjectGenerator.Domain/Models/SoftwareProject.cs deleted file mode 100644 index 23c86aea..00000000 --- a/ProjectGenerator.Domain/Models/SoftwareProject.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace ProjectGenerator.Domain.Models; - -/// -/// Программный проект -/// -public sealed class SoftwareProject -{ - /// - /// Идентификатор в системе - /// - public int Id { get; set; } - - /// - /// Название проекта - /// - public required string ProjectName { get; init; } - - /// - /// Заказчик проекта - /// - public required string Customer { get; init; } - - /// - /// Менеджер проекта - /// - public required string ProjectManager { get; init; } - - /// - /// Дата начала - /// - public DateOnly StartDate { get; init; } - - /// - /// Плановая дата завершения - /// - public DateOnly PlannedEndDate { get; init; } - - /// - /// Фактическая дата завершения - /// - public DateOnly? ActualEndDate { get; init; } - - /// - /// Бюджет - /// - public decimal Budget { get; init; } - - /// - /// Фактические затраты - /// - public decimal ActualCosts { get; init; } - - /// - /// Процент выполнения - /// - public int CompletionPercentage { get; init; } -} diff --git a/ProjectGenerator.Domain/ProjectGenerator.Domain.csproj b/ProjectGenerator.Domain/ProjectGenerator.Domain.csproj deleted file mode 100644 index c60e6513..00000000 --- a/ProjectGenerator.Domain/ProjectGenerator.Domain.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - diff --git a/ProjectGenerator/ProjectGenerator.AppHost/AppHost.cs b/ProjectGenerator/ProjectGenerator.AppHost/AppHost.cs deleted file mode 100644 index d43a3743..00000000 --- a/ProjectGenerator/ProjectGenerator.AppHost/AppHost.cs +++ /dev/null @@ -1,20 +0,0 @@ -var builder = DistributedApplication.CreateBuilder(args); - -var cache = builder.AddRedis("cache") - .WithRedisInsight(); - -var apiGateway = builder.AddProject("api-gateway"); - -for (var i = 0; i < 5; i++) -{ - var generationApi = builder.AddProject($"generation-api-{i}", launchProfileName: null) - .WithHttpsEndpoint(5000 + i) - .WithReference(cache) - .WaitFor(cache); - apiGateway.WaitFor(generationApi); -} - -builder.AddProject("client-wasm") - .WaitFor(apiGateway); - -builder.Build().Run(); diff --git a/ProjectGenerator/ProjectGenerator.AppHost/ProjectGenerator.AppHost.csproj b/ProjectGenerator/ProjectGenerator.AppHost/ProjectGenerator.AppHost.csproj deleted file mode 100644 index 0d95d202..00000000 --- a/ProjectGenerator/ProjectGenerator.AppHost/ProjectGenerator.AppHost.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Exe - net8.0 - enable - enable - 5f0fcf55-56c6-4723-953b-9e24de107d8f - - - - - - - - - - - - - - diff --git a/ProjectGenerator/ProjectGenerator.AppHost/Properties/launchSettings.json b/ProjectGenerator/ProjectGenerator.AppHost/Properties/launchSettings.json deleted file mode 100644 index 475d465e..00000000 --- a/ProjectGenerator/ProjectGenerator.AppHost/Properties/launchSettings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:17246;http://localhost:15108", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21233", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22087" - } - }, - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15108", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19053", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20103" - } - } - } -} diff --git a/ProjectGenerator/ProjectGenerator.AppHost/appsettings.Development.json b/ProjectGenerator/ProjectGenerator.AppHost/appsettings.Development.json deleted file mode 100644 index 0c208ae9..00000000 --- a/ProjectGenerator/ProjectGenerator.AppHost/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/ProjectGenerator/ProjectGenerator.AppHost/appsettings.json b/ProjectGenerator/ProjectGenerator.AppHost/appsettings.json deleted file mode 100644 index 31c092aa..00000000 --- a/ProjectGenerator/ProjectGenerator.AppHost/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Aspire.Hosting.Dcp": "Warning" - } - } -} diff --git a/ProjectGenerator/ProjectGenerator.ServiceDefaults/Extensions.cs b/ProjectGenerator/ProjectGenerator.ServiceDefaults/Extensions.cs deleted file mode 100644 index b72c8753..00000000 --- a/ProjectGenerator/ProjectGenerator.ServiceDefaults/Extensions.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; - -namespace Microsoft.Extensions.Hosting; - -// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. -// This project should be referenced by each service project in your solution. -// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults -public static class Extensions -{ - private const string HealthEndpointPath = "/health"; - private const string AlivenessEndpointPath = "/alive"; - - public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.ConfigureOpenTelemetry(); - - builder.AddDefaultHealthChecks(); - - builder.Services.AddServiceDiscovery(); - - builder.Services.ConfigureHttpClientDefaults(http => - { - // Turn on resilience by default - http.AddStandardResilienceHandler(); - - // Turn on service discovery by default - http.AddServiceDiscovery(); - }); - - // Uncomment the following to restrict the allowed schemes for service discovery. - // builder.Services.Configure(options => - // { - // options.AllowedSchemes = ["https"]; - // }); - - return builder; - } - - public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.Logging.AddOpenTelemetry(logging => - { - logging.IncludeFormattedMessage = true; - logging.IncludeScopes = true; - }); - - builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => - { - metrics.AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation(); - }) - .WithTracing(tracing => - { - tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation(tracing => - // Exclude health check requests from tracing - tracing.Filter = context => - !context.Request.Path.StartsWithSegments(HealthEndpointPath) - && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) - ) - // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) - //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(); - }); - - builder.AddOpenTelemetryExporters(); - - return builder; - } - - private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - - if (useOtlpExporter) - { - builder.Services.AddOpenTelemetry().UseOtlpExporter(); - } - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} - - return builder; - } - - public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); - - return builder; - } - - public static WebApplication MapDefaultEndpoints(this WebApplication app) - { - // Adding health checks endpoints to applications in non-development environments has security implications. - // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. - if (app.Environment.IsDevelopment()) - { - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks(HealthEndpointPath); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("live") - }); - } - - return app; - } -} diff --git a/ProjectGenerator/ProjectGenerator.ServiceDefaults/ProjectGenerator.ServiceDefaults.csproj b/ProjectGenerator/ProjectGenerator.ServiceDefaults/ProjectGenerator.ServiceDefaults.csproj deleted file mode 100644 index 1b6e209a..00000000 --- a/ProjectGenerator/ProjectGenerator.ServiceDefaults/ProjectGenerator.ServiceDefaults.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net8.0 - enable - enable - true - - - - - - - - - - - - - - - diff --git a/README.md b/README.md index 3effe569..f774b4f0 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,73 @@ -# Лабораторная работа №2 — Балансировка нагрузки +# Лабораторная работа №1 «Кэширование» ## Описание -Реализация API Gateway с балансировкой нагрузки на основе параметра запроса (Query Based Load Balancing) по алгоритму: `индекс_реплики = id % количество_реплик`. +Реализация сервиса генерации данных о транспортных средствах с кэшированием ответов в Redis. +Оркестрация проектов при помощи .NET Aspire. -## Стек технологий +## Технологии - .NET 8 -- .NET Aspire — оркестрация и запуск нескольких реплик сервиса генерации -- Ocelot — API Gateway с кастомным балансировщиком нагрузки +- .NET Aspire 9.5 +- Bogus — генерация фейковых данных +- Redis — распределённое кэширование (`IDistributedCache`) +- Blazor WebAssembly — клиентское приложение +- Blazorise — UI-компоненты +- OpenTelemetry — структурное логирование и трассировка + +## Характеристики генерируемого объекта + +| № | Характеристика | Тип данных | Источник Bogus | +|---|---|---|---| +| 1 | Идентификатор в системе | `int` | Параметр запроса | +| 2 | VIN-номер | `string` | `Vehicle.Vin()` | +| 3 | Производитель | `string` | `Vehicle.Manufacturer()` | +| 4 | Модель | `string` | `Vehicle.Model()` | +| 5 | Год выпуска | `int` | `Random.Int(1960, текущий год)` | +| 6 | Тип корпуса | `string` | `Vehicle.Type()` | +| 7 | Тип топлива | `string` | `Vehicle.Fuel()` | +| 8 | Цвет корпуса | `string` | `Commerce.Color()` | +| 9 | Пробег | `double` | `Random.Double(0, 1000000)` | +| 10 | Дата последнего техобслуживания | `DateOnly` | Между годом выпуска и текущей датой | + +### Кэширование + +- Ключ кэша: `vehicle:{id}` +- Время жизни: настраивается в `appsettings.json` (`Cache:ExpirationMinutes`, по умолчанию 5 минут) +- При ошибке чтения/записи кэша — логируется warning, запрос обрабатывается без кэша -## Оркестрация реплик +## API -В `AppHost.cs` запускаются 5 реплик сервиса генерации (`generation-api-0` … `generation-api-4`) на портах 5000–5004: +``` +GET /api/vehicle?id={id} +``` -# Лабораторная работа №1 — Кэширование +Возвращает JSON с данными транспортного средства. При повторном запросе с тем же `id` — данные берутся из кэша Redis. -## Описание +--- -Реализация сервиса генерации контрактов предметной области «Программный проект» с кэшированием ответов через Redis. +# Лабораторная работа №2 «Балансировка нагрузки» -## Стек технологий +## Описание -- .NET 8 -- .NET Aspire — оркестрация сервисов -- Bogus — генерация тестовых данных -- Redis — распределённый кэш (IDistributedCache) -- Blazor WebAssembly — клиентское приложение +Реализация API-шлюза на основе Ocelot с алгоритмом балансировки нагрузки **Weighted Round Robin**. +Оркестрация запускает несколько реплик сервиса генерации через .NET Aspire. -## Предметная область — Программный проект - -| № | Характеристика | Тип данных | -|---|---|---| -| 1 | Идентификатор в системе | int | -| 2 | Название проекта | string | -| 3 | Заказчик проекта | string | -| 4 | Менеджер проекта | string | -| 5 | Дата начала | DateOnly | -| 6 | Плановая дата завершения | DateOnly | -| 7 | Фактическая дата завершения | DateOnly? | -| 8 | Бюджет | decimal | -| 9 | Фактические затраты | decimal | -| 10 | Процент выполнения | int | +## Алгоритм: Weighted Round Robin -## API +Каждой реплике присваивается числовой вес из конфигурации (`LoadBalancer:Weights` в `appsettings.json` шлюза). +Реплики перебираются циклически; реплика с весом `W` обрабатывает `W` запросов подряд перед переходом к следующей. +### Конфигурация весов (`Api.Gateway/appsettings.json`) + +```json +"LoadBalancer": { + "Weights": [ 3, 2, 1, 1, 1 ] +} ``` -GET /generate/?id={id} -``` -Возвращает JSON с программным проектом. При повторном запросе с тем же id данные берутся из кэша Redis (TTL — 15 минут). +Индекс массива соответствует порядковому номеру реплики (0-based). Если вес не задан — используется значение `1`. + +## Оркестрация реплик (`VehicleVault.AppHost`) + +Aspire запускает 5 независимых экземпляров `VehicleVault.Api` на портах `8000–8004`: diff --git a/VehicleVault.Api/Program.cs b/VehicleVault.Api/Program.cs index 9b4f3d2c..c41d76a0 100644 --- a/VehicleVault.Api/Program.cs +++ b/VehicleVault.Api/Program.cs @@ -8,22 +8,9 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); -var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; - -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy(policy => - { - policy.WithOrigins(allowedOrigins) - .AllowAnyHeader() - .WithMethods("GET"); - }); -}); - var app = builder.Build(); app.MapDefaultEndpoints(); -app.UseCors(); app.MapGet("api/vehicle", async (int id, IVehicleCacheService vehicleService) => await vehicleService.GetOrGenerate(id)); diff --git a/VehicleVault.Api/appsettings.json b/VehicleVault.Api/appsettings.json index e7350acf..4b07986a 100644 --- a/VehicleVault.Api/appsettings.json +++ b/VehicleVault.Api/appsettings.json @@ -8,11 +8,5 @@ "AllowedHosts": "*", "Cache": { "ExpirationMinutes": 5 - }, - "Cors": { - "AllowedOrigins": [ - "http://localhost:5127", - "https://localhost:7282" - ] } } diff --git a/VehicleVault/VehicleVault.AppHost/AppHost.cs b/VehicleVault/VehicleVault.AppHost/AppHost.cs index a4202cd9..17b8a7ab 100644 --- a/VehicleVault/VehicleVault.AppHost/AppHost.cs +++ b/VehicleVault/VehicleVault.AppHost/AppHost.cs @@ -3,11 +3,17 @@ var cache = builder.AddRedis("cache") .WithRedisInsight(); -var api = builder.AddProject("vehiclevault-api") - .WithReference(cache) - .WaitFor(cache); +var apiGateway = builder.AddProject("api-gateway"); +for (var i = 0; i < 5; i++) +{ + var api = builder.AddProject($"vehiclevault-api-{i}", launchProfileName: null) + .WithHttpsEndpoint(8000 + i) + .WithReference(cache) + .WaitFor(cache); + apiGateway.WaitFor(api); +} builder.AddProject("client-wasm") - .WaitFor(api); + .WaitFor(apiGateway); builder.Build().Run(); diff --git a/VehicleVault/VehicleVault.AppHost/VehicleVault.AppHost.csproj b/VehicleVault/VehicleVault.AppHost/VehicleVault.AppHost.csproj index b73474a2..674cec95 100644 --- a/VehicleVault/VehicleVault.AppHost/VehicleVault.AppHost.csproj +++ b/VehicleVault/VehicleVault.AppHost/VehicleVault.AppHost.csproj @@ -1,4 +1,4 @@ - + @@ -16,6 +16,7 @@ +