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/3] =?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 661f118..3df3c2c 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 d1fe7ab..e852c9b 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 cb48241..0fc425d 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 0000000..7c4359f --- /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 0000000..4aa8536 --- /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 0000000..acf8a51 --- /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 0000000..d4e83ec --- /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 0000000..1262a7e --- /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 0000000..12243f1 --- /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 0000000..cc6cd4c --- /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 0000000..0c208ae --- /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 0000000..10f68b8 --- /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 0000000..98c208e --- /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 0000000..bafe08b --- /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 0000000..fc84e21 --- /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 0000000..0c208ae --- /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 0000000..31c092a --- /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 0000000..1b6e209 --- /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 0000000..cc077fb --- /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/3] =?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 0d824ea..47b57f1 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 0fc425d..2235913 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 7c4359f..0c46690 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 d4e83ec..19c6fe1 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 12243f1..2765a5d 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 cc6cd4c..129fbc3 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 98c208e..4eca000 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 bafe08b..fef672a 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 4aa8536..0000000 --- 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 dcaa5eb..ce9355a 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/3] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ce9355a..ffa1811 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,8 @@ - RedisInsight - ApiService - WebFrontend + +## Пример работы приложения +image +image +image