From 4ff96078c19cdf9cb70e7e3b18e23c564cd91dee Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Thu, 5 Mar 2026 14:17:58 +0400
Subject: [PATCH 01/18] adding AppHost and Generator with caching
---
AspireApp1/AspireApp1.AppHost/AppHost.cs | 3 +
.../AspireApp1.AppHost.csproj | 17 +++
.../Properties/launchSettings.json | 29 +++++
.../appsettings.Development.json | 8 ++
.../AspireApp1.AppHost/appsettings.json | 9 ++
.../CompanyEmployees.ServiceDefaults.csproj | 28 +++++
.../AspireApp1.ServiceDefaults/Extensions.cs | 104 ++++++++++++++++
Client.Wasm/Client.Wasm.csproj | 1 +
Client.Wasm/Components/StudentCard.razor | 10 +-
Client.Wasm/Properties/launchSettings.json | 12 +-
Client.Wasm/wwwroot/appsettings.json | 4 +-
CloudDevelopment.sln | 27 ++++
.../CompanyEmployees.AppHost.csproj | 24 ++++
CompanyEmployees.AppHost/Program.cs | 13 ++
.../Properties/launchSettings.json | 29 +++++
.../appsettings.Development.json | 8 ++
CompanyEmployees.AppHost/appsettings.json | 9 ++
.../CompanyEmployees.Generator.csproj | 20 +++
.../Models/CompanyEmployee.cs | 64 ++++++++++
CompanyEmployees.Generator/Program.cs | 58 +++++++++
.../Properties/launchSettings.json | 23 ++++
.../Services/CompanyEmployeeGenerator.cs | 117 ++++++++++++++++++
.../Services/CompanyEmployeeService.cs | 68 ++++++++++
.../appsettings.Development.json | 8 ++
CompanyEmployees.Generator/appsettings.json | 8 ++
25 files changed, 689 insertions(+), 12 deletions(-)
create mode 100644 AspireApp1/AspireApp1.AppHost/AppHost.cs
create mode 100644 AspireApp1/AspireApp1.AppHost/AspireApp1.AppHost.csproj
create mode 100644 AspireApp1/AspireApp1.AppHost/Properties/launchSettings.json
create mode 100644 AspireApp1/AspireApp1.AppHost/appsettings.Development.json
create mode 100644 AspireApp1/AspireApp1.AppHost/appsettings.json
create mode 100644 AspireApp1/AspireApp1.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
create mode 100644 AspireApp1/AspireApp1.ServiceDefaults/Extensions.cs
create mode 100644 CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
create mode 100644 CompanyEmployees.AppHost/Program.cs
create mode 100644 CompanyEmployees.AppHost/Properties/launchSettings.json
create mode 100644 CompanyEmployees.AppHost/appsettings.Development.json
create mode 100644 CompanyEmployees.AppHost/appsettings.json
create mode 100644 CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
create mode 100644 CompanyEmployees.Generator/Models/CompanyEmployee.cs
create mode 100644 CompanyEmployees.Generator/Program.cs
create mode 100644 CompanyEmployees.Generator/Properties/launchSettings.json
create mode 100644 CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
create mode 100644 CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
create mode 100644 CompanyEmployees.Generator/appsettings.Development.json
create mode 100644 CompanyEmployees.Generator/appsettings.json
diff --git a/AspireApp1/AspireApp1.AppHost/AppHost.cs b/AspireApp1/AspireApp1.AppHost/AppHost.cs
new file mode 100644
index 00000000..c62c3a0f
--- /dev/null
+++ b/AspireApp1/AspireApp1.AppHost/AppHost.cs
@@ -0,0 +1,3 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+builder.Build().Run();
diff --git a/AspireApp1/AspireApp1.AppHost/AspireApp1.AppHost.csproj b/AspireApp1/AspireApp1.AppHost/AspireApp1.AppHost.csproj
new file mode 100644
index 00000000..8fa25709
--- /dev/null
+++ b/AspireApp1/AspireApp1.AppHost/AspireApp1.AppHost.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ d412956e-dd1b-41e4-9327-6d3cb514b1d3
+
+
+
+
+
+
+
diff --git a/AspireApp1/AspireApp1.AppHost/Properties/launchSettings.json b/AspireApp1/AspireApp1.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000..815321e7
--- /dev/null
+++ b/AspireApp1/AspireApp1.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:17200;http://localhost:15157",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21258",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22276"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15157",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19082",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20132"
+ }
+ }
+ }
+}
diff --git a/AspireApp1/AspireApp1.AppHost/appsettings.Development.json b/AspireApp1/AspireApp1.AppHost/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/AspireApp1/AspireApp1.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/AspireApp1/AspireApp1.AppHost/appsettings.json b/AspireApp1/AspireApp1.AppHost/appsettings.json
new file mode 100644
index 00000000..31c092aa
--- /dev/null
+++ b/AspireApp1/AspireApp1.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/AspireApp1/AspireApp1.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj b/AspireApp1/AspireApp1.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
new file mode 100644
index 00000000..4e142da7
--- /dev/null
+++ b/AspireApp1/AspireApp1.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ Debug;Release;123
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AspireApp1/AspireApp1.ServiceDefaults/Extensions.cs b/AspireApp1/AspireApp1.ServiceDefaults/Extensions.cs
new file mode 100644
index 00000000..f1836a63
--- /dev/null
+++ b/AspireApp1/AspireApp1.ServiceDefaults/Extensions.cs
@@ -0,0 +1,104 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+using Serilog;
+
+namespace CompanyEmployees.ServiceDefaults;
+
+public static class Extensions
+{
+ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
+ {
+ builder.ConfigureSerilog();
+ builder.ConfigureOpenTelemetry();
+ builder.AddDefaultHealthChecks();
+ builder.Services.AddServiceDiscovery();
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ http.AddStandardResilienceHandler();
+ http.AddServiceDiscovery();
+ });
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureSerilog(this IHostApplicationBuilder builder)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(builder.Configuration)
+ .Enrich.FromLogContext()
+ .Enrich.WithEnvironmentName()
+ .Enrich.WithThreadId()
+ .Enrich.WithMachineName()
+ .WriteTo.Console(outputTemplate:
+ "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}")
+ .CreateLogger();
+
+ builder.Services.AddSerilog();
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
+ {
+ builder.Services.AddHealthChecks()
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ app.MapHealthChecks("/health");
+ app.MapHealthChecks("/alive", new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/Client.Wasm/Client.Wasm.csproj b/Client.Wasm/Client.Wasm.csproj
index 0ba9f90c..0c323bf4 100644
--- a/Client.Wasm/Client.Wasm.csproj
+++ b/Client.Wasm/Client.Wasm.csproj
@@ -4,6 +4,7 @@
net8.0
enable
enable
+ Debug;Release;123
diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor
index 661f1181..a53bf00f 100644
--- a/Client.Wasm/Components/StudentCard.razor
+++ b/Client.Wasm/Components/StudentCard.razor
@@ -4,10 +4,12 @@
- Номер №X "Название лабораторной"
- Вариант №Х "Название варианта"
- Выполнена Фамилией Именем 65ХХ
- Ссылка на форк
+ Номер №1 "Кэширование"
+
+ Вариант №8 "Сотрудник компании"
+
+ Выполнена Земляновым Владимиром 6511
+ Ссылка на форк
diff --git a/Client.Wasm/Properties/launchSettings.json b/Client.Wasm/Properties/launchSettings.json
index 0d824ea7..6ad691ad 100644
--- a/Client.Wasm/Properties/launchSettings.json
+++ b/Client.Wasm/Properties/launchSettings.json
@@ -12,9 +12,9 @@
"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",
+ "applicationUrl": "http://localhost:5128",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -22,20 +22,20 @@
"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",
+ "applicationUrl": "https://localhost:7283;http://localhost:5128",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
- "launchBrowser": true,
+ "launchBrowser": false,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
-}
+}
\ No newline at end of file
diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json
index d1fe7ab3..fcd8f03c 100644
--- a/Client.Wasm/wwwroot/appsettings.json
+++ b/Client.Wasm/wwwroot/appsettings.json
@@ -6,5 +6,5 @@
}
},
"AllowedHosts": "*",
- "BaseAddress": ""
-}
+ "BaseAddress": "https://localhost:7171/employee"
+}
\ No newline at end of file
diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln
index cb48241d..cf6c0383 100644
--- a/CloudDevelopment.sln
+++ b/CloudDevelopment.sln
@@ -5,16 +5,43 @@ 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}") = "CompanyEmployees.AppHost", "CompanyEmployees.AppHost\CompanyEmployees.AppHost.csproj", "{96FC536F-9C20-45C4-B624-F3E2DEE96BA3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.Generator", "CompanyEmployees.Generator\CompanyEmployees.Generator.csproj", "{418E6A76-7DE1-04F1-F18A-9A7C9ED44857}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.ServiceDefaults", "AspireApp1\AspireApp1.ServiceDefaults\CompanyEmployees.ServiceDefaults.csproj", "{F9222B91-99B7-2420-265D-E51F0CE78C78}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ 123|Any CPU = 123|Any CPU
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.123|Any CPU.ActiveCfg = 123|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.123|Any CPU.Build.0 = 123|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
+ {96FC536F-9C20-45C4-B624-F3E2DEE96BA3}.123|Any CPU.ActiveCfg = 123|Any CPU
+ {96FC536F-9C20-45C4-B624-F3E2DEE96BA3}.123|Any CPU.Build.0 = 123|Any CPU
+ {96FC536F-9C20-45C4-B624-F3E2DEE96BA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {96FC536F-9C20-45C4-B624-F3E2DEE96BA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {96FC536F-9C20-45C4-B624-F3E2DEE96BA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {96FC536F-9C20-45C4-B624-F3E2DEE96BA3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.123|Any CPU.ActiveCfg = Release|Any CPU
+ {418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.123|Any CPU.Build.0 = Release|Any CPU
+ {418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F9222B91-99B7-2420-265D-E51F0CE78C78}.123|Any CPU.ActiveCfg = Debug|Any CPU
+ {F9222B91-99B7-2420-265D-E51F0CE78C78}.123|Any CPU.Build.0 = Debug|Any CPU
+ {F9222B91-99B7-2420-265D-E51F0CE78C78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F9222B91-99B7-2420-265D-E51F0CE78C78}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F9222B91-99B7-2420-265D-E51F0CE78C78}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F9222B91-99B7-2420-265D-E51F0CE78C78}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
new file mode 100644
index 00000000..7e140d32
--- /dev/null
+++ b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+ enable
+ true
+ Debug;Release;123
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CompanyEmployees.AppHost/Program.cs b/CompanyEmployees.AppHost/Program.cs
new file mode 100644
index 00000000..6e2759c6
--- /dev/null
+++ b/CompanyEmployees.AppHost/Program.cs
@@ -0,0 +1,13 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var redis = builder.AddRedis("redis")
+ .WithRedisCommander();
+
+var generator = builder.AddProject("generator")
+ .WithReference(redis)
+ .WithExternalHttpEndpoints();
+
+builder.AddProject("client")
+ .WithReference(generator);
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/CompanyEmployees.AppHost/Properties/launchSettings.json b/CompanyEmployees.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000..09cd82db
--- /dev/null
+++ b/CompanyEmployees.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17170;http://localhost:15170",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21170",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22170"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15170",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19170",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20170"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CompanyEmployees.AppHost/appsettings.Development.json b/CompanyEmployees.AppHost/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/CompanyEmployees.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CompanyEmployees.AppHost/appsettings.json b/CompanyEmployees.AppHost/appsettings.json
new file mode 100644
index 00000000..31c092aa
--- /dev/null
+++ b/CompanyEmployees.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj b/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
new file mode 100644
index 00000000..114d0c49
--- /dev/null
+++ b/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Debug;Release;123
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CompanyEmployees.Generator/Models/CompanyEmployee.cs b/CompanyEmployees.Generator/Models/CompanyEmployee.cs
new file mode 100644
index 00000000..82440823
--- /dev/null
+++ b/CompanyEmployees.Generator/Models/CompanyEmployee.cs
@@ -0,0 +1,64 @@
+namespace CompanyEmployees.Generator.Models;
+
+///
+/// Модель сотрудника компании
+///
+public class CompanyEmployeeModel
+{
+ ///
+ /// Идентификатор сотрудника в системе
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// ФИО
+ ///
+ /// Конкатенация фамилии, имени и отчества через пробел
+ ///
+ ///
+ public required string FullName { get; set; }
+
+ ///
+ /// Должность
+ /// Выбирается из справочника профессий (“Developer”, “Manager”, “Analyst” и т.д.) и справочника суффиксов к ним (“Junior”, “Middle”, “Senior” и т.д.)
+ ///
+ public required string Position { get; set; }
+
+ ///
+ /// Отдел
+ ///
+ public required string Section { get; set; }
+
+ ///
+ /// Дата приема
+ /// Ограничение: не более 10 лет назад от текущей даты
+ ///
+ public required DateOnly AdmissionDate { get; set; }
+
+ ///
+ /// Оклад
+ /// Значение коррелирует с суффиксом должности
+ ///
+ public required decimal Salary { get; set; }
+
+ ///
+ /// Электронная почта
+ ///
+ public required string Email { get; set; }
+
+ ///
+ /// Номер телефона формата +7(***)***-**-**
+ ///
+ public required string PhoneNumber { get; set; }
+
+ ///
+ /// Индикатор увольнения
+ ///
+ public required bool Dismissal { get; set; } = false;
+
+ ///
+ /// Дата увольнения
+ /// При отсутствии индикатора увольнения дата увольнения не заполняется
+ ///
+ public DateOnly? DismissalDate { get; set; }
+}
diff --git a/CompanyEmployees.Generator/Program.cs b/CompanyEmployees.Generator/Program.cs
new file mode 100644
index 00000000..582edefa
--- /dev/null
+++ b/CompanyEmployees.Generator/Program.cs
@@ -0,0 +1,58 @@
+using CompanyEmployees.Generator.Services;
+using CompanyEmployees.ServiceDefaults;
+using Serilog;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddRedisDistributedCache("redis");
+
+builder.Services.AddSingleton();
+builder.Services.AddScoped();
+
+builder.Services.AddCors(options =>
+{
+ options.AddDefaultPolicy(policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+});
+
+var app = builder.Build();
+
+app.UseCors();
+app.UseSerilogRequestLogging();
+
+app.MapDefaultEndpoints();
+
+app.MapGet("/employee", async (
+ int id,
+ CompanyEmployeeService service,
+ ILogger logger,
+ CancellationToken cancellationToken) =>
+{
+ logger.LogInformation("Received request for company employee with ID: {Id}", id);
+
+ if (id <= 0)
+ {
+ logger.LogWarning("Received invalid ID: {Id}", id);
+ return Results.BadRequest(new { error = "ID must be a positive number" });
+ }
+
+ try
+ {
+ var application = await service.GetByIdAsync(id, cancellationToken);
+ return Results.Ok(application);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error while getting company employee {Id}", id);
+ return Results.Problem("An error occurred while processing the request");
+ }
+})
+.WithName("GetCompanyEmployee");
+
+app.Run();
\ No newline at end of file
diff --git a/CompanyEmployees.Generator/Properties/launchSettings.json b/CompanyEmployees.Generator/Properties/launchSettings.json
new file mode 100644
index 00000000..ea1476ca
--- /dev/null
+++ b/CompanyEmployees.Generator/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5171",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7171;http://localhost:5171",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs b/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
new file mode 100644
index 00000000..2362b334
--- /dev/null
+++ b/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
@@ -0,0 +1,117 @@
+using Bogus;
+using Bogus.DataSets;
+using CompanyEmployees.Generator.Models;
+
+namespace CompanyEmployees.Generator.Services;
+
+///
+/// Генератор сотрудника по идентификатору
+///
+/// Логгер
+public class CompanyEmployeeGenerator(ILogger logger)
+{
+ ///
+ /// Справочник профессий сотрудников
+ ///
+ private static readonly string[] _positionProfessions =
+ [
+ "Developer",
+ "Manager",
+ "Analyst",
+ "DevOps Engineer",
+ "QA Engineer",
+ "System Architect"
+ ];
+
+ ///
+ /// Справочник суффиксов для профессий
+ ///
+ private enum PositionSuffix
+ {
+ Junior = 0,
+ Middle = 1,
+ Senior = 2,
+ TeamLead = 3
+ };
+
+ ///
+ /// Словарь множителей на основе суффиксов профессий
+ ///
+ private static readonly Dictionary _positionSuffixSalaryMultipliers = new()
+ {
+ [PositionSuffix.Junior] = 1m,
+ [PositionSuffix.Middle] = 1.5m,
+ [PositionSuffix.Senior] = 2.1m,
+ [PositionSuffix.TeamLead] = 2.6m
+ };
+
+ private readonly ILogger _logger = logger;
+
+ ///
+ /// Функция для генерации сотрудника на основе параметра id
+ ///
+ /// Идентификатор сотрудника в системе
+ /// Сгенерированные данные о сотруднике
+ public CompanyEmployeeModel Generate(int id)
+ {
+ _logger.LogInformation("Start generating company employee application with ID: {Id}", id);
+
+ var faker = new Faker("ru")
+ .UseSeed(id)
+ .RuleFor(x => x.Id, _ => id)
+ .RuleFor(x => x.FullName, f => GenerateEmployeeFullName(f))
+ .RuleFor(x => x.Position, f => GenerateEmployeePosition(f))
+ .RuleFor(x => x.Section, f => f.Commerce.Department())
+ .RuleFor(x => x.AdmissionDate, f => f.Date.PastDateOnly(10))
+ .RuleFor(x => x.Salary, (f, x) => GenerateEmployeeSalary(f, x))
+ .RuleFor(x => x.Email, f => f.Internet.Email())
+ .RuleFor(x => x.PhoneNumber, f => f.Phone.PhoneNumber("+7(###)###-##-##"))
+ .RuleFor(x => x.Dismissal, f => f.Random.Bool())
+ .RuleFor(x => x.DismissalDate, (f, x) => GenerateEmployeeDismissalDate(f, x));
+
+ _logger.LogInformation("Finally generate employee with ID: {Id}", id);
+
+ return faker.Generate();
+ }
+
+ private static string GenerateEmployeeFullName(Faker faker)
+ {
+ var gender = faker.PickRandom(Enum.GetValues(typeof(Name.Gender)).Cast().ToArray());
+
+ var firstName = faker.Name.FirstName(gender);
+ var lastName = faker.Name.LastName(gender);
+ var patronymic = faker.Name.FirstName(gender) + (gender == Name.Gender.Male ? "еевич" : "еевна");
+
+ return string.Join(' ', firstName, lastName, patronymic);
+ }
+
+ private static string GenerateEmployeePosition(Faker faker)
+ {
+ return string.Join(' ', faker.PickRandom(_positionProfessions), faker.PickRandom());
+ }
+
+ private static decimal GenerateEmployeeSalary(Faker faker, CompanyEmployeeModel x)
+ {
+ var baseSalary = faker.Random.Decimal(100, 200);
+ var finalSalary = baseSalary;
+
+ foreach (var kvp in _positionSuffixSalaryMultipliers)
+ {
+ if (x.Position.Contains(kvp.Key.ToString()))
+ {
+ finalSalary = baseSalary * kvp.Value;
+ break;
+ }
+ }
+
+ return Math.Round(baseSalary, 2);
+ }
+
+ private static DateOnly? GenerateEmployeeDismissalDate(Faker faker, CompanyEmployeeModel x)
+ {
+
+ if (x.Dismissal) return null;
+
+ return faker.Date.BetweenDateOnly(x.AdmissionDate, DateOnly.FromDateTime(DateTime.UtcNow));
+ }
+}
diff --git a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
new file mode 100644
index 00000000..6be51c91
--- /dev/null
+++ b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
@@ -0,0 +1,68 @@
+using CompanyEmployees.Generator.Models;
+using Microsoft.Extensions.Caching.Distributed;
+using System.Text.Json;
+
+namespace CompanyEmployees.Generator.Services;
+
+///
+/// Сервис для работы с сотрудниками компании
+/// Каждый сгенерированный сотрудник сохраняется в кэш, если ранее он небыл получен
+/// В ином случае значение берется из кэша
+///
+/// Генератор необходимый для создания сотрудника
+/// Кэш хранящий ранее сгенерированных сотрудников
+/// Логгер
+public class CompanyEmployeeService(
+ CompanyEmployeeGenerator generator,
+ IDistributedCache cache,
+ ILogger logger
+)
+{
+ private readonly CompanyEmployeeGenerator _generator = generator;
+ private readonly IDistributedCache _cache = cache;
+ private readonly ILogger _logger = logger;
+
+ private static readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(5);
+ private const string CacheKeyPrefix = "company-employee:";
+
+ ///
+ /// Функция для генерации или взятия из кэша сотрудника по id
+ ///
+ /// Id сотрудника
+ /// Токен для отмены операции
+ ///
+ public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var cacheKey = $"{CacheKeyPrefix}{id}";
+
+ _logger.LogInformation("Request for credit application with ID: {Id}", id);
+
+ var cachedData = await _cache.GetStringAsync(cacheKey, cancellationToken);
+
+ if (!string.IsNullOrEmpty(cachedData))
+ {
+ _logger.LogInformation("Credit application {Id} found in cache", id);
+ var cachedApplication = JsonSerializer.Deserialize(cachedData);
+ return cachedApplication!;
+ }
+
+ _logger.LogInformation("Credit application {Id} not found in cache, generating new one", id);
+
+ var application = _generator.Generate(id);
+
+ var serializedData = JsonSerializer.Serialize(application);
+ var cacheOptions = new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = _cacheExpiration
+ };
+
+ await _cache.SetStringAsync(cacheKey, serializedData, cacheOptions, cancellationToken);
+
+ _logger.LogInformation(
+ "Credit application {Id} saved to cache with TTL {CacheExpiration} minutes",
+ id,
+ _cacheExpiration.TotalMinutes);
+
+ return application;
+ }
+}
diff --git a/CompanyEmployees.Generator/appsettings.Development.json b/CompanyEmployees.Generator/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/CompanyEmployees.Generator/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CompanyEmployees.Generator/appsettings.json b/CompanyEmployees.Generator/appsettings.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/CompanyEmployees.Generator/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
From 21dd0d5f9c87b1aa404c6189f92fc3cb62af8bf3 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Thu, 5 Mar 2026 14:18:38 +0400
Subject: [PATCH 02/18] some fix
---
AspireApp1/AspireApp1.AppHost/AppHost.cs | 3 -
.../AspireApp1.AppHost.csproj | 17 ---
.../Properties/launchSettings.json | 29 -----
.../appsettings.Development.json | 8 --
.../AspireApp1.AppHost/appsettings.json | 9 --
.../CompanyEmployees.ServiceDefaults.csproj | 28 -----
.../AspireApp1.ServiceDefaults/Extensions.cs | 104 ------------------
7 files changed, 198 deletions(-)
delete mode 100644 AspireApp1/AspireApp1.AppHost/AppHost.cs
delete mode 100644 AspireApp1/AspireApp1.AppHost/AspireApp1.AppHost.csproj
delete mode 100644 AspireApp1/AspireApp1.AppHost/Properties/launchSettings.json
delete mode 100644 AspireApp1/AspireApp1.AppHost/appsettings.Development.json
delete mode 100644 AspireApp1/AspireApp1.AppHost/appsettings.json
delete mode 100644 AspireApp1/AspireApp1.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
delete mode 100644 AspireApp1/AspireApp1.ServiceDefaults/Extensions.cs
diff --git a/AspireApp1/AspireApp1.AppHost/AppHost.cs b/AspireApp1/AspireApp1.AppHost/AppHost.cs
deleted file mode 100644
index c62c3a0f..00000000
--- a/AspireApp1/AspireApp1.AppHost/AppHost.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-var builder = DistributedApplication.CreateBuilder(args);
-
-builder.Build().Run();
diff --git a/AspireApp1/AspireApp1.AppHost/AspireApp1.AppHost.csproj b/AspireApp1/AspireApp1.AppHost/AspireApp1.AppHost.csproj
deleted file mode 100644
index 8fa25709..00000000
--- a/AspireApp1/AspireApp1.AppHost/AspireApp1.AppHost.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
- Exe
- net8.0
- enable
- enable
- d412956e-dd1b-41e4-9327-6d3cb514b1d3
-
-
-
-
-
-
-
diff --git a/AspireApp1/AspireApp1.AppHost/Properties/launchSettings.json b/AspireApp1/AspireApp1.AppHost/Properties/launchSettings.json
deleted file mode 100644
index 815321e7..00000000
--- a/AspireApp1/AspireApp1.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:17200;http://localhost:15157",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "DOTNET_ENVIRONMENT": "Development",
- "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21258",
- "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22276"
- }
- },
- "http": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "applicationUrl": "http://localhost:15157",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "DOTNET_ENVIRONMENT": "Development",
- "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19082",
- "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20132"
- }
- }
- }
-}
diff --git a/AspireApp1/AspireApp1.AppHost/appsettings.Development.json b/AspireApp1/AspireApp1.AppHost/appsettings.Development.json
deleted file mode 100644
index 0c208ae9..00000000
--- a/AspireApp1/AspireApp1.AppHost/appsettings.Development.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- }
-}
diff --git a/AspireApp1/AspireApp1.AppHost/appsettings.json b/AspireApp1/AspireApp1.AppHost/appsettings.json
deleted file mode 100644
index 31c092aa..00000000
--- a/AspireApp1/AspireApp1.AppHost/appsettings.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning",
- "Aspire.Hosting.Dcp": "Warning"
- }
- }
-}
diff --git a/AspireApp1/AspireApp1.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj b/AspireApp1/AspireApp1.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
deleted file mode 100644
index 4e142da7..00000000
--- a/AspireApp1/AspireApp1.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- net8.0
- enable
- enable
- true
- Debug;Release;123
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/AspireApp1/AspireApp1.ServiceDefaults/Extensions.cs b/AspireApp1/AspireApp1.ServiceDefaults/Extensions.cs
deleted file mode 100644
index f1836a63..00000000
--- a/AspireApp1/AspireApp1.ServiceDefaults/Extensions.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Diagnostics.HealthChecks;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Diagnostics.HealthChecks;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using OpenTelemetry;
-using OpenTelemetry.Metrics;
-using OpenTelemetry.Trace;
-using Serilog;
-
-namespace CompanyEmployees.ServiceDefaults;
-
-public static class Extensions
-{
- public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
- {
- builder.ConfigureSerilog();
- builder.ConfigureOpenTelemetry();
- builder.AddDefaultHealthChecks();
- builder.Services.AddServiceDiscovery();
- builder.Services.ConfigureHttpClientDefaults(http =>
- {
- http.AddStandardResilienceHandler();
- http.AddServiceDiscovery();
- });
-
- return builder;
- }
-
- public static IHostApplicationBuilder ConfigureSerilog(this IHostApplicationBuilder builder)
- {
- Log.Logger = new LoggerConfiguration()
- .ReadFrom.Configuration(builder.Configuration)
- .Enrich.FromLogContext()
- .Enrich.WithEnvironmentName()
- .Enrich.WithThreadId()
- .Enrich.WithMachineName()
- .WriteTo.Console(outputTemplate:
- "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}")
- .CreateLogger();
-
- builder.Services.AddSerilog();
-
- return builder;
- }
-
- public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
- {
- builder.Logging.AddOpenTelemetry(logging =>
- {
- logging.IncludeFormattedMessage = true;
- logging.IncludeScopes = true;
- });
-
- builder.Services.AddOpenTelemetry()
- .WithMetrics(metrics =>
- {
- metrics.AddAspNetCoreInstrumentation()
- .AddHttpClientInstrumentation()
- .AddRuntimeInstrumentation();
- })
- .WithTracing(tracing =>
- {
- tracing.AddAspNetCoreInstrumentation()
- .AddHttpClientInstrumentation();
- });
-
- builder.AddOpenTelemetryExporters();
-
- return builder;
- }
-
- private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
- {
- var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
-
- if (useOtlpExporter)
- {
- builder.Services.AddOpenTelemetry().UseOtlpExporter();
- }
-
- return builder;
- }
-
- public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
- {
- builder.Services.AddHealthChecks()
- .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
-
- return builder;
- }
-
- public static WebApplication MapDefaultEndpoints(this WebApplication app)
- {
- app.MapHealthChecks("/health");
- app.MapHealthChecks("/alive", new HealthCheckOptions
- {
- Predicate = r => r.Tags.Contains("live")
- });
-
- return app;
- }
-}
\ No newline at end of file
From 5d39f5e753340324e7c4941ab057fd9376171c0a Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Fri, 6 Mar 2026 10:39:25 +0400
Subject: [PATCH 03/18] fix ServicesDefaults
---
CloudDevelopment.sln | 14 +--
.../CompanyEmployees.AppHost.csproj | 2 +-
.../CompanyEmployees.Generator.csproj | 3 +-
.../CompanyEmployees.ServiceDefaults.csproj | 28 +++++
.../Extensions.cs | 104 ++++++++++++++++++
5 files changed, 141 insertions(+), 10 deletions(-)
create mode 100644 CompanyEmployees.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
create mode 100644 CompanyEmployees.ServiceDefaults/Extensions.cs
diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln
index cf6c0383..ec784b5f 100644
--- a/CloudDevelopment.sln
+++ b/CloudDevelopment.sln
@@ -9,7 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.AppHost",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.Generator", "CompanyEmployees.Generator\CompanyEmployees.Generator.csproj", "{418E6A76-7DE1-04F1-F18A-9A7C9ED44857}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.ServiceDefaults", "AspireApp1\AspireApp1.ServiceDefaults\CompanyEmployees.ServiceDefaults.csproj", "{F9222B91-99B7-2420-265D-E51F0CE78C78}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.ServiceDefaults", "CompanyEmployees.ServiceDefaults\CompanyEmployees.ServiceDefaults.csproj", "{53C4EDF4-E3C2-A484-171F-78A5FD7190F6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -36,12 +36,12 @@ Global
{418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.Debug|Any CPU.Build.0 = Debug|Any CPU
{418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.Release|Any CPU.ActiveCfg = Release|Any CPU
{418E6A76-7DE1-04F1-F18A-9A7C9ED44857}.Release|Any CPU.Build.0 = Release|Any CPU
- {F9222B91-99B7-2420-265D-E51F0CE78C78}.123|Any CPU.ActiveCfg = Debug|Any CPU
- {F9222B91-99B7-2420-265D-E51F0CE78C78}.123|Any CPU.Build.0 = Debug|Any CPU
- {F9222B91-99B7-2420-265D-E51F0CE78C78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F9222B91-99B7-2420-265D-E51F0CE78C78}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F9222B91-99B7-2420-265D-E51F0CE78C78}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F9222B91-99B7-2420-265D-E51F0CE78C78}.Release|Any CPU.Build.0 = Release|Any CPU
+ {53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.123|Any CPU.ActiveCfg = 123|Any CPU
+ {53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.123|Any CPU.Build.0 = 123|Any CPU
+ {53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
index 7e140d32..dd87fa51 100644
--- a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
+++ b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
@@ -16,9 +16,9 @@
-
+
diff --git a/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj b/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
index 114d0c49..b0d3ba24 100644
--- a/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
+++ b/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
@@ -10,11 +10,10 @@
-
-
+
diff --git a/CompanyEmployees.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj b/CompanyEmployees.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
new file mode 100644
index 00000000..4e142da7
--- /dev/null
+++ b/CompanyEmployees.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ Debug;Release;123
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CompanyEmployees.ServiceDefaults/Extensions.cs b/CompanyEmployees.ServiceDefaults/Extensions.cs
new file mode 100644
index 00000000..f1836a63
--- /dev/null
+++ b/CompanyEmployees.ServiceDefaults/Extensions.cs
@@ -0,0 +1,104 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+using Serilog;
+
+namespace CompanyEmployees.ServiceDefaults;
+
+public static class Extensions
+{
+ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
+ {
+ builder.ConfigureSerilog();
+ builder.ConfigureOpenTelemetry();
+ builder.AddDefaultHealthChecks();
+ builder.Services.AddServiceDiscovery();
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ http.AddStandardResilienceHandler();
+ http.AddServiceDiscovery();
+ });
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureSerilog(this IHostApplicationBuilder builder)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(builder.Configuration)
+ .Enrich.FromLogContext()
+ .Enrich.WithEnvironmentName()
+ .Enrich.WithThreadId()
+ .Enrich.WithMachineName()
+ .WriteTo.Console(outputTemplate:
+ "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}")
+ .CreateLogger();
+
+ builder.Services.AddSerilog();
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
+ {
+ builder.Services.AddHealthChecks()
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ app.MapHealthChecks("/health");
+ app.MapHealthChecks("/alive", new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+
+ return app;
+ }
+}
\ No newline at end of file
From 5694a9956eab8a86c72f5fadfdf43c5bbd95abb8 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Thu, 12 Mar 2026 11:56:25 +0400
Subject: [PATCH 04/18] Update Generator and Service after review
---
.../CompanyEmployees.AppHost.csproj | 8 ++-
.../Services/CompanyEmployeeGenerator.cs | 69 ++++++++-----------
.../Services/CompanyEmployeeService.cs | 27 ++++----
CompanyEmployees.Generator/appsettings.json | 3 +
4 files changed, 50 insertions(+), 57 deletions(-)
diff --git a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
index dd87fa51..24c7495c 100644
--- a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
+++ b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
@@ -1,5 +1,7 @@
+
+
net8.0
enable
@@ -10,9 +12,9 @@
-
-
-
+
+
+
diff --git a/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs b/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
index 2362b334..fac51eb2 100644
--- a/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
+++ b/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
@@ -23,6 +23,18 @@ public class CompanyEmployeeGenerator(ILogger logger)
"System Architect"
];
+ private readonly Faker _faker = new("ru");
+
+ ///
+ /// Минимальное значение оклада
+ ///
+ private static readonly decimal _salaryMin = 100;
+
+ ///
+ /// Максимальное значение оклада
+ ///
+ private static readonly decimal _salaryMax = 200;
+
///
/// Справочник суффиксов для профессий
///
@@ -45,8 +57,6 @@ private enum PositionSuffix
[PositionSuffix.TeamLead] = 2.6m
};
- private readonly ILogger _logger = logger;
-
///
/// Функция для генерации сотрудника на основе параметра id
///
@@ -54,30 +64,32 @@ private enum PositionSuffix
/// Сгенерированные данные о сотруднике
public CompanyEmployeeModel Generate(int id)
{
- _logger.LogInformation("Start generating company employee application with ID: {Id}", id);
+ logger.LogInformation("Start generating company employee application with ID: {Id}", id);
- var faker = new Faker("ru")
- .UseSeed(id)
+ _faker.UseSeed(id)
.RuleFor(x => x.Id, _ => id)
- .RuleFor(x => x.FullName, f => GenerateEmployeeFullName(f))
- .RuleFor(x => x.Position, f => GenerateEmployeePosition(f))
+ .RuleFor(x => x.FullName, GenerateEmployeeFullName)
+ .RuleFor(x => x.Position, f => $"{f.PickRandom(_positionProfessions)} {f.PickRandom()}")
.RuleFor(x => x.Section, f => f.Commerce.Department())
.RuleFor(x => x.AdmissionDate, f => f.Date.PastDateOnly(10))
- .RuleFor(x => x.Salary, (f, x) => GenerateEmployeeSalary(f, x))
+ .RuleFor(x => x.Salary, GenerateEmployeeSalary)
.RuleFor(x => x.Email, f => f.Internet.Email())
.RuleFor(x => x.PhoneNumber, f => f.Phone.PhoneNumber("+7(###)###-##-##"))
.RuleFor(x => x.Dismissal, f => f.Random.Bool())
- .RuleFor(x => x.DismissalDate, (f, x) => GenerateEmployeeDismissalDate(f, x));
+ .RuleFor(x => x.DismissalDate, (f, employeeObject) =>
+ employeeObject.Dismissal
+ ? f.Date.BetweenDateOnly(employeeObject.AdmissionDate, DateOnly.FromDateTime(DateTime.UtcNow))
+ : null
+ );
- _logger.LogInformation("Finally generate employee with ID: {Id}", id);
+ logger.LogInformation("Finally generate employee with ID: {Id}", id);
- return faker.Generate();
+ return _faker.Generate();
}
private static string GenerateEmployeeFullName(Faker faker)
{
- var gender = faker.PickRandom(Enum.GetValues(typeof(Name.Gender)).Cast().ToArray());
-
+ var gender = faker.Person.Gender;
var firstName = faker.Name.FirstName(gender);
var lastName = faker.Name.LastName(gender);
var patronymic = faker.Name.FirstName(gender) + (gender == Name.Gender.Male ? "еевич" : "еевна");
@@ -85,33 +97,12 @@ private static string GenerateEmployeeFullName(Faker faker)
return string.Join(' ', firstName, lastName, patronymic);
}
- private static string GenerateEmployeePosition(Faker faker)
+ private static decimal GenerateEmployeeSalary(Faker faker, CompanyEmployeeModel employeeObject)
{
- return string.Join(' ', faker.PickRandom(_positionProfessions), faker.PickRandom());
- }
-
- private static decimal GenerateEmployeeSalary(Faker faker, CompanyEmployeeModel x)
- {
- var baseSalary = faker.Random.Decimal(100, 200);
- var finalSalary = baseSalary;
-
- foreach (var kvp in _positionSuffixSalaryMultipliers)
- {
- if (x.Position.Contains(kvp.Key.ToString()))
- {
- finalSalary = baseSalary * kvp.Value;
- break;
- }
- }
-
- return Math.Round(baseSalary, 2);
- }
-
- private static DateOnly? GenerateEmployeeDismissalDate(Faker faker, CompanyEmployeeModel x)
- {
-
- if (x.Dismissal) return null;
+ var baseSalary = faker.Random.Decimal(_salaryMin, _salaryMax);
+ var salaryMap = _positionSuffixSalaryMultipliers
+ .FirstOrDefault(pair => employeeObject.Position.Contains(pair.Key.ToString()), new KeyValuePair(PositionSuffix.Junior, 1m));
- return faker.Date.BetweenDateOnly(x.AdmissionDate, DateOnly.FromDateTime(DateTime.UtcNow));
+ return Math.Round(baseSalary * salaryMap.Value, 2);
}
}
diff --git a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
index 6be51c91..06ea3872 100644
--- a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
+++ b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
@@ -15,14 +15,11 @@ namespace CompanyEmployees.Generator.Services;
public class CompanyEmployeeService(
CompanyEmployeeGenerator generator,
IDistributedCache cache,
- ILogger logger
+ ILogger logger,
+ IConfiguration config
)
{
- private readonly CompanyEmployeeGenerator _generator = generator;
- private readonly IDistributedCache _cache = cache;
- private readonly ILogger _logger = logger;
-
- private static readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(5);
+ private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(int.Parse(config["CacheSetting:CacheExpirationMinutes"] ?? "5"));
private const string CacheKeyPrefix = "company-employee:";
///
@@ -35,20 +32,20 @@ public async Task GetByIdAsync(int id, CancellationToken c
{
var cacheKey = $"{CacheKeyPrefix}{id}";
- _logger.LogInformation("Request for credit application with ID: {Id}", id);
+ logger.LogInformation("Request for company employee application with ID: {Id}", id);
- var cachedData = await _cache.GetStringAsync(cacheKey, cancellationToken);
+ var cachedData = await cache.GetStringAsync(cacheKey, cancellationToken);
if (!string.IsNullOrEmpty(cachedData))
{
- _logger.LogInformation("Credit application {Id} found in cache", id);
+ logger.LogInformation("Company employee application {Id} found in cache", id);
var cachedApplication = JsonSerializer.Deserialize(cachedData);
- return cachedApplication!;
+ if (cachedApplication != null) return cachedApplication;
}
- _logger.LogInformation("Credit application {Id} not found in cache, generating new one", id);
+ logger.LogInformation("Company employee application {Id} not found in cache, generating new one", id);
- var application = _generator.Generate(id);
+ var application = generator.Generate(id);
var serializedData = JsonSerializer.Serialize(application);
var cacheOptions = new DistributedCacheEntryOptions
@@ -56,10 +53,10 @@ public async Task GetByIdAsync(int id, CancellationToken c
AbsoluteExpirationRelativeToNow = _cacheExpiration
};
- await _cache.SetStringAsync(cacheKey, serializedData, cacheOptions, cancellationToken);
+ await cache.SetStringAsync(cacheKey, serializedData, cacheOptions, cancellationToken);
- _logger.LogInformation(
- "Credit application {Id} saved to cache with TTL {CacheExpiration} minutes",
+ logger.LogInformation(
+ "Company employee application {Id} saved to cache with TTL {CacheExpiration} minutes",
id,
_cacheExpiration.TotalMinutes);
diff --git a/CompanyEmployees.Generator/appsettings.json b/CompanyEmployees.Generator/appsettings.json
index 0c208ae9..01cc5cec 100644
--- a/CompanyEmployees.Generator/appsettings.json
+++ b/CompanyEmployees.Generator/appsettings.json
@@ -4,5 +4,8 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "CacheSetting": {
+ "CacheExpirationMinutes": "5"
}
}
From 672d98ff6013972da647615a82fffffb6a85c399 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Thu, 12 Mar 2026 12:12:47 +0400
Subject: [PATCH 05/18] Update README.md
---
README.md | 165 +++++++++++++++---------------------------------------
1 file changed, 45 insertions(+), 120 deletions(-)
diff --git a/README.md b/README.md
index dcaa5eb7..e28f3726 100644
--- a/README.md
+++ b/README.md
@@ -1,128 +1,53 @@
-# Современные технологии разработки программного обеспечения
-[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1an43o-iqlq4V_kDtkr_y7DC221hY9qdhGPrpII27sH8/edit?usp=sharing)
-
-## Задание
-### Цель
-Реализация проекта микросервисного бекенда.
-
-### Задачи
-* Реализация межсервисной коммуникации,
-* Изучение работы с брокерами сообщений,
-* Изучение архитектурных паттернов,
-* Изучение работы со средствами оркестрации на примере .NET Aspire,
-* Повторение основ работы с системами контроля версий,
-* Интеграционное тестирование.
-
-### Лабораторные работы
+# Приложение генератор "Сотрудника компании"
+
+## 📝 Лабораторная работа №1
+
+Реализация микросервиса генерации сотрудника компании с кэшированием и оркестрацией на базе .NET Aspire.
+
+### Цели
+- Реализация генератора данных на основе **Bogus** с детерминированным seed
+- Кэширование результатов с помощью **IDistributedCache** и **Redis**
+- Структурированное логирование через **Serilog**
+- Трассировка и метрики через **OpenTelemetry**
+- Оркестрация сервисов через **.NET Aspire**
+
+### Детали варианта
+**Вариант №8 — Сотрудник компании**
+
+| Параметр | Возможные значения |
+|----------|----------|
+| ФИО | Конкатенация фамилии, имени и отчества через пробел |
+| Должность | Выбирается из справочника профессий (“Developer”, “Manager”, “Analyst” и т.д.) и справочника суффиксов к ним (“Junior”, “Middle”, “Senior” и т.д.) |
+| Отдел | Отдел, в который определили сотрудника на его ближайшие несколько лет жизни |
+| Дата приема | Дата когда человек подписал ~~приговор~~ договор. Ограничение: не более 10 лет, от текущей даты |
+| Оклад | Значение коррелирует с суффиксом должности
- Junior = 1x
- Middle = 1.5x
- Senior = 2.1x
- Team Lead = 2.6x |
+| Электронная почта | Почта сотрудника для связи |
+| Номер телефона | Номер телефона сотрудника для связи |
+| Дата увольнения | Данное поле присутствует только если инификатор увольнения `true` |
+
+### Реализованные фичи
+- Добавлен сервис генерации сотрудников с кешированием
+- Выполнена оркестрация Aspire
+- Логирование основных действий
+
+### Скриншоты работащего приложения
-1. «Кэширование» - Реализация сервиса генерации контрактов, кэширование его ответов
-
-
-В рамках первой лабораторной работы необходимо:
-* Реализовать сервис генерации контрактов на основе Bogus,
-* Реализовать кеширование при помощи IDistributedCache и Redis,
-* Реализовать структурное логирование сервиса генерации,
-* Настроить оркестрацию Aspire.
-
+
+ Поднятые контейнеры
+
+
-
-2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы
-
-
-В рамках второй лабораторной работы необходимо:
-* Настроить оркестрацию на запуск нескольких реплик сервиса генерации,
-* Реализовать апи гейтвей на основе Ocelot,
-* Имплементировать алгоритм балансировки нагрузки согласно варианту.
-
-
-3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда
-
-
-В рамках третьей лабораторной работы необходимо:
-* Добавить в оркестрацию объектное хранилище,
-* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище,
-* Реализовать отправку генерируемых данных в файловый сервис посредством брокера,
-* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе.
-
-
+
+ Пример полученного сотрудника
+
+
-
-4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud
-
-
-В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы:
-* Клиент - в хостинг через отдельный бакет Object Storage,
-* Сервис генерации - в Cloud Function,
-* Апи гейтвей - в Serverless Integration как API Gateway,
-* Брокер сообщений - в Message Queue,
-* Файловый сервис - в Cloud Function,
-* Объектное хранилище - в отдельный бакет Object Storage,
-
-
-
-
-## Задание. Общая часть
-**Обязательно**:
-* Реализация серверной части на [.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 с информацией о задании, скриншоты приложения и прочая информация.
-
-**Факультативно**:
-* Перенос бекенда на облачную инфраструктуру Yandex Cloud
-
-Внимательно прочитайте [дискуссии](https://github.com/itsecd/cloud-development/discussions/1) о том, как работает автоматическое распределение на ревью.
-Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю.
-По итогу работы в семестре должна получиться следующая информационная система:
-C4 диаграмма
-
+
+ Трассировки и демонстрация работы кэширования
+
+
-
-## Варианты заданий
-Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи.
-
-[Список вариантов](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).
-
From 67a846f19628de59693f88bd5f26160103345ae2 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Thu, 12 Mar 2026 13:58:50 +0400
Subject: [PATCH 06/18] Some update in generator and service
---
.../Services/CompanyEmployeeGenerator.cs | 44 ++++++++-----------
.../Services/CompanyEmployeeService.cs | 2 +-
CompanyEmployees.Generator/appsettings.json | 2 +-
3 files changed, 21 insertions(+), 27 deletions(-)
diff --git a/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs b/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
index fac51eb2..b307e8e1 100644
--- a/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
+++ b/CompanyEmployees.Generator/Services/CompanyEmployeeGenerator.cs
@@ -23,17 +23,30 @@ public class CompanyEmployeeGenerator(ILogger logger)
"System Architect"
];
- private readonly Faker _faker = new("ru");
+ private readonly Faker _faker = new Faker("ru")
+ .RuleFor(x => x.FullName, GenerateEmployeeFullName)
+ .RuleFor(x => x.Position, f => $"{f.PickRandom(_positionProfessions)} {f.PickRandom()}")
+ .RuleFor(x => x.Section, f => f.Commerce.Department())
+ .RuleFor(x => x.AdmissionDate, f => f.Date.PastDateOnly(10))
+ .RuleFor(x => x.Salary, GenerateEmployeeSalary)
+ .RuleFor(x => x.Email, f => f.Internet.Email())
+ .RuleFor(x => x.PhoneNumber, f => f.Phone.PhoneNumber("+7(###)###-##-##"))
+ .RuleFor(x => x.Dismissal, f => f.Random.Bool())
+ .RuleFor(x => x.DismissalDate, (f, employeeObject) =>
+ employeeObject.Dismissal
+ ? f.Date.BetweenDateOnly(employeeObject.AdmissionDate, DateOnly.FromDateTime(DateTime.UtcNow))
+ : null
+ );
///
/// Минимальное значение оклада
///
- private static readonly decimal _salaryMin = 100;
+ private const decimal SalaryMin = 100;
///
/// Максимальное значение оклада
///
- private static readonly decimal _salaryMax = 200;
+ private const decimal SalaryMax = 200;
///
/// Справочник суффиксов для профессий
@@ -65,26 +78,7 @@ private enum PositionSuffix
public CompanyEmployeeModel Generate(int id)
{
logger.LogInformation("Start generating company employee application with ID: {Id}", id);
-
- _faker.UseSeed(id)
- .RuleFor(x => x.Id, _ => id)
- .RuleFor(x => x.FullName, GenerateEmployeeFullName)
- .RuleFor(x => x.Position, f => $"{f.PickRandom(_positionProfessions)} {f.PickRandom()}")
- .RuleFor(x => x.Section, f => f.Commerce.Department())
- .RuleFor(x => x.AdmissionDate, f => f.Date.PastDateOnly(10))
- .RuleFor(x => x.Salary, GenerateEmployeeSalary)
- .RuleFor(x => x.Email, f => f.Internet.Email())
- .RuleFor(x => x.PhoneNumber, f => f.Phone.PhoneNumber("+7(###)###-##-##"))
- .RuleFor(x => x.Dismissal, f => f.Random.Bool())
- .RuleFor(x => x.DismissalDate, (f, employeeObject) =>
- employeeObject.Dismissal
- ? f.Date.BetweenDateOnly(employeeObject.AdmissionDate, DateOnly.FromDateTime(DateTime.UtcNow))
- : null
- );
-
- logger.LogInformation("Finally generate employee with ID: {Id}", id);
-
- return _faker.Generate();
+ return _faker.UseSeed(id).RuleFor(x => x.Id, _ => id).Generate();
}
private static string GenerateEmployeeFullName(Faker faker)
@@ -92,14 +86,14 @@ private static string GenerateEmployeeFullName(Faker faker)
var gender = faker.Person.Gender;
var firstName = faker.Name.FirstName(gender);
var lastName = faker.Name.LastName(gender);
- var patronymic = faker.Name.FirstName(gender) + (gender == Name.Gender.Male ? "еевич" : "еевна");
+ var patronymic = faker.Name.FirstName(Name.Gender.Male) + (gender == Name.Gender.Male ? "еевич" : "еевна");
return string.Join(' ', firstName, lastName, patronymic);
}
private static decimal GenerateEmployeeSalary(Faker faker, CompanyEmployeeModel employeeObject)
{
- var baseSalary = faker.Random.Decimal(_salaryMin, _salaryMax);
+ var baseSalary = faker.Random.Decimal(SalaryMin, SalaryMax);
var salaryMap = _positionSuffixSalaryMultipliers
.FirstOrDefault(pair => employeeObject.Position.Contains(pair.Key.ToString()), new KeyValuePair(PositionSuffix.Junior, 1m));
diff --git a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
index 06ea3872..39db4c7e 100644
--- a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
+++ b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
@@ -19,7 +19,7 @@ public class CompanyEmployeeService(
IConfiguration config
)
{
- private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(int.Parse(config["CacheSetting:CacheExpirationMinutes"] ?? "5"));
+ private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(config.GetSection("CacheSetting").GetValue("CacheExpirationMinutes", 5));
private const string CacheKeyPrefix = "company-employee:";
///
diff --git a/CompanyEmployees.Generator/appsettings.json b/CompanyEmployees.Generator/appsettings.json
index 01cc5cec..bd27b3da 100644
--- a/CompanyEmployees.Generator/appsettings.json
+++ b/CompanyEmployees.Generator/appsettings.json
@@ -6,6 +6,6 @@
}
},
"CacheSetting": {
- "CacheExpirationMinutes": "5"
+ "CacheExpirationMinutes": 5
}
}
From ab1cca05231e042e0d6d0f814f11c1fbe757ff9b Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Sat, 14 Mar 2026 10:38:40 +0400
Subject: [PATCH 07/18] Add ApiGateway with LoadBalancer and some fix in
another projects
---
Client.Wasm/wwwroot/appsettings.json | 2 +-
CloudDevelopment.sln | 8 +++
.../CompanyEmployees.ApiGateway.csproj | 17 +++++++
.../LoadBalancer/QueryBased.cs | 50 +++++++++++++++++++
CompanyEmployees.ApiGateway/Program.cs | 19 +++++++
.../Properties/launchSettings.json | 14 ++++++
.../appsettings.Development.json | 12 +++++
CompanyEmployees.ApiGateway/appsettings.json | 9 ++++
CompanyEmployees.ApiGateway/ocelot.json | 27 ++++++++++
.../CompanyEmployees.AppHost.csproj | 1 +
CompanyEmployees.AppHost/Program.cs | 28 +++++++++--
.../Properties/launchSettings.json | 9 ----
12 files changed, 183 insertions(+), 13 deletions(-)
create mode 100644 CompanyEmployees.ApiGateway/CompanyEmployees.ApiGateway.csproj
create mode 100644 CompanyEmployees.ApiGateway/LoadBalancer/QueryBased.cs
create mode 100644 CompanyEmployees.ApiGateway/Program.cs
create mode 100644 CompanyEmployees.ApiGateway/Properties/launchSettings.json
create mode 100644 CompanyEmployees.ApiGateway/appsettings.Development.json
create mode 100644 CompanyEmployees.ApiGateway/appsettings.json
create mode 100644 CompanyEmployees.ApiGateway/ocelot.json
diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json
index fcd8f03c..e8ce9103 100644
--- a/Client.Wasm/wwwroot/appsettings.json
+++ b/Client.Wasm/wwwroot/appsettings.json
@@ -6,5 +6,5 @@
}
},
"AllowedHosts": "*",
- "BaseAddress": "https://localhost:7171/employee"
+ "BaseAddress": "http://localhost:5200/employee"
}
\ No newline at end of file
diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln
index ec784b5f..27bc9ec2 100644
--- a/CloudDevelopment.sln
+++ b/CloudDevelopment.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.Generator"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.ServiceDefaults", "CompanyEmployees.ServiceDefaults\CompanyEmployees.ServiceDefaults.csproj", "{53C4EDF4-E3C2-A484-171F-78A5FD7190F6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.ApiGateway", "CompanyEmployees.ApiGateway\CompanyEmployees.ApiGateway.csproj", "{A1370E88-073B-C8F9-1C33-D962F1D383BD}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
123|Any CPU = 123|Any CPU
@@ -42,6 +44,12 @@ Global
{53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53C4EDF4-E3C2-A484-171F-78A5FD7190F6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1370E88-073B-C8F9-1C33-D962F1D383BD}.123|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1370E88-073B-C8F9-1C33-D962F1D383BD}.123|Any CPU.Build.0 = Debug|Any CPU
+ {A1370E88-073B-C8F9-1C33-D962F1D383BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1370E88-073B-C8F9-1C33-D962F1D383BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1370E88-073B-C8F9-1C33-D962F1D383BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1370E88-073B-C8F9-1C33-D962F1D383BD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CompanyEmployees.ApiGateway/CompanyEmployees.ApiGateway.csproj b/CompanyEmployees.ApiGateway/CompanyEmployees.ApiGateway.csproj
new file mode 100644
index 00000000..b5eae40a
--- /dev/null
+++ b/CompanyEmployees.ApiGateway/CompanyEmployees.ApiGateway.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CompanyEmployees.ApiGateway/LoadBalancer/QueryBased.cs b/CompanyEmployees.ApiGateway/LoadBalancer/QueryBased.cs
new file mode 100644
index 00000000..2a151153
--- /dev/null
+++ b/CompanyEmployees.ApiGateway/LoadBalancer/QueryBased.cs
@@ -0,0 +1,50 @@
+using Ocelot.LoadBalancer.Errors;
+using Ocelot.LoadBalancer.Interfaces;
+using Ocelot.Responses;
+using Ocelot.ServiceDiscovery.Providers;
+using Ocelot.Values;
+
+namespace CompanyEmployees.ApiGateway.LoadBalancer;
+public class QueryBased(IServiceDiscoveryProvider serviceDiscovery)
+ : ILoadBalancer
+{
+ private const string IdQueryParamName = "id";
+
+ public string Type => nameof(QueryBased);
+
+ public async Task> LeaseAsync(HttpContext httpContext)
+ {
+ var services = await serviceDiscovery.GetAsync();
+
+ if (services is null)
+ return new ErrorResponse(
+ new ServicesAreNullError("Service discovery returned null"));
+
+ if (services.Count == 0)
+ return new ErrorResponse(
+ new ServicesAreNullError("No downstream services are available"));
+
+ var query = httpContext.Request.Query;
+
+ if (!query.TryGetValue(IdQueryParamName, out var idValues) || idValues.Count <= 0)
+ {
+ return SelectRandomService(services);
+ }
+
+ var idStr = idValues.First();
+
+ if (string.IsNullOrEmpty(idStr) || !int.TryParse(idStr, out var id) || id < 0)
+ {
+ return SelectRandomService(services);
+ }
+
+ return new OkResponse(services[id % services.Count].HostAndPort);
+ }
+
+ private OkResponse SelectRandomService(List currentServices)
+ {
+ var randomIndex = Random.Shared.Next(currentServices.Count);
+ return new OkResponse(currentServices[randomIndex].HostAndPort);
+ }
+ public void Release(ServiceHostAndPort hostAndPort) { }
+}
\ No newline at end of file
diff --git a/CompanyEmployees.ApiGateway/Program.cs b/CompanyEmployees.ApiGateway/Program.cs
new file mode 100644
index 00000000..f03ca028
--- /dev/null
+++ b/CompanyEmployees.ApiGateway/Program.cs
@@ -0,0 +1,19 @@
+using CompanyEmployees.ApiGateway.LoadBalancer;
+using Ocelot.DependencyInjection;
+using Ocelot.Middleware;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Configuration
+ .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true)
+ .AddOcelot();
+
+builder.Services
+ .AddOcelot(builder.Configuration)
+ .AddCustomLoadBalancer((route, serviceDiscovery) =>
+ new QueryBased(serviceDiscovery));
+
+var app = builder.Build();
+
+await app.UseOcelot();
+await app.RunAsync();
\ No newline at end of file
diff --git a/CompanyEmployees.ApiGateway/Properties/launchSettings.json b/CompanyEmployees.ApiGateway/Properties/launchSettings.json
new file mode 100644
index 00000000..7387ba5c
--- /dev/null
+++ b/CompanyEmployees.ApiGateway/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5200",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CompanyEmployees.ApiGateway/appsettings.Development.json b/CompanyEmployees.ApiGateway/appsettings.Development.json
new file mode 100644
index 00000000..3579dea2
--- /dev/null
+++ b/CompanyEmployees.ApiGateway/appsettings.Development.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Ocelot": "Debug"
+ }
+ },
+ "GlobalConfiguration": {
+ "BaseUrl": "http://localhost:5200"
+ }
+}
\ No newline at end of file
diff --git a/CompanyEmployees.ApiGateway/appsettings.json b/CompanyEmployees.ApiGateway/appsettings.json
new file mode 100644
index 00000000..10f68b8c
--- /dev/null
+++ b/CompanyEmployees.ApiGateway/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/CompanyEmployees.ApiGateway/ocelot.json b/CompanyEmployees.ApiGateway/ocelot.json
new file mode 100644
index 00000000..76bb5186
--- /dev/null
+++ b/CompanyEmployees.ApiGateway/ocelot.json
@@ -0,0 +1,27 @@
+{
+ "Routes": [
+ {
+ "DownstreamPathTemplate": "/employee",
+ "DownstreamScheme": "http",
+ "DownstreamHostAndPorts": [
+ {
+ "Host": "localhost",
+ "Port": 5201
+ },
+ {
+ "Host": "localhost",
+ "Port": 5202
+ },
+ {
+ "Host": "localhost",
+ "Port": 5203
+ }
+ ],
+ "UpstreamPathTemplate": "/employee",
+ "UpstreamHttpMethod": [ "GET" ],
+ "LoadBalancerOptions": {
+ "Type": "QueryBased"
+ }
+ }
+ ]
+}
diff --git a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
index 24c7495c..ce64158d 100644
--- a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
+++ b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
@@ -19,6 +19,7 @@
+
diff --git a/CompanyEmployees.AppHost/Program.cs b/CompanyEmployees.AppHost/Program.cs
index 6e2759c6..f664cc26 100644
--- a/CompanyEmployees.AppHost/Program.cs
+++ b/CompanyEmployees.AppHost/Program.cs
@@ -3,11 +3,33 @@
var redis = builder.AddRedis("redis")
.WithRedisCommander();
-var generator = builder.AddProject("generator")
+
+var generator1 = builder.AddProject("generator-1")
+ .WithEndpoint("http", endpoint => endpoint.Port = 5201)
+ .WithReference(redis)
+ .WaitFor(redis);
+
+var generator2 = builder.AddProject("generator-2")
+ .WithEndpoint("http", endpoint => endpoint.Port = 5202)
.WithReference(redis)
- .WithExternalHttpEndpoints();
+ .WaitFor(redis);
+
+var generator3 = builder.AddProject("generator-3")
+ .WithEndpoint("http", endpoint => endpoint.Port = 5203)
+ .WithReference(redis)
+ .WaitFor(redis);
+
+var gateway = builder.AddProject("companyemployees-apigateway")
+ .WithReference(generator1)
+ .WithReference(generator2)
+ .WithReference(generator3)
+ .WithExternalHttpEndpoints()
+ .WaitFor(generator1)
+ .WaitFor(generator2)
+ .WaitFor(generator3);
builder.AddProject("client")
- .WithReference(generator);
+ .WithReference(gateway)
+ .WaitFor(gateway);
builder.Build().Run();
\ No newline at end of file
diff --git a/CompanyEmployees.Generator/Properties/launchSettings.json b/CompanyEmployees.Generator/Properties/launchSettings.json
index ea1476ca..8c4696c2 100644
--- a/CompanyEmployees.Generator/Properties/launchSettings.json
+++ b/CompanyEmployees.Generator/Properties/launchSettings.json
@@ -9,15 +9,6 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
- },
- "https": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": false,
- "applicationUrl": "https://localhost:7171;http://localhost:5171",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
}
}
}
From 6a867222d4f7a6cb6cb25631f613aaa75a3a565c Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Sat, 14 Mar 2026 10:52:19 +0400
Subject: [PATCH 08/18] Update README.md
---
README.md | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/README.md b/README.md
index e28f3726..b27d6044 100644
--- a/README.md
+++ b/README.md
@@ -51,3 +51,16 @@
+
+## 📝 Лабораторная работа №2
+
+Реализация ApiGateway, настройка его работы
+
+### Цели
+- Настроить оркестрацию на запуск нескольких реплик сервиса генерации
+- Реализовать ApiGateway на основе **Ocelot**
+- Имплементировать алгоритм балансировки **QueryBased**
+
+### Реализованные фичи
+- Реализован алгоритм балансировки
+- Реализовн ApiGateway и подняты три реплики сервиса генерации
From d0146b7d3dfa1dbe655a8c39f3b5ad466b0b8c95 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Sat, 14 Mar 2026 11:00:33 +0400
Subject: [PATCH 09/18] Update README.md
---
README.md | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/README.md b/README.md
index b27d6044..7eaebf0a 100644
--- a/README.md
+++ b/README.md
@@ -64,3 +64,25 @@
### Реализованные фичи
- Реализован алгоритм балансировки
- Реализовн ApiGateway и подняты три реплики сервиса генерации
+
+### Скриншоты работащего приложения
+
+
+ Поднятые контейнеры
+
+
+
+
+
+
+ Граф
+
+
+
+
+
+
+ Логи генерации
+
+
+
From 924b2bbb1d3ee7df0b6ffd382acbce48f9c9bd10 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Sat, 14 Mar 2026 16:56:02 +0400
Subject: [PATCH 10/18] Some fix
---
Client.Wasm/Components/StudentCard.razor | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor
index a53bf00f..cfde0e65 100644
--- a/Client.Wasm/Components/StudentCard.razor
+++ b/Client.Wasm/Components/StudentCard.razor
@@ -4,7 +4,7 @@
- Номер №1 "Кэширование"
+ Номер №2 "Балансировка нагрузки"
Вариант №8 "Сотрудник компании"
From 3ae4aabc24662e245119107e4d0857b60323ceb5 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Mon, 16 Mar 2026 16:52:16 +0400
Subject: [PATCH 11/18] add some fixes
---
.../LoadBalancer/QueryBased.cs | 2 +-
CompanyEmployees.ApiGateway/Program.cs | 26 ++++++-
CompanyEmployees.ApiGateway/ocelot.json | 1 +
CompanyEmployees.AppHost/Program.cs | 39 ++++-------
CompanyEmployees.AppHost/appsettings.json | 3 +-
.../CompanyEmployees.ServiceDefaults.csproj | 1 +
.../Extensions.cs | 70 ++++++++++++++++---
7 files changed, 105 insertions(+), 37 deletions(-)
diff --git a/CompanyEmployees.ApiGateway/LoadBalancer/QueryBased.cs b/CompanyEmployees.ApiGateway/LoadBalancer/QueryBased.cs
index 2a151153..ec8d2cbc 100644
--- a/CompanyEmployees.ApiGateway/LoadBalancer/QueryBased.cs
+++ b/CompanyEmployees.ApiGateway/LoadBalancer/QueryBased.cs
@@ -41,7 +41,7 @@ public async Task> LeaseAsync(HttpContext httpConte
return new OkResponse(services[id % services.Count].HostAndPort);
}
- private OkResponse SelectRandomService(List currentServices)
+ private static OkResponse SelectRandomService(List currentServices)
{
var randomIndex = Random.Shared.Next(currentServices.Count);
return new OkResponse(currentServices[randomIndex].HostAndPort);
diff --git a/CompanyEmployees.ApiGateway/Program.cs b/CompanyEmployees.ApiGateway/Program.cs
index f03ca028..cc72b4f8 100644
--- a/CompanyEmployees.ApiGateway/Program.cs
+++ b/CompanyEmployees.ApiGateway/Program.cs
@@ -1,18 +1,40 @@
using CompanyEmployees.ApiGateway.LoadBalancer;
+using Grpc.Net.Client.Balancer;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
+using System.Runtime.InteropServices;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
- .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true)
- .AddOcelot();
+ .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
+
+var generators = builder.Configuration.GetSection("Generators").Get() ?? [];
+
+var addressOverrides = new List>();
+
+for (var i = 0; i < generators.Length; ++i)
+{
+ var name = generators[i];
+ var url = builder.Configuration[$"services:{name}: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);
builder.Services
.AddOcelot(builder.Configuration)
.AddCustomLoadBalancer((route, serviceDiscovery) =>
new QueryBased(serviceDiscovery));
+Console.WriteLine(builder.Configuration);
+
var app = builder.Build();
await app.UseOcelot();
diff --git a/CompanyEmployees.ApiGateway/ocelot.json b/CompanyEmployees.ApiGateway/ocelot.json
index 76bb5186..c41c83ac 100644
--- a/CompanyEmployees.ApiGateway/ocelot.json
+++ b/CompanyEmployees.ApiGateway/ocelot.json
@@ -1,4 +1,5 @@
{
+ "Generators": [ "generator-1", "generator-2", "generator-3" ],
"Routes": [
{
"DownstreamPathTemplate": "/employee",
diff --git a/CompanyEmployees.AppHost/Program.cs b/CompanyEmployees.AppHost/Program.cs
index f664cc26..c907c076 100644
--- a/CompanyEmployees.AppHost/Program.cs
+++ b/CompanyEmployees.AppHost/Program.cs
@@ -3,30 +3,21 @@
var redis = builder.AddRedis("redis")
.WithRedisCommander();
-
-var generator1 = builder.AddProject("generator-1")
- .WithEndpoint("http", endpoint => endpoint.Port = 5201)
- .WithReference(redis)
- .WaitFor(redis);
-
-var generator2 = builder.AddProject("generator-2")
- .WithEndpoint("http", endpoint => endpoint.Port = 5202)
- .WithReference(redis)
- .WaitFor(redis);
-
-var generator3 = builder.AddProject("generator-3")
- .WithEndpoint("http", endpoint => endpoint.Port = 5203)
- .WithReference(redis)
- .WaitFor(redis);
-
-var gateway = builder.AddProject("companyemployees-apigateway")
- .WithReference(generator1)
- .WithReference(generator2)
- .WithReference(generator3)
- .WithExternalHttpEndpoints()
- .WaitFor(generator1)
- .WaitFor(generator2)
- .WaitFor(generator3);
+var gatewayPort = builder.Configuration.GetValue("GatewayPort");
+var gateway = builder.AddProject("companyemployees-apigateway");
+
+for (var i = 0; i < 3; ++i)
+{
+ var currGenerator = builder.AddProject($"generator-{i + 1}")
+ .WithEndpoint("http", endpoint => endpoint.Port = gatewayPort + 1 + i)
+ .WithReference(redis)
+ .WaitFor(redis);
+
+ gateway
+ .WithReference(currGenerator)
+ .WithExternalHttpEndpoints()
+ .WaitFor(currGenerator);
+}
builder.AddProject("client")
.WithReference(gateway)
diff --git a/CompanyEmployees.AppHost/appsettings.json b/CompanyEmployees.AppHost/appsettings.json
index 31c092aa..81ffa49f 100644
--- a/CompanyEmployees.AppHost/appsettings.json
+++ b/CompanyEmployees.AppHost/appsettings.json
@@ -5,5 +5,6 @@
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
- }
+ },
+ "GatewayPort": 5200
}
diff --git a/CompanyEmployees.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj b/CompanyEmployees.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
index 4e142da7..0d65e7b0 100644
--- a/CompanyEmployees.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
+++ b/CompanyEmployees.ServiceDefaults/CompanyEmployees.ServiceDefaults.csproj
@@ -23,6 +23,7 @@
+
diff --git a/CompanyEmployees.ServiceDefaults/Extensions.cs b/CompanyEmployees.ServiceDefaults/Extensions.cs
index f1836a63..5fafe4db 100644
--- a/CompanyEmployees.ServiceDefaults/Extensions.cs
+++ b/CompanyEmployees.ServiceDefaults/Extensions.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
@@ -8,6 +9,7 @@
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Serilog;
+using Serilog.Sinks.OpenTelemetry;
namespace CompanyEmployees.ServiceDefaults;
@@ -30,15 +32,31 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu
public static IHostApplicationBuilder ConfigureSerilog(this IHostApplicationBuilder builder)
{
- Log.Logger = new LoggerConfiguration()
+ var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.Enrich.WithEnvironmentName()
.Enrich.WithThreadId()
.Enrich.WithMachineName()
.WriteTo.Console(outputTemplate:
- "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}")
- .CreateLogger();
+ "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}");
+
+ var otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"];
+ if (!string.IsNullOrWhiteSpace(otlpEndpoint))
+ {
+ loggerConfig.WriteTo.OpenTelemetry(options =>
+ {
+ options.Endpoint = otlpEndpoint;
+ options.Protocol = OtlpProtocol.Grpc;
+ options.ResourceAttributes = new Dictionary
+ {
+ ["service.name"] = builder.Configuration["OTEL_SERVICE_NAME"]
+ ?? builder.Environment.ApplicationName
+ };
+ });
+ }
+
+ Log.Logger = loggerConfig.CreateLogger();
builder.Services.AddSerilog();
@@ -47,12 +65,6 @@ public static IHostApplicationBuilder ConfigureSerilog(this IHostApplicationBuil
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
{
- builder.Logging.AddOpenTelemetry(logging =>
- {
- logging.IncludeFormattedMessage = true;
- logging.IncludeScopes = true;
- });
-
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
@@ -91,6 +103,46 @@ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicati
return builder;
}
+ public static IHostApplicationBuilder AddGatewayDefaults(this IHostApplicationBuilder builder)
+ {
+ builder.ConfigureSerilog();
+ builder.ConfigureOpenTelemetry();
+ builder.AddDefaultHealthChecks();
+ builder.AddDefaultCors();
+
+ return builder;
+ }
+
+ public const string CorsPolicyName = "GatewayPolicy";
+
+ public static IHostApplicationBuilder AddDefaultCors(this IHostApplicationBuilder builder)
+ {
+ builder.Services.AddCors(options =>
+ {
+ options.AddPolicy(CorsPolicyName, policy =>
+ {
+ if (builder.Environment.IsDevelopment())
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ }
+ else
+ {
+ var allowedOrigins = builder.Configuration
+ .GetSection("Cors:AllowedOrigins")
+ .Get() ?? [];
+
+ policy.WithOrigins(allowedOrigins)
+ .WithMethods("GET")
+ .WithHeaders("Content-Type", "Authorization");
+ }
+ });
+ });
+
+ return builder;
+ }
+
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
app.MapHealthChecks("/health");
From f4af71c6a2408280fbb98e09f0f178915f72edfb Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Thu, 19 Mar 2026 13:37:22 +0400
Subject: [PATCH 12/18] some fix after review
---
CompanyEmployees.ApiGateway/Program.cs | 4 ----
CompanyEmployees.AppHost/Program.cs | 5 +++--
CompanyEmployees.ServiceDefaults/Extensions.cs | 10 ----------
3 files changed, 3 insertions(+), 16 deletions(-)
diff --git a/CompanyEmployees.ApiGateway/Program.cs b/CompanyEmployees.ApiGateway/Program.cs
index cc72b4f8..dd9d9add 100644
--- a/CompanyEmployees.ApiGateway/Program.cs
+++ b/CompanyEmployees.ApiGateway/Program.cs
@@ -1,8 +1,6 @@
using CompanyEmployees.ApiGateway.LoadBalancer;
-using Grpc.Net.Client.Balancer;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
-using System.Runtime.InteropServices;
var builder = WebApplication.CreateBuilder(args);
@@ -33,8 +31,6 @@
.AddCustomLoadBalancer((route, serviceDiscovery) =>
new QueryBased(serviceDiscovery));
-Console.WriteLine(builder.Configuration);
-
var app = builder.Build();
await app.UseOcelot();
diff --git a/CompanyEmployees.AppHost/Program.cs b/CompanyEmployees.AppHost/Program.cs
index c907c076..3763c130 100644
--- a/CompanyEmployees.AppHost/Program.cs
+++ b/CompanyEmployees.AppHost/Program.cs
@@ -4,7 +4,9 @@
.WithRedisCommander();
var gatewayPort = builder.Configuration.GetValue("GatewayPort");
-var gateway = builder.AddProject("companyemployees-apigateway");
+var gateway = builder
+ .AddProject("companyemployees-apigateway")
+ .WithExternalHttpEndpoints();
for (var i = 0; i < 3; ++i)
{
@@ -15,7 +17,6 @@
gateway
.WithReference(currGenerator)
- .WithExternalHttpEndpoints()
.WaitFor(currGenerator);
}
diff --git a/CompanyEmployees.ServiceDefaults/Extensions.cs b/CompanyEmployees.ServiceDefaults/Extensions.cs
index 5fafe4db..96cb5f7a 100644
--- a/CompanyEmployees.ServiceDefaults/Extensions.cs
+++ b/CompanyEmployees.ServiceDefaults/Extensions.cs
@@ -103,16 +103,6 @@ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicati
return builder;
}
- public static IHostApplicationBuilder AddGatewayDefaults(this IHostApplicationBuilder builder)
- {
- builder.ConfigureSerilog();
- builder.ConfigureOpenTelemetry();
- builder.AddDefaultHealthChecks();
- builder.AddDefaultCors();
-
- return builder;
- }
-
public const string CorsPolicyName = "GatewayPolicy";
public static IHostApplicationBuilder AddDefaultCors(this IHostApplicationBuilder builder)
From 4cd321cde9dafa77188caa699b8169059a4d219d Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Mon, 13 Apr 2026 13:37:48 +0400
Subject: [PATCH 13/18] adding file service
---
CloudDevelopment.sln | 8 +++
.../CompanyEmployees.AppHost.csproj | 1 +
CompanyEmployees.AppHost/Program.cs | 32 ++++++++-
CompanyEmployees.AppHost/appsettings.json | 6 +-
.../.minio.sys/buckets/.heal/mrf/list.bin | Bin 0 -> 4 bytes
.../xl.meta.bkp | Bin 0 -> 598 bytes
.../minio-data/.minio.sys/format.json | 1 +
.../tmp/a37b78ac-d0d4-445f-a8c4-78d3c21f8b47 | Bin 0 -> 2049 bytes
.../CompanyEmployees.FileService.csproj | 20 ++++++
.../Configuration/MinioConfiguration.cs | 10 +++
.../Models/EmployeeMessage.cs | 14 ++++
CompanyEmployees.FileService/Program.cs | 63 ++++++++++++++++++
.../Properties/launchSettings.json | 38 +++++++++++
.../Services/EmployeeConsumer.cs | 37 ++++++++++
.../Services/Minio.cs | 33 +++++++++
.../Services/SQSService.cs | 32 +++++++++
.../appsettings.Development.json | 8 +++
CompanyEmployees.FileService/appsettings.json | 19 ++++++
.../CompanyEmployees.Generator.csproj | 1 +
CompanyEmployees.Generator/Program.cs | 27 ++++++++
.../Services/CompanyEmployeeService.cs | 9 ++-
.../Services/EmployeePublisher.cs | 27 ++++++++
.../Services/IEmployeePublisher.cs | 8 +++
23 files changed, 391 insertions(+), 3 deletions(-)
create mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.heal/mrf/list.bin
create mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.usage-cache.bin/ff552525-5761-4c67-9d00-66fb5002c37a/xl.meta.bkp
create mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/format.json
create mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/a37b78ac-d0d4-445f-a8c4-78d3c21f8b47
create mode 100644 CompanyEmployees.FileService/CompanyEmployees.FileService.csproj
create mode 100644 CompanyEmployees.FileService/Configuration/MinioConfiguration.cs
create mode 100644 CompanyEmployees.FileService/Models/EmployeeMessage.cs
create mode 100644 CompanyEmployees.FileService/Program.cs
create mode 100644 CompanyEmployees.FileService/Properties/launchSettings.json
create mode 100644 CompanyEmployees.FileService/Services/EmployeeConsumer.cs
create mode 100644 CompanyEmployees.FileService/Services/Minio.cs
create mode 100644 CompanyEmployees.FileService/Services/SQSService.cs
create mode 100644 CompanyEmployees.FileService/appsettings.Development.json
create mode 100644 CompanyEmployees.FileService/appsettings.json
create mode 100644 CompanyEmployees.Generator/Services/EmployeePublisher.cs
create mode 100644 CompanyEmployees.Generator/Services/IEmployeePublisher.cs
diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln
index 27bc9ec2..39e3071d 100644
--- a/CloudDevelopment.sln
+++ b/CloudDevelopment.sln
@@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.ServiceDef
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.ApiGateway", "CompanyEmployees.ApiGateway\CompanyEmployees.ApiGateway.csproj", "{A1370E88-073B-C8F9-1C33-D962F1D383BD}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.FileService", "CompanyEmployees.FileService\CompanyEmployees.FileService.csproj", "{079C1DF9-133E-FD81-C1FA-1FABEBFB1101}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
123|Any CPU = 123|Any CPU
@@ -50,6 +52,12 @@ Global
{A1370E88-073B-C8F9-1C33-D962F1D383BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1370E88-073B-C8F9-1C33-D962F1D383BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1370E88-073B-C8F9-1C33-D962F1D383BD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.123|Any CPU.ActiveCfg = Debug|Any CPU
+ {079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.123|Any CPU.Build.0 = Debug|Any CPU
+ {079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
index ce64158d..d5eb61b4 100644
--- a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
+++ b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
@@ -20,6 +20,7 @@
+
diff --git a/CompanyEmployees.AppHost/Program.cs b/CompanyEmployees.AppHost/Program.cs
index 3763c130..1167ad9e 100644
--- a/CompanyEmployees.AppHost/Program.cs
+++ b/CompanyEmployees.AppHost/Program.cs
@@ -1,8 +1,26 @@
+using Aspire.Hosting;
+
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("redis")
.WithRedisCommander();
+var minioAccessKey = builder.Configuration["MinIO:AccessKey"]!;
+var minioSecretKey = builder.Configuration["MinIO:SecretKey"]!;
+
+var minio = builder.AddContainer("minio", "minio/minio")
+ .WithEnvironment("MINIO_ROOT_USER", minioAccessKey)
+ .WithEnvironment("MINIO_ROOT_PASSWORD", minioSecretKey)
+ .WithArgs("server", "/data", "--console-address", ":9001")
+ .WithHttpEndpoint(port: 9000, targetPort: 9000, name: "api")
+ .WithHttpEndpoint(port: 9001, targetPort: 9001, name: "console")
+ .WithBindMount("minio-data", "/data")
+ .WaitFor(redis);
+
+var sqs = builder.AddContainer("elasticmq", "softwaremill/elasticmq-native")
+ .WithHttpEndpoint(targetPort: 9324, name: "http")
+ .WithHttpHealthCheck("/?Action=ListQueues", endpointName: "http");
+
var gatewayPort = builder.Configuration.GetValue("GatewayPort");
var gateway = builder
.AddProject("companyemployees-apigateway")
@@ -13,13 +31,25 @@
var currGenerator = builder.AddProject($"generator-{i + 1}")
.WithEndpoint("http", endpoint => endpoint.Port = gatewayPort + 1 + i)
.WithReference(redis)
- .WaitFor(redis);
+ .WaitFor(redis)
+ .WithEnvironment("Sqs__ServiceUrl", sqs.GetEndpoint("http"))
+ .WaitFor(sqs);
gateway
.WithReference(currGenerator)
.WaitFor(currGenerator);
}
+var fileService = builder.AddProject("company-employee-fileservice")
+ .WithEnvironment("Sqs__ServiceUrl", sqs.GetEndpoint("http"))
+ .WithEnvironment("MinIO__ServiceUrl", minio.GetEndpoint("api"))
+ .WithEnvironment("MinIO__AccessKey", "minioadmin")
+ .WithEnvironment("MinIO__SecretKey", "minioadmin")
+ .WithEnvironment("MinIO__BucketName", "company-employee")
+ .WithEnvironment("BucketName", "company-employee")
+ .WaitFor(sqs)
+ .WaitFor(minio);
+
builder.AddProject("client")
.WithReference(gateway)
.WaitFor(gateway);
diff --git a/CompanyEmployees.AppHost/appsettings.json b/CompanyEmployees.AppHost/appsettings.json
index 81ffa49f..8a44f4b2 100644
--- a/CompanyEmployees.AppHost/appsettings.json
+++ b/CompanyEmployees.AppHost/appsettings.json
@@ -6,5 +6,9 @@
"Aspire.Hosting.Dcp": "Warning"
}
},
- "GatewayPort": 5200
+ "GatewayPort": 5200,
+ "MinIO": {
+ "AccessKey": "minioadmin",
+ "SecretKey": "minioadmin"
+ }
}
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.heal/mrf/list.bin b/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.heal/mrf/list.bin
new file mode 100644
index 0000000000000000000000000000000000000000..a3568729c698aec1d183514222bafc72dafa7007
GIT binary patch
literal 4
LcmZQ%U}OLQ015yD
literal 0
HcmV?d00001
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.usage-cache.bin/ff552525-5761-4c67-9d00-66fb5002c37a/xl.meta.bkp b/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.usage-cache.bin/ff552525-5761-4c67-9d00-66fb5002c37a/xl.meta.bkp
new file mode 100644
index 0000000000000000000000000000000000000000..0f3dfb2e4002137d24e60953582752d92ad80366
GIT binary patch
literal 598
zcmZXRO>7cD6vt--O%EPgW0M*a#Y43oHl#}}%gMArQVdWc;78)YVHuXu{mRZjfE#F1
zk8=nRc}Pg$%`@GJQ@!kjH$aEdeAw%{NK#`y~+Q*YjNHOfd{+?
z09HI6XrEoTC&52wH1kNV3#U=g_6}NW%kT(*@8G37R;H?B*uBE9=5BofQ)VpkKWarH
z#IPs#v$L~TQCw<)!td0fL9G`Rmt;wUov4^_LlSI6#pN`q;XUAd?<6rbk1NorixA41
za5J1PDPv?ag$$M~DYOZeobu8Aq9ck1=f5KX&73>s!C~;&^K1r!;f(+^^xo@b#~~iMf~lKVI;}3-S{UE2b8-
zyH*k@xb5Fc;tC5_MHz#B98s3l9Rv8o(ZlXO$jz)HDGEKm}Lq&dBq8|MjyD{%yMxZw8dSQyYB_Qsqbgv
OdyVrKY2nA(2>k^ygWWCw
literal 0
HcmV?d00001
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/format.json b/CompanyEmployees.AppHost/minio-data/.minio.sys/format.json
new file mode 100644
index 00000000..0f5af294
--- /dev/null
+++ b/CompanyEmployees.AppHost/minio-data/.minio.sys/format.json
@@ -0,0 +1 @@
+{"version":"1","format":"xl-single","id":"b933a2c5-a208-45aa-bb1f-2a0a527ef98c","xl":{"version":"3","this":"49aa2d91-6d02-4361-8044-75a366d0daf7","sets":[["49aa2d91-6d02-4361-8044-75a366d0daf7"]],"distributionAlgo":"SIPMOD+PARITY"}}
\ No newline at end of file
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/a37b78ac-d0d4-445f-a8c4-78d3c21f8b47 b/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/a37b78ac-d0d4-445f-a8c4-78d3c21f8b47
new file mode 100644
index 0000000000000000000000000000000000000000..e36ee44665fb70c4cacbf2be6feee8abc1e3a467
GIT binary patch
literal 2049
dcmZQz7zLvtFd71*Aut*OqaiRF0wXO1v;YVz04o3h
literal 0
HcmV?d00001
diff --git a/CompanyEmployees.FileService/CompanyEmployees.FileService.csproj b/CompanyEmployees.FileService/CompanyEmployees.FileService.csproj
new file mode 100644
index 00000000..b7bca91f
--- /dev/null
+++ b/CompanyEmployees.FileService/CompanyEmployees.FileService.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CompanyEmployees.FileService/Configuration/MinioConfiguration.cs b/CompanyEmployees.FileService/Configuration/MinioConfiguration.cs
new file mode 100644
index 00000000..541fc579
--- /dev/null
+++ b/CompanyEmployees.FileService/Configuration/MinioConfiguration.cs
@@ -0,0 +1,10 @@
+namespace CompanyEmployees.FileService.Configuration;
+
+public class MinioConfiguration
+{
+ public string Endpoint { get; set; } = "localhost:9000";
+ public string AccessKey { get; set; } = "minioadmin";
+ public string SecretKey { get; set; } = "minioadmin";
+ public bool UseSSL { get; set; } = false;
+ public string BucketName { get; set; } = "company-employee";
+}
diff --git a/CompanyEmployees.FileService/Models/EmployeeMessage.cs b/CompanyEmployees.FileService/Models/EmployeeMessage.cs
new file mode 100644
index 00000000..cb33c09a
--- /dev/null
+++ b/CompanyEmployees.FileService/Models/EmployeeMessage.cs
@@ -0,0 +1,14 @@
+namespace CompanyEmployees.FileService.Models;
+
+public record EmployeeMessage(
+ int Id,
+ string FullName,
+ string Position,
+ string Section,
+ DateOnly AdmissionDate,
+ decimal Salary,
+ string Email,
+ string PhoneNumber,
+ bool Dismissal,
+ DateOnly? DismissalDate
+);
diff --git a/CompanyEmployees.FileService/Program.cs b/CompanyEmployees.FileService/Program.cs
new file mode 100644
index 00000000..96489800
--- /dev/null
+++ b/CompanyEmployees.FileService/Program.cs
@@ -0,0 +1,63 @@
+using Amazon.Runtime;
+using Amazon.S3;
+using Amazon.SQS;
+using CompanyEmployees.FileService.Configuration;
+using CompanyEmployees.FileService.Services;
+using CompanyEmployees.ServiceDefaults;
+using MassTransit;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+var minioSettings = builder.Configuration.GetSection("MinIO").Get() ?? new MinioConfiguration();
+
+builder.Services.AddSingleton(minioSettings);
+
+builder.Services.AddSingleton(_ => new AmazonS3Client(
+ new BasicAWSCredentials(minioSettings.AccessKey, minioSettings.SecretKey),
+ new AmazonS3Config
+ {
+ ServiceURL = minioSettings.Endpoint,
+ ForcePathStyle = true,
+ AuthenticationRegion = "us-east-1"
+ }
+));
+
+var sqsServiceUrl = builder.Configuration["SQS:ServiceUrl"] ?? "http://localhost:9324";
+
+builder.Services.AddHostedService();
+
+builder.Services.AddMassTransit(x =>
+{
+ x.AddConsumer();
+
+ x.UsingAmazonSqs((context, cfg) =>
+ {
+ cfg.Host("us-east-1", h =>
+ {
+ h.AccessKey("test");
+ h.SecretKey("test");
+ h.Config(new AmazonSQSConfig
+ {
+ ServiceURL = sqsServiceUrl,
+ AuthenticationRegion = "us-east-1"
+ });
+ });
+
+ cfg.ReceiveEndpoint("employees", e =>
+ {
+ e.ConfigureConsumeTopology = false;
+ e.UseRawJsonDeserializer(RawSerializerOptions.AnyMessageType);
+ e.Consumer(context);
+ });
+ });
+});
+
+builder.Services.AddHostedService();
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+app.Run();
\ No newline at end of file
diff --git a/CompanyEmployees.FileService/Properties/launchSettings.json b/CompanyEmployees.FileService/Properties/launchSettings.json
new file mode 100644
index 00000000..2c364ee0
--- /dev/null
+++ b/CompanyEmployees.FileService/Properties/launchSettings.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:64732",
+ "sslPort": 44310
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5269",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7208;http://localhost:5269",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CompanyEmployees.FileService/Services/EmployeeConsumer.cs b/CompanyEmployees.FileService/Services/EmployeeConsumer.cs
new file mode 100644
index 00000000..3b676ce9
--- /dev/null
+++ b/CompanyEmployees.FileService/Services/EmployeeConsumer.cs
@@ -0,0 +1,37 @@
+using Amazon.S3;
+using Amazon.S3.Model;
+using MassTransit;
+using System.Text.Json;
+using CompanyEmployees.FileService.Models;
+
+namespace CompanyEmployees.FileService.Services;
+
+public class EmployeeConsumer(
+ IAmazonS3 s3Client,
+ ILogger logger,
+ IConfiguration configuration
+) : IConsumer
+{
+ private readonly string _bucketName = configuration["MinIO:BucketName"] ?? "company-employee";
+ private static readonly JsonSerializerOptions _jsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ public async Task Consume(ConsumeContext context)
+ {
+ var employee= context.Message;
+ var fileName = $"employee-{employee.Id}.json";
+ var json = JsonSerializer.Serialize(employee, _jsonOptions);
+
+ await s3Client.PutObjectAsync(new PutObjectRequest
+ {
+ BucketName = _bucketName,
+ Key = fileName,
+ ContentBody = json,
+ ContentType = "application/json"
+ }, context.CancellationToken);
+
+ logger.LogInformation("Saved employee {EmployeeId} to minio with filename: {FileName}", employee.Id, fileName);
+ }
+}
\ No newline at end of file
diff --git a/CompanyEmployees.FileService/Services/Minio.cs b/CompanyEmployees.FileService/Services/Minio.cs
new file mode 100644
index 00000000..c810bc92
--- /dev/null
+++ b/CompanyEmployees.FileService/Services/Minio.cs
@@ -0,0 +1,33 @@
+using Amazon.S3;
+using Amazon.S3.Model;
+
+namespace CompanyEmployees.FileService.Services;
+
+public class MinioInitializer(IAmazonS3 s3Client, ILogger logger, IConfiguration configuration) : BackgroundService
+{
+
+ private readonly string _bucketName = configuration["MinIO:BucketName"] ?? "company-employee";
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await s3Client.PutBucketAsync(new PutBucketRequest { BucketName = _bucketName }, stoppingToken);
+ logger.LogInformation("Minio bucket '{BucketName}' ready", _bucketName);
+ return;
+ }
+ catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.Conflict)
+ {
+ logger.LogInformation("Minio bucket '{BucketName}' already exists", _bucketName);
+ return;
+ }
+ catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
+ {
+ logger.LogWarning(ex, "Failed to initialize Minio bucket, retrying in 3 seconds...");
+ await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CompanyEmployees.FileService/Services/SQSService.cs b/CompanyEmployees.FileService/Services/SQSService.cs
new file mode 100644
index 00000000..5dec6c0b
--- /dev/null
+++ b/CompanyEmployees.FileService/Services/SQSService.cs
@@ -0,0 +1,32 @@
+namespace CompanyEmployees.FileService.Services;
+
+public class SQSService(IConfiguration configuration, ILogger logger) : IHostedService
+{
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ var sqsUrl = configuration["SQS:ServiceUrl"] ?? "http://localhost:9324";
+
+ using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
+
+ var deadline = DateTime.UtcNow.AddMinutes(2);
+
+ while (DateTime.UtcNow < deadline && !cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ await httpClient.GetAsync($"{sqsUrl}/?Action=ListQueues", cancellationToken);
+ logger.LogInformation("SQS is ready at {Url}", sqsUrl);
+ return;
+ }
+ catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
+ {
+ logger.LogWarning(ex, "SQS not ready at {Url}, retrying in 2 seconds...", sqsUrl);
+ await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
+ }
+ }
+
+ logger.LogError("SQS did not become ready within the timeout");
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
\ No newline at end of file
diff --git a/CompanyEmployees.FileService/appsettings.Development.json b/CompanyEmployees.FileService/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/CompanyEmployees.FileService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CompanyEmployees.FileService/appsettings.json b/CompanyEmployees.FileService/appsettings.json
new file mode 100644
index 00000000..3585edcf
--- /dev/null
+++ b/CompanyEmployees.FileService/appsettings.json
@@ -0,0 +1,19 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "MinIO": {
+ "Endpoint": "http://localhost:9000",
+ "AccessKey": "minioadmin",
+ "SecretKey": "minioadmin",
+ "UseSSL": false,
+ "BucketName": "company-employee"
+ },
+ "SQS": {
+ "ServiceUrl": "http://localhost:9324"
+ }
+}
diff --git a/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj b/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
index b0d3ba24..f0aacf28 100644
--- a/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
+++ b/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
@@ -10,6 +10,7 @@
+
diff --git a/CompanyEmployees.Generator/Program.cs b/CompanyEmployees.Generator/Program.cs
index 582edefa..98f8943a 100644
--- a/CompanyEmployees.Generator/Program.cs
+++ b/CompanyEmployees.Generator/Program.cs
@@ -1,6 +1,8 @@
using CompanyEmployees.Generator.Services;
using CompanyEmployees.ServiceDefaults;
using Serilog;
+using MassTransit;
+using Amazon.SQS;
var builder = WebApplication.CreateBuilder(args);
@@ -21,6 +23,30 @@
});
});
+var sqsServiceUrl = builder.Configuration["Sqs:ServiceUrl"];
+if (!string.IsNullOrEmpty(sqsServiceUrl))
+{
+ builder.Services.AddMassTransit(x =>
+ {
+ x.UsingAmazonSqs((_, cfg) =>
+ {
+ cfg.Host("us-east-1", h =>
+ {
+ h.AccessKey("test");
+ h.SecretKey("test");
+ h.Config(new AmazonSQSConfig
+ {
+ ServiceURL = sqsServiceUrl,
+ AuthenticationRegion = "us-east-1"
+ });
+ });
+ cfg.UseRawJsonSerializer();
+ });
+ });
+
+ builder.Services.AddScoped();
+}
+
var app = builder.Build();
app.UseCors();
@@ -45,6 +71,7 @@
try
{
var application = await service.GetByIdAsync(id, cancellationToken);
+
return Results.Ok(application);
}
catch (Exception ex)
diff --git a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
index 39db4c7e..a3a51cb6 100644
--- a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
+++ b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
@@ -16,7 +16,8 @@ public class CompanyEmployeeService(
CompanyEmployeeGenerator generator,
IDistributedCache cache,
ILogger logger,
- IConfiguration config
+ IConfiguration config,
+ IEmployeePublisher? employeePublisher = null
)
{
private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(config.GetSection("CacheSetting").GetValue("CacheExpirationMinutes", 5));
@@ -55,6 +56,12 @@ public async Task GetByIdAsync(int id, CancellationToken c
await cache.SetStringAsync(cacheKey, serializedData, cacheOptions, cancellationToken);
+ if (employeePublisher is not null)
+ {
+ var res = employeePublisher.PublishAsync(application, cancellationToken);
+ logger.LogInformation("Publisher complete task");
+ }
+
logger.LogInformation(
"Company employee application {Id} saved to cache with TTL {CacheExpiration} minutes",
id,
diff --git a/CompanyEmployees.Generator/Services/EmployeePublisher.cs b/CompanyEmployees.Generator/Services/EmployeePublisher.cs
new file mode 100644
index 00000000..c86e3e27
--- /dev/null
+++ b/CompanyEmployees.Generator/Services/EmployeePublisher.cs
@@ -0,0 +1,27 @@
+using CompanyEmployees.Generator.Models;
+using MassTransit;
+
+namespace CompanyEmployees.Generator.Services;
+
+public class EmployeePublisher(
+ ISendEndpointProvider sendEndpointProvider,
+ ILogger logger) : IEmployeePublisher
+{
+ public Task PublishAsync(CompanyEmployeeModel employee, CancellationToken cancellationToken = default)
+ {
+ return Task.Run(async () =>
+ {
+ try
+ {
+ var sendEndpoint = await sendEndpointProvider.GetSendEndpoint(new Uri("queue:employees"));
+ await sendEndpoint.Send(employee, cancellationToken);
+
+ logger.LogInformation("Sent employee {EmployeeId} to SQS queue", employee.Id);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to send employee {EmployeeId} to SQS", employee.Id);
+ }
+ }, cancellationToken);
+ }
+}
diff --git a/CompanyEmployees.Generator/Services/IEmployeePublisher.cs b/CompanyEmployees.Generator/Services/IEmployeePublisher.cs
new file mode 100644
index 00000000..be52212e
--- /dev/null
+++ b/CompanyEmployees.Generator/Services/IEmployeePublisher.cs
@@ -0,0 +1,8 @@
+using CompanyEmployees.Generator.Models;
+
+namespace CompanyEmployees.Generator.Services;
+
+public interface IEmployeePublisher
+{
+ public Task PublishAsync(CompanyEmployeeModel employee, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
From b213180bad7d7a36124fdefbb848fa7df433e832 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Tue, 14 Apr 2026 11:45:34 +0400
Subject: [PATCH 14/18] add testing
---
CloudDevelopment.sln | 8 +
.../CompanyEmployees.Tests.csproj | 26 +++
CompanyEmployee.Tests/Fixture.cs | 98 ++++++++++
CompanyEmployee.Tests/IntegrationTests.cs | 184 ++++++++++++++++++
.../Properties/launchSettings.json | 38 ++++
.../appsettings.Development.json | 8 +
CompanyEmployee.Tests/appsettings.json | 9 +
CompanyEmployees.ApiGateway/Program.cs | 8 +-
.../CompanyEmployees.AppHost.csproj | 1 +
CompanyEmployees.AppHost/Program.cs | 2 -
...7 => 05678231-c811-49c5-9e05-55188d7b2e92} | Bin
11 files changed, 379 insertions(+), 3 deletions(-)
create mode 100644 CompanyEmployee.Tests/CompanyEmployees.Tests.csproj
create mode 100644 CompanyEmployee.Tests/Fixture.cs
create mode 100644 CompanyEmployee.Tests/IntegrationTests.cs
create mode 100644 CompanyEmployee.Tests/Properties/launchSettings.json
create mode 100644 CompanyEmployee.Tests/appsettings.Development.json
create mode 100644 CompanyEmployee.Tests/appsettings.json
rename CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/{a37b78ac-d0d4-445f-a8c4-78d3c21f8b47 => 05678231-c811-49c5-9e05-55188d7b2e92} (100%)
diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln
index 39e3071d..aa94057e 100644
--- a/CloudDevelopment.sln
+++ b/CloudDevelopment.sln
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.ApiGateway
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.FileService", "CompanyEmployees.FileService\CompanyEmployees.FileService.csproj", "{079C1DF9-133E-FD81-C1FA-1FABEBFB1101}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployees.Tests", "CompanyEmployee.Tests\CompanyEmployees.Tests.csproj", "{F9C9F199-85EA-B6C8-1B29-47442974A133}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
123|Any CPU = 123|Any CPU
@@ -58,6 +60,12 @@ Global
{079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.Debug|Any CPU.Build.0 = Debug|Any CPU
{079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.Release|Any CPU.ActiveCfg = Release|Any CPU
{079C1DF9-133E-FD81-C1FA-1FABEBFB1101}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F9C9F199-85EA-B6C8-1B29-47442974A133}.123|Any CPU.ActiveCfg = Debug|Any CPU
+ {F9C9F199-85EA-B6C8-1B29-47442974A133}.123|Any CPU.Build.0 = Debug|Any CPU
+ {F9C9F199-85EA-B6C8-1B29-47442974A133}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F9C9F199-85EA-B6C8-1B29-47442974A133}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F9C9F199-85EA-B6C8-1B29-47442974A133}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F9C9F199-85EA-B6C8-1B29-47442974A133}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CompanyEmployee.Tests/CompanyEmployees.Tests.csproj b/CompanyEmployee.Tests/CompanyEmployees.Tests.csproj
new file mode 100644
index 00000000..ec17788a
--- /dev/null
+++ b/CompanyEmployee.Tests/CompanyEmployees.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CompanyEmployee.Tests/Fixture.cs b/CompanyEmployee.Tests/Fixture.cs
new file mode 100644
index 00000000..afde6d1a
--- /dev/null
+++ b/CompanyEmployee.Tests/Fixture.cs
@@ -0,0 +1,98 @@
+using Amazon.Runtime;
+using Amazon.S3;
+using Amazon.S3.Model;
+using Aspire.Hosting;
+using Aspire.Hosting.Testing;
+using Xunit;
+
+namespace CompanyEmployees.Tests;
+
+///
+/// Фикстура, поднимающая AppHost для интеграционных тестов.
+///
+public class Fixture : IAsyncLifetime
+{
+ public DistributedApplication App { get; private set; } = null!;
+ public AmazonS3Client S3Client { get; private set; } = null!;
+
+ public async Task InitializeAsync()
+ {
+ var appHost = await DistributedApplicationTestingBuilder.CreateAsync();
+
+ appHost.Services.ConfigureHttpClientDefaults(http =>
+ http.AddStandardResilienceHandler(options =>
+ {
+ options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(3);
+ options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(60);
+ options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(3);
+ options.Retry.MaxRetryAttempts = 10;
+ options.Retry.Delay = TimeSpan.FromSeconds(3);
+ }));
+
+ App = await appHost.BuildAsync();
+ await App.StartAsync();
+
+ await Task.WhenAll(
+ App.ResourceNotifications.WaitForResourceAsync("minio"),
+ App.ResourceNotifications.WaitForResourceAsync("elasticmq"),
+ App.ResourceNotifications.WaitForResourceAsync("companyemployees-apigateway"),
+ App.ResourceNotifications.WaitForResourceAsync("company-employee-fileservice")
+ ).WaitAsync(TimeSpan.FromMinutes(5));
+
+ await Task.Delay(TimeSpan.FromSeconds(5));
+
+ using var minioClient = App.CreateHttpClient("minio", "api");
+ var minioUrl = minioClient.BaseAddress!.ToString().TrimEnd('/');
+
+ S3Client = new AmazonS3Client(
+ new BasicAWSCredentials("minioadmin", "minioadmin"),
+ new AmazonS3Config
+ {
+ ServiceURL = minioUrl,
+ ForcePathStyle = true,
+ AuthenticationRegion = "us-east-1"
+ });
+ }
+
+ public async Task> WaitForS3ObjectAsync(string key, int maxAttempts = 15)
+ {
+ for (var i = 0; i < maxAttempts; i++)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2));
+
+ try
+ {
+ var response = await S3Client.ListObjectsAsync(new ListObjectsRequest
+ {
+ BucketName = "company-employee",
+ Prefix = key
+ });
+
+ if (response.S3Objects is not null && response.S3Objects.Count > 0)
+ return response.S3Objects;
+ }
+ catch (AmazonS3Exception ex) when (ex.Message.Contains("NoSuchBucket"))
+ {
+ Console.WriteLine($"Bucket not ready yet, attempt {i + 1}/{maxAttempts}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error listing objects: {ex.Message}");
+ }
+ }
+
+ return [];
+ }
+
+ public async Task DisposeAsync()
+ {
+ S3Client?.Dispose();
+
+ try
+ {
+ await App.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(30));
+ }
+ catch (TimeoutException) { }
+ catch (OperationCanceledException) { }
+ }
+}
\ No newline at end of file
diff --git a/CompanyEmployee.Tests/IntegrationTests.cs b/CompanyEmployee.Tests/IntegrationTests.cs
new file mode 100644
index 00000000..6a58cc3a
--- /dev/null
+++ b/CompanyEmployee.Tests/IntegrationTests.cs
@@ -0,0 +1,184 @@
+using Amazon.S3.Model;
+using Aspire.Hosting.Testing;
+using System.Net;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Xunit;
+using CompanyEmployees.Generator.Models;
+
+namespace CompanyEmployees.Tests;
+
+///
+/// Интеграционные тесты, проверяющие корректную совместную работу всех сервисов.
+///
+public class IntegrationTests(Fixture fixture) : IClassFixture
+{
+ private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true };
+
+ private string _bucketName = "company-employee";
+
+ ///
+ /// Тест для проверки того, что данные генерируются добросовестно
+ ///
+ [Fact]
+ public async Task GetEmployee_TestValid()
+ {
+ using var client = fixture.App.CreateHttpClient("companyemployees-apigateway", "http");
+
+ using var response = await client.GetAsync("/employee?id=1");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var employee = await response.Content.ReadFromJsonAsync(_jsonOptions);
+
+ Assert.NotNull(employee);
+ Assert.Equal(1, employee.Id);
+ Assert.False(string.IsNullOrEmpty(employee.FullName));
+ Assert.False(string.IsNullOrEmpty(employee.Position));
+ Assert.False(string.IsNullOrEmpty(employee.Section));
+ Assert.False(string.IsNullOrEmpty(employee.Email));
+ Assert.False(string.IsNullOrEmpty(employee.PhoneNumber));
+ Assert.True(employee.Salary > 0);
+
+ var isDismissal = employee.Dismissal;
+
+ Assert.True((isDismissal && employee.DismissalDate is not null) || (!isDismissal && employee.DismissalDate is null));
+ }
+
+ ///
+ /// Тест для проверки совпадения объектов сгенерированных с одинаковым Id
+ ///
+ [Fact]
+ public async Task GetEmployee_CheckEmployessWithEqualIDs()
+ {
+ var randomId = Random.Shared.Next(10, 20);
+ using var client = fixture.App.CreateHttpClient("companyemployees-apigateway", "http");
+
+ var employee = await client.GetFromJsonAsync($"/employee?id={randomId}", _jsonOptions);
+ var employeeRepeat = await client.GetFromJsonAsync($"/employee?id={randomId}", _jsonOptions);
+
+ Assert.NotNull(employee);
+ Assert.NotNull(employeeRepeat);
+
+ Assert.Equal(employee.Id, employeeRepeat.Id);
+ Assert.Equal(employee.FullName, employeeRepeat.FullName);
+ Assert.Equal(employee.Position, employeeRepeat.Position);
+ Assert.Equal(employee.Section, employeeRepeat.Section);
+ Assert.Equal(employee.Email, employeeRepeat.Email);
+ Assert.Equal(employee.PhoneNumber, employeeRepeat.PhoneNumber);
+ Assert.Equal(employee.AdmissionDate.ToString(), employeeRepeat.AdmissionDate.ToString());
+ }
+
+ ///
+ /// Тест для проверки не совпадения объектов сгенерированных с разными Id
+ ///
+ [Fact]
+ public async Task GetEmployee_CheckEmployessWithNotEqualIDs()
+ {
+ using var client = fixture.App.CreateHttpClient("companyemployees-apigateway", "http");
+
+ var employee201 = await client.GetFromJsonAsync("/employee?id=201", _jsonOptions);
+ var employee202 = await client.GetFromJsonAsync("/employee?id=202", _jsonOptions);
+
+ Assert.NotNull(employee201);
+ Assert.NotNull(employee202);
+ Assert.Equal(201, employee201.Id);
+ Assert.Equal(202, employee202.Id);
+ Assert.NotEqual(employee201.FullName, employee202.FullName);
+ Assert.NotEqual(employee201.PhoneNumber, employee202.PhoneNumber);
+ }
+
+ ///
+ /// Тест для проверки балансировщика
+ ///
+ [Fact]
+ public async Task GetEmployee_CheckLoadBalancer()
+ {
+ using var client = fixture.App.CreateHttpClient("companyemployees-apigateway", "http");
+
+ var baseId = Random.Shared.Next(100, 200);
+ var tasks = Enumerable.Range(0, 6)
+ .Select(i => client.GetAsync($"/employee?id={baseId + i}"));
+
+ var responses = await Task.WhenAll(tasks);
+
+ Assert.All(responses, r => Assert.Equal(HttpStatusCode.OK, r.StatusCode));
+ }
+
+ ///
+ /// Тест для проверки пайплана:
+ /// Gateway → Generator → SQS → FileService → Minio.
+ ///
+ [Fact]
+ public async Task GetEmployee_CheckPipeline()
+ {
+ var id = 23;
+ var expectedKey = $"employee-{id}.json";
+
+ using var client = fixture.App.CreateHttpClient("companyemployees-apigateway", "http");
+ using var response = await client.GetAsync($"/employee?id={id}");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var objects = await fixture.WaitForS3ObjectAsync(expectedKey);
+
+ Assert.NotEmpty(objects);
+ }
+
+ ///
+ /// Тест для проверки данных сохранённых в бакете
+ ///
+ [Fact]
+ public async Task GetEmployee_CheckBucketData()
+ {
+ var id = 23;
+ var expectedKey = $"employee-{id}.json";
+
+ using var client = fixture.App.CreateHttpClient("companyemployees-apigateway", "http");
+ var employee = await client.GetFromJsonAsync($"/employee?id={id}", _jsonOptions);
+ Assert.NotNull(employee);
+
+ var objects = await fixture.WaitForS3ObjectAsync(expectedKey);
+ Assert.NotEmpty(objects);
+
+ var getResponse = await fixture.S3Client.GetObjectAsync(_bucketName, expectedKey);
+ using var reader = new StreamReader(getResponse.ResponseStream);
+ var json = await reader.ReadToEndAsync();
+ var cached = JsonNode.Parse(json)?.AsObject();
+
+ Assert.NotNull(cached);
+ Assert.Equal(id, cached["id"]!.GetValue());
+ Assert.Equal(employee.FullName, cached["fullName"]!.GetValue());
+ Assert.Equal(employee.Email, cached["email"]!.GetValue());
+ Assert.Equal(employee.Salary, cached["salary"]!.GetValue());
+ }
+
+ ///
+ /// Тест для проверки избежания дублирования данных в Minio
+ ///
+ [Fact]
+ public async Task GetEmployee_CheckNotDuplicate()
+ {
+ var id = 23;
+ var expectedKey = $"employee-{id}.json";
+
+ using var client = fixture.App.CreateHttpClient("companyemployees-apigateway", "http");
+
+ using var firstResponse = await client.GetAsync($"/employee?id={id}");
+ firstResponse.EnsureSuccessStatusCode();
+ var objectsAfterFirst = await fixture.WaitForS3ObjectAsync(expectedKey);
+ Assert.NotEmpty(objectsAfterFirst);
+
+ using var secondResponse = await client.GetAsync($"/employee?id={id}");
+ secondResponse.EnsureSuccessStatusCode();
+
+ await Task.Delay(TimeSpan.FromSeconds(5));
+
+ var listResponse = await fixture.S3Client.ListObjectsV2Async(new ListObjectsV2Request
+ {
+ BucketName = _bucketName,
+ Prefix = expectedKey
+ });
+
+ Assert.Single(listResponse.S3Objects);
+ }
+}
\ No newline at end of file
diff --git a/CompanyEmployee.Tests/Properties/launchSettings.json b/CompanyEmployee.Tests/Properties/launchSettings.json
new file mode 100644
index 00000000..cda96a6f
--- /dev/null
+++ b/CompanyEmployee.Tests/Properties/launchSettings.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:64848",
+ "sslPort": 44342
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5049",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7120;http://localhost:5049",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CompanyEmployee.Tests/appsettings.Development.json b/CompanyEmployee.Tests/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/CompanyEmployee.Tests/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CompanyEmployee.Tests/appsettings.json b/CompanyEmployee.Tests/appsettings.json
new file mode 100644
index 00000000..10f68b8c
--- /dev/null
+++ b/CompanyEmployee.Tests/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/CompanyEmployees.ApiGateway/Program.cs b/CompanyEmployees.ApiGateway/Program.cs
index dd9d9add..0b54a86b 100644
--- a/CompanyEmployees.ApiGateway/Program.cs
+++ b/CompanyEmployees.ApiGateway/Program.cs
@@ -1,9 +1,12 @@
using CompanyEmployees.ApiGateway.LoadBalancer;
+using CompanyEmployees.ServiceDefaults;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
var builder = WebApplication.CreateBuilder(args);
+builder.AddServiceDefaults();
+
builder.Configuration
.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
@@ -33,5 +36,8 @@
var app = builder.Build();
+app.MapDefaultEndpoints();
+
await app.UseOcelot();
-await app.RunAsync();
\ No newline at end of file
+
+app.Run();
\ No newline at end of file
diff --git a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
index d5eb61b4..c7fad305 100644
--- a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
+++ b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
@@ -19,6 +19,7 @@
+
diff --git a/CompanyEmployees.AppHost/Program.cs b/CompanyEmployees.AppHost/Program.cs
index 1167ad9e..bceaba6b 100644
--- a/CompanyEmployees.AppHost/Program.cs
+++ b/CompanyEmployees.AppHost/Program.cs
@@ -1,5 +1,3 @@
-using Aspire.Hosting;
-
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("redis")
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/a37b78ac-d0d4-445f-a8c4-78d3c21f8b47 b/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92
similarity index 100%
rename from CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/a37b78ac-d0d4-445f-a8c4-78d3c21f8b47
rename to CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92
From 3900a09b519ad8b5f5d57b9c8daef094dd2e5881 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Wed, 15 Apr 2026 15:57:29 +0400
Subject: [PATCH 15/18] update gitignore
---
.gitignore | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.gitignore b/.gitignore
index ce892922..9b1d158f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -416,3 +416,5 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp
+
+/minio-data
\ No newline at end of file
From 486068ea8bf383c5eef5445862e5360769e8fb55 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Wed, 15 Apr 2026 15:57:53 +0400
Subject: [PATCH 16/18] Delete CompanyEmployees.AppHost/minio-data/.minio.sys
directory
---
.../.minio.sys/buckets/.heal/mrf/list.bin | Bin 4 -> 0 bytes
.../xl.meta.bkp | Bin 598 -> 0 bytes
.../minio-data/.minio.sys/format.json | 1 -
.../tmp/05678231-c811-49c5-9e05-55188d7b2e92 | Bin 2049 -> 0 bytes
4 files changed, 1 deletion(-)
delete mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.heal/mrf/list.bin
delete mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.usage-cache.bin/ff552525-5761-4c67-9d00-66fb5002c37a/xl.meta.bkp
delete mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/format.json
delete mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.heal/mrf/list.bin b/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.heal/mrf/list.bin
deleted file mode 100644
index a3568729c698aec1d183514222bafc72dafa7007..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 4
LcmZQ%U}OLQ015yD
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.usage-cache.bin/ff552525-5761-4c67-9d00-66fb5002c37a/xl.meta.bkp b/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/.usage-cache.bin/ff552525-5761-4c67-9d00-66fb5002c37a/xl.meta.bkp
deleted file mode 100644
index 0f3dfb2e4002137d24e60953582752d92ad80366..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 598
zcmZXRO>7cD6vt--O%EPgW0M*a#Y43oHl#}}%gMArQVdWc;78)YVHuXu{mRZjfE#F1
zk8=nRc}Pg$%`@GJQ@!kjH$aEdeAw%{NK#`y~+Q*YjNHOfd{+?
z09HI6XrEoTC&52wH1kNV3#U=g_6}NW%kT(*@8G37R;H?B*uBE9=5BofQ)VpkKWarH
z#IPs#v$L~TQCw<)!td0fL9G`Rmt;wUov4^_LlSI6#pN`q;XUAd?<6rbk1NorixA41
za5J1PDPv?ag$$M~DYOZeobu8Aq9ck1=f5KX&73>s!C~;&^K1r!;f(+^^xo@b#~~iMf~lKVI;}3-S{UE2b8-
zyH*k@xb5Fc;tC5_MHz#B98s3l9Rv8o(ZlXO$jz)HDGEKm}Lq&dBq8|MjyD{%yMxZw8dSQyYB_Qsqbgv
OdyVrKY2nA(2>k^ygWWCw
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/format.json b/CompanyEmployees.AppHost/minio-data/.minio.sys/format.json
deleted file mode 100644
index 0f5af294..00000000
--- a/CompanyEmployees.AppHost/minio-data/.minio.sys/format.json
+++ /dev/null
@@ -1 +0,0 @@
-{"version":"1","format":"xl-single","id":"b933a2c5-a208-45aa-bb1f-2a0a527ef98c","xl":{"version":"3","this":"49aa2d91-6d02-4361-8044-75a366d0daf7","sets":[["49aa2d91-6d02-4361-8044-75a366d0daf7"]],"distributionAlgo":"SIPMOD+PARITY"}}
\ No newline at end of file
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92 b/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92
deleted file mode 100644
index e36ee44665fb70c4cacbf2be6feee8abc1e3a467..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 2049
dcmZQz7zLvtFd71*Aut*OqaiRF0wXO1v;YVz04o3h
From 87f5eeb2d016faa78ae53bcc0fa6cb70a64aff35 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Thu, 16 Apr 2026 15:21:54 +0400
Subject: [PATCH 17/18] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?=
=?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=B5=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 5 ++++-
.../tmp/05678231-c811-49c5-9e05-55188d7b2e92 | Bin 2049 -> 0 bytes
2 files changed, 4 insertions(+), 1 deletion(-)
delete mode 100644 CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92
diff --git a/.gitignore b/.gitignore
index 9b1d158f..be8e996b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -417,4 +417,7 @@ FodyWeavers.xsd
*.msm
*.msp
-/minio-data
\ No newline at end of file
+/minio-data
+/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/808628fb-0b20-4cea-9ca9-1b28fea8d489
+/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92
+/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp
diff --git a/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92 b/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92
deleted file mode 100644
index e36ee44665fb70c4cacbf2be6feee8abc1e3a467..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 2049
dcmZQz7zLvtFd71*Aut*OqaiRF0wXO1v;YVz04o3h
From 0f1956f9af32e8a347926683e22c66b555d31467 Mon Sep 17 00:00:00 2001
From: AvtoBBus <113329450+AvtoBBus@users.noreply.github.com>
Date: Thu, 16 Apr 2026 15:22:45 +0400
Subject: [PATCH 18/18] =?UTF-8?q?=D0=95=D1=89=D1=91=20=D0=BA=D0=B0=D0=BA?=
=?UTF-8?q?=D0=B8=D0=B5-=D1=82=D0=BE=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?=
=?UTF-8?q?=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 1 +
CompanyEmployee.Tests/Fixture.cs | 35 +++++-
CompanyEmployee.Tests/IntegrationTests.cs | 103 +++++++++++++++++-
.../CompanyEmployees.AppHost.csproj | 3 +
CompanyEmployees.AppHost/Program.cs | 14 +--
.../CompanyEmployees.FileService.csproj | 1 +
CompanyEmployees.FileService/Program.cs | 15 ++-
.../Services/EmployeeConsumer.cs | 28 +++--
.../Services/SQSService.cs | 2 +-
CompanyEmployees.FileService/appsettings.json | 2 +-
.../CompanyEmployees.Generator.csproj | 1 +
CompanyEmployees.Generator/Program.cs | 3 +-
.../Services/CompanyEmployeeService.cs | 2 +-
13 files changed, 173 insertions(+), 37 deletions(-)
diff --git a/.gitignore b/.gitignore
index be8e996b..bdb5e93d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -421,3 +421,4 @@ FodyWeavers.xsd
/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/808628fb-0b20-4cea-9ca9-1b28fea8d489
/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp/05678231-c811-49c5-9e05-55188d7b2e92
/CompanyEmployees.AppHost/minio-data/.minio.sys/tmp
+/CompanyEmployees.AppHost/minio-data/.minio.sys/buckets/company-employee/.usage-cache.bin/e1e86355-5b8e-4d71-b0b2-a9cc2939b942
diff --git a/CompanyEmployee.Tests/Fixture.cs b/CompanyEmployee.Tests/Fixture.cs
index afde6d1a..bf2ae34a 100644
--- a/CompanyEmployee.Tests/Fixture.cs
+++ b/CompanyEmployee.Tests/Fixture.cs
@@ -2,6 +2,7 @@
using Amazon.S3;
using Amazon.S3.Model;
using Aspire.Hosting;
+using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Testing;
using Xunit;
@@ -14,6 +15,8 @@ public class Fixture : IAsyncLifetime
{
public DistributedApplication App { get; private set; } = null!;
public AmazonS3Client S3Client { get; private set; } = null!;
+
+ public string sqsUrl = "";
public async Task InitializeAsync()
{
@@ -41,17 +44,34 @@ await Task.WhenAll(
await Task.Delay(TimeSpan.FromSeconds(5));
- using var minioClient = App.CreateHttpClient("minio", "api");
+ using var minioClient = App.CreateHttpClient("minio", "http");
var minioUrl = minioClient.BaseAddress!.ToString().TrimEnd('/');
+
+ var sqsHttpClient = App.CreateHttpClient("elasticmq", "http");
+ sqsUrl = sqsHttpClient.BaseAddress?.ToString().TrimEnd('/');
+
+
S3Client = new AmazonS3Client(
new BasicAWSCredentials("minioadmin", "minioadmin"),
new AmazonS3Config
{
ServiceURL = minioUrl,
ForcePathStyle = true,
+ UseHttp = true,
AuthenticationRegion = "us-east-1"
});
+
+ var doesExist = await Amazon.S3.Util.AmazonS3Util.DoesS3BucketExistV2Async(S3Client, "company-employee");
+
+ if (!doesExist)
+ {
+ await S3Client.PutBucketAsync(new PutBucketRequest
+ {
+ BucketName = "company-employee"
+ });
+ }
+
}
public async Task> WaitForS3ObjectAsync(string key, int maxAttempts = 15)
@@ -62,11 +82,14 @@ public async Task> WaitForS3ObjectAsync(string key, int maxAttemp
try
{
- var response = await S3Client.ListObjectsAsync(new ListObjectsRequest
- {
- BucketName = "company-employee",
- Prefix = key
- });
+ var doesExist = await Amazon.S3.Util.AmazonS3Util.DoesS3BucketExistV2Async(S3Client, "company-employee");
+
+ var response = await S3Client.ListObjectsV2Async(new ListObjectsV2Request
+ {
+ BucketName = "company-employee",
+ Prefix = key,
+ }
+ );
if (response.S3Objects is not null && response.S3Objects.Count > 0)
return response.S3Objects;
diff --git a/CompanyEmployee.Tests/IntegrationTests.cs b/CompanyEmployee.Tests/IntegrationTests.cs
index 6a58cc3a..e8bf007c 100644
--- a/CompanyEmployee.Tests/IntegrationTests.cs
+++ b/CompanyEmployee.Tests/IntegrationTests.cs
@@ -1,10 +1,14 @@
-using Amazon.S3.Model;
+using Amazon.Runtime;
+using Amazon.S3;
+using Amazon.S3.Model;
+using Amazon.SQS;
+using Amazon.SQS.Model;
using Aspire.Hosting.Testing;
+using CompanyEmployees.Generator.Models;
using System.Net;
using System.Text.Json;
using System.Text.Json.Nodes;
using Xunit;
-using CompanyEmployees.Generator.Models;
namespace CompanyEmployees.Tests;
@@ -124,19 +128,112 @@ public async Task GetEmployee_CheckPipeline()
Assert.NotEmpty(objects);
}
+ [Fact]
+ public async Task Debug_MinioConnection()
+ {
+ // Проверяем, видит ли FileService тот же MinIO
+ using var client = fixture.App.CreateHttpClient("company-employee-fileservice", "http");
+
+ // Добавьте эндпоинт в FileService для диагностики
+ var response = await client.GetAsync("/health");
+
+ // Также проверяем через S3 клиент
+ var buckets = await fixture.S3Client.ListBucketsAsync();
+ Console.WriteLine($"Found {buckets.Buckets.Count} buckets:");
+ foreach (var bucket in buckets.Buckets)
+ {
+ Console.WriteLine($"- {bucket.BucketName}");
+ }
+
+ // Проверяем, может ли FileService писать в MinIO
+ var testKey = "test.txt";
+ await fixture.S3Client.PutObjectAsync(new PutObjectRequest
+ {
+ BucketName = "company-employee",
+ Key = testKey,
+ ContentBody = "test"
+ });
+
+ var objects = await fixture.S3Client.ListObjectsV2Async(new ListObjectsV2Request
+ {
+ BucketName = "company-employee"
+ });
+
+ Assert.True(objects.S3Objects.Count > 0, "No objects found in bucket");
+ Console.WriteLine($"Successfully wrote to MinIO, found {objects.S3Objects.Count} objects");
+ }
+
+ [Fact]
+ public async Task Debug_SqsQueue()
+ {
+ // Создаем SQS клиент
+ var sqsConfig = new AmazonSQSConfig
+ {
+ ServiceURL = fixture.sqsUrl,
+ AuthenticationRegion = "us-east-1"
+ };
+ using var sqsClient = new AmazonSQSClient(
+ new BasicAWSCredentials("test", "test"),
+ sqsConfig);
+
+ // Получаем URL очереди
+ try
+ {
+ var getQueueUrlResponse = await sqsClient.GetQueueUrlAsync("employees");
+ Console.WriteLine($"✅ Queue URL: {getQueueUrlResponse.QueueUrl}");
+
+ // Проверяем атрибуты очереди
+ var attributes = await sqsClient.GetQueueAttributesAsync(
+ getQueueUrlResponse.QueueUrl,
+ new List { "ApproximateNumberOfMessages", "QueueArn" });
+
+ Console.WriteLine($"Queue ARN: {attributes.QueueARN}");
+ Console.WriteLine($"Messages in queue: {attributes.ApproximateNumberOfMessages}");
+
+ // Отправляем тестовое сообщение напрямую в SQS
+ var testMessage = new { Id = 999, FullName = "Test User", Position = "Tester" };
+ var sendResponse = await sqsClient.SendMessageAsync(new SendMessageRequest
+ {
+ QueueUrl = getQueueUrlResponse.QueueUrl,
+ MessageBody = JsonSerializer.Serialize(testMessage)
+ });
+
+ Console.WriteLine($"✅ Test message sent, MD5: {sendResponse.MD5OfMessageBody}");
+
+ // Ждем и проверяем, что сообщение появилось
+ await Task.Delay(2000);
+
+ var attributesAfter = await sqsClient.GetQueueAttributesAsync(
+ getQueueUrlResponse.QueueUrl,
+ new List { "ApproximateNumberOfMessages" });
+ Console.WriteLine($"Messages after send: {attributesAfter.ApproximateNumberOfMessages}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ Error: {ex.Message}");
+ }
+ }
+
///
/// Тест для проверки данных сохранённых в бакете
///
[Fact]
public async Task GetEmployee_CheckBucketData()
{
- var id = 23;
+ var id = 12;
var expectedKey = $"employee-{id}.json";
using var client = fixture.App.CreateHttpClient("companyemployees-apigateway", "http");
var employee = await client.GetFromJsonAsync($"/employee?id={id}", _jsonOptions);
Assert.NotNull(employee);
+ await Task.Delay(TimeSpan.FromSeconds(10));
+
+ var response = await fixture.S3Client.ListObjectsV2Async(new ListObjectsV2Request
+ {
+ BucketName = "company-employee"
+ });
+
var objects = await fixture.WaitForS3ObjectAsync(expectedKey);
Assert.NotEmpty(objects);
diff --git a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
index c7fad305..ba4101e1 100644
--- a/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
+++ b/CompanyEmployees.AppHost/CompanyEmployees.AppHost.csproj
@@ -14,7 +14,10 @@
+
+
+
diff --git a/CompanyEmployees.AppHost/Program.cs b/CompanyEmployees.AppHost/Program.cs
index bceaba6b..c12a67ff 100644
--- a/CompanyEmployees.AppHost/Program.cs
+++ b/CompanyEmployees.AppHost/Program.cs
@@ -6,15 +6,15 @@
var minioAccessKey = builder.Configuration["MinIO:AccessKey"]!;
var minioSecretKey = builder.Configuration["MinIO:SecretKey"]!;
-var minio = builder.AddContainer("minio", "minio/minio")
+var minio = builder.AddMinioContainer("minio")
.WithEnvironment("MINIO_ROOT_USER", minioAccessKey)
.WithEnvironment("MINIO_ROOT_PASSWORD", minioSecretKey)
- .WithArgs("server", "/data", "--console-address", ":9001")
- .WithHttpEndpoint(port: 9000, targetPort: 9000, name: "api")
- .WithHttpEndpoint(port: 9001, targetPort: 9001, name: "console")
- .WithBindMount("minio-data", "/data")
+ //.WithBindMount("minio-data", "/data")
.WaitFor(redis);
+minio.WithEndpoint("http", endpoint => endpoint.Port = 9000);
+minio.WithEndpoint("console", endpoint => endpoint.Port = 9001);
+
var sqs = builder.AddContainer("elasticmq", "softwaremill/elasticmq-native")
.WithHttpEndpoint(targetPort: 9324, name: "http")
.WithHttpHealthCheck("/?Action=ListQueues", endpointName: "http");
@@ -40,11 +40,9 @@
var fileService = builder.AddProject("company-employee-fileservice")
.WithEnvironment("Sqs__ServiceUrl", sqs.GetEndpoint("http"))
- .WithEnvironment("MinIO__ServiceUrl", minio.GetEndpoint("api"))
- .WithEnvironment("MinIO__AccessKey", "minioadmin")
- .WithEnvironment("MinIO__SecretKey", "minioadmin")
.WithEnvironment("MinIO__BucketName", "company-employee")
.WithEnvironment("BucketName", "company-employee")
+ .WithReference(minio)
.WaitFor(sqs)
.WaitFor(minio);
diff --git a/CompanyEmployees.FileService/CompanyEmployees.FileService.csproj b/CompanyEmployees.FileService/CompanyEmployees.FileService.csproj
index b7bca91f..3d5e68bd 100644
--- a/CompanyEmployees.FileService/CompanyEmployees.FileService.csproj
+++ b/CompanyEmployees.FileService/CompanyEmployees.FileService.csproj
@@ -8,6 +8,7 @@
+
diff --git a/CompanyEmployees.FileService/Program.cs b/CompanyEmployees.FileService/Program.cs
index 96489800..35d21fa5 100644
--- a/CompanyEmployees.FileService/Program.cs
+++ b/CompanyEmployees.FileService/Program.cs
@@ -10,15 +10,20 @@
builder.AddServiceDefaults();
-var minioSettings = builder.Configuration.GetSection("MinIO").Get() ?? new MinioConfiguration();
+var minioSettings = builder.Configuration.GetSection("MinIO");
-builder.Services.AddSingleton(minioSettings);
+// ,
+var minioAccessKey = minioSettings.Get()?.AccessKey;
+var minioSecretKey = minioSettings.Get()?.SecretKey;
+var minioEndpoint = minioSettings.Get()?.Endpoint;
+
+builder.Services.AddSingleton(minioSettings).Configure(minioSettings);
builder.Services.AddSingleton(_ => new AmazonS3Client(
- new BasicAWSCredentials(minioSettings.AccessKey, minioSettings.SecretKey),
+ new BasicAWSCredentials(minioAccessKey, minioSecretKey),
new AmazonS3Config
{
- ServiceURL = minioSettings.Endpoint,
+ ServiceURL = minioEndpoint,
ForcePathStyle = true,
AuthenticationRegion = "us-east-1"
}
@@ -26,7 +31,7 @@
var sqsServiceUrl = builder.Configuration["SQS:ServiceUrl"] ?? "http://localhost:9324";
-builder.Services.AddHostedService();
+builder.Services.AddHostedService();
builder.Services.AddMassTransit(x =>
{
diff --git a/CompanyEmployees.FileService/Services/EmployeeConsumer.cs b/CompanyEmployees.FileService/Services/EmployeeConsumer.cs
index 3b676ce9..9e9b2988 100644
--- a/CompanyEmployees.FileService/Services/EmployeeConsumer.cs
+++ b/CompanyEmployees.FileService/Services/EmployeeConsumer.cs
@@ -20,18 +20,24 @@ IConfiguration configuration
public async Task Consume(ConsumeContext context)
{
- var employee= context.Message;
- var fileName = $"employee-{employee.Id}.json";
- var json = JsonSerializer.Serialize(employee, _jsonOptions);
-
- await s3Client.PutObjectAsync(new PutObjectRequest
+ try
{
- BucketName = _bucketName,
- Key = fileName,
- ContentBody = json,
- ContentType = "application/json"
- }, context.CancellationToken);
+ var employee = context.Message;
+ var fileName = $"employee-{employee.Id}.json";
+ var json = JsonSerializer.Serialize(employee, _jsonOptions);
+
+ var response = await s3Client.PutObjectAsync(new PutObjectRequest
+ {
+ BucketName = _bucketName,
+ Key = fileName,
+ ContentBody = json,
+ ContentType = "application/json"
+ }, context.CancellationToken);
- logger.LogInformation("Saved employee {EmployeeId} to minio with filename: {FileName}", employee.Id, fileName);
+ logger.LogInformation("Saved employee {EmployeeId} to minio with filename: {FileName}", employee.Id, fileName);
+ } catch (Exception ex)
+ {
+ logger.LogError(ex.Message);
+ }
}
}
\ No newline at end of file
diff --git a/CompanyEmployees.FileService/Services/SQSService.cs b/CompanyEmployees.FileService/Services/SQSService.cs
index 5dec6c0b..f8a17d06 100644
--- a/CompanyEmployees.FileService/Services/SQSService.cs
+++ b/CompanyEmployees.FileService/Services/SQSService.cs
@@ -1,6 +1,6 @@
namespace CompanyEmployees.FileService.Services;
-public class SQSService(IConfiguration configuration, ILogger logger) : IHostedService
+public class SqsService(IConfiguration configuration, ILogger logger) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
diff --git a/CompanyEmployees.FileService/appsettings.json b/CompanyEmployees.FileService/appsettings.json
index 3585edcf..d38aef02 100644
--- a/CompanyEmployees.FileService/appsettings.json
+++ b/CompanyEmployees.FileService/appsettings.json
@@ -14,6 +14,6 @@
"BucketName": "company-employee"
},
"SQS": {
- "ServiceUrl": "http://localhost:9324"
+ "ServiceUrl": "http://localhost:4566"
}
}
diff --git a/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj b/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
index f0aacf28..c0447bd6 100644
--- a/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
+++ b/CompanyEmployees.Generator/CompanyEmployees.Generator.csproj
@@ -10,6 +10,7 @@
+
diff --git a/CompanyEmployees.Generator/Program.cs b/CompanyEmployees.Generator/Program.cs
index 98f8943a..68f1d5cb 100644
--- a/CompanyEmployees.Generator/Program.cs
+++ b/CompanyEmployees.Generator/Program.cs
@@ -3,6 +3,7 @@
using Serilog;
using MassTransit;
using Amazon.SQS;
+using LocalStack.Client.Extensions;
var builder = WebApplication.CreateBuilder(args);
@@ -40,7 +41,7 @@
AuthenticationRegion = "us-east-1"
});
});
- cfg.UseRawJsonSerializer();
+ cfg.UseRawJsonSerializer(RawSerializerOptions.AnyMessageType);
});
});
diff --git a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
index a3a51cb6..b19ad3b8 100644
--- a/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
+++ b/CompanyEmployees.Generator/Services/CompanyEmployeeService.cs
@@ -58,7 +58,7 @@ public async Task GetByIdAsync(int id, CancellationToken c
if (employeePublisher is not null)
{
- var res = employeePublisher.PublishAsync(application, cancellationToken);
+ await employeePublisher.PublishAsync(application, cancellationToken);
logger.LogInformation("Publisher complete task");
}