From c8bf7127d1a9800b2eaef4a16aa0d1f40d1ae028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BB=D1=8F=D0=BA=D0=BE=D0=B2=D0=B0=20=D0=92?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=BD=D0=B8=D0=BA=D0=B0?= <113890061+Cat-sandwich@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:01:11 +0400 Subject: [PATCH 1/6] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20=D0=BF=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=BC=D0=B5=D1=82=D0=BD=D0=BE=D0=B9=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=82=D0=B8,=20=D0=BD=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80=20=D0=B8=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client.Wasm/Components/StudentCard.razor | 8 +- Client.Wasm/wwwroot/appsettings.json | 2 +- CloudDevelopment.sln | 18 +++ .../Employee.ApiService.csproj | 20 +++ .../Employee.ApiService.http | 6 + .../Models/EmployeeModel.cs | 58 ++++++++ Employee/Employee.ApiService/Program.cs | 43 ++++++ .../Properties/launchSettings.json | 23 +++ .../Services/EmployeeGenerator.cs | 139 ++++++++++++++++++ .../Services/EmployeeService.cs | 83 +++++++++++ .../appsettings.Development.json | 8 + Employee/Employee.ApiService/appsettings.json | 9 ++ Employee/Employee.AppHost/AppHost.cs | 15 ++ .../Employee.AppHost/Employee.AppHost.csproj | 23 +++ .../Properties/launchSettings.json | 29 ++++ .../appsettings.Development.json | 8 + Employee/Employee.AppHost/appsettings.json | 9 ++ .../Employee.ServiceDefaults.csproj | 22 +++ .../Employee.ServiceDefaults/Extensions.cs | 119 +++++++++++++++ 19 files changed, 637 insertions(+), 5 deletions(-) create mode 100644 Employee/Employee.ApiService/Employee.ApiService.csproj create mode 100644 Employee/Employee.ApiService/Employee.ApiService.http create mode 100644 Employee/Employee.ApiService/Models/EmployeeModel.cs create mode 100644 Employee/Employee.ApiService/Program.cs create mode 100644 Employee/Employee.ApiService/Properties/launchSettings.json create mode 100644 Employee/Employee.ApiService/Services/EmployeeGenerator.cs create mode 100644 Employee/Employee.ApiService/Services/EmployeeService.cs create mode 100644 Employee/Employee.ApiService/appsettings.Development.json create mode 100644 Employee/Employee.ApiService/appsettings.json create mode 100644 Employee/Employee.AppHost/AppHost.cs create mode 100644 Employee/Employee.AppHost/Employee.AppHost.csproj create mode 100644 Employee/Employee.AppHost/Properties/launchSettings.json create mode 100644 Employee/Employee.AppHost/appsettings.Development.json create mode 100644 Employee/Employee.AppHost/appsettings.json create mode 100644 Employee/Employee.ServiceDefaults/Employee.ServiceDefaults.csproj create mode 100644 Employee/Employee.ServiceDefaults/Extensions.cs diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..3df3c2ce 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,10 +4,10 @@ - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №1 "Кэширование" + Вариант №2 "Сотрудник компании" + Выполнена Беляковой Вероникой 6511 + Ссылка на форк diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..e852c9bb 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "https://localhost:7491/api/employee" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..0fc425de 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -5,6 +5,12 @@ VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.AppHost", "Employee\Employee.AppHost\Employee.AppHost.csproj", "{8575D1CE-B605-4372-A759-BA1598A8C6F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.ServiceDefaults", "Employee\Employee.ServiceDefaults\Employee.ServiceDefaults.csproj", "{6194C225-7198-D470-1A5E-025DA0008A3E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.ApiService", "Employee\Employee.ApiService\Employee.ApiService.csproj", "{EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +21,18 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {8575D1CE-B605-4372-A759-BA1598A8C6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8575D1CE-B605-4372-A759-BA1598A8C6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8575D1CE-B605-4372-A759-BA1598A8C6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8575D1CE-B605-4372-A759-BA1598A8C6F9}.Release|Any CPU.Build.0 = Release|Any CPU + {6194C225-7198-D470-1A5E-025DA0008A3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6194C225-7198-D470-1A5E-025DA0008A3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6194C225-7198-D470-1A5E-025DA0008A3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6194C225-7198-D470-1A5E-025DA0008A3E}.Release|Any CPU.Build.0 = Release|Any CPU + {EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Employee/Employee.ApiService/Employee.ApiService.csproj b/Employee/Employee.ApiService/Employee.ApiService.csproj new file mode 100644 index 00000000..7c4359fb --- /dev/null +++ b/Employee/Employee.ApiService/Employee.ApiService.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Employee/Employee.ApiService/Employee.ApiService.http b/Employee/Employee.ApiService/Employee.ApiService.http new file mode 100644 index 00000000..4aa8536f --- /dev/null +++ b/Employee/Employee.ApiService/Employee.ApiService.http @@ -0,0 +1,6 @@ +@ApiService_HostAddress = http://localhost:5547 + +GET {{ApiService_HostAddress}}/weatherforecast/ +Accept: application/json + +### \ No newline at end of file diff --git a/Employee/Employee.ApiService/Models/EmployeeModel.cs b/Employee/Employee.ApiService/Models/EmployeeModel.cs new file mode 100644 index 00000000..acf8a517 --- /dev/null +++ b/Employee/Employee.ApiService/Models/EmployeeModel.cs @@ -0,0 +1,58 @@ +namespace Employee.ApiService.Models; + +/// +/// Класс сотрудник компании +/// +public class EmployeeModel +{ + + /// + /// Идентификатор сотрудника в системе + /// + public required int Id { get; set; } + + /// + /// ФИО + /// + public required string Name { get; set; } + + /// + /// Должность + /// + public required string Position { get; set; } + + /// + /// Отдел + /// + public required string Department { get; set; } + + /// + /// Дата приема + /// + public required DateOnly DateAdmission { get; set; } + + /// + /// Оклад + /// + public required decimal Salary { get; set; } + + /// + /// Электронная почта + /// + public required string Email { get; set; } + + /// + /// Номер телефона + /// + public required string Phone { get; set; } + + /// + /// Индикатор увольнения + /// + public bool DismissalIndicator { get; set; } = false; + + /// + /// Дата увольнения + /// + public DateOnly? DateDismissal { get; set; } +} diff --git a/Employee/Employee.ApiService/Program.cs b/Employee/Employee.ApiService/Program.cs new file mode 100644 index 00000000..d4e83ec8 --- /dev/null +++ b/Employee/Employee.ApiService/Program.cs @@ -0,0 +1,43 @@ +using Employee.ApiService.Services; +using Employee.ServiceDefaults; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("redis"); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("wasm", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapDefaultEndpoints(); +app.UseHttpsRedirection(); +app.UseCors("wasm"); + +app.MapGet("/api/employee", async (int id, EmployeeService service) => +{ + var employee = await service.GetEmployeeAsync(id); + return Results.Ok(employee); +}); + +app.Run(); \ No newline at end of file diff --git a/Employee/Employee.ApiService/Properties/launchSettings.json b/Employee/Employee.ApiService/Properties/launchSettings.json new file mode 100644 index 00000000..1262a7ef --- /dev/null +++ b/Employee/Employee.ApiService/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5547", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7491;http://localhost:5547", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Employee/Employee.ApiService/Services/EmployeeGenerator.cs b/Employee/Employee.ApiService/Services/EmployeeGenerator.cs new file mode 100644 index 00000000..12243f1d --- /dev/null +++ b/Employee/Employee.ApiService/Services/EmployeeGenerator.cs @@ -0,0 +1,139 @@ +using Bogus; +using Employee.ApiService.Models; + +namespace Employee.ApiService.Services; + +/// +/// Генератор тестовых сотрудников +/// +public class EmployeeGenerator +{ + /// + /// Справочник профессий + /// + private static readonly string[] _professions = + [ + "Developer", + "Manager", + "Analyst", + "QA", + "DevOps", + "Designer" + ]; + + /// + /// Справочник суффиксов должностей и коэффициентов зарплаты + /// + private static readonly Dictionary _positionLevels = new() + { + { "Junior", 0.7m }, + { "Middle", 1.0m }, + { "Senior", 1.5m }, + { "Lead", 2.0m } + }; + + /// + /// Константа базовой зарплаты + /// + private const decimal BaseSalary = 100000m; + + /// + /// Генерация должности + /// + private static string GeneratePosition(Faker f) + { + var level = f.PickRandom(_positionLevels.Keys.ToArray()); + var profession = f.PickRandom(_professions); + + return $"{level} {profession}"; + } + + /// + /// Генерация даты приема + /// + private static DateOnly GenerateAdmissionDate(Faker f) + { + return DateOnly.FromDateTime(f.Date.Past(10)); + } + + /// + /// Генерация зарплаты с учетом коэффициента уровня + /// + private static decimal GenerateSalary(Faker f, string position) + { + var level = _positionLevels.Keys.FirstOrDefault(position.Contains); + + decimal coefficient = 1; + + if (level != null) + { + coefficient = _positionLevels[level]; + } + + var randomFactor = f.Random.Decimal(0.9m, 1.1m); + + var salary = BaseSalary * coefficient * randomFactor; + + return Math.Round(salary, 2); + } + + /// + /// Генерация даты увольнения + /// + private static DateOnly? GenerateDismissalDate(Faker f, EmployeeModel employee) + { + if (!employee.DismissalIndicator) + return null; + + var start = employee.DateAdmission.ToDateTime(TimeOnly.MinValue); + + var dismissal = f.Date.Between(start, DateTime.Now); + + return DateOnly.FromDateTime(dismissal); + } + /// + /// Генерация ФИО + /// + private static string GenerateFullName(Faker f) + { + var gender = f.PickRandom(); + + var firstName = f.Name.FirstName(gender); + var lastName = f.Name.LastName(gender); + + // имя отца + var fatherName = f.Name.FirstName(Bogus.DataSets.Name.Gender.Male); + + var patronymic = gender == Bogus.DataSets.Name.Gender.Male + ? fatherName + "ович" + : fatherName + "овна"; + + return $"{lastName} {firstName} {patronymic}"; + } + + /// + /// Преднастроенный генератор + /// + private static readonly Faker _faker = new Faker("ru") + .RuleFor(e => e.Name, f => GenerateFullName(f)) + .RuleFor(e => e.Position, f => GeneratePosition(f)) + .RuleFor(e => e.Department, f => f.Commerce.Department()) + .RuleFor(e => e.DateAdmission, f => GenerateAdmissionDate(f)) + .RuleFor(e => e.Salary, (f, e) => GenerateSalary(f, e.Position)) + .RuleFor(e => e.Email, (f, e) => f.Internet.Email()) + .RuleFor(e => e.Phone, f => f.Phone.PhoneNumber("+7(###)###-##-##")) + .RuleFor(e => e.DismissalIndicator, f => f.Random.Bool(0.2f)) + .RuleFor(e => e.DateDismissal, (f, e) => GenerateDismissalDate(f, e)); + + /// + /// Генерация сотрудника + /// + public EmployeeModel Generate(int id) + { + var employee = _faker.Generate(); + employee.Id = id; + + return employee; + } + +} diff --git a/Employee/Employee.ApiService/Services/EmployeeService.cs b/Employee/Employee.ApiService/Services/EmployeeService.cs new file mode 100644 index 00000000..cc6cd4cc --- /dev/null +++ b/Employee/Employee.ApiService/Services/EmployeeService.cs @@ -0,0 +1,83 @@ +using Employee.ApiService.Models; +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace Employee.ApiService.Services; + +/// +/// Сервис получения сотрудников +/// +/// кэш +/// конфигурация +/// логирование +/// генератор +public class EmployeeService( + IDistributedCache _cache, + IConfiguration _configuration, + ILogger _logger, + EmployeeGenerator _generator) +{ + + /// + /// Получение сотрудника по id + /// + /// идентификатор + /// + public async Task GetEmployeeAsync(int id) + { + var cacheKey = $"employee:{id}"; + + _logger.LogInformation("Попытка получить сотрудника {EmployeeId} из кэша", id); + + var cachedData = await _cache.GetStringAsync(cacheKey); + + if (!string.IsNullOrEmpty(cachedData)) + { + try + { + var cachedEmployee = JsonSerializer.Deserialize(cachedData); + + if (cachedEmployee != null) + { + _logger.LogInformation("Сотрудник {EmployeeId} получен из кэша", id); + return cachedEmployee; + } + + _logger.LogWarning("Сотрудник {EmployeeId} найден в кэше, но десериализация вернула null", id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка десериализации сотрудника {EmployeeId}", id); + } + } + + _logger.LogInformation("Сотрудник {EmployeeId} отсутствует в кэше. Генерация нового", id); + + var employee = _generator.Generate(id); + + try + { + var expirationMinutes = _configuration.GetValue("CacheSettings:ExpirationMinutes", 5); + + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(expirationMinutes) + }; + + await _cache.SetStringAsync( + cacheKey, + JsonSerializer.Serialize(employee), + cacheOptions + ); + + _logger.LogInformation("Сотрудник {EmployeeId} сохранён в кэш", id); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Не удалось сохранить сотрудника {EmployeeId} в кэш", id); + } + + return employee; + } + +} diff --git a/Employee/Employee.ApiService/appsettings.Development.json b/Employee/Employee.ApiService/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Employee/Employee.ApiService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Employee/Employee.ApiService/appsettings.json b/Employee/Employee.ApiService/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Employee/Employee.ApiService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Employee/Employee.AppHost/AppHost.cs b/Employee/Employee.AppHost/AppHost.cs new file mode 100644 index 00000000..98c208eb --- /dev/null +++ b/Employee/Employee.AppHost/AppHost.cs @@ -0,0 +1,15 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var redis = builder.AddRedis("redis"); + +var apiService = builder.AddProject("apiservice") + .WithReference(redis) + .WithHttpHealthCheck("/health"); + +builder.AddProject("webfrontend") + .WithExternalHttpEndpoints() + .WithHttpHealthCheck("/health") + .WithReference(apiService) + .WaitFor(apiService); + +builder.Build().Run(); \ No newline at end of file diff --git a/Employee/Employee.AppHost/Employee.AppHost.csproj b/Employee/Employee.AppHost/Employee.AppHost.csproj new file mode 100644 index 00000000..bafe08b8 --- /dev/null +++ b/Employee/Employee.AppHost/Employee.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net8.0 + enable + enable + 6ec37d7c-c0fa-45cb-977e-7d69bf172ad2 + + + + + + + + + + + + + diff --git a/Employee/Employee.AppHost/Properties/launchSettings.json b/Employee/Employee.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..fc84e215 --- /dev/null +++ b/Employee/Employee.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:17278;http://localhost:15137", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21242", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22095" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15137", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19281", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20030" + } + } + } +} diff --git a/Employee/Employee.AppHost/appsettings.Development.json b/Employee/Employee.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Employee/Employee.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Employee/Employee.AppHost/appsettings.json b/Employee/Employee.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/Employee/Employee.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/Employee/Employee.ServiceDefaults/Employee.ServiceDefaults.csproj b/Employee/Employee.ServiceDefaults/Employee.ServiceDefaults.csproj new file mode 100644 index 00000000..1b6e209a --- /dev/null +++ b/Employee/Employee.ServiceDefaults/Employee.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/Employee/Employee.ServiceDefaults/Extensions.cs b/Employee/Employee.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..cc077fbe --- /dev/null +++ b/Employee/Employee.ServiceDefaults/Extensions.cs @@ -0,0 +1,119 @@ +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; + +namespace Employee.ServiceDefaults; + +/// +/// +/// +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + /// + /// + /// + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + + http.AddServiceDiscovery(); + }); + + return builder; + } + + /// + /// + /// + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + /// + /// + /// + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + /// + /// + /// + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + /// + /// + /// + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks(HealthEndpointPath); + + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} From 8b4f875cd7d11000f3772259b82076b7e2f21429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BB=D1=8F=D0=BA=D0=BE=D0=B2=D0=B0=20=D0=92?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=BD=D0=B8=D0=BA=D0=B0?= <113890061+Cat-sandwich@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:12:39 +0400 Subject: [PATCH 2/6] =?UTF-8?q?=D0=B2=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client.Wasm/Properties/launchSettings.json | 4 +- CloudDevelopment.sln | 30 ++-- .../Employee.ApiService.csproj | 1 + .../Models/EmployeeModel.cs | 0 .../Program.cs | 16 +- .../Properties/launchSettings.json | 0 .../Services/EmployeeGenerator.cs | 50 ++----- .../Services/EmployeeService.cs | 9 +- .../appsettings.Development.json | 0 .../appsettings.json | 0 .../AppHost.cs | 5 +- .../Employee.AppHost.csproj | 2 +- .../Properties/launchSettings.json | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 .../Employee.ServiceDefaults.csproj | 0 .../Extensions.cs | 0 .../Employee.ApiService.http | 6 - README.md | 141 +++--------------- 19 files changed, 76 insertions(+), 188 deletions(-) rename {Employee/Employee.ApiService => Employee.ApiService}/Employee.ApiService.csproj (91%) rename {Employee/Employee.ApiService => Employee.ApiService}/Models/EmployeeModel.cs (100%) rename {Employee/Employee.ApiService => Employee.ApiService}/Program.cs (65%) rename {Employee/Employee.ApiService => Employee.ApiService}/Properties/launchSettings.json (100%) rename {Employee/Employee.ApiService => Employee.ApiService}/Services/EmployeeGenerator.cs (70%) rename {Employee/Employee.ApiService => Employee.ApiService}/Services/EmployeeService.cs (89%) rename {Employee/Employee.ApiService => Employee.ApiService}/appsettings.Development.json (100%) rename {Employee/Employee.ApiService => Employee.ApiService}/appsettings.json (100%) rename {Employee/Employee.AppHost => Employee.AppHost}/AppHost.cs (81%) rename {Employee/Employee.AppHost => Employee.AppHost}/Employee.AppHost.csproj (90%) rename {Employee/Employee.AppHost => Employee.AppHost}/Properties/launchSettings.json (100%) rename {Employee/Employee.AppHost => Employee.AppHost}/appsettings.Development.json (100%) rename {Employee/Employee.AppHost => Employee.AppHost}/appsettings.json (100%) rename {Employee/Employee.ServiceDefaults => Employee.ServiceDefaults}/Employee.ServiceDefaults.csproj (100%) rename {Employee/Employee.ServiceDefaults => Employee.ServiceDefaults}/Extensions.cs (100%) delete mode 100644 Employee/Employee.ApiService/Employee.ApiService.http diff --git a/Client.Wasm/Properties/launchSettings.json b/Client.Wasm/Properties/launchSettings.json index 0d824ea7..47b57f1b 100644 --- a/Client.Wasm/Properties/launchSettings.json +++ b/Client.Wasm/Properties/launchSettings.json @@ -12,7 +12,7 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5127", "environmentVariables": { @@ -22,7 +22,7 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:7282;http://localhost:5127", "environmentVariables": { diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index 0fc425de..22359132 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -5,11 +5,11 @@ VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.AppHost", "Employee\Employee.AppHost\Employee.AppHost.csproj", "{8575D1CE-B605-4372-A759-BA1598A8C6F9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.ApiService", "Employee.ApiService\Employee.ApiService.csproj", "{3642B4FB-560F-150E-907B-C9C514725E87}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.ServiceDefaults", "Employee\Employee.ServiceDefaults\Employee.ServiceDefaults.csproj", "{6194C225-7198-D470-1A5E-025DA0008A3E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.AppHost", "Employee.AppHost\Employee.AppHost.csproj", "{BEB73446-37AA-8D05-2D56-A35572F27A3B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.ApiService", "Employee\Employee.ApiService\Employee.ApiService.csproj", "{EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.ServiceDefaults", "Employee.ServiceDefaults\Employee.ServiceDefaults.csproj", "{D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,18 +21,18 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU - {8575D1CE-B605-4372-A759-BA1598A8C6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8575D1CE-B605-4372-A759-BA1598A8C6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8575D1CE-B605-4372-A759-BA1598A8C6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8575D1CE-B605-4372-A759-BA1598A8C6F9}.Release|Any CPU.Build.0 = Release|Any CPU - {6194C225-7198-D470-1A5E-025DA0008A3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6194C225-7198-D470-1A5E-025DA0008A3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6194C225-7198-D470-1A5E-025DA0008A3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6194C225-7198-D470-1A5E-025DA0008A3E}.Release|Any CPU.Build.0 = Release|Any CPU - {EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EBEFA5C3-3225-5108-6105-B7C3D2FA6DC2}.Release|Any CPU.Build.0 = Release|Any CPU + {3642B4FB-560F-150E-907B-C9C514725E87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3642B4FB-560F-150E-907B-C9C514725E87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3642B4FB-560F-150E-907B-C9C514725E87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3642B4FB-560F-150E-907B-C9C514725E87}.Release|Any CPU.Build.0 = Release|Any CPU + {BEB73446-37AA-8D05-2D56-A35572F27A3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEB73446-37AA-8D05-2D56-A35572F27A3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEB73446-37AA-8D05-2D56-A35572F27A3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEB73446-37AA-8D05-2D56-A35572F27A3B}.Release|Any CPU.Build.0 = Release|Any CPU + {D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Employee/Employee.ApiService/Employee.ApiService.csproj b/Employee.ApiService/Employee.ApiService.csproj similarity index 91% rename from Employee/Employee.ApiService/Employee.ApiService.csproj rename to Employee.ApiService/Employee.ApiService.csproj index 7c4359fb..0c46690f 100644 --- a/Employee/Employee.ApiService/Employee.ApiService.csproj +++ b/Employee.ApiService/Employee.ApiService.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true diff --git a/Employee/Employee.ApiService/Models/EmployeeModel.cs b/Employee.ApiService/Models/EmployeeModel.cs similarity index 100% rename from Employee/Employee.ApiService/Models/EmployeeModel.cs rename to Employee.ApiService/Models/EmployeeModel.cs diff --git a/Employee/Employee.ApiService/Program.cs b/Employee.ApiService/Program.cs similarity index 65% rename from Employee/Employee.ApiService/Program.cs rename to Employee.ApiService/Program.cs index d4e83ec8..19c6fe1c 100644 --- a/Employee/Employee.ApiService/Program.cs +++ b/Employee.ApiService/Program.cs @@ -1,3 +1,4 @@ +using Employee.ApiService.Models; using Employee.ApiService.Services; using Employee.ServiceDefaults; @@ -7,7 +8,13 @@ builder.AddRedisDistributedCache("redis"); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + + options.IncludeXmlComments(xmlPath); +}); builder.Services.AddCors(options => { @@ -33,11 +40,14 @@ app.MapDefaultEndpoints(); app.UseHttpsRedirection(); app.UseCors("wasm"); - +app.UseRouting(); app.MapGet("/api/employee", async (int id, EmployeeService service) => { var employee = await service.GetEmployeeAsync(id); return Results.Ok(employee); -}); +}) +.WithSummary(" ") +.WithDescription(" id") +.Produces(StatusCodes.Status200OK); app.Run(); \ No newline at end of file diff --git a/Employee/Employee.ApiService/Properties/launchSettings.json b/Employee.ApiService/Properties/launchSettings.json similarity index 100% rename from Employee/Employee.ApiService/Properties/launchSettings.json rename to Employee.ApiService/Properties/launchSettings.json diff --git a/Employee/Employee.ApiService/Services/EmployeeGenerator.cs b/Employee.ApiService/Services/EmployeeGenerator.cs similarity index 70% rename from Employee/Employee.ApiService/Services/EmployeeGenerator.cs rename to Employee.ApiService/Services/EmployeeGenerator.cs index 12243f1d..2765a5d9 100644 --- a/Employee/Employee.ApiService/Services/EmployeeGenerator.cs +++ b/Employee.ApiService/Services/EmployeeGenerator.cs @@ -37,25 +37,6 @@ public class EmployeeGenerator /// private const decimal BaseSalary = 100000m; - /// - /// Генерация должности - /// - private static string GeneratePosition(Faker f) - { - var level = f.PickRandom(_positionLevels.Keys.ToArray()); - var profession = f.PickRandom(_professions); - - return $"{level} {profession}"; - } - - /// - /// Генерация даты приема - /// - private static DateOnly GenerateAdmissionDate(Faker f) - { - return DateOnly.FromDateTime(f.Date.Past(10)); - } - /// /// Генерация зарплаты с учетом коэффициента уровня /// @@ -77,20 +58,6 @@ private static decimal GenerateSalary(Faker f, string position) return Math.Round(salary, 2); } - /// - /// Генерация даты увольнения - /// - private static DateOnly? GenerateDismissalDate(Faker f, EmployeeModel employee) - { - if (!employee.DismissalIndicator) - return null; - - var start = employee.DateAdmission.ToDateTime(TimeOnly.MinValue); - - var dismissal = f.Date.Between(start, DateTime.Now); - - return DateOnly.FromDateTime(dismissal); - } /// /// Генерация ФИО /// @@ -116,14 +83,23 @@ private static string GenerateFullName(Faker f) /// private static readonly Faker _faker = new Faker("ru") .RuleFor(e => e.Name, f => GenerateFullName(f)) - .RuleFor(e => e.Position, f => GeneratePosition(f)) + .RuleFor(e => e.Position, f => + { + var level = f.PickRandom(_positionLevels.Keys.ToArray()); + var profession = f.PickRandom(_professions); + return $"{level} {profession}"; + }) .RuleFor(e => e.Department, f => f.Commerce.Department()) - .RuleFor(e => e.DateAdmission, f => GenerateAdmissionDate(f)) + .RuleFor(e => e.DateAdmission, f => f.Date.PastDateOnly(10)) .RuleFor(e => e.Salary, (f, e) => GenerateSalary(f, e.Position)) - .RuleFor(e => e.Email, (f, e) => f.Internet.Email()) + .RuleFor(e => e.Email, f => f.Internet.Email()) .RuleFor(e => e.Phone, f => f.Phone.PhoneNumber("+7(###)###-##-##")) .RuleFor(e => e.DismissalIndicator, f => f.Random.Bool(0.2f)) - .RuleFor(e => e.DateDismissal, (f, e) => GenerateDismissalDate(f, e)); + .RuleFor(e => e.DateDismissal, (f, e) => + e.DismissalIndicator + ? f.Date.BetweenDateOnly(e.DateAdmission, DateOnly.FromDateTime(DateTime.Now)) + : null + ); /// /// Генерация сотрудника diff --git a/Employee/Employee.ApiService/Services/EmployeeService.cs b/Employee.ApiService/Services/EmployeeService.cs similarity index 89% rename from Employee/Employee.ApiService/Services/EmployeeService.cs rename to Employee.ApiService/Services/EmployeeService.cs index cc6cd4cc..129fbc3c 100644 --- a/Employee/Employee.ApiService/Services/EmployeeService.cs +++ b/Employee.ApiService/Services/EmployeeService.cs @@ -17,6 +17,11 @@ public class EmployeeService( ILogger _logger, EmployeeGenerator _generator) { + /// + /// Время жизни записи в кэше + /// + private readonly TimeSpan _cacheExpiration = + TimeSpan.FromMinutes(_configuration.GetValue("CacheSettings:ExpirationMinutes", 5)); /// /// Получение сотрудника по id @@ -57,11 +62,9 @@ public async Task GetEmployeeAsync(int id) try { - var expirationMinutes = _configuration.GetValue("CacheSettings:ExpirationMinutes", 5); - var cacheOptions = new DistributedCacheEntryOptions { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(expirationMinutes) + AbsoluteExpirationRelativeToNow = _cacheExpiration }; await _cache.SetStringAsync( diff --git a/Employee/Employee.ApiService/appsettings.Development.json b/Employee.ApiService/appsettings.Development.json similarity index 100% rename from Employee/Employee.ApiService/appsettings.Development.json rename to Employee.ApiService/appsettings.Development.json diff --git a/Employee/Employee.ApiService/appsettings.json b/Employee.ApiService/appsettings.json similarity index 100% rename from Employee/Employee.ApiService/appsettings.json rename to Employee.ApiService/appsettings.json diff --git a/Employee/Employee.AppHost/AppHost.cs b/Employee.AppHost/AppHost.cs similarity index 81% rename from Employee/Employee.AppHost/AppHost.cs rename to Employee.AppHost/AppHost.cs index 98c208eb..4eca0004 100644 --- a/Employee/Employee.AppHost/AppHost.cs +++ b/Employee.AppHost/AppHost.cs @@ -1,9 +1,12 @@ var builder = DistributedApplication.CreateBuilder(args); -var redis = builder.AddRedis("redis"); +var redis = builder + .AddRedis("redis") + .WithRedisCommander(); var apiService = builder.AddProject("apiservice") .WithReference(redis) + .WaitFor(redis) .WithHttpHealthCheck("/health"); builder.AddProject("webfrontend") diff --git a/Employee/Employee.AppHost/Employee.AppHost.csproj b/Employee.AppHost/Employee.AppHost.csproj similarity index 90% rename from Employee/Employee.AppHost/Employee.AppHost.csproj rename to Employee.AppHost/Employee.AppHost.csproj index bafe08b8..fef672ad 100644 --- a/Employee/Employee.AppHost/Employee.AppHost.csproj +++ b/Employee.AppHost/Employee.AppHost.csproj @@ -11,7 +11,7 @@ - + diff --git a/Employee/Employee.AppHost/Properties/launchSettings.json b/Employee.AppHost/Properties/launchSettings.json similarity index 100% rename from Employee/Employee.AppHost/Properties/launchSettings.json rename to Employee.AppHost/Properties/launchSettings.json diff --git a/Employee/Employee.AppHost/appsettings.Development.json b/Employee.AppHost/appsettings.Development.json similarity index 100% rename from Employee/Employee.AppHost/appsettings.Development.json rename to Employee.AppHost/appsettings.Development.json diff --git a/Employee/Employee.AppHost/appsettings.json b/Employee.AppHost/appsettings.json similarity index 100% rename from Employee/Employee.AppHost/appsettings.json rename to Employee.AppHost/appsettings.json diff --git a/Employee/Employee.ServiceDefaults/Employee.ServiceDefaults.csproj b/Employee.ServiceDefaults/Employee.ServiceDefaults.csproj similarity index 100% rename from Employee/Employee.ServiceDefaults/Employee.ServiceDefaults.csproj rename to Employee.ServiceDefaults/Employee.ServiceDefaults.csproj diff --git a/Employee/Employee.ServiceDefaults/Extensions.cs b/Employee.ServiceDefaults/Extensions.cs similarity index 100% rename from Employee/Employee.ServiceDefaults/Extensions.cs rename to Employee.ServiceDefaults/Extensions.cs diff --git a/Employee/Employee.ApiService/Employee.ApiService.http b/Employee/Employee.ApiService/Employee.ApiService.http deleted file mode 100644 index 4aa8536f..00000000 --- a/Employee/Employee.ApiService/Employee.ApiService.http +++ /dev/null @@ -1,6 +0,0 @@ -@ApiService_HostAddress = http://localhost:5547 - -GET {{ApiService_HostAddress}}/weatherforecast/ -Accept: application/json - -### \ No newline at end of file diff --git a/README.md b/README.md index dcaa5eb7..ce9355ad 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,29 @@ -# Современные технологии разработки программного обеспечения -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1an43o-iqlq4V_kDtkr_y7DC221hY9qdhGPrpII27sH8/edit?usp=sharing) +## Описание проекта -## Задание -### Цель -Реализация проекта микросервисного бекенда. +Проект представляет собой сервис для получения информации о сотрудниках с использованием кэширования Redis. -### Задачи -* Реализация межсервисной коммуникации, -* Изучение работы с брокерами сообщений, -* Изучение архитектурных паттернов, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Интеграционное тестирование. +## Архитектура проекта -### Лабораторные работы -
-1. «Кэширование» - Реализация сервиса генерации контрактов, кэширование его ответов -
- -В рамках первой лабораторной работы необходимо: -* Реализовать сервис генерации контрактов на основе Bogus, -* Реализовать кеширование при помощи IDistributedCache и Redis, -* Реализовать структурное логирование сервиса генерации, -* Настроить оркестрацию Aspire. - -
-
-2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы -
- -В рамках второй лабораторной работы необходимо: -* Настроить оркестрацию на запуск нескольких реплик сервиса генерации, -* Реализовать апи гейтвей на основе Ocelot, -* Имплементировать алгоритм балансировки нагрузки согласно варианту. +Решение состоит из нескольких проектов: -
-
-
-3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда -
+- **Employee.ApiService** – Web API сервис +- **Employee.AppHost** – Aspire orchestrator +- **Employee.ServiceDefaults** – общие настройки сервисов +- **Client.Wasm** – клиент -В рамках третьей лабораторной работы необходимо: -* Добавить в оркестрацию объектное хранилище, -* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище, -* Реализовать отправку генерируемых данных в файловый сервис посредством брокера, -* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе. +## Основная логика работы -
-
-
-4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud -
- -В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы: -* Клиент - в хостинг через отдельный бакет Object Storage, -* Сервис генерации - в Cloud Function, -* Апи гейтвей - в Serverless Integration как API Gateway, -* Брокер сообщений - в Message Queue, -* Файловый сервис - в Cloud Function, -* Объектное хранилище - в отдельный бакет Object Storage, +1. Клиент отправляет запрос на получение сотрудника по `id`. +2. Сервис проверяет наличие данных в Redis. +3. Если данные есть в кэше, то они возвращаются из Redis. +4. Если данных нет, то сотрудник генерируется с помощью Bogus. -
-
- -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.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 диаграмма -Современные_технологии_разработки_ПО_drawio -
- -## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1WGmLYwffTTaAj4TgFCk5bUyW3XKbFMiBm-DHZrfFWr4/edit?usp=sharing) -[Список предметных областей и алгоритмов балансировки](https://docs.google.com/document/d/1PLn2lKe4swIdJDZhwBYzxqFSu0AbY2MFY1SUPkIKOM4/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. -Не укладываетесь в дедлайн - получаете минимально возможный балл. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соответствующим разделом дискуссий](https://github.com/itsecd/cloud-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/cloud-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/cloud-development/discussions/categories/ideas). +## Запуск проекта +1. Запустить проект **Employee.AppHost**. +2. Aspire Dashboard откроется автоматически. +3. В Dashboard будут запущены: + - Redis + - RedisInsight + - ApiService + - WebFrontend From 47545413efeffb4f6bf32787521913219ffdf8a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BB=D1=8F=D0=BA=D0=BE=D0=B2=D0=B0=20=D0=92?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=BD=D0=B8=D0=BA=D0=B0?= <113890061+Cat-sandwich@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:17:34 +0400 Subject: [PATCH 3/6] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ce9355ad..ffa1811f 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,8 @@ - RedisInsight - ApiService - WebFrontend + +## Пример работы приложения +image +image +image From 0c10853df4e25ebe60cf1227007bafaf0a752f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BB=D1=8F=D0=BA=D0=BE=D0=B2=D0=B0=20=D0=92?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=BD=D0=B8=D0=BA=D0=B0?= <113890061+Cat-sandwich@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:47:17 +0400 Subject: [PATCH 4/6] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Employee.ApiService/Program.cs | 1 - .../Services/EmployeeGenerator.cs | 4 +-- .../Services/EmployeeService.cs | 36 +++++++++---------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/Employee.ApiService/Program.cs b/Employee.ApiService/Program.cs index 19c6fe1c..5c96b82c 100644 --- a/Employee.ApiService/Program.cs +++ b/Employee.ApiService/Program.cs @@ -26,7 +26,6 @@ }); }); -builder.Services.AddSingleton(); builder.Services.AddScoped(); var app = builder.Build(); diff --git a/Employee.ApiService/Services/EmployeeGenerator.cs b/Employee.ApiService/Services/EmployeeGenerator.cs index 2765a5d9..364964bf 100644 --- a/Employee.ApiService/Services/EmployeeGenerator.cs +++ b/Employee.ApiService/Services/EmployeeGenerator.cs @@ -6,7 +6,7 @@ namespace Employee.ApiService.Services; /// /// Генератор тестовых сотрудников /// -public class EmployeeGenerator +public static class EmployeeGenerator { /// /// Справочник профессий @@ -104,7 +104,7 @@ private static string GenerateFullName(Faker f) /// /// Генерация сотрудника /// - public EmployeeModel Generate(int id) + public static EmployeeModel Generate(int id) { var employee = _faker.Generate(); employee.Id = id; diff --git a/Employee.ApiService/Services/EmployeeService.cs b/Employee.ApiService/Services/EmployeeService.cs index 129fbc3c..ebaa2d77 100644 --- a/Employee.ApiService/Services/EmployeeService.cs +++ b/Employee.ApiService/Services/EmployeeService.cs @@ -7,21 +7,19 @@ namespace Employee.ApiService.Services; /// /// Сервис получения сотрудников /// -/// кэш -/// конфигурация -/// логирование -/// генератор +/// кэш +/// конфигурация +/// логирование public class EmployeeService( - IDistributedCache _cache, - IConfiguration _configuration, - ILogger _logger, - EmployeeGenerator _generator) + IDistributedCache cache, + IConfiguration configuration, + ILogger logger) { /// /// Время жизни записи в кэше /// private readonly TimeSpan _cacheExpiration = - TimeSpan.FromMinutes(_configuration.GetValue("CacheSettings:ExpirationMinutes", 5)); + TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationMinutes", 5)); /// /// Получение сотрудника по id @@ -32,9 +30,9 @@ public async Task GetEmployeeAsync(int id) { var cacheKey = $"employee:{id}"; - _logger.LogInformation("Попытка получить сотрудника {EmployeeId} из кэша", id); + logger.LogInformation("Попытка получить сотрудника {EmployeeId} из кэша", id); - var cachedData = await _cache.GetStringAsync(cacheKey); + var cachedData = await cache.GetStringAsync(cacheKey); if (!string.IsNullOrEmpty(cachedData)) { @@ -44,21 +42,21 @@ public async Task GetEmployeeAsync(int id) if (cachedEmployee != null) { - _logger.LogInformation("Сотрудник {EmployeeId} получен из кэша", id); + logger.LogInformation("Сотрудник {EmployeeId} получен из кэша", id); return cachedEmployee; } - _logger.LogWarning("Сотрудник {EmployeeId} найден в кэше, но десериализация вернула null", id); + logger.LogWarning("Сотрудник {EmployeeId} найден в кэше, но десериализация вернула null", id); } catch (Exception ex) { - _logger.LogError(ex, "Ошибка десериализации сотрудника {EmployeeId}", id); + logger.LogError(ex, "Ошибка десериализации сотрудника {EmployeeId}", id); } } - _logger.LogInformation("Сотрудник {EmployeeId} отсутствует в кэше. Генерация нового", id); + logger.LogInformation("Сотрудник {EmployeeId} отсутствует в кэше. Генерация нового", id); - var employee = _generator.Generate(id); + var employee = EmployeeGenerator.Generate(id); try { @@ -67,17 +65,17 @@ public async Task GetEmployeeAsync(int id) AbsoluteExpirationRelativeToNow = _cacheExpiration }; - await _cache.SetStringAsync( + await cache.SetStringAsync( cacheKey, JsonSerializer.Serialize(employee), cacheOptions ); - _logger.LogInformation("Сотрудник {EmployeeId} сохранён в кэш", id); + logger.LogInformation("Сотрудник {EmployeeId} сохранён в кэш", id); } catch (Exception ex) { - _logger.LogWarning(ex, "Не удалось сохранить сотрудника {EmployeeId} в кэш", id); + logger.LogWarning(ex, "Не удалось сохранить сотрудника {EmployeeId} в кэш", id); } return employee; From 15ea719236f05e5df773940113aa53f8e068941d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BB=D1=8F=D0=BA=D0=BE=D0=B2=D0=B0=20=D0=92?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=BD=D0=B8=D0=BA=D0=B0?= <113890061+Cat-sandwich@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:07:25 +0400 Subject: [PATCH 5/6] =?UTF-8?q?2=20=D0=BB=D1=80=20-=20=D0=B2=D1=8B=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B0=20=D0=B1=D0=B0=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B0=D0=BB?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=D0=BE=D0=BC=20Query=20Base?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client.Wasm/wwwroot/appsettings.json | 2 +- CloudDevelopment.sln | 6 ++ .../Employee.ApiGateway.csproj | 17 +++++ .../LoadBalancer/QueryBasedLoadBalanser.cs | 69 +++++++++++++++++++ Employee.ApiGateway/Program.cs | 44 ++++++++++++ .../Properties/launchSettings.json | 38 ++++++++++ .../appsettings.Development.json | 11 +++ Employee.ApiGateway/appsettings.json | 9 +++ Employee.ApiGateway/ocelot.json | 28 ++++++++ .../Properties/launchSettings.json | 4 +- .../Services/EmployeeService.cs | 14 ++-- Employee.AppHost/AppHost.cs | 30 ++++++-- Employee.AppHost/Employee.AppHost.csproj | 1 + .../Properties/launchSettings.json | 14 +--- 14 files changed, 258 insertions(+), 29 deletions(-) create mode 100644 Employee.ApiGateway/Employee.ApiGateway.csproj create mode 100644 Employee.ApiGateway/LoadBalancer/QueryBasedLoadBalanser.cs create mode 100644 Employee.ApiGateway/Program.cs create mode 100644 Employee.ApiGateway/Properties/launchSettings.json create mode 100644 Employee.ApiGateway/appsettings.Development.json create mode 100644 Employee.ApiGateway/appsettings.json create mode 100644 Employee.ApiGateway/ocelot.json diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index e852c9bb..da1bc60c 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "https://localhost:7491/api/employee" + "BaseAddress": "http://localhost:5200/api/employee" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index 22359132..20cd1d62 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.AppHost", "Employe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.ServiceDefaults", "Employee.ServiceDefaults\Employee.ServiceDefaults.csproj", "{D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Employee.ApiGateway", "Employee.ApiGateway\Employee.ApiGateway.csproj", "{08732133-D1F2-00C9-671B-4197752A18B0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D85BA7A1-4E0F-9EEE-41E8-7A478C12984D}.Release|Any CPU.Build.0 = Release|Any CPU + {08732133-D1F2-00C9-671B-4197752A18B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08732133-D1F2-00C9-671B-4197752A18B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08732133-D1F2-00C9-671B-4197752A18B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08732133-D1F2-00C9-671B-4197752A18B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Employee.ApiGateway/Employee.ApiGateway.csproj b/Employee.ApiGateway/Employee.ApiGateway.csproj new file mode 100644 index 00000000..172916a0 --- /dev/null +++ b/Employee.ApiGateway/Employee.ApiGateway.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Employee.ApiGateway/LoadBalancer/QueryBasedLoadBalanser.cs b/Employee.ApiGateway/LoadBalancer/QueryBasedLoadBalanser.cs new file mode 100644 index 00000000..f621aef3 --- /dev/null +++ b/Employee.ApiGateway/LoadBalancer/QueryBasedLoadBalanser.cs @@ -0,0 +1,69 @@ +using Ocelot.Values; +using Ocelot.Responses; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.ServiceDiscovery.Providers; + +namespace Employee.ApiGateway.LoadBalancer; + +/// +/// Балансировщик нагрузки, выбирающий реплику по значению параметра "id" +/// +/// Провайдер для получения списка доступных сервисов +public class QueryBasedLoadBalancer(IServiceDiscoveryProvider serviceDiscovery) : ILoadBalancer +{ + private const string IdQuery = "id"; + + public string Type => nameof(QueryBasedLoadBalancer); + + /// + /// Функция выбора сервиса по параметру "id" + /// + /// Контекст HTTP-запроса + /// Адрес выбранного сервиса или ошибка + public async Task> LeaseAsync(HttpContext httpContext) + { + var services = await serviceDiscovery.GetAsync(); + + if (services is null || services.Count == 0) + { + return new ErrorResponse( + new ServicesAreNullError("Нет доступных сервисов")); + } + + var idResult = TryGetValidId(httpContext.Request.Query); + + if (!idResult.IsSuccess) + { + return new ErrorResponse( + new UnableToFindLoadBalancerError(idResult.ErrorMessage)); + } + + var id = idResult.Value; + + var index = id % services.Count; + var selected = services[index]; + + return new OkResponse(selected.HostAndPort); + } + + /// + /// Функция проверки параметра запроса + /// + /// Запрос + /// Значение id и сообщение об ошибке + private static (bool IsSuccess, int Value, string ErrorMessage) TryGetValidId(IQueryCollection query) + { + if (!query.TryGetValue(IdQuery, out var idValues) || string.IsNullOrWhiteSpace(idValues)) + return (false, 0, "Отсутствует или пустой параметр 'id'"); + + if (!int.TryParse(idValues.First(), out var id)) + return (false, 0, "Параметр 'id' должен быть числом"); + + if (id < 0) + return (false, 0, "Параметр 'id' не может быть отрицательным"); + + return (true, id, string.Empty); + } + public void Release(ServiceHostAndPort hostAndPort) { } +} \ No newline at end of file diff --git a/Employee.ApiGateway/Program.cs b/Employee.ApiGateway/Program.cs new file mode 100644 index 00000000..84b93062 --- /dev/null +++ b/Employee.ApiGateway/Program.cs @@ -0,0 +1,44 @@ +using Employee.ApiGateway.LoadBalancer; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); + +var generators = builder.Configuration.GetSection("Generators").Get() ?? []; + +var overrides = new List>(); + +for (var i = 0; i < generators.Length; i++) +{ + var serviceName = generators[i]; + var url = builder.Configuration[$"services:{serviceName}:http:0"]; + + if (string.IsNullOrWhiteSpace(url)) + continue; + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + continue; + + overrides.Add(new KeyValuePair( + $"Routes:0:DownstreamHostAndPorts:{i}:Host", uri.Host)); + + overrides.Add(new KeyValuePair( + $"Routes:0:DownstreamHostAndPorts:{i}:Port", uri.Port.ToString())); +} + +if (overrides.Any()) +{ + builder.Configuration.AddInMemoryCollection(overrides); +} + +builder.Services + .AddOcelot(builder.Configuration) + .AddCustomLoadBalancer((route, sp) => + new QueryBasedLoadBalancer(sp)); + +var app = builder.Build(); + +await app.UseOcelot(); +await app.RunAsync(); \ No newline at end of file diff --git a/Employee.ApiGateway/Properties/launchSettings.json b/Employee.ApiGateway/Properties/launchSettings.json new file mode 100644 index 00000000..1c654edc --- /dev/null +++ b/Employee.ApiGateway/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50608", + "sslPort": 44382 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5200", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7041;http://localhost:5200", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Employee.ApiGateway/appsettings.Development.json b/Employee.ApiGateway/appsettings.Development.json new file mode 100644 index 00000000..32dc94eb --- /dev/null +++ b/Employee.ApiGateway/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "GlobalConfiguration": { + "BaseUrl": "http://localhost:5200" + } +} diff --git a/Employee.ApiGateway/appsettings.json b/Employee.ApiGateway/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Employee.ApiGateway/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Employee.ApiGateway/ocelot.json b/Employee.ApiGateway/ocelot.json new file mode 100644 index 00000000..c212a905 --- /dev/null +++ b/Employee.ApiGateway/ocelot.json @@ -0,0 +1,28 @@ +{ + "Generators": [ "generator-1", "generator-2", "generator-3" ], + "Routes": [ + { + "DownstreamPathTemplate": "/api/employee", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5201 + }, + { + "Host": "localhost", + "Port": 5202 + }, + { + "Host": "localhost", + "Port": 5203 + } + ], + "UpstreamPathTemplate": "/api/employee", + "UpstreamHttpMethod": [ "GET" ], + "LoadBalancerOptions": { + "Type": "QueryBasedLoadBalancer" + } + } + ] +} \ No newline at end of file diff --git a/Employee.ApiService/Properties/launchSettings.json b/Employee.ApiService/Properties/launchSettings.json index 1262a7ef..09b1f87b 100644 --- a/Employee.ApiService/Properties/launchSettings.json +++ b/Employee.ApiService/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5547", + "applicationUrl": "", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -14,7 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7491;http://localhost:5547", + "applicationUrl": "", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Employee.ApiService/Services/EmployeeService.cs b/Employee.ApiService/Services/EmployeeService.cs index ebaa2d77..f153f7a0 100644 --- a/Employee.ApiService/Services/EmployeeService.cs +++ b/Employee.ApiService/Services/EmployeeService.cs @@ -30,7 +30,7 @@ public async Task GetEmployeeAsync(int id) { var cacheKey = $"employee:{id}"; - logger.LogInformation("Попытка получить сотрудника {EmployeeId} из кэша", id); + logger.LogInformation("Attempting to retrieve employee {EmployeeId} from cache", id); var cachedData = await cache.GetStringAsync(cacheKey); @@ -42,19 +42,19 @@ public async Task GetEmployeeAsync(int id) if (cachedEmployee != null) { - logger.LogInformation("Сотрудник {EmployeeId} получен из кэша", id); + logger.LogInformation("Employee {EmployeeId} retrieved from cache", id); return cachedEmployee; } - logger.LogWarning("Сотрудник {EmployeeId} найден в кэше, но десериализация вернула null", id); + logger.LogWarning("Employee {EmployeeId} found in cache, but deserialization returned null", id); } catch (Exception ex) { - logger.LogError(ex, "Ошибка десериализации сотрудника {EmployeeId}", id); + logger.LogError(ex, "Error deserializing employee {EmployeeId}", id); } } - logger.LogInformation("Сотрудник {EmployeeId} отсутствует в кэше. Генерация нового", id); + logger.LogInformation("Employee {EmployeeId} not found in cache. Generating new one", id); var employee = EmployeeGenerator.Generate(id); @@ -71,11 +71,11 @@ await cache.SetStringAsync( cacheOptions ); - logger.LogInformation("Сотрудник {EmployeeId} сохранён в кэш", id); + logger.LogInformation("Employee {EmployeeId} saved to cache", id); } catch (Exception ex) { - logger.LogWarning(ex, "Не удалось сохранить сотрудника {EmployeeId} в кэш", id); + logger.LogWarning(ex, "Failed to save employee {EmployeeId} to cache", id); } return employee; diff --git a/Employee.AppHost/AppHost.cs b/Employee.AppHost/AppHost.cs index 4eca0004..2b44ed34 100644 --- a/Employee.AppHost/AppHost.cs +++ b/Employee.AppHost/AppHost.cs @@ -4,15 +4,31 @@ .AddRedis("redis") .WithRedisCommander(); -var apiService = builder.AddProject("apiservice") - .WithReference(redis) - .WaitFor(redis) - .WithHttpHealthCheck("/health"); +var generators = new List>(); + +for (var i = 1; i <= 3; i++) +{ + var generator = builder + .AddProject($"generator-{i}") + .WithReference(redis) + .WithHttpEndpoint(name: $"http{i}", port: 5200 + i); + + generators.Add(generator); +} + +var apiGateway = builder.AddProject("employee-apigateway") + .WithHttpEndpoint(name: "gateway", port: 5200); + +foreach (var generator in generators) +{ + apiGateway + .WithReference(generator) + .WaitFor(generator); +} builder.AddProject("webfrontend") .WithExternalHttpEndpoints() - .WithHttpHealthCheck("/health") - .WithReference(apiService) - .WaitFor(apiService); + .WithReference(apiGateway) + .WaitFor(apiGateway); builder.Build().Run(); \ No newline at end of file diff --git a/Employee.AppHost/Employee.AppHost.csproj b/Employee.AppHost/Employee.AppHost.csproj index fef672ad..93b27a05 100644 --- a/Employee.AppHost/Employee.AppHost.csproj +++ b/Employee.AppHost/Employee.AppHost.csproj @@ -12,6 +12,7 @@ + diff --git a/Employee.AppHost/Properties/launchSettings.json b/Employee.AppHost/Properties/launchSettings.json index fc84e215..1de2e31b 100644 --- a/Employee.AppHost/Properties/launchSettings.json +++ b/Employee.AppHost/Properties/launchSettings.json @@ -1,18 +1,7 @@ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:17278;http://localhost:15137", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21242", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22095" - } - }, + "http": { "commandName": "Project", "dotnetRunMessages": true, @@ -21,6 +10,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19281", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20030" } From 63577f749ea5e77d40679a087f345688c3b00f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BB=D1=8F=D0=BA=D0=BE=D0=B2=D0=B0=20=D0=92?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=BD=D0=B8=D0=BA=D0=B0?= <113890061+Cat-sandwich@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:15:30 +0400 Subject: [PATCH 6/6] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=80=D0=B8=D0=B4=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=20=D0=B2=D1=82=D0=BE=D1=80=D1=83=D1=8E=20?= =?UTF-8?q?=D0=BB=D0=B0=D0=B1=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ffa1811f..8c5047e6 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,34 @@ ## Описание проекта -Проект представляет собой сервис для получения информации о сотрудниках с использованием кэширования Redis. +Проект представляет собой распределённую систему для получения информации о сотрудниках с использованием кэширования Redis и балансировки нагрузки по алгоритму Query Based. ## Архитектура проекта Решение состоит из нескольких проектов: - **Employee.ApiService** – Web API сервис +- **Employee.ApiGateway** – API Gateway на базе Ocelot - **Employee.AppHost** – Aspire orchestrator - **Employee.ServiceDefaults** – общие настройки сервисов - **Client.Wasm** – клиент ## Основная логика работы -1. Клиент отправляет запрос на получение сотрудника по `id`. -2. Сервис проверяет наличие данных в Redis. -3. Если данные есть в кэше, то они возвращаются из Redis. -4. Если данных нет, то сотрудник генерируется с помощью Bogus. +1. Клиент отправляет запрос в API Gateway (`/api/employee?id={id}`). +2. API Gateway (Ocelot) принимает запрос и передаёт его в один из сервисов генерации. +3. Выбор сервиса осуществляется с помощью кастомного балансировщика `QueryBasedLoadBalancer`. +4. Сервис: + - проверяет наличие данных в Redis, + - если данные есть — возвращает их из кэша, + - если нет — генерирует нового сотрудника и сохраняет его в кэш. + +## Оркестрация сервисов + +С помощью Aspire настроен запуск нескольких реплик сервиса генерации: + +- generator-1 → http://localhost:5201 +- generator-2 → http://localhost:5202 +- generator-3 → http://localhost:5203 ## Запуск проекта @@ -24,11 +36,13 @@ 2. Aspire Dashboard откроется автоматически. 3. В Dashboard будут запущены: - Redis - - RedisInsight - - ApiService + - Redis Commander + - 3 реплики Employee.ApiService + - API Gateway - WebFrontend - + - ## Пример работы приложения -image -image -image +image +image + +