diff --git a/1.PNG b/1.PNG new file mode 100644 index 00000000..7f18802a Binary files /dev/null and b/1.PNG differ diff --git a/2.PNG b/2.PNG new file mode 100644 index 00000000..155f396a Binary files /dev/null and b/2.PNG differ diff --git a/3.PNG b/3.PNG new file mode 100644 index 00000000..fdbd0462 Binary files /dev/null and b/3.PNG differ diff --git a/AspireApp/AspireApp.AppHost/AppHost.cs b/AspireApp/AspireApp.AppHost/AppHost.cs new file mode 100644 index 00000000..54b3941e --- /dev/null +++ b/AspireApp/AspireApp.AppHost/AppHost.cs @@ -0,0 +1,13 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("employee-cache") + .WithRedisInsight(containerName: "employee-insight"); + +var service = builder.AddProject("service-api") + .WithReference(cache, "RedisCache") + .WaitFor(cache); + +builder.AddProject("employee-wasm") + .WaitFor(service); + +builder.Build().Run(); diff --git a/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj b/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj new file mode 100644 index 00000000..c993eb76 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/AspireApp/AspireApp.AppHost/Properties/launchSettings.json b/AspireApp/AspireApp.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..df45cce3 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17096;http://localhost:15155", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21139", + "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "https://localhost:21140", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22017" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15155", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19197", + "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://localhost:19198", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20116" + } + } + } +} diff --git a/AspireApp/AspireApp.AppHost/appsettings.Development.json b/AspireApp/AspireApp.AppHost/appsettings.Development.json new file mode 100644 index 00000000..167eb683 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Aspire.Hosting": "Information" + } + } +} diff --git a/AspireApp/AspireApp.AppHost/appsettings.json b/AspireApp/AspireApp.AppHost/appsettings.json new file mode 100644 index 00000000..167eb683 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Aspire.Hosting": "Information" + } + } +} diff --git a/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj b/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj new file mode 100644 index 00000000..530518fa --- /dev/null +++ b/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + diff --git a/AspireApp/AspireApp.ServiceDefaults/Extensions.cs b/AspireApp/AspireApp.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..e8399ba7 --- /dev/null +++ b/AspireApp/AspireApp.ServiceDefaults/Extensions.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + 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() + .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) + { + return builder; + } + + builder.Logging.AddOpenTelemetry(logging => logging.AddOtlpExporter()); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => metrics.AddOtlpExporter()) + .WithTracing(tracing => tracing.AddOtlpExporter()); + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = registration => registration.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index c646a839..f2b945ac 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -1,4 +1,4 @@ -@inject IConfiguration Configuration +@inject IConfiguration Configuration @inject HttpClient Client @@ -7,7 +7,12 @@ Характеристики текущего объекта - + @if (!string.IsNullOrWhiteSpace(ErrorMessage)) + { + @ErrorMessage + } + +
# @@ -30,7 +35,7 @@ foreach (var property in array) { - @(Array.IndexOf(array, property)+1) + @(Array.IndexOf(array, property) + 1) @property.Key @property.Value?.ToString() @@ -51,10 +56,10 @@ Идентификатор нового объекта: - + - + @@ -63,12 +68,33 @@ @code { private JsonObject? Value { get; set; } - private int Id { get; set; } + private string? ErrorMessage { get; set; } + private int Id { get; set; } = 1; private async Task RequestNewData() { - var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); - StateHasChanged(); + ErrorMessage = null; + + if (Id <= 0) + { + ErrorMessage = "Идентификатор должен быть больше нуля."; + return; + } + + var baseAddress = Configuration["BaseAddress"]; + if (string.IsNullOrWhiteSpace(baseAddress)) + { + baseAddress = "http://localhost:7099/employee"; + } + + try + { + Value = await Client.GetFromJsonAsync($"{baseAddress.TrimEnd('/')}?id={Id}", new JsonSerializerOptions()); + } + catch (Exception exception) + { + Value = null; + ErrorMessage = $"Не удалось получить данные сотрудника: {exception.Message}"; + } } } diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..f12c48a0 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -1,13 +1,17 @@  - Лабораторная работа + + Лабораторная работа + - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №1 "Кэширование" + Вариант №28 "Сотрудник компании" + Выполнена Миронюк Матвеем 6512 + + Ссылка на форк + diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..39898806 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "http://localhost:7099/employee" } diff --git a/Client.Wasm/wwwroot/index.html b/Client.Wasm/wwwroot/index.html index b74ee328..e9863ba2 100644 --- a/Client.Wasm/wwwroot/index.html +++ b/Client.Wasm/wwwroot/index.html @@ -9,10 +9,10 @@ - - + + - + diff --git a/README.md b/README.md index dcaa5eb7..658ed378 100644 --- a/README.md +++ b/README.md @@ -26,103 +26,51 @@
-2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы -
- -В рамках второй лабораторной работы необходимо: -* Настроить оркестрацию на запуск нескольких реплик сервиса генерации, -* Реализовать апи гейтвей на основе Ocelot, -* Имплементировать алгоритм балансировки нагрузки согласно варианту. - -
-
-
-3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда -
- -В рамках третьей лабораторной работы необходимо: -* Добавить в оркестрацию объектное хранилище, -* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище, -* Реализовать отправку генерируемых данных в файловый сервис посредством брокера, -* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе. - -
-
-
-4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud -
- -В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы: -* Клиент - в хостинг через отдельный бакет Object Storage, -* Сервис генерации - в Cloud Function, -* Апи гейтвей - в Serverless Integration как API Gateway, -* Брокер сообщений - в Message Queue, -* Файловый сервис - в Cloud Function, -* Объектное хранилище - в отдельный бакет Object Storage, -
+### Вариант №28 +| Доменная область | Балансировка | Брокер | Хостинг S3 | +| --- | --- | --- | --- | +| Сотрудник компании | Weighted Round Robin | SNS | Localstack |
-## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.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 + «Сотрудник компании» +Необходимо сгенерировать информацию о сотруднике компании со следующими характеристиками: + ++----+-------------------------------------------+-------------+ +| № | Название | Тип данных | ++----+-------------------------------------------+-------------+ +| 1 | Идентификатор сотрудника в системе | int | +| 2 | ФИО | string | +| 3 | Должность | string | +| 4 | Отдел | string | +| 5 | Дата приема | DateOnly | +| 6 | Оклад | decimal | +| 7 | Электронная почта | string | +| 8 | Номер телефона | string | +| 9 | Индикатор увольнения | bool | +| 10 | Дата увольнения | DateOnly? | ++----+-------------------------------------------+-------------+ + +Комментарии к характеристикам: +ФИО - конкатенация фамилии, имени и отчества через пробел, все из раздела Name +Должность выбирается из двух заранее подготовленных справочников - справочника профессий (“Developer”, “Manager”, “Analyst” и т.д.) и суффиксов к ним (“Junior”, “Middle”, “Senior” и т.д.) +Отдел берется из раздела Commerce +Дата приема берется не более, чем 10 лет назад от текущей даты +Генерация суммы оклада должна коррелировать с суффиксом должности и не иметь более 2 знаков в дробной части +Электронная почта генерируется из раздела Internet +При отсутствии индикатора увольнения дата увольнения не заполняется +Телефонный номер должен браться из раздела Phone и быть формата +7(***)***-**-**
-## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](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, получают дополнительный балл - -## Вопросы и обратная связь по курсу +##### Aspire +![alt text](1.PNG) -Чтобы задать вопрос по лабораторной, воспользуйтесь [соответствующим разделом дискуссий](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). +##### Получение данных +![alt text](2.PNG) +##### Логи +![alt text](3.PNG) \ No newline at end of file diff --git a/ServiceApi/Entities/Employee.cs b/ServiceApi/Entities/Employee.cs new file mode 100644 index 00000000..fec349fb --- /dev/null +++ b/ServiceApi/Entities/Employee.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Service.Api.Entities; + +/// +/// Сотрудник компании. +/// +public sealed class Employee +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("fullName")] + public string FullName { get; set; } = string.Empty; + + [JsonPropertyName("position")] + public string Position { get; set; } = string.Empty; + + [JsonPropertyName("department")] + public string Department { get; set; } = string.Empty; + + [JsonPropertyName("hireDate")] + public DateOnly HireDate { get; set; } + + [JsonPropertyName("salary")] + public decimal Salary { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; + + [JsonPropertyName("phone")] + public string Phone { get; set; } = string.Empty; + + [JsonPropertyName("isFired")] + public bool IsFired { get; set; } + + [JsonPropertyName("fireDate")] + public DateOnly? FireDate { get; set; } +} diff --git a/ServiceApi/Generator/EmployeeGenerator.cs b/ServiceApi/Generator/EmployeeGenerator.cs new file mode 100644 index 00000000..b3e7645e --- /dev/null +++ b/ServiceApi/Generator/EmployeeGenerator.cs @@ -0,0 +1,92 @@ +using Bogus; +using Bogus.DataSets; +using Service.Api.Entities; + +namespace Service.Api.Generator; + +/// +/// Генератор случайных сотрудников компании. +/// +public static class EmployeeGenerator +{ + private static readonly string[] ProfessionCatalog = + { + "Developer", + "Manager", + "Analyst", + "Tester", + "Administrator", + "Designer" + }; + + private static readonly string[] PositionLevels = + { + "Junior", + "Middle", + "Senior" + }; + + public static Employee Generate(int id) + { + var faker = new Faker("ru"); + + var gender = faker.PickRandom(); + var firstName = faker.Name.FirstName(gender); + var lastName = faker.Name.LastName(gender); + var patronymic = BuildPatronymic(faker.Name.FirstName(Name.Gender.Male), gender); + + var level = faker.PickRandom(PositionLevels); + var profession = faker.PickRandom(ProfessionCatalog); + var hireDate = faker.Date.Past(10, DateTime.Today); + var isFired = faker.Random.Bool(0.18f); + + DateOnly? fireDate = null; + if (isFired) + { + fireDate = DateOnly.FromDateTime(faker.Date.Between(hireDate, DateTime.Today)); + } + + return new Employee + { + Id = id, + FullName = $"{lastName} {firstName} {patronymic}", + Position = $"{level} {profession}", + Department = faker.Commerce.Department(), + HireDate = DateOnly.FromDateTime(hireDate), + Salary = CalculateSalary(level, faker), + Email = faker.Internet.Email(firstName, lastName), + Phone = faker.Phone.PhoneNumber("+7(###)###-##-##"), + IsFired = isFired, + FireDate = fireDate + }; + } + + private static string BuildPatronymic(string sourceName, Name.Gender gender) + { + if (string.IsNullOrWhiteSpace(sourceName)) + { + return gender == Name.Gender.Male ? "Иванович" : "Ивановна"; + } + + return gender switch + { + Name.Gender.Male when sourceName.EndsWith('й') => $"{sourceName[..^1]}евич", + Name.Gender.Female when sourceName.EndsWith('й') => $"{sourceName[..^1]}евна", + Name.Gender.Male => $"{sourceName}ович", + _ => $"{sourceName}овна" + }; + } + + private static decimal CalculateSalary(string level, Faker faker) + { + var value = level switch + { + "Junior" => faker.Random.Decimal(60_000m, 95_000m), + "Middle" => faker.Random.Decimal(100_000m, 170_000m), + "Senior" => faker.Random.Decimal(180_000m, 280_000m), + _ => faker.Random.Decimal(80_000m, 120_000m) + }; + + return Math.Round(value, 2, MidpointRounding.AwayFromZero); + } +} diff --git a/ServiceApi/Generator/EmployeeGeneratorService.cs b/ServiceApi/Generator/EmployeeGeneratorService.cs new file mode 100644 index 00000000..64f4a716 --- /dev/null +++ b/ServiceApi/Generator/EmployeeGeneratorService.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using Microsoft.Extensions.Caching.Distributed; +using Service.Api.Entities; + +namespace Service.Api.Generator; + +public sealed class EmployeeGeneratorService( + IDistributedCache cache, + ILogger logger, + IConfiguration configuration) : IEmployeeGeneratorService +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + + private readonly TimeSpan _cacheExpiration = + TimeSpan.FromMinutes(configuration.GetValue("CacheExpirationMinutes") ?? 30); + + public async Task ProcessEmployee(int id, CancellationToken cancellationToken = default) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id); + + var cacheKey = id.ToString(); + + using var _ = logger.BeginScope(new Dictionary + { + ["EmployeeId"] = id, + ["CacheKey"] = cacheKey + }); + + logger.LogInformation("Employee request received"); + + try + { + var employee = await RetrieveFromCache(cacheKey, cancellationToken); + if (employee is not null) + { + logger.LogInformation("Cache hit. Returning employee from Redis"); + return employee; + } + + logger.LogInformation("Cache miss. Generating new employee"); + employee = EmployeeGenerator.Generate(id); + await PopulateCache(cacheKey, employee, cancellationToken); + + logger.LogInformation( + "Employee {EmployeeId} stored in cache for {CacheLifetimeMinutes} minutes", + id, + _cacheExpiration.TotalMinutes); + + return employee; + } + catch (Exception exception) + { + logger.LogError(exception, "Error while processing employee {EmployeeId}", id); + throw; + } + } + + private async Task RetrieveFromCache(string cacheKey, CancellationToken cancellationToken) + { + var json = await cache.GetStringAsync(cacheKey, cancellationToken); + if (string.IsNullOrWhiteSpace(json)) + { + return null; + } + + return JsonSerializer.Deserialize(json, JsonOptions); + } + + private async Task PopulateCache(string cacheKey, Employee employee, CancellationToken cancellationToken) + { + var json = JsonSerializer.Serialize(employee, JsonOptions); + + await cache.SetStringAsync( + cacheKey, + json, + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheExpiration + }, + cancellationToken); + } +} diff --git a/ServiceApi/Generator/IEmployeeGeneratorService.cs b/ServiceApi/Generator/IEmployeeGeneratorService.cs new file mode 100644 index 00000000..c8de0288 --- /dev/null +++ b/ServiceApi/Generator/IEmployeeGeneratorService.cs @@ -0,0 +1,11 @@ +using Service.Api.Entities; + +namespace Service.Api.Generator; + +/// +/// Интерфейс обработки запросов на получение сотрудника. +/// +public interface IEmployeeGeneratorService +{ + Task ProcessEmployee(int id, CancellationToken cancellationToken = default); +} diff --git a/ServiceApi/Program.cs b/ServiceApi/Program.cs new file mode 100644 index 00000000..d4ddf2d9 --- /dev/null +++ b/ServiceApi/Program.cs @@ -0,0 +1,60 @@ +using Service.Api.Generator; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("RedisCache"); + +builder.Logging.ClearProviders(); +builder.Logging.AddJsonConsole(options => +{ + options.IncludeScopes = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; +}); + +builder.Services.AddScoped(); + +builder.Services.AddCors(options => options.AddDefaultPolicy(policy => +{ + policy.SetIsOriginAllowed(origin => + Uri.TryCreate(origin, UriKind.Absolute, out var uri) + && uri.IsLoopback + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + .AllowAnyHeader() + .WithMethods("GET"); +})); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +app.MapGet("/", () => Results.Ok(new +{ + service = "Service.Api", + description = "Сервис генерации сотрудников компании", + endpoints = new[] { "/employee?id=1", "/employee/1" } +})); + +app.MapGet("/employee", async (IEmployeeGeneratorService service, int id, CancellationToken cancellationToken) => +{ + if (id <= 0) + { + return Results.BadRequest(new { message = "Идентификатор сотрудника должен быть больше нуля." }); + } + + return Results.Ok(await service.ProcessEmployee(id, cancellationToken)); +}); + +app.MapGet("/employee/{id:int}", async (IEmployeeGeneratorService service, int id, CancellationToken cancellationToken) => +{ + if (id <= 0) + { + return Results.BadRequest(new { message = "Идентификатор сотрудника должен быть больше нуля." }); + } + + return Results.Ok(await service.ProcessEmployee(id, cancellationToken)); +}); + +app.UseCors(); + +app.Run(); diff --git a/ServiceApi/Properties/launchSettings.json b/ServiceApi/Properties/launchSettings.json new file mode 100644 index 00000000..0641ca27 --- /dev/null +++ b/ServiceApi/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:7099", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ServiceApi/Service.Api.csproj b/ServiceApi/Service.Api.csproj new file mode 100644 index 00000000..e609ccc7 --- /dev/null +++ b/ServiceApi/Service.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + Service.Api + Service.Api + + + + + + + + + + + + diff --git a/ServiceApi/appsettings.Development.json b/ServiceApi/appsettings.Development.json new file mode 100644 index 00000000..6e16757b --- /dev/null +++ b/ServiceApi/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "CacheExpirationMinutes": 30, + "ConnectionStrings": { + "RedisCache": "localhost:6379" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ServiceApi/appsettings.json b/ServiceApi/appsettings.json new file mode 100644 index 00000000..5f1aabbb --- /dev/null +++ b/ServiceApi/appsettings.json @@ -0,0 +1,10 @@ +{ + "CacheExpirationMinutes": 30, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/var.md b/var.md new file mode 100644 index 00000000..1fdd282b --- /dev/null +++ b/var.md @@ -0,0 +1,33 @@ +# Вариант №28 +Сотрудник компании +Weighted Round Robin +SNS +Localstack + +# «Сотрудник компании» +Необходимо сгенерировать информацию о сотруднике компании со следующими характеристиками: + ++----+-------------------------------------------+-------------+ +| № | Название | Тип данных | ++----+-------------------------------------------+-------------+ +| 1 | Идентификатор сотрудника в системе | int | +| 2 | ФИО | string | +| 3 | Должность | string | +| 4 | Отдел | string | +| 5 | Дата приема | DateOnly | +| 6 | Оклад | decimal | +| 7 | Электронная почта | string | +| 8 | Номер телефона | string | +| 9 | Индикатор увольнения | bool | +| 10 | Дата увольнения | DateOnly? | ++----+-------------------------------------------+-------------+ + +Комментарии к характеристикам: +ФИО - конкатенация фамилии, имени и отчества через пробел, все из раздела Name +Должность выбирается из двух заранее подготовленных справочников - справочника профессий (“Developer”, “Manager”, “Analyst” и т.д.) и суффиксов к ним (“Junior”, “Middle”, “Senior” и т.д.) +Отдел берется из раздела Commerce +Дата приема берется не более, чем 10 лет назад от текущей даты +Генерация суммы оклада должна коррелировать с суффиксом должности и не иметь более 2 знаков в дробной части +Электронная почта генерируется из раздела Internet +При отсутствии индикатора увольнения дата увольнения не заполняется +Телефонный номер должен браться из раздела Phone и быть формата +7(***)***-**-**