Conversation
…аб. роботы В решение добавлены проекты CreditApplication.AppHost (Aspire-хост), CreditApplication.Generator (API генерации кредитных заявок с кэшированием в Redis) и CreditApplication.ServiceDefaults (общие настройки сервисов: логирование, OpenTelemetry, resilience, health checks). Реализована модель заявки, генератор на Bogus, сервис с кэшированием, API-эндпоинт /credit-application.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
PR implements Lab #2 for the “Credit Application Generator” microservice system by adding an API Gateway layer and running multiple generator replicas under .NET Aspire orchestration.
Changes:
- Added API Gateway service (Ocelot) with a custom weighted-random load balancer.
- Added Aspire AppHost orchestration to run Redis + 3 generator replicas + gateway + client.
- Introduced a shared
ServiceDefaultsproject to centralize Serilog/OpenTelemetry/health checks/CORS wiring.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Replaced course template README with project/lab-specific documentation and architecture notes. |
| CreditApplication.ServiceDefaults/Extensions.cs | Added shared host defaults (logging, telemetry, health, CORS, service discovery). |
| CreditApplication.ServiceDefaults/CreditApplication.ServiceDefaults.csproj | New shared project packaging required defaults dependencies. |
| CreditApplication.Generator/* | New generator service (Bogus-based), Redis cache-aside service, and minimal API endpoint. |
| CreditApplication.Gateway/* | New gateway service, Ocelot routes config, and custom WeightedRandomLoadBalancer. |
| CreditApplication.AppHost/* | New Aspire AppHost wiring 3 generator replicas + gateway + redis (+ commander in Dev) + client. |
| CloudDevelopment.sln | Added new projects to the solution + extra build configurations. |
| Client.Wasm/wwwroot/appsettings.json | Pointed client configuration to the gateway endpoint. |
| Client.Wasm/Properties/launchSettings.json | Adjusted local run ports and disabled auto-launch browser. |
| Client.Wasm/Components/StudentCard.razor | Updated student/lab info displayed in UI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (id <= 0) | ||
| { | ||
| logger.LogWarning("Received invalid ID: {Id}", id); | ||
| return Results.BadRequest(new { error = "ID must be a positive number" }); | ||
| } |
There was a problem hiding this comment.
The API returns an English validation message ("ID must be a positive number") while the rest of the service/user-facing text is in Russian. Consider making the error message language consistent (or using a standardized error format/message across the project).
| builder.Services.AddCors(options => | ||
| { | ||
| options.AddDefaultPolicy(policy => | ||
| { | ||
| if (builder.Environment.IsDevelopment()) | ||
| { | ||
| policy.AllowAnyOrigin() | ||
| .AllowAnyMethod() | ||
| .AllowAnyHeader(); | ||
| } | ||
| else | ||
| { | ||
| var allowedOrigins = builder.Configuration | ||
| .GetSection("Cors:AllowedOrigins") | ||
| .Get<string[]>() ?? []; | ||
|
|
||
| policy.WithOrigins(allowedOrigins) | ||
| .WithMethods("GET") | ||
| .WithHeaders("Content-Type", "Authorization"); | ||
| } | ||
| }); | ||
| }); | ||
|
|
There was a problem hiding this comment.
AddGatewayDefaults() already registers a default CORS policy via AddDefaultCors(), but the gateway also calls builder.Services.AddCors() again and redefines the default policy. This double-registration makes the effective policy order-dependent and harder to maintain; consider configuring CORS in a single place (either keep it in ServiceDefaults and remove this block, or remove CORS from AddGatewayDefaults).
| builder.Services.AddCors(options => | |
| { | |
| options.AddDefaultPolicy(policy => | |
| { | |
| if (builder.Environment.IsDevelopment()) | |
| { | |
| policy.AllowAnyOrigin() | |
| .AllowAnyMethod() | |
| .AllowAnyHeader(); | |
| } | |
| else | |
| { | |
| var allowedOrigins = builder.Configuration | |
| .GetSection("Cors:AllowedOrigins") | |
| .Get<string[]>() ?? []; | |
| policy.WithOrigins(allowedOrigins) | |
| .WithMethods("GET") | |
| .WithHeaders("Content-Type", "Authorization"); | |
| } | |
| }); | |
| }); |
| builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); | ||
|
|
||
| var generatorNames = builder.Configuration.GetSection("GeneratorServices").Get<string[]>() ?? []; | ||
| var addressOverrides = new List<KeyValuePair<string, string?>>(); | ||
| for (var i = 0; i < generatorNames.Length; i++) | ||
| { | ||
| var url = builder.Configuration[$"services:{generatorNames[i]}:http:0"]; | ||
| if (!string.IsNullOrEmpty(url) && Uri.TryCreate(url, UriKind.Absolute, out var uri)) | ||
| { | ||
| addressOverrides.Add(new($"Routes:0:DownstreamHostAndPorts:{i}:Host", uri.Host)); | ||
| addressOverrides.Add(new($"Routes:0:DownstreamHostAndPorts:{i}:Port", uri.Port.ToString())); | ||
| } | ||
| } | ||
| if (addressOverrides.Count > 0) | ||
| builder.Configuration.AddInMemoryCollection(addressOverrides); | ||
|
|
||
| var weights = builder.Configuration | ||
| .GetSection("ReplicaWeights") | ||
| .Get<Dictionary<string, double>>() ?? new Dictionary<string, double>(); | ||
|
|
||
| builder.Services | ||
| .AddOcelot(builder.Configuration) | ||
| .AddCustomLoadBalancer((route, serviceDiscovery) => | ||
| new WeightedRandomLoadBalancer(serviceDiscovery, weights)); |
There was a problem hiding this comment.
The weights dictionary is keyed by the downstream host:port strings from config, but this file also overrides DownstreamHostAndPorts at runtime from Aspire service-discovery environment variables. After an override, the actual downstream host/port may no longer match the ReplicaWeights keys, causing the load balancer to fall back to the default weight (1.0) and ignore the intended 0.5/0.3/0.2 distribution. Consider keying weights by service name (e.g., generator-1/2/3) or rebuilding/rewriting the weight keys after applying the address overrides.
| .GetSection("CorsAllowedOrigins") | ||
| .Get<string[]>() ?? []; | ||
|
|
There was a problem hiding this comment.
AddDefaultCors() reads allowed origins from configuration section CorsAllowedOrigins, but the gateway (and typical appsettings structure) uses Cors:AllowedOrigins. Since nothing else in this PR defines CorsAllowedOrigins, production CORS settings configured under Cors:AllowedOrigins would be ignored by services using AddServiceDefaults(). Consider standardizing on one configuration path (e.g., Cors:AllowedOrigins) and updating ServiceDefaults accordingly.
| .GetSection("CorsAllowedOrigins") | |
| .Get<string[]>() ?? []; | |
| .GetSection("Cors:AllowedOrigins") | |
| .Get<string[]>(); | |
| if (allowedOrigins is null || allowedOrigins.Length == 0) | |
| { | |
| allowedOrigins = builder.Configuration | |
| .GetSection("CorsAllowedOrigins") | |
| .Get<string[]>() ?? []; | |
| } |
| private static void SetStatusDependentFields(Faker faker, CreditApplicationModel app) | ||
| { | ||
| var isTerminal = _terminalStatuses.Contains(app.Status); | ||
|
|
||
| if (isTerminal) | ||
| { | ||
| app.DecisionDate = faker.Date.BetweenDateOnly(app.ApplicationDate, DateOnly.FromDateTime(DateTime.Today)); | ||
|
|
||
| if (app.Status == "Одобрена") | ||
| { | ||
| var maxApproved = app.RequestedAmount; | ||
| var minApproved = maxApproved * 0.5m; | ||
| app.ApprovedAmount = Math.Round(faker.Random.Decimal(minApproved, maxApproved), 2); | ||
| } |
There was a problem hiding this comment.
The generator is described as deterministic via UseSeed(id), but DecisionDate is generated using DateTime.Today as an upper bound. This makes the same id potentially produce different results on different days (range length changes), which breaks determinism guarantees. Consider deriving DecisionDate deterministically from ApplicationDate (e.g., add a seeded random offset capped by a fixed max) rather than using the current date.
| <UnorderedListItem>Номер <Strong>№1 «Кэширование»</Strong></UnorderedListItem> | ||
| <UnorderedListItem>Вариант <Strong>№4 «Кредитная заявка»</Strong></UnorderedListItem> | ||
| <UnorderedListItem>Выполнена <Strong>Горшениным Дмитрием 6511</Strong> </UnorderedListItem> | ||
| <UnorderedListItem><Link To="https://github.com/dmgorshenin/cloud-development?tab=readme-ov-file">Ссылка на форк</Link></UnorderedListItem> |
There was a problem hiding this comment.
This card states that lab work №1 («Кэширование») was completed, but this PR is for lab №2 (API Gateway + load balancing). If the UI is intended to reflect the current lab/PR, update the lab number/title accordingly so the client page matches the submission.
| <Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" /> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <IsAspireHost>true</IsAspireHost> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Aspire.Hosting" Version="9.5.2" /> | ||
| <PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.2" /> | ||
| <PackageReference Include="Aspire.Hosting.Redis" Version="9.5.2" /> |
There was a problem hiding this comment.
Aspire.AppHost.Sdk is pinned to 9.0.0 while the Aspire package references are 9.5.2. Mixing SDK and package versions can lead to subtle build/target mismatches; consider aligning the SDK version with the referenced Aspire package version (or vice versa) to keep the toolchain consistent.
ФИО: Горшенин Дмитрий
Номер группы: 6511
Номер лабораторной: 2
Номер варианта: 4
Краткое описание предметной области: Генератор кредитной заявки
Краткое описание добавленных фич: Реализация Gateway, настройка его работы