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.
| 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.
| <UnorderedListItem>Вариант <Strong>№Х "Название варианта"</Strong></UnorderedListItem> | ||
| <UnorderedListItem>Выполнена <Strong>Фамилией Именем 65ХХ</Strong> </UnorderedListItem> | ||
| <UnorderedListItem><Link To="https://puginarug.com/">Ссылка на форк</Link></UnorderedListItem> | ||
| <UnorderedListItem>Номер <Strong>№1 «Кэширование»</Strong></UnorderedListItem> |
| foreach (var (service, weight) in weighted) | ||
| { | ||
| cumulative += weight; | ||
| if (roll < cumulative) |
There was a problem hiding this comment.
Здесь скорее всего лучше <=:
Теоретически самое большое значение у roll будет total, а самое большое значение у cumulative будет тоже total, то есть возможна ситуация когда cumulative == roll и неравенство не будет достигнуто
| public sealed class WeightedRandomLoadBalancer : ILoadBalancer | ||
| { | ||
| private readonly IServiceDiscoveryProvider _serviceDiscovery; | ||
| private readonly IReadOnlyDictionary<string, double> _weights; | ||
|
|
||
| public WeightedRandomLoadBalancer( | ||
| IServiceDiscoveryProvider serviceDiscovery, | ||
| IReadOnlyDictionary<string, double> weights) | ||
| { | ||
| _serviceDiscovery = serviceDiscovery; | ||
| _weights = weights; | ||
| } |
| return new OkResponse<ServiceHostAndPort>(SelectByWeight(services).HostAndPort); | ||
| } | ||
|
|
||
| private Service SelectByWeight(IList<Service> services) |
There was a problem hiding this comment.
В этот приватный метод вряд ли будет передаваться что-то помимо List<Service>, так что для производительности можно этот тип явно и указать
|
|
||
| builder.Services.AddCors(options => | ||
| { | ||
| options.AddDefaultPolicy(policy => |
There was a problem hiding this comment.
Тут копилот тоже прав, в ServiceDefaults есть AddDefaultPolicy, тут лучше задать именованную политику
|
|
||
| var weights = builder.Configuration | ||
| .GetSection("ReplicaWeights") | ||
| .Get<Dictionary<string, double>>() ?? new Dictionary<string, double>(); |
There was a problem hiding this comment.
.Get<Dictionary<string, double>>() ?? [];| var generator1 = builder.AddProject<Projects.CreditApplication_Generator>("generator-1") | ||
| .WithEndpoint("http", endpoint => endpoint.Port = 5101) | ||
| .WithReference(redis); | ||
|
|
||
| var generator2 = builder.AddProject<Projects.CreditApplication_Generator>("generator-2") | ||
| .WithEndpoint("http", endpoint => endpoint.Port = 5102) | ||
| .WithReference(redis); | ||
|
|
||
| var generator3 = builder.AddProject<Projects.CreditApplication_Generator>("generator-3") | ||
| .WithEndpoint("http", endpoint => endpoint.Port = 5103) | ||
| .WithReference(redis); |
| @@ -0,0 +1,25 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
|
|
|||
| <Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" /> | |||
|
|
||
| var app = builder.Build(); | ||
|
|
||
| app.UseSerilogRequestLogging(); |
There was a problem hiding this comment.
Serilog можно настроить так, чтобы он писал структурированные логи в aspire (Serilog.Sinks.OpenTelemetry)
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.5.2" /> |
ФИО: Горшенин Дмитрий
Номер группы: 6511
Номер лабораторной: 2
Номер варианта: 4
Краткое описание предметной области: Генератор кредитной заявки
Краткое описание добавленных фич: Реализация Gateway, настройка его работы