From 1aadc5dc72ca2fb3de145d533b8959f6e7695309 Mon Sep 17 00:00:00 2001 From: MagGoldi Date: Wed, 18 Mar 2026 12:52:52 +0400 Subject: [PATCH 1/5] done --- Client.Wasm/Client.Wasm.csproj | 1 + Client.Wasm/Components/DataCard.razor | 7 +- Client.Wasm/Components/StudentCard.razor | 10 +- Client.Wasm/Program.cs | 7 +- Client.Wasm/VehicleApiClient.cs | 12 ++ Client.Wasm/wwwroot/appsettings.json | 2 +- CloudDevelopment.sln | 27 ++- .../Controllers/VehicleController.cs | 29 +++ ProjectApp.Api/Program.cs | 66 +++++++ ProjectApp.Api/ProjectApp.Api.csproj | 23 +++ ProjectApp.Api/Properties/launchSettings.json | 41 +++++ .../IVehicleGeneratorService.cs | 14 ++ .../VehicleGeneratorService/VehicleFaker.cs | 41 +++++ .../VehicleGeneratorService.cs | 76 ++++++++ ProjectApp.Api/appsettings.Development.json | 12 ++ ProjectApp.Api/appsettings.json | 15 ++ ProjectApp.AppHost/Program.cs | 15 ++ ProjectApp.AppHost/ProjectApp.AppHost.csproj | 24 +++ .../Properties/launchSettings.json | 29 +++ .../appsettings.Development.json | 8 + ProjectApp.AppHost/appsettings.json | 9 + ProjectApp.Domain/Entities/Vehicle.cs | 18 ++ ProjectApp.Domain/ProjectApp.Domain.csproj | 11 ++ ProjectApp.ServiceDefaults/Extensions.cs | 104 +++++++++++ .../ProjectApp.ServiceDefaults.csproj | 20 +++ README.md | 168 +++++------------- 26 files changed, 648 insertions(+), 141 deletions(-) create mode 100644 Client.Wasm/VehicleApiClient.cs create mode 100644 ProjectApp.Api/Controllers/VehicleController.cs create mode 100644 ProjectApp.Api/Program.cs create mode 100644 ProjectApp.Api/ProjectApp.Api.csproj create mode 100644 ProjectApp.Api/Properties/launchSettings.json create mode 100644 ProjectApp.Api/Services/VehicleGeneratorService/IVehicleGeneratorService.cs create mode 100644 ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs create mode 100644 ProjectApp.Api/Services/VehicleGeneratorService/VehicleGeneratorService.cs create mode 100644 ProjectApp.Api/appsettings.Development.json create mode 100644 ProjectApp.Api/appsettings.json create mode 100644 ProjectApp.AppHost/Program.cs create mode 100644 ProjectApp.AppHost/ProjectApp.AppHost.csproj create mode 100644 ProjectApp.AppHost/Properties/launchSettings.json create mode 100644 ProjectApp.AppHost/appsettings.Development.json create mode 100644 ProjectApp.AppHost/appsettings.json create mode 100644 ProjectApp.Domain/Entities/Vehicle.cs create mode 100644 ProjectApp.Domain/ProjectApp.Domain.csproj create mode 100644 ProjectApp.ServiceDefaults/Extensions.cs create mode 100644 ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj diff --git a/Client.Wasm/Client.Wasm.csproj b/Client.Wasm/Client.Wasm.csproj index 0ba9f90c..40edaeee 100644 --- a/Client.Wasm/Client.Wasm.csproj +++ b/Client.Wasm/Client.Wasm.csproj @@ -16,6 +16,7 @@ + diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index c646a839..20b06fa8 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -1,5 +1,5 @@ -@inject IConfiguration Configuration -@inject HttpClient Client +@using Client.Wasm +@inject VehicleApiClient ApiClient @@ -67,8 +67,7 @@ private async Task RequestNewData() { - var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); + Value = await ApiClient.GetVehicleAsync(Id); StateHasChanged(); } } diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..faf6c115 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -1,13 +1,13 @@ - + Лабораторная работа - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №1 "Кэширование" + Вариант №29 "Транспортное средство" + Выполнена Нестеренко Андреем 6512 + Ссылка на репозиторий diff --git a/Client.Wasm/Program.cs b/Client.Wasm/Program.cs index a182a920..87c7cad9 100644 --- a/Client.Wasm/Program.cs +++ b/Client.Wasm/Program.cs @@ -9,7 +9,12 @@ builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); -builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddHttpClient(client => +{ + var baseAddress = builder.Configuration["BaseAddress"] + ?? throw new InvalidOperationException("BaseAddress not found in configuration."); + client.BaseAddress = new Uri(baseAddress); +}); builder.Services.AddBlazorise(options => { options.Immediate = true; }) .AddBootstrapProviders() .AddFontAwesomeIcons(); diff --git a/Client.Wasm/VehicleApiClient.cs b/Client.Wasm/VehicleApiClient.cs new file mode 100644 index 00000000..80fab7a9 --- /dev/null +++ b/Client.Wasm/VehicleApiClient.cs @@ -0,0 +1,12 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; + +namespace Client.Wasm; + +public class VehicleApiClient(HttpClient httpClient) +{ + public async Task GetVehicleAsync(int id) + { + return await httpClient.GetFromJsonAsync($"api/vehicle/{id}"); + } +} diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..c95e6fd0 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "http://localhost:5179/" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..7e69b888 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -1,10 +1,17 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36811.4 +VisualStudioVersion = 17.13.35931.197 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}") = "ProjectApp.Api", "ProjectApp.Api\ProjectApp.Api.csproj", "{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Domain", "ProjectApp.Domain\ProjectApp.Domain.csproj", "{CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.ServiceDefaults", "ProjectApp.ServiceDefaults\ProjectApp.ServiceDefaults.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.AppHost", "ProjectApp.AppHost\ProjectApp.AppHost.csproj", "{2A5FB573-9376-4FEB-9289-A8387F435C13}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +22,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 + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.Build.0 = Release|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ProjectApp.Api/Controllers/VehicleController.cs b/ProjectApp.Api/Controllers/VehicleController.cs new file mode 100644 index 00000000..f8f950d0 --- /dev/null +++ b/ProjectApp.Api/Controllers/VehicleController.cs @@ -0,0 +1,29 @@ +using ProjectApp.Api.Services.VehicleGeneratorService; +using ProjectApp.Domain.Entities; +using Microsoft.AspNetCore.Mvc; + +namespace ProjectApp.Api.Controllers; + +/// +/// Контроллер для генерации и получения характеристик транспортных средств +/// + +[Route("api/[controller]")] +[ApiController] +public class VehicleController(IVehicleGeneratorService vehicleService, ILogger logger) : ControllerBase +{ + /// + /// Возвращает сгенерированное транспортное средство по его уникальному идентификатору + /// + /// Идентификатор машины + /// Токен отмены + [HttpGet("{id}")] + public async Task> GetById([FromRoute] int id, CancellationToken cancellationToken) + { + logger.LogInformation("Request received for vehicle id {Id}", id); + + var vehicle = await vehicleService.FetchByIdAsync(id, cancellationToken); + + return Ok(vehicle); + } +} diff --git a/ProjectApp.Api/Program.cs b/ProjectApp.Api/Program.cs new file mode 100644 index 00000000..0b024e68 --- /dev/null +++ b/ProjectApp.Api/Program.cs @@ -0,0 +1,66 @@ +using ProjectApp.Api.Services.VehicleGeneratorService; +using ProjectApp.ServiceDefaults; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.AddRedisDistributedCache("cache"); + +var allowedOrigins = builder.Configuration.GetSection("AllowedOrigins").Get() ?? ["http://localhost:5127"]; +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.WithOrigins(allowedOrigins) + .WithMethods("GET") + .WithHeaders("Content-Type"); + }); +}); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => + ActivatorUtilities.CreateInstance(sp, + sp.GetRequiredService())); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new Microsoft.OpenApi.OpenApiInfo + { + Title = "Vehicle Generator API" + }); + + var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename); + if (File.Exists(xmlPath)) + { + options.IncludeXmlComments(xmlPath); + } + + var domainXmlPath = Path.Combine(AppContext.BaseDirectory, "ProjectApp.Domain.xml"); + if (File.Exists(domainXmlPath)) + { + options.IncludeXmlComments(domainXmlPath); + } +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +if (!app.Environment.IsDevelopment()) +{ + app.UseHttpsRedirection(); +} +app.UseCors(); +app.MapControllers(); +app.MapDefaultEndpoints(); + +app.Run(); \ No newline at end of file diff --git a/ProjectApp.Api/ProjectApp.Api.csproj b/ProjectApp.Api/ProjectApp.Api.csproj new file mode 100644 index 00000000..2aea6015 --- /dev/null +++ b/ProjectApp.Api/ProjectApp.Api.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + + + + + + + + diff --git a/ProjectApp.Api/Properties/launchSettings.json b/ProjectApp.Api/Properties/launchSettings.json new file mode 100644 index 00000000..bbe9ee66 --- /dev/null +++ b/ProjectApp.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:46825", + "sslPort": 44333 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5179", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7170;http://localhost:5179", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ProjectApp.Api/Services/VehicleGeneratorService/IVehicleGeneratorService.cs b/ProjectApp.Api/Services/VehicleGeneratorService/IVehicleGeneratorService.cs new file mode 100644 index 00000000..671ca786 --- /dev/null +++ b/ProjectApp.Api/Services/VehicleGeneratorService/IVehicleGeneratorService.cs @@ -0,0 +1,14 @@ +using ProjectApp.Domain.Entities; + +namespace ProjectApp.Api.Services.VehicleGeneratorService; + +/// +/// Интерфейс сервиса получения характеристик машины +/// +public interface IVehicleGeneratorService +{ + /// + /// Запрашивает машину по ID + /// + public Task FetchByIdAsync(int id, CancellationToken cancellationToken = default); +} diff --git a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs new file mode 100644 index 00000000..1db9a5c0 --- /dev/null +++ b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs @@ -0,0 +1,41 @@ +using Bogus; +using ProjectApp.Domain.Entities; + +namespace ProjectApp.Api.Services.VehicleGeneratorService; + +public class VehicleFaker +{ + private readonly Faker _faker; + + public VehicleFaker() + { + var fuelTypes = new[] { "Бензин", "Дизель", "Электро", "Гибрид", "Газ" }; + + _faker = new Faker("ru") + .RuleFor(v => v.Id, f => f.IndexFaker + 1) + .RuleFor(v => v.Brand, f => f.PickRandom( + "Toyota", "BMW", "Mercedes", "Volkswagen", "Hyundai", + "Ford", "Kia", "Audi", "Nissan", "Renault")) + .RuleFor(v => v.Model, (f, v) => f.PickRandom( + "Comfort", "Sport", "Elite", "Plus", "Pro", + "Active", "Max", "Base", "Premium", "Line")) + .RuleFor(v => v.RegistrationNumber, f => + { + var letters = "АВЕКМНОРСТУХ"; + var letter1 = letters[f.Random.Int(0, letters.Length - 1)]; + var digits = f.Random.Int(100, 999); + var letter2 = letters[f.Random.Int(0, letters.Length - 1)]; + var letter3 = letters[f.Random.Int(0, letters.Length - 1)]; + var region = f.Random.Int(10, 199); + return $"{letter1}{digits}{letter2}{letter3} {region}"; + }) + .RuleFor(v => v.OwnerName, f => f.Name.FullName()) + .RuleFor(v => v.Year, f => f.Random.Int(1984, 2026)) + .RuleFor(v => v.EngineVolume, f => Math.Round(f.Random.Decimal(0.8m, 6.0m), 1)) + .RuleFor(v => v.Mileage, f => f.Random.Int(0, 500000)) + .RuleFor(v => v.FuelType, f => f.PickRandom(fuelTypes)) + .RuleFor(v => v.Price, f => Math.Round(f.Random.Decimal(100000m, 10000000m), 0)); + } + + public Vehicle Generate() => _faker.Generate(); +} diff --git a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleGeneratorService.cs b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleGeneratorService.cs new file mode 100644 index 00000000..311a49c9 --- /dev/null +++ b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleGeneratorService.cs @@ -0,0 +1,76 @@ +using ProjectApp.Domain.Entities; +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace ProjectApp.Api.Services.VehicleGeneratorService; + +public class VehicleGeneratorService( + VehicleFaker faker, + ILogger logger) : IVehicleGeneratorService +{ + public Task FetchByIdAsync(int id, CancellationToken cancellationToken = default) + { + logger.LogInformation("Generating new vehicle data for id {Id}", id); + var vehicle = faker.Generate(); + vehicle.Id = id; + return Task.FromResult(vehicle); + } +} + +public class CachedVehicleGeneratorService( + IVehicleGeneratorService innerService, + IDistributedCache cache, + IConfiguration configuration, + ILogger logger) : IVehicleGeneratorService +{ + private readonly int _ttlMinutes = configuration.GetValue("CacheSettings:ExpirationMinutes", 10); + + public async Task FetchByIdAsync(int id, CancellationToken cancellationToken = default) + { + var key = $"vehicle-{id}"; + + try + { + var raw = await cache.GetStringAsync(key, cancellationToken); + if (!string.IsNullOrEmpty(raw)) + { + var vehicle = JsonSerializer.Deserialize(raw); + if (vehicle != null) + { + logger.LogInformation("Vehicle {Id} retrieved from cache", id); + return vehicle; + } + } + } + catch (Exception ex) + { + // отлавливаем падение при недоступном Redis + logger.LogWarning(ex, "Cache read failed for vehicle {Id}. Falling back to generation.", id); + } + + // если кэш упал - делегируем генерацию сервису + var generatedVehicle = await innerService.FetchByIdAsync(id, cancellationToken); + + try + { + var opts = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_ttlMinutes) + }; + + await cache.SetStringAsync( + key, + JsonSerializer.Serialize(generatedVehicle), + opts, + cancellationToken); + + logger.LogInformation("Vehicle {Id} successfully stored in cache.", id); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to store vehicle {Id} in cache.", id); + } + + return generatedVehicle; + } +} diff --git a/ProjectApp.Api/appsettings.Development.json b/ProjectApp.Api/appsettings.Development.json new file mode 100644 index 00000000..b642d7aa --- /dev/null +++ b/ProjectApp.Api/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CacheSettings": { + "ExpirationMinutes": 10 + } +} diff --git a/ProjectApp.Api/appsettings.json b/ProjectApp.Api/appsettings.json new file mode 100644 index 00000000..56fded7d --- /dev/null +++ b/ProjectApp.Api/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AllowedOrigins": [ + "http://localhost:5127" + ], + "CacheSettings": { + "ExpirationMinutes": 10 + } +} diff --git a/ProjectApp.AppHost/Program.cs b/ProjectApp.AppHost/Program.cs new file mode 100644 index 00000000..13bf1f02 --- /dev/null +++ b/ProjectApp.AppHost/Program.cs @@ -0,0 +1,15 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var redis = builder.AddRedis("cache") + .WithRedisCommander(); + +var api = builder.AddProject("projectapp-api") + .WithReference(redis) + .WaitFor(redis) + .WithEndpoint("http", e => e.Port = 5179); + +builder.AddProject("client") + .WithReference(api) + .WaitFor(api); + +builder.Build().Run(); diff --git a/ProjectApp.AppHost/ProjectApp.AppHost.csproj b/ProjectApp.AppHost/ProjectApp.AppHost.csproj new file mode 100644 index 00000000..7704751d --- /dev/null +++ b/ProjectApp.AppHost/ProjectApp.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + net8.0 + enable + enable + true + b8f3eae0-771a-4f3a-8df3-ef0a21b09b55 + + + + + + + + + + + + + diff --git a/ProjectApp.AppHost/Properties/launchSettings.json b/ProjectApp.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..fa890ea9 --- /dev/null +++ b/ProjectApp.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:17214;http://localhost:15105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21185", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22273" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19190", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20136" + } + } + } +} diff --git a/ProjectApp.AppHost/appsettings.Development.json b/ProjectApp.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ProjectApp.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ProjectApp.AppHost/appsettings.json b/ProjectApp.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/ProjectApp.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/ProjectApp.Domain/Entities/Vehicle.cs b/ProjectApp.Domain/Entities/Vehicle.cs new file mode 100644 index 00000000..18c773e8 --- /dev/null +++ b/ProjectApp.Domain/Entities/Vehicle.cs @@ -0,0 +1,18 @@ +namespace ProjectApp.Domain.Entities; + +/// +/// Сущность транспортного средства +/// +public class Vehicle +{ + public required int Id { get; set; } + public required string Brand { get; set; } + public required string Model { get; set; } + public required string RegistrationNumber { get; set; } + public required string OwnerName { get; set; } + public int Year { get; set; } + public decimal EngineVolume { get; set; } + public int Mileage { get; set; } + public required string FuelType { get; set; } + public decimal Price { get; set; } +} diff --git a/ProjectApp.Domain/ProjectApp.Domain.csproj b/ProjectApp.Domain/ProjectApp.Domain.csproj new file mode 100644 index 00000000..fcfb2654 --- /dev/null +++ b/ProjectApp.Domain/ProjectApp.Domain.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + diff --git a/ProjectApp.ServiceDefaults/Extensions.cs b/ProjectApp.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..9f167125 --- /dev/null +++ b/ProjectApp.ServiceDefaults/Extensions.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace ProjectApp.ServiceDefaults; + + /// + /// Вспомогательный класс для настройки общих сервисов (логи, метрики, проверки состояния) + /// +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; + } + + /// + /// Подключение и настройка OpenTelemetry (логирование и сбор метрик) + /// + 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; + } + + /// + /// Базовые проверки сервиса (health checks) + /// + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + /// + /// Маппинг endpoints для мониторинга доступности + /// + 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; + } +} diff --git a/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj b/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj new file mode 100644 index 00000000..bf9f33a7 --- /dev/null +++ b/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/README.md b/README.md index dcaa5eb7..c868a66a 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,40 @@ -# Современные технологии разработки программного обеспечения -[Таблица с успеваемостью](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 — «Кэширование» + +**Вариант:** №29 — «Транспортное средство» +**Балансировка:** Query Based +**Брокер:** SQS +**Хостинг S3:** Minio +**Выполнил:** Нестеренко Андрей, 6512 + +## Что реализовано + +- Генерация сущности «Транспортное средство» через Bogus. +- Кэширование результатов генерации через IDistributedCache (Redis) с TTL 10 минут. +- Структурное логирование запросов и результатов генерации. +- Оркестрация сервисов через .NET Aspire. +- REST endpoint: `GET /api/vehicle/{id}`. + +## Характеристики генерируемого транспортного средства + +1. Идентификатор в системе — `int` +2. Марка — `string` +3. Модель — `string` +4. Регистрационный номер — `string` +5. Владелец — `string` +6. Год выпуска — `int` +7. Объём двигателя (л) — `decimal` +8. Пробег (км) — `int` +9. Тип топлива — `string` +10. Стоимость (₽) — `decimal` + +## Правила генерации + +- Марка: случайно выбирается из фиксированного списка популярных производителей. +- Модель: случайная комплектация из набора суффиксов. +- Регистрационный номер: форматируется по схеме `А000АА 000` с кириллическими буквами. +- Владелец: полное имя (Name.FullName). +- Год выпуска: от 1984 до 2026. +- Объём двигателя: от 0.8 до 6.0 литра с округлением до 1 знака. +- Пробег: от 0 до 500 000 км. +- Тип топлива: выбирается из списка (Бензин, Дизель, Электро, Гибрид, Газ). +- Стоимость: от 100 000 до 10 000 000 ₽. From b6f3c708b8ae56dabd7981a913537b2174cc73e3 Mon Sep 17 00:00:00 2001 From: MagGoldi Date: Wed, 18 Mar 2026 13:00:58 +0400 Subject: [PATCH 2/5] commit because im disgrace --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c868a66a..ddced879 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,4 @@ - Пробег: от 0 до 500 000 км. - Тип топлива: выбирается из списка (Бензин, Дизель, Электро, Гибрид, Газ). - Стоимость: от 100 000 до 10 000 000 ₽. + From 125caae0a533b06b50d44f82d1dc8974a41378f2 Mon Sep 17 00:00:00 2001 From: MagGoldi Date: Tue, 24 Mar 2026 12:06:24 +0400 Subject: [PATCH 3/5] corrections --- .../Controllers/VehicleController.cs | 7 ++- .../CachedVehicleGeneratorService.cs | 61 +++++++++++++++++++ .../VehicleGeneratorService/VehicleFaker.cs | 35 +++++++---- .../VehicleGeneratorService.cs | 60 ------------------ ProjectApp.AppHost/Program.cs | 1 - ProjectApp.Domain/Entities/Vehicle.cs | 19 ++++++ 6 files changed, 106 insertions(+), 77 deletions(-) create mode 100644 ProjectApp.Api/Services/VehicleGeneratorService/CachedVehicleGeneratorService.cs diff --git a/ProjectApp.Api/Controllers/VehicleController.cs b/ProjectApp.Api/Controllers/VehicleController.cs index f8f950d0..9989ac47 100644 --- a/ProjectApp.Api/Controllers/VehicleController.cs +++ b/ProjectApp.Api/Controllers/VehicleController.cs @@ -7,7 +7,6 @@ namespace ProjectApp.Api.Controllers; /// /// Контроллер для генерации и получения характеристик транспортных средств /// - [Route("api/[controller]")] [ApiController] public class VehicleController(IVehicleGeneratorService vehicleService, ILogger logger) : ControllerBase @@ -15,9 +14,11 @@ public class VehicleController(IVehicleGeneratorService vehicleService, ILogger< /// /// Возвращает сгенерированное транспортное средство по его уникальному идентификатору /// - /// Идентификатор машины + /// Идентификатор транспортного средства /// Токен отмены - [HttpGet("{id}")] + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(Vehicle), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetById([FromRoute] int id, CancellationToken cancellationToken) { logger.LogInformation("Request received for vehicle id {Id}", id); diff --git a/ProjectApp.Api/Services/VehicleGeneratorService/CachedVehicleGeneratorService.cs b/ProjectApp.Api/Services/VehicleGeneratorService/CachedVehicleGeneratorService.cs new file mode 100644 index 00000000..b1df1600 --- /dev/null +++ b/ProjectApp.Api/Services/VehicleGeneratorService/CachedVehicleGeneratorService.cs @@ -0,0 +1,61 @@ +using ProjectApp.Domain.Entities; +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace ProjectApp.Api.Services.VehicleGeneratorService; + +public class CachedVehicleGeneratorService( + IVehicleGeneratorService innerService, + IDistributedCache cache, + IConfiguration configuration, + ILogger logger) : IVehicleGeneratorService +{ + private readonly int _ttlMinutes = configuration.GetValue("CacheSettings:ExpirationMinutes", 10); + + public async Task FetchByIdAsync(int id, CancellationToken cancellationToken = default) + { + var key = $"vehicle-{id}"; + + try + { + var raw = await cache.GetStringAsync(key, cancellationToken); + if (!string.IsNullOrEmpty(raw)) + { + var vehicle = JsonSerializer.Deserialize(raw); + if (vehicle != null) + { + logger.LogInformation("Vehicle {Id} retrieved from cache", id); + return vehicle; + } + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Cache read failed for vehicle {Id}. Falling back to generation.", id); + } + + var generatedVehicle = await innerService.FetchByIdAsync(id, cancellationToken); + + try + { + var opts = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_ttlMinutes) + }; + + await cache.SetStringAsync( + key, + JsonSerializer.Serialize(generatedVehicle), + opts, + cancellationToken); + + logger.LogInformation("Vehicle {Id} successfully stored in cache.", id); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to store vehicle {Id} in cache.", id); + } + + return generatedVehicle; + } +} diff --git a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs index 1db9a5c0..70ffb82e 100644 --- a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs +++ b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs @@ -5,27 +5,36 @@ namespace ProjectApp.Api.Services.VehicleGeneratorService; public class VehicleFaker { + private static readonly string[] FuelTypes = ["Бензин", "Дизель", "Электро", "Гибрид", "Газ"]; + + private static readonly string[] Brands = + [ + "Toyota", "BMW", "Mercedes", "Volkswagen", "Hyundai", + "Ford", "Kia", "Audi", "Nissan", "Renault" + ]; + + private static readonly string[] Trims = + [ + "Comfort", "Sport", "Elite", "Plus", "Pro", + "Active", "Max", "Base", "Premium", "Line" + ]; + + private const string RegistrationLetters = "АВЕКМНОРСТУХ"; + private readonly Faker _faker; public VehicleFaker() { - var fuelTypes = new[] { "Бензин", "Дизель", "Электро", "Гибрид", "Газ" }; - _faker = new Faker("ru") .RuleFor(v => v.Id, f => f.IndexFaker + 1) - .RuleFor(v => v.Brand, f => f.PickRandom( - "Toyota", "BMW", "Mercedes", "Volkswagen", "Hyundai", - "Ford", "Kia", "Audi", "Nissan", "Renault")) - .RuleFor(v => v.Model, (f, v) => f.PickRandom( - "Comfort", "Sport", "Elite", "Plus", "Pro", - "Active", "Max", "Base", "Premium", "Line")) + .RuleFor(v => v.Brand, f => f.PickRandom(Brands)) + .RuleFor(v => v.Model, f => f.PickRandom(Trims)) .RuleFor(v => v.RegistrationNumber, f => { - var letters = "АВЕКМНОРСТУХ"; - var letter1 = letters[f.Random.Int(0, letters.Length - 1)]; + var letter1 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; var digits = f.Random.Int(100, 999); - var letter2 = letters[f.Random.Int(0, letters.Length - 1)]; - var letter3 = letters[f.Random.Int(0, letters.Length - 1)]; + var letter2 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; + var letter3 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; var region = f.Random.Int(10, 199); return $"{letter1}{digits}{letter2}{letter3} {region}"; }) @@ -33,7 +42,7 @@ public VehicleFaker() .RuleFor(v => v.Year, f => f.Random.Int(1984, 2026)) .RuleFor(v => v.EngineVolume, f => Math.Round(f.Random.Decimal(0.8m, 6.0m), 1)) .RuleFor(v => v.Mileage, f => f.Random.Int(0, 500000)) - .RuleFor(v => v.FuelType, f => f.PickRandom(fuelTypes)) + .RuleFor(v => v.FuelType, f => f.PickRandom(FuelTypes)) .RuleFor(v => v.Price, f => Math.Round(f.Random.Decimal(100000m, 10000000m), 0)); } diff --git a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleGeneratorService.cs b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleGeneratorService.cs index 311a49c9..645f7b49 100644 --- a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleGeneratorService.cs +++ b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleGeneratorService.cs @@ -1,6 +1,4 @@ using ProjectApp.Domain.Entities; -using Microsoft.Extensions.Caching.Distributed; -using System.Text.Json; namespace ProjectApp.Api.Services.VehicleGeneratorService; @@ -16,61 +14,3 @@ public Task FetchByIdAsync(int id, CancellationToken cancellationToken return Task.FromResult(vehicle); } } - -public class CachedVehicleGeneratorService( - IVehicleGeneratorService innerService, - IDistributedCache cache, - IConfiguration configuration, - ILogger logger) : IVehicleGeneratorService -{ - private readonly int _ttlMinutes = configuration.GetValue("CacheSettings:ExpirationMinutes", 10); - - public async Task FetchByIdAsync(int id, CancellationToken cancellationToken = default) - { - var key = $"vehicle-{id}"; - - try - { - var raw = await cache.GetStringAsync(key, cancellationToken); - if (!string.IsNullOrEmpty(raw)) - { - var vehicle = JsonSerializer.Deserialize(raw); - if (vehicle != null) - { - logger.LogInformation("Vehicle {Id} retrieved from cache", id); - return vehicle; - } - } - } - catch (Exception ex) - { - // отлавливаем падение при недоступном Redis - logger.LogWarning(ex, "Cache read failed for vehicle {Id}. Falling back to generation.", id); - } - - // если кэш упал - делегируем генерацию сервису - var generatedVehicle = await innerService.FetchByIdAsync(id, cancellationToken); - - try - { - var opts = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_ttlMinutes) - }; - - await cache.SetStringAsync( - key, - JsonSerializer.Serialize(generatedVehicle), - opts, - cancellationToken); - - logger.LogInformation("Vehicle {Id} successfully stored in cache.", id); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to store vehicle {Id} in cache.", id); - } - - return generatedVehicle; - } -} diff --git a/ProjectApp.AppHost/Program.cs b/ProjectApp.AppHost/Program.cs index 13bf1f02..f086b3e6 100644 --- a/ProjectApp.AppHost/Program.cs +++ b/ProjectApp.AppHost/Program.cs @@ -9,7 +9,6 @@ .WithEndpoint("http", e => e.Port = 5179); builder.AddProject("client") - .WithReference(api) .WaitFor(api); builder.Build().Run(); diff --git a/ProjectApp.Domain/Entities/Vehicle.cs b/ProjectApp.Domain/Entities/Vehicle.cs index 18c773e8..67fa639d 100644 --- a/ProjectApp.Domain/Entities/Vehicle.cs +++ b/ProjectApp.Domain/Entities/Vehicle.cs @@ -5,14 +5,33 @@ namespace ProjectApp.Domain.Entities; /// public class Vehicle { + /// Уникальный идентификатор транспортного средства в системе public required int Id { get; set; } + + /// Марка (производитель) транспортного средства public required string Brand { get; set; } + + /// Модель (комплектация) транспортного средства public required string Model { get; set; } + + /// Государственный регистрационный номер public required string RegistrationNumber { get; set; } + + /// Полное имя владельца транспортного средства public required string OwnerName { get; set; } + + /// Год выпуска транспортного средства public int Year { get; set; } + + /// Объём двигателя в литрах public decimal EngineVolume { get; set; } + + /// Пробег в километрах public int Mileage { get; set; } + + /// Тип используемого топлива public required string FuelType { get; set; } + + /// Рыночная стоимость транспортного средства в рублях public decimal Price { get; set; } } From ff3b0e0a665e9e5728fbf884ed33c2a98b8631c5 Mon Sep 17 00:00:00 2001 From: MagGoldi Date: Wed, 25 Mar 2026 12:15:59 +0400 Subject: [PATCH 4/5] brand-model dictionary for more realistic data generation --- .../VehicleGeneratorService/VehicleFaker.cs | 45 +++++++++++-------- ProjectApp.Domain/Entities/Vehicle.cs | 2 +- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs index 70ffb82e..6aa39754 100644 --- a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs +++ b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs @@ -7,17 +7,24 @@ public class VehicleFaker { private static readonly string[] FuelTypes = ["Бензин", "Дизель", "Электро", "Гибрид", "Газ"]; - private static readonly string[] Brands = - [ - "Toyota", "BMW", "Mercedes", "Volkswagen", "Hyundai", - "Ford", "Kia", "Audi", "Nissan", "Renault" - ]; - - private static readonly string[] Trims = - [ - "Comfort", "Sport", "Elite", "Plus", "Pro", - "Active", "Max", "Base", "Premium", "Line" - ]; + private static readonly Dictionary BrandModels = new() + { + ["Toyota"] = ["Camry", "Corolla", "RAV4", "Land Cruiser", "Yaris", "Highlander", "Prado"], + ["BMW"] = ["3 Series", "5 Series", "7 Series", "X3", "X5", "X6", "M4"], + ["Mercedes"] = ["C-Class", "E-Class", "S-Class", "GLE", "GLC", "A-Class", "CLA"], + ["Volkswagen"] = ["Passat", "Golf", "Tiguan", "Polo", "Touareg", "Jetta", "ID.4"], + ["Hyundai"] = ["Solaris", "Tucson", "Santa Fe", "Creta", "Elantra", "Sonata", "i30"], + ["Ford"] = ["Focus", "Mondeo", "Explorer", "Kuga", "Puma", "Mustang", "Ranger"], + ["Kia"] = ["Rio", "Sportage", "Sorento", "Ceed", "K5", "Seltos", "Stinger"], + ["Audi"] = ["A3", "A4", "A6", "Q3", "Q5", "Q7", "TT"], + ["Nissan"] = ["Qashqai", "X-Trail", "Altima", "Leaf", "Juke", "Murano", "Note"], + ["Renault"] = ["Logan", "Duster", "Sandero", "Megane", "Arkana", "Captur", "Laguna"], + ["Lada"] = ["Granta", "Vesta", "Largus", "Niva Travel", "XRAY", "4x4"], + ["Skoda"] = ["Octavia", "Superb", "Kodiaq", "Karoq", "Fabia", "Scala", "Kamiq"], + ["Mazda"] = ["Mazda3", "Mazda6", "CX-5", "CX-9", "MX-5", "CX-30"], + ["Honda"] = ["Civic", "Accord", "CR-V", "HR-V", "Jazz", "Pilot"], + ["Subaru"] = ["Outback", "Forester", "Impreza", "XV", "Legacy", "WRX"], + }; private const string RegistrationLetters = "АВЕКМНОРСТУХ"; @@ -27,23 +34,23 @@ public VehicleFaker() { _faker = new Faker("ru") .RuleFor(v => v.Id, f => f.IndexFaker + 1) - .RuleFor(v => v.Brand, f => f.PickRandom(Brands)) - .RuleFor(v => v.Model, f => f.PickRandom(Trims)) + .RuleFor(v => v.Brand, f => f.PickRandom(BrandModels.Keys.ToArray())) + .RuleFor(v => v.Model, (f, v) => f.PickRandom(BrandModels[v.Brand])) .RuleFor(v => v.RegistrationNumber, f => { - var letter1 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; + var l1 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; var digits = f.Random.Int(100, 999); - var letter2 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; - var letter3 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; + var l2 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; + var l3 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; var region = f.Random.Int(10, 199); - return $"{letter1}{digits}{letter2}{letter3} {region}"; + return $"{l1}{digits}{l2}{l3} {region}"; }) .RuleFor(v => v.OwnerName, f => f.Name.FullName()) .RuleFor(v => v.Year, f => f.Random.Int(1984, 2026)) .RuleFor(v => v.EngineVolume, f => Math.Round(f.Random.Decimal(0.8m, 6.0m), 1)) - .RuleFor(v => v.Mileage, f => f.Random.Int(0, 500000)) + .RuleFor(v => v.Mileage, f => f.Random.Int(0, 500_000)) .RuleFor(v => v.FuelType, f => f.PickRandom(FuelTypes)) - .RuleFor(v => v.Price, f => Math.Round(f.Random.Decimal(100000m, 10000000m), 0)); + .RuleFor(v => v.Price, f => Math.Round(f.Random.Decimal(100_000m, 10_000_000m), 0)); } public Vehicle Generate() => _faker.Generate(); diff --git a/ProjectApp.Domain/Entities/Vehicle.cs b/ProjectApp.Domain/Entities/Vehicle.cs index 67fa639d..d8d74e85 100644 --- a/ProjectApp.Domain/Entities/Vehicle.cs +++ b/ProjectApp.Domain/Entities/Vehicle.cs @@ -11,7 +11,7 @@ public class Vehicle /// Марка (производитель) транспортного средства public required string Brand { get; set; } - /// Модель (комплектация) транспортного средства + /// Модель транспортного средства public required string Model { get; set; } /// Государственный регистрационный номер From bc7294b7ddaca0616e473a4a0ecef5201831afb7 Mon Sep 17 00:00:00 2001 From: MagGoldi Date: Sun, 29 Mar 2026 16:03:10 +0400 Subject: [PATCH 5/5] corrections 2 --- .../VehicleGeneratorService/VehicleFaker.cs | 70 +++++++++---------- ProjectApp.Domain/Entities/Vehicle.cs | 28 ++++---- README.md | 37 +++++----- 3 files changed, 67 insertions(+), 68 deletions(-) diff --git a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs index 6aa39754..55e9d193 100644 --- a/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs +++ b/ProjectApp.Api/Services/VehicleGeneratorService/VehicleFaker.cs @@ -5,52 +5,52 @@ namespace ProjectApp.Api.Services.VehicleGeneratorService; public class VehicleFaker { - private static readonly string[] FuelTypes = ["Бензин", "Дизель", "Электро", "Гибрид", "Газ"]; + private static readonly string[] _fuelTypes = ["Бензин", "Дизель", "Электро", "Гибрид", "Газ"]; - private static readonly Dictionary BrandModels = new() + private static readonly string[] _bodyTypes = ["Седан", "Хэтчбек", "Универсал", "Кроссовер", "Внедорожник", "Купе", "Минивэн", "Пикап"]; + + private static readonly Dictionary _brandModels = new() { - ["Toyota"] = ["Camry", "Corolla", "RAV4", "Land Cruiser", "Yaris", "Highlander", "Prado"], - ["BMW"] = ["3 Series", "5 Series", "7 Series", "X3", "X5", "X6", "M4"], - ["Mercedes"] = ["C-Class", "E-Class", "S-Class", "GLE", "GLC", "A-Class", "CLA"], - ["Volkswagen"] = ["Passat", "Golf", "Tiguan", "Polo", "Touareg", "Jetta", "ID.4"], - ["Hyundai"] = ["Solaris", "Tucson", "Santa Fe", "Creta", "Elantra", "Sonata", "i30"], - ["Ford"] = ["Focus", "Mondeo", "Explorer", "Kuga", "Puma", "Mustang", "Ranger"], - ["Kia"] = ["Rio", "Sportage", "Sorento", "Ceed", "K5", "Seltos", "Stinger"], - ["Audi"] = ["A3", "A4", "A6", "Q3", "Q5", "Q7", "TT"], - ["Nissan"] = ["Qashqai", "X-Trail", "Altima", "Leaf", "Juke", "Murano", "Note"], - ["Renault"] = ["Logan", "Duster", "Sandero", "Megane", "Arkana", "Captur", "Laguna"], - ["Lada"] = ["Granta", "Vesta", "Largus", "Niva Travel", "XRAY", "4x4"], - ["Skoda"] = ["Octavia", "Superb", "Kodiaq", "Karoq", "Fabia", "Scala", "Kamiq"], - ["Mazda"] = ["Mazda3", "Mazda6", "CX-5", "CX-9", "MX-5", "CX-30"], - ["Honda"] = ["Civic", "Accord", "CR-V", "HR-V", "Jazz", "Pilot"], - ["Subaru"] = ["Outback", "Forester", "Impreza", "XV", "Legacy", "WRX"], + ["Toyota"] = ["Camry", "Corolla", "RAV4", "Land Cruiser", "Yaris", "Highlander", "Prado"], + ["BMW"] = ["3 Series", "5 Series", "7 Series", "X3", "X5", "X6", "M4"], + ["Mercedes"] = ["C-Class", "E-Class", "S-Class", "GLE", "GLC", "A-Class", "CLA"], + ["Volkswagen"] = ["Passat", "Golf", "Tiguan", "Polo", "Touareg", "Jetta", "ID.4"], + ["Hyundai"] = ["Solaris", "Tucson", "Santa Fe", "Creta", "Elantra", "Sonata", "i30"], + ["Ford"] = ["Focus", "Mondeo", "Explorer", "Kuga", "Puma", "Mustang", "Ranger"], + ["Kia"] = ["Rio", "Sportage", "Sorento", "Ceed", "K5", "Seltos", "Stinger"], + ["Audi"] = ["A3", "A4", "A6", "Q3", "Q5", "Q7", "TT"], + ["Nissan"] = ["Qashqai", "X-Trail", "Altima", "Leaf", "Juke", "Murano", "Note"], + ["Renault"] = ["Logan", "Duster", "Sandero", "Megane", "Arkana", "Captur", "Laguna"], + ["Lada"] = ["Granta", "Vesta", "Largus", "Niva Travel", "XRAY", "4x4"], + ["Skoda"] = ["Octavia", "Superb", "Kodiaq", "Karoq", "Fabia", "Scala", "Kamiq"], + ["Mazda"] = ["Mazda3", "Mazda6", "CX-5", "CX-9", "MX-5", "CX-30"], + ["Honda"] = ["Civic", "Accord", "CR-V", "HR-V", "Jazz", "Pilot"], + ["Subaru"] = ["Outback", "Forester", "Impreza", "XV", "Legacy", "WRX"], }; - private const string RegistrationLetters = "АВЕКМНОРСТУХ"; - private readonly Faker _faker; public VehicleFaker() { + var currentYear = DateTime.Now.Year; + _faker = new Faker("ru") .RuleFor(v => v.Id, f => f.IndexFaker + 1) - .RuleFor(v => v.Brand, f => f.PickRandom(BrandModels.Keys.ToArray())) - .RuleFor(v => v.Model, (f, v) => f.PickRandom(BrandModels[v.Brand])) - .RuleFor(v => v.RegistrationNumber, f => + .RuleFor(v => v.Vin, f => f.Vehicle.Vin()) + .RuleFor(v => v.Brand, f => f.PickRandom(_brandModels.Keys.ToArray())) + .RuleFor(v => v.Model, (f, v) => f.PickRandom(_brandModels[v.Brand])) + .RuleFor(v => v.Year, f => f.Random.Int(1984, currentYear)) + .RuleFor(v => v.BodyType, f => f.PickRandom(_bodyTypes)) + .RuleFor(v => v.FuelType, f => f.PickRandom(_fuelTypes)) + .RuleFor(v => v.Color, f => f.Commerce.Color()) + .RuleFor(v => v.Mileage, (f, v) => Math.Round(f.Random.Double(0, 500_000), 1)) + .RuleFor(v => v.LastServiceDate, (f, v) => { - var l1 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; - var digits = f.Random.Int(100, 999); - var l2 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; - var l3 = RegistrationLetters[f.Random.Int(0, RegistrationLetters.Length - 1)]; - var region = f.Random.Int(10, 199); - return $"{l1}{digits}{l2}{l3} {region}"; - }) - .RuleFor(v => v.OwnerName, f => f.Name.FullName()) - .RuleFor(v => v.Year, f => f.Random.Int(1984, 2026)) - .RuleFor(v => v.EngineVolume, f => Math.Round(f.Random.Decimal(0.8m, 6.0m), 1)) - .RuleFor(v => v.Mileage, f => f.Random.Int(0, 500_000)) - .RuleFor(v => v.FuelType, f => f.PickRandom(FuelTypes)) - .RuleFor(v => v.Price, f => Math.Round(f.Random.Decimal(100_000m, 10_000_000m), 0)); + var minDate = new DateOnly(v.Year, 1, 1); + var maxDate = DateOnly.FromDateTime(DateTime.Now); + var range = maxDate.DayNumber - minDate.DayNumber; + return minDate.AddDays(f.Random.Int(0, range)); + }); } public Vehicle Generate() => _faker.Generate(); diff --git a/ProjectApp.Domain/Entities/Vehicle.cs b/ProjectApp.Domain/Entities/Vehicle.cs index d8d74e85..250ec9a3 100644 --- a/ProjectApp.Domain/Entities/Vehicle.cs +++ b/ProjectApp.Domain/Entities/Vehicle.cs @@ -8,30 +8,30 @@ public class Vehicle /// Уникальный идентификатор транспортного средства в системе public required int Id { get; set; } - /// Марка (производитель) транспортного средства + /// VIN-номер транспортного средства + public required string Vin { get; set; } + + /// Производитель транспортного средства public required string Brand { get; set; } /// Модель транспортного средства public required string Model { get; set; } - /// Государственный регистрационный номер - public required string RegistrationNumber { get; set; } - - /// Полное имя владельца транспортного средства - public required string OwnerName { get; set; } - /// Год выпуска транспортного средства public int Year { get; set; } - /// Объём двигателя в литрах - public decimal EngineVolume { get; set; } - - /// Пробег в километрах - public int Mileage { get; set; } + /// Тип корпуса (кузова) + public required string BodyType { get; set; } /// Тип используемого топлива public required string FuelType { get; set; } - /// Рыночная стоимость транспортного средства в рублях - public decimal Price { get; set; } + /// Цвет корпуса + public required string Color { get; set; } + + /// Пробег в километрах + public double Mileage { get; set; } + + /// Дата последнего техобслуживания + public DateOnly LastServiceDate { get; set; } } diff --git a/README.md b/README.md index ddced879..8497b667 100644 --- a/README.md +++ b/README.md @@ -17,25 +17,24 @@ ## Характеристики генерируемого транспортного средства 1. Идентификатор в системе — `int` -2. Марка — `string` -3. Модель — `string` -4. Регистрационный номер — `string` -5. Владелец — `string` -6. Год выпуска — `int` -7. Объём двигателя (л) — `decimal` -8. Пробег (км) — `int` -9. Тип топлива — `string` -10. Стоимость (₽) — `decimal` +2. VIN-номер — `string` +3. Производитель — `string` +4. Модель — `string` +5. Год выпуска — `int` +6. Тип корпуса — `string` +7. Тип топлива — `string` +8. Цвет корпуса — `string` +9. Пробег — `double` +10. Дата последнего техобслуживания — `DateOnly` ## Правила генерации -- Марка: случайно выбирается из фиксированного списка популярных производителей. -- Модель: случайная комплектация из набора суффиксов. -- Регистрационный номер: форматируется по схеме `А000АА 000` с кириллическими буквами. -- Владелец: полное имя (Name.FullName). -- Год выпуска: от 1984 до 2026. -- Объём двигателя: от 0.8 до 6.0 литра с округлением до 1 знака. -- Пробег: от 0 до 500 000 км. -- Тип топлива: выбирается из списка (Бензин, Дизель, Электро, Гибрид, Газ). -- Стоимость: от 100 000 до 10 000 000 ₽. - +- VIN-номер: берётся из раздела Vehicle (Bogus). +- Производитель: случайно выбирается из фиксированного списка популярных марок. +- Модель: выбирается из набора моделей, соответствующих производителю. +- Год выпуска: от 1984 до текущего года включительно. +- Тип корпуса: берётся из раздела Vehicle (Bogus). +- Тип топлива: берётся из раздела Vehicle (Bogus); выбирается из списка (Бензин, Дизель, Электро, Гибрид, Газ). +- Цвет корпуса: берётся из раздела Commerce (Bogus). +- Пробег: от 0 до 500 000 км, не может быть меньше нуля. +- Дата последнего техобслуживания: не ранее 1 января года выпуска и не позже сегодняшней даты.