diff --git a/AppHost/AppHost.csproj b/AppHost/AppHost.csproj new file mode 100644 index 00000000..22b84afa --- /dev/null +++ b/AppHost/AppHost.csproj @@ -0,0 +1,23 @@ + + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AppHost/Program.cs b/AppHost/Program.cs new file mode 100644 index 00000000..08a8fe6b --- /dev/null +++ b/AppHost/Program.cs @@ -0,0 +1,16 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var redis = builder.AddRedis("redis") + .WithRedisInsight(); + +var client = builder.AddProject("client"); + +builder.AddProject("generator-service") + .WithReference(redis) + .WaitFor(redis) + .WithEnvironment("Cors__AllowedOrigin", client.GetEndpoint("http")) + .WaitFor(client) + .WithUrlForEndpoint("http", url => url.Url += "/swagger") + .WithUrlForEndpoint("https", url => url.Url += "/swagger"); + +builder.Build().Run(); \ No newline at end of file diff --git a/AppHost/Properties/launchSettings.json b/AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..6d91260d --- /dev/null +++ b/AppHost/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "AppHost": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17193;http://localhost:15237", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21193", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057" + } + } + } +} \ No newline at end of file diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..a96e557a 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,10 +4,10 @@ - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер № 1 + Вариант № 18 + Выполнена Чумаковым Иваном 6511 + Ссылка на форк diff --git a/Client.Wasm/Properties/launchSettings.json b/Client.Wasm/Properties/launchSettings.json index 0d824ea7..60120ec3 100644 --- a/Client.Wasm/Properties/launchSettings.json +++ b/Client.Wasm/Properties/launchSettings.json @@ -12,7 +12,7 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5127", "environmentVariables": { @@ -22,7 +22,7 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:7282;http://localhost:5127", "environmentVariables": { @@ -31,7 +31,7 @@ }, "IIS Express": { "commandName": "IISExpress", - "launchBrowser": true, + "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..1e5114fa 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -1,10 +1,3 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "http://localhost:3343/patient" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..e7b068d0 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -5,6 +5,14 @@ VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "AppHost\AppHost.csproj", "{B1C2D3E4-F5A6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratorService", "GeneratorService\GeneratorService.csproj", "{139BD442-54A6-9109-CF9A-53DA218D46F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratorService.Tests", "GeneratorService.Tests\GeneratorService.Tests.csproj", "{EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceDefaults", "ServiceDefaults\ServiceDefaults.csproj", "{9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +23,22 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {B1C2D3E4-F5A6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1C2D3E4-F5A6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1C2D3E4-F5A6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1C2D3E4-F5A6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {139BD442-54A6-9109-CF9A-53DA218D46F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {139BD442-54A6-9109-CF9A-53DA218D46F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {139BD442-54A6-9109-CF9A-53DA218D46F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {139BD442-54A6-9109-CF9A-53DA218D46F2}.Release|Any CPU.Build.0 = Release|Any CPU + {EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBCA1829-1CB1-773B-8241-CE00EBFBCF9C}.Release|Any CPU.Build.0 = Release|Any CPU + {9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F0B1437-6E39-4706-8A9D-89BCE4B31AA0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/GeneratorService.Tests/GeneratorService.Tests.csproj b/GeneratorService.Tests/GeneratorService.Tests.csproj new file mode 100644 index 00000000..c9bc33ca --- /dev/null +++ b/GeneratorService.Tests/GeneratorService.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/GeneratorService.Tests/MedicalPatientGeneratorTests.cs b/GeneratorService.Tests/MedicalPatientGeneratorTests.cs new file mode 100644 index 00000000..5daec200 --- /dev/null +++ b/GeneratorService.Tests/MedicalPatientGeneratorTests.cs @@ -0,0 +1,62 @@ +using GeneratorService.Generators; +using Xunit; + +namespace GeneratorService.Tests; + +public sealed class MedicalPatientGeneratorTests +{ + public static readonly DateOnly Today = DateOnly.FromDateTime(DateTime.Today); + + public static IEnumerable Patients() => + Enumerable.Range(1, 300).Select(i => new object[] { MedicalPatientGenerator.Generate(i) }); + + [Theory] + [MemberData(nameof(Patients))] + public void Id_MatchesRequested(GeneratorService.Models.MedicalPatient p) + => Assert.True(p.Id > 0); + + [Theory] + [MemberData(nameof(Patients))] + public void FullName_HasThreeParts(GeneratorService.Models.MedicalPatient p) + => Assert.Equal(3, p.FullName.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length); + + [Theory] + [MemberData(nameof(Patients))] + public void BirthDate_NotInFuture(GeneratorService.Models.MedicalPatient p) + => Assert.True(p.BirthDate <= Today); + + [Theory] + [MemberData(nameof(Patients))] + public void Height_InReasonableBounds(GeneratorService.Models.MedicalPatient p) + => Assert.InRange(p.Height, 50.0, 220.0); + + [Theory] + [MemberData(nameof(Patients))] + public void Height_RoundedToTwoDecimals(GeneratorService.Models.MedicalPatient p) + => Assert.Equal(p.Height, Math.Round(p.Height, 2)); + + [Theory] + [MemberData(nameof(Patients))] + public void Weight_InReasonableBounds(GeneratorService.Models.MedicalPatient p) + => Assert.InRange(p.Weight, 2.5, 200.0); + + [Theory] + [MemberData(nameof(Patients))] + public void Weight_RoundedToTwoDecimals(GeneratorService.Models.MedicalPatient p) + => Assert.Equal(p.Weight, Math.Round(p.Weight, 2)); + + [Theory] + [MemberData(nameof(Patients))] + public void BloodGroup_Between1And4(GeneratorService.Models.MedicalPatient p) + => Assert.InRange(p.BloodGroup, 1, 4); + + [Theory] + [MemberData(nameof(Patients))] + public void LastExamination_NotBeforeBirthDate(GeneratorService.Models.MedicalPatient p) + => Assert.True(p.LastExaminationDate >= p.BirthDate); + + [Theory] + [MemberData(nameof(Patients))] + public void LastExamination_NotInFuture(GeneratorService.Models.MedicalPatient p) + => Assert.True(p.LastExaminationDate <= Today); +} diff --git a/GeneratorService/GeneratorService.csproj b/GeneratorService/GeneratorService.csproj new file mode 100644 index 00000000..676109b5 --- /dev/null +++ b/GeneratorService/GeneratorService.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GeneratorService/Generators/MedicalPatientGenerator.cs b/GeneratorService/Generators/MedicalPatientGenerator.cs new file mode 100644 index 00000000..e5dbc7a6 --- /dev/null +++ b/GeneratorService/Generators/MedicalPatientGenerator.cs @@ -0,0 +1,39 @@ +using Bogus; +using GeneratorService.Models; + +namespace GeneratorService.Generators; + +public static class MedicalPatientGenerator +{ + private static readonly Faker _faker = new Faker("ru") + .RuleFor(p => p.Id, _ => 0) + .RuleFor(p => p.FullName, f => + $"{f.Name.LastName()} {f.Name.FirstName()} {f.Name.LastName()}ович") + .RuleFor(p => p.Address, f => f.Address.FullAddress()) + .RuleFor(p => p.BirthDate, f => f.Date.PastDateOnly(100)) + .RuleFor(p => p.Height, f => Math.Round(f.Random.Double(50.0, 220.0), 2)) + .RuleFor(p => p.Weight, f => Math.Round(f.Random.Double(2.5, 200.0), 2)) + .RuleFor(p => p.BloodGroup, f => f.Random.Int(1, 4)) + .RuleFor(p => p.RhFactor, f => f.Random.Bool()) + .RuleFor(p => p.LastExaminationDate, (f, p) => + f.Date.BetweenDateOnly(p.BirthDate, DateOnly.FromDateTime(DateTime.Today))) + .RuleFor(p => p.IsVaccinated, f => f.Random.Bool()); + + public static MedicalPatient Generate(int id) + { + var generated = _faker.Generate(); + return new MedicalPatient + { + Id = id, + FullName = generated.FullName, + Address = generated.Address, + BirthDate = generated.BirthDate, + Height = generated.Height, + Weight = generated.Weight, + BloodGroup = generated.BloodGroup, + RhFactor = generated.RhFactor, + LastExaminationDate = generated.LastExaminationDate, + IsVaccinated = generated.IsVaccinated + }; + } +} \ No newline at end of file diff --git a/GeneratorService/Models/MedicalPatient.cs b/GeneratorService/Models/MedicalPatient.cs new file mode 100644 index 00000000..156e56fd --- /dev/null +++ b/GeneratorService/Models/MedicalPatient.cs @@ -0,0 +1,37 @@ +namespace GeneratorService.Models; + +/// +/// Медицинская карта пациента. +/// +public sealed class MedicalPatient +{ + /// Уникальный идентификатор пациента. + public int Id { get; init; } + + /// Полное имя пациента (фамилия, имя, отчество). + public required string FullName { get; init; } + + /// Адрес проживания пациента. + public required string Address { get; init; } + + /// Дата рождения пациента. + public DateOnly BirthDate { get; init; } + + /// Рост пациента в сантиметрах. + public double Height { get; init; } + + /// Вес пациента в килограммах. + public double Weight { get; init; } + + /// Группа крови (1–4). + public int BloodGroup { get; init; } + + /// Резус-фактор: true — положительный, false — отрицательный. + public bool RhFactor { get; init; } + + /// Дата последнего медицинского осмотра. + public DateOnly LastExaminationDate { get; init; } + + /// Наличие прививок. + public bool IsVaccinated { get; init; } +} \ No newline at end of file diff --git a/GeneratorService/Program.cs b/GeneratorService/Program.cs new file mode 100644 index 00000000..9940f24c --- /dev/null +++ b/GeneratorService/Program.cs @@ -0,0 +1,87 @@ +using GeneratorService.Models; +using GeneratorService.Services; +using Serilog; +using Serilog.Events; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.FromLogContext() + .Enrich.WithEnvironmentName() + .Enrich.WithThreadId() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] tid={ThreadId} | {Message:lj}{NewLine}{Exception}") + .CreateBootstrapLogger(); + +try +{ + var builder = WebApplication.CreateBuilder(args); + + builder.Host.UseSerilog((ctx, _, cfg) => cfg + .ReadFrom.Configuration(ctx.Configuration) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.FromLogContext() + .Enrich.WithEnvironmentName() + .Enrich.WithThreadId() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] tid={ThreadId} | {Message:lj}{NewLine}{Exception}") + .WriteTo.OpenTelemetry(options => + { + options.Endpoint = ctx.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] ?? "http://localhost:4317"; + options.ResourceAttributes = new Dictionary + { + ["service.name"] = "generator-service" + }; + })); + + builder.AddRedisDistributedCache("redis"); + + builder.Services.AddScoped(); + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(o => + { + o.SwaggerDoc("v1", new() { Title = "GeneratorService — Medical Patient", Version = "v1" }); + + var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + o.IncludeXmlComments(xmlPath); + }); + builder.Services.AddCors(); + + builder.AddServiceDefaults(); + + var app = builder.Build(); + + app.MapDefaultEndpoints(); + + var allowedOrigin = builder.Configuration["Cors:AllowedOrigin"] + ?? throw new InvalidOperationException("Cors:AllowedOrigin is not configured"); + + app.UseSerilogRequestLogging(); + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseCors(policy => policy + .WithOrigins(allowedOrigin) + .AllowAnyMethod() + .AllowAnyHeader()); + + app.MapGet("/patient", async (int id, PatientService svc, CancellationToken ct) => + id <= 0 + ? Results.BadRequest("id must be > 0") + : Results.Ok(await svc.GetAsync(id, ct))) + .WithName("GetPatient") + .WithSummary("Возвращает медицинскую карту пациента по идентификатору") + .Produces() + .ProducesProblem(400); + + app.Logger.LogInformation("CORS AllowedOrigin = {Origin}", + builder.Configuration["Cors:AllowedOrigin"] ?? "NOT SET"); + + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Application terminated unexpectedly"); +} +finally +{ + Log.CloseAndFlush(); +} \ No newline at end of file diff --git a/GeneratorService/Properties/launchSettings.json b/GeneratorService/Properties/launchSettings.json new file mode 100644 index 00000000..53d7af01 --- /dev/null +++ b/GeneratorService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "GeneratorService": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:3342;http://localhost:3343" + } + } +} diff --git a/GeneratorService/Services/PatientService.cs b/GeneratorService/Services/PatientService.cs new file mode 100644 index 00000000..7a789654 --- /dev/null +++ b/GeneratorService/Services/PatientService.cs @@ -0,0 +1,43 @@ +using System.Text.Json; +using GeneratorService.Generators; +using GeneratorService.Models; +using Microsoft.Extensions.Caching.Distributed; + +namespace GeneratorService.Services; + +public sealed class PatientService(IDistributedCache cache, ILogger logger, IConfiguration configuration) +{ + public async Task GetAsync(int id, CancellationToken ct = default) + { + var key = $"patient:{id}"; + + var cached = await cache.GetStringAsync(key, ct); + if (cached is not null) + { + var cachedPatient = JsonSerializer.Deserialize(cached); + if (cachedPatient is not null) + { + logger.LogInformation("Cache HIT | id={Id}", id); + return cachedPatient; + } + } + + logger.LogInformation("Cache MISS | id={Id} — generating", id); + + var patient = MedicalPatientGenerator.Generate(id); + + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes( + configuration.GetValue("CacheSettings:AbsoluteExpirationMinutes")) + }; + + await cache.SetStringAsync(key, JsonSerializer.Serialize(patient), cacheOptions, ct); + + logger.LogInformation( + "Generated | id={Id} Name={FullName} BirthDate={BirthDate} BloodGroup={BloodGroup} RhFactor={RhFactor}", + patient.Id, patient.FullName, patient.BirthDate, patient.BloodGroup, patient.RhFactor); + + return patient; + } +} \ No newline at end of file diff --git a/GeneratorService/appsettings.json b/GeneratorService/appsettings.json new file mode 100644 index 00000000..619a0143 --- /dev/null +++ b/GeneratorService/appsettings.json @@ -0,0 +1,15 @@ +{ + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + } + }, + "CacheSettings": { + "AbsoluteExpirationMinutes": 5 + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/README.md b/README.md index dcaa5eb7..7a3645b7 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,35 @@ -# Современные технологии разработки программного обеспечения -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1an43o-iqlq4V_kDtkr_y7DC221hY9qdhGPrpII27sH8/edit?usp=sharing) - -## Задание -### Цель -Реализация проекта микросервисного бекенда. - -### Задачи -* Реализация межсервисной коммуникации, -* Изучение работы с брокерами сообщений, -* Изучение архитектурных паттернов, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Интеграционное тестирование. - -### Лабораторные работы -
-1. «Кэширование» - Реализация сервиса генерации контрактов, кэширование его ответов -
- -В рамках первой лабораторной работы необходимо: -* Реализовать сервис генерации контрактов на основе Bogus, -* Реализовать кеширование при помощи IDistributedCache и Redis, -* Реализовать структурное логирование сервиса генерации, -* Настроить оркестрацию Aspire. - -
-
-2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы -
- -В рамках второй лабораторной работы необходимо: -* Настроить оркестрацию на запуск нескольких реплик сервиса генерации, -* Реализовать апи гейтвей на основе Ocelot, -* Имплементировать алгоритм балансировки нагрузки согласно варианту. - -
-
-
-3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда -
- -В рамках третьей лабораторной работы необходимо: -* Добавить в оркестрацию объектное хранилище, -* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище, -* Реализовать отправку генерируемых данных в файловый сервис посредством брокера, -* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе. - -
-
-
-4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud -
- -В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы: -* Клиент - в хостинг через отдельный бакет Object Storage, -* Сервис генерации - в Cloud Function, -* Апи гейтвей - в Serverless Integration как API Gateway, -* Брокер сообщений - в Message Queue, -* Файловый сервис - в Cloud Function, -* Объектное хранилище - в отдельный бакет Object Storage, - -
-
- -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview). -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus). -* Реализация тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. - -**Факультативно**: -* Перенос бекенда на облачную инфраструктуру Yandex Cloud - -Внимательно прочитайте [дискуссии](https://github.com/itsecd/cloud-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. - -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма -Современные_технологии_разработки_ПО_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 «Кэширование» +## Вариант №18 — «Медицинский пациент» +## Описание +Реализован сервис генерации данных о медицинских пациентах с кэшированием ответов в Redis и оркестрацией через .NET Aspire + +## Студент +**Чумаков Иван Игоревич**, группа 6511 + +## Что реализовано +### Генерация данных (Bogus) +- Класс `MedicalPatientGenerator` с `RuleFor` для каждого поля +- Генерация ФИО с отчеством на основе фамилии с суффиксом «ович» +- Локаль `ru` для русскоязычных имён + +### Кэширование (Redis + IDistributedCache) +- Сервис `PatientService` с кэшированием через `IDistributedCache` +- TTL вынесен в `appsettings.json` (`CacheSettings:AbsoluteExpirationMinutes`) + +### Структурное логирование +- Логирование через `ILogger` с Serilog +- Структурные параметры `{Id}`, `{FullName}`, `{BirthDate}` и др. +- `Information` для Cache HIT/MISS и успешной генерации + +### CORS +- Разрешён только `localhost`-origin в Development-окружении +- Доверенный origin клиента передаётся через Aspire (`Cors:AllowedOrigin`) + +### Оркестрация (.NET Aspire) +- Redis с RedisInsight +- API сервис ждёт Redis (`WaitFor(redis)`) +- Клиент WASM ждёт API сервис (`WaitFor(generatorService)`) + +### API +- Единственный эндпоинт: `GET /patient?id={id}` +- Minimal API с XML-документацией для Swagger \ No newline at end of file diff --git a/ServiceDefaults/Extensions.cs b/ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..c497d055 --- /dev/null +++ b/ServiceDefaults/Extensions.cs @@ -0,0 +1,80 @@ +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 Microsoft.Extensions.Hosting; + +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + builder.Services.AddServiceDiscovery(); + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + if (useOtlpExporter) + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + return app; + } +} \ No newline at end of file diff --git a/ServiceDefaults/ServiceDefaults.csproj b/ServiceDefaults/ServiceDefaults.csproj new file mode 100644 index 00000000..6cb5f230 --- /dev/null +++ b/ServiceDefaults/ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 00000000..75a80e90 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor" + } +} \ No newline at end of file