diff --git a/ApiGateway/ApiGateway.csproj b/ApiGateway/ApiGateway.csproj
new file mode 100644
index 00000000..d3911d2e
--- /dev/null
+++ b/ApiGateway/ApiGateway.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ApiGateway/LoadBalancing/ServicesAreEmptyError.cs b/ApiGateway/LoadBalancing/ServicesAreEmptyError.cs
new file mode 100644
index 00000000..2ce3efb6
--- /dev/null
+++ b/ApiGateway/LoadBalancing/ServicesAreEmptyError.cs
@@ -0,0 +1,6 @@
+using Ocelot.Errors;
+
+namespace ApiGateway.LoadBalancing;
+
+public sealed class ServicesAreEmptyError(string message)
+ : Error(message, OcelotErrorCode.UnableToFindDownstreamRouteError, 503);
diff --git a/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs b/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs
new file mode 100644
index 00000000..b7b97740
--- /dev/null
+++ b/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs
@@ -0,0 +1,48 @@
+using Ocelot.LoadBalancer.LoadBalancers;
+using Ocelot.Responses;
+using Ocelot.Values;
+
+namespace ApiGateway.LoadBalancing;
+
+///
+/// Взвешенная карусель (Weighted Round Robin).
+/// Каждой реплике присваивается вес — она обслуживает ровно weight запросов подряд,
+/// после чего очередь переходит к следующей реплике.
+/// Веса: R1=3, R2=2, R3=1 → R1,R1,R1,R2,R2,R3,R1,...
+///
+public sealed class WeightedRoundRobinBalancer(Func>> services) : ILoadBalancer
+{
+ private readonly Func>> _services = services;
+ private readonly int[] _weights = [3, 2, 1];
+ private readonly object _lock = new();
+
+ private int _currentIndex = -1;
+ private int _remainingCalls = 0;
+
+ public string Type => nameof(WeightedRoundRobinBalancer);
+
+ public async Task> LeaseAsync(HttpContext httpContext)
+ {
+ var available = await _services.Invoke();
+
+ if (available is null || available.Count == 0)
+ return new ErrorResponse(
+ new ServicesAreEmptyError("No downstream services available"));
+
+ lock (_lock)
+ {
+ if (_currentIndex == -1 || _remainingCalls == 0)
+ {
+ _currentIndex = (_currentIndex + 1) % available.Count;
+ _remainingCalls = _weights[_currentIndex % _weights.Length];
+ }
+
+ var service = available[_currentIndex];
+ _remainingCalls--;
+
+ return new OkResponse(service.HostAndPort);
+ }
+ }
+
+ public void Release(ServiceHostAndPort hostAndPort) { }
+}
diff --git a/ApiGateway/Program.cs b/ApiGateway/Program.cs
new file mode 100644
index 00000000..3ab3f5f7
--- /dev/null
+++ b/ApiGateway/Program.cs
@@ -0,0 +1,38 @@
+using ApiGateway.LoadBalancing;
+using Ocelot.DependencyInjection;
+using Ocelot.Middleware;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
+
+for (var i = 0; i < 3; i++)
+{
+ var url = builder.Configuration[$"services:generator-service-{i}:http:0"];
+ if (url is null) break;
+ var uri = new Uri(url);
+ builder.Configuration[$"Routes:0:DownstreamHostAndPorts:{i}:Host"] = uri.Host;
+ builder.Configuration[$"Routes:0:DownstreamHostAndPorts:{i}:Port"] = uri.Port.ToString();
+}
+
+builder.Services.AddOcelot()
+ .AddCustomLoadBalancer((_, _, provider) => new(provider.GetAsync));
+
+var allowedOrigin = builder.Configuration["Cors:AllowedOrigin"]
+ ?? throw new InvalidOperationException("Cors:AllowedOrigin is not configured");
+
+builder.Services.AddCors(options => options.AddDefaultPolicy(policy =>
+{
+ policy.WithOrigins(allowedOrigin);
+ policy.WithMethods("GET");
+ policy.WithHeaders("Content-Type");
+}));
+
+var app = builder.Build();
+
+app.UseCors();
+
+await app.UseOcelot();
+
+app.Run();
diff --git a/ApiGateway/Properties/launchSettings.json b/ApiGateway/Properties/launchSettings.json
new file mode 100644
index 00000000..647a0d20
--- /dev/null
+++ b/ApiGateway/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "ApiGateway": {
+ "commandName": "Project",
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5200",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/ApiGateway/appsettings.json b/ApiGateway/appsettings.json
new file mode 100644
index 00000000..10f68b8c
--- /dev/null
+++ b/ApiGateway/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/ApiGateway/ocelot.json b/ApiGateway/ocelot.json
new file mode 100644
index 00000000..ff4e1ddd
--- /dev/null
+++ b/ApiGateway/ocelot.json
@@ -0,0 +1,18 @@
+{
+ "Routes": [
+ {
+ "UpstreamPathTemplate": "/patient",
+ "UpstreamHttpMethod": [ "GET" ],
+ "DownstreamPathTemplate": "/patient",
+ "DownstreamScheme": "http",
+ "LoadBalancerOptions": {
+ "Type": "WeightedRoundRobinBalancer"
+ },
+ "DownstreamHostAndPorts": [
+ { "Host": "localhost", "Port": 15000 },
+ { "Host": "localhost", "Port": 15001 },
+ { "Host": "localhost", "Port": 15002 }
+ ]
+ }
+ ]
+}
diff --git a/AppHost/AppHost.csproj b/AppHost/AppHost.csproj
new file mode 100644
index 00000000..12fc787a
--- /dev/null
+++ b/AppHost/AppHost.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AppHost/Program.cs b/AppHost/Program.cs
new file mode 100644
index 00000000..11c56981
--- /dev/null
+++ b/AppHost/Program.cs
@@ -0,0 +1,20 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var redis = builder.AddRedis("redis")
+ .WithRedisInsight();
+
+var client = builder.AddProject("client");
+
+var gateway = builder.AddProject("api-gateway")
+ .WithEnvironment("Cors__AllowedOrigin", client.GetEndpoint("http"));
+
+for (var i = 0; i < 3; i++)
+{
+ var replica = builder.AddProject($"generator-service-{i}", launchProfileName: null)
+ .WithHttpEndpoint(port: 15000 + i)
+ .WithReference(redis)
+ .WaitFor(redis);
+ gateway.WithReference(replica).WaitFor(replica);
+}
+
+builder.Build().Run();
diff --git a/AppHost/Properties/launchSettings.json b/AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000..6d91260d
--- /dev/null
+++ b/AppHost/Properties/launchSettings.json
@@ -0,0 +1,16 @@
+{
+ "profiles": {
+ "AppHost": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17193;http://localhost:15237",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21193",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor
index 661f1181..a96e557a 100644
--- a/Client.Wasm/Components/StudentCard.razor
+++ b/Client.Wasm/Components/StudentCard.razor
@@ -4,10 +4,10 @@
- Номер №X "Название лабораторной"
- Вариант №Х "Название варианта"
- Выполнена Фамилией Именем 65ХХ
- Ссылка на форк
+ Номер № 1
+ Вариант № 18
+ Выполнена Чумаковым Иваном 6511
+ Ссылка на форк
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..0ed61cc5 100644
--- a/Client.Wasm/wwwroot/appsettings.json
+++ b/Client.Wasm/wwwroot/appsettings.json
@@ -1,10 +1,3 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "AllowedHosts": "*",
- "BaseAddress": ""
+ "BaseAddress": "http://localhost:5200/patient"
}
diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln
index cb48241d..1adefa99 100644
--- a/CloudDevelopment.sln
+++ b/CloudDevelopment.sln
@@ -1,10 +1,20 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
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}") = "AppHost", "AppHost\AppHost.csproj", "{B1C2D3E4-F5A6-7890-ABCD-EF1234567890}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratorService", "GeneratorService\GeneratorService.csproj", "{139BD442-54A6-9109-CF9A-53DA218D46F2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratorService.Tests", "GeneratorService.Tests\GeneratorService.Tests.csproj", "{EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceDefaults", "ServiceDefaults\ServiceDefaults.csproj", "{9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiGateway", "ApiGateway\ApiGateway.csproj", "{C2D3E4F5-A6B7-8901-CDEF-234567890ABC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +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
+ {B1C2D3E4-F5A6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1C2D3E4-F5A6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1C2D3E4-F5A6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1C2D3E4-F5A6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
+ {139BD442-54A6-9109-CF9A-53DA218D46F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {139BD442-54A6-9109-CF9A-53DA218D46F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {139BD442-54A6-9109-CF9A-53DA218D46F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {139BD442-54A6-9109-CF9A-53DA218D46F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C2D3E4F5-A6B7-8901-CDEF-234567890ABC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2D3E4F5-A6B7-8901-CDEF-234567890ABC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2D3E4F5-A6B7-8901-CDEF-234567890ABC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2D3E4F5-A6B7-8901-CDEF-234567890ABC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/GeneratorService.Tests/GeneratorService.Tests.csproj b/GeneratorService.Tests/GeneratorService.Tests.csproj
new file mode 100644
index 00000000..c9bc33ca
--- /dev/null
+++ b/GeneratorService.Tests/GeneratorService.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/GeneratorService.Tests/MedicalPatientGeneratorTests.cs b/GeneratorService.Tests/MedicalPatientGeneratorTests.cs
new file mode 100644
index 00000000..5daec200
--- /dev/null
+++ b/GeneratorService.Tests/MedicalPatientGeneratorTests.cs
@@ -0,0 +1,62 @@
+using GeneratorService.Generators;
+using Xunit;
+
+namespace GeneratorService.Tests;
+
+public sealed class MedicalPatientGeneratorTests
+{
+ public static readonly DateOnly Today = DateOnly.FromDateTime(DateTime.Today);
+
+ public static IEnumerable