From 0a769f764fbdc064909b52983b58ed2af4733207 Mon Sep 17 00:00:00 2001 From: Donistr Date: Sun, 22 Feb 2026 19:31:47 +0400 Subject: [PATCH 01/37] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20ServiceDefaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions.cs | 127 ++++++++++++++++++ ...ResidentialBuilding.ServiceDefaults.csproj | 26 ++++ 2 files changed, 153 insertions(+) create mode 100644 ResidentialBuilding.ServiceDefaults/Extensions.cs create mode 100644 ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj diff --git a/ResidentialBuilding.ServiceDefaults/Extensions.cs b/ResidentialBuilding.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..5e59484d --- /dev/null +++ b/ResidentialBuilding.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +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 ResidentialBuilding.ServiceDefaults; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj b/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj new file mode 100644 index 00000000..051b99a2 --- /dev/null +++ b/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + From 69e6dbafb1160f90e3d4b7fc4f722760f728a85a Mon Sep 17 00:00:00 2001 From: Donistr Date: Sun, 22 Feb 2026 19:34:23 +0400 Subject: [PATCH 02/37] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=20Generator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ResidentialBuildingController.cs | 31 +++++++ .../DTO/ResidentialBuildingDto.cs | 57 ++++++++++++ .../Generator/ResidentialBuildingGenerator.cs | 93 +++++++++++++++++++ ResidentialBuilding.Generator/Program.cs | 46 +++++++++ .../Properties/launchSettings.json | 38 ++++++++ .../Service/IResidentialBuildingService.cs | 21 +++++ .../Service/ResidentialBuildingService.cs | 86 +++++++++++++++++ .../appsettings.Development.json | 11 +++ .../appsettings.json | 9 ++ 9 files changed, 392 insertions(+) create mode 100644 ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs create mode 100644 ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs create mode 100644 ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs create mode 100644 ResidentialBuilding.Generator/Program.cs create mode 100644 ResidentialBuilding.Generator/Properties/launchSettings.json create mode 100644 ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs create mode 100644 ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs create mode 100644 ResidentialBuilding.Generator/appsettings.Development.json create mode 100644 ResidentialBuilding.Generator/appsettings.json diff --git a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs new file mode 100644 index 00000000..5d921d27 --- /dev/null +++ b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs @@ -0,0 +1,31 @@ +using Generator.DTO; +using Generator.Service; +using Microsoft.AspNetCore.Mvc; + +namespace Generator.Controller; + +/// +/// Контроллер для объектов жилого строительства. +/// +[Route("api/residential-building")] +[ApiController] +public class ResidentialBuildingController(ILogger logger, IResidentialBuildingService residentialBuildingService) : ControllerBase +{ + /// + /// Получение объекта жилого строительства по id. + /// + [HttpGet("{id:int}")] + public async Task> GetResidentialBuilding(int id) + { + if (id <= 0) + { + return BadRequest("id must be >= 0"); + } + + logger.LogInformation("Getting residential building with Id={id}.", id); + var result = await residentialBuildingService.GetByIdAsync(id); + logger.LogInformation("Residential building with Id={id} successfully received.", id); + + return Ok(result); + } +} \ No newline at end of file diff --git a/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs b/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs new file mode 100644 index 00000000..2c9eac03 --- /dev/null +++ b/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs @@ -0,0 +1,57 @@ +namespace Generator.DTO; + +/// +/// DTO объекта жилого строительства +/// +public class ResidentialBuildingDto +{ + /// + /// Идентификатор в системе + /// + public int Id { get; set; } + + /// + /// Адрес + /// + public string Address { get; set; } = string.Empty; + + /// + /// Тип недвижимости + /// + public string PropertyType { get; set; } = string.Empty; + + /// + /// Год постройки + /// + public int BuildYear { get; set; } + + /// + /// Общая площадь + /// + public double TotalArea { get; set; } + + /// + /// Жилая площадь + /// + public double LivingArea { get; set; } + + /// + /// Этаж + /// + public int? Floor { get; set; } + + /// + /// Этажность + /// + public int TotalFloors { get; set; } + + /// + /// Кадастровый номер + /// + public string CadastralNumber { get; set; } = string.Empty; + + /// + /// Кадастровая стоимость + /// + public decimal CadastralValue { get; set; } +} \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs new file mode 100644 index 00000000..1d5a534e --- /dev/null +++ b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs @@ -0,0 +1,93 @@ +using Bogus; +using Generator.DTO; + +namespace Generator.Generator; + +/// +/// Генератор объектов жилого строительства на основе Bogus +/// +public class ResidentialBuildingGenerator(ILogger logger) +{ + private static readonly string[] _propertyTypes = + [ + "Квартира", + "ИЖС", + "Апартаменты", + "Офис" + ]; + + private const int MinBuildYear = 1900; + + private const double MinTotalArea = 10.0; + + private const double MaxTotalArea = 1000.0; + + private const double MinLivingAreaPartOfTotalArea = 0.5; + + private const double MaxLivingAreaPartOfTotalArea = 0.85; + + private const int MinTotalFloors = 1; + + private const int MaxTotalFloors = 100; + + private const double MinPricePerM2 = 35; + + private const double MaxPricePerM2 = 200; + + /// + /// Генерирует объект жилого строительства для заданного идентификатора + /// + /// Идентификатор объекта жилого строительства + /// Сгенерированный объект жилого строительства + public ResidentialBuildingDto Generate(int id) + { + logger.LogInformation("Generating Residential Building for Id={id}", id); + + var faker = new Faker("ru") + .RuleFor(x => x.Id, _ => id) + .RuleFor(x => x.Address, f => f.Address.FullAddress()) + .RuleFor(x => x.PropertyType, f => f.PickRandom(_propertyTypes)) + .RuleFor(x => x.BuildYear, f => f.Random.Int(MinBuildYear, DateTime.Today.Year)) + .RuleFor(x => x.TotalArea, f => Math.Round(f.Random.Double(MinTotalArea, MaxTotalArea), 2)) + .RuleFor(x => x.LivingArea, (f, dto) => + { + var livingAreaPartOfTotalArea = + f.Random.Double(MinLivingAreaPartOfTotalArea, MaxLivingAreaPartOfTotalArea); + return Math.Round(livingAreaPartOfTotalArea * dto.TotalArea, 2); + }) + .RuleFor(x => x.TotalFloors, f => f.Random.Int(MinTotalFloors, MaxTotalFloors)) + .RuleFor(x => x.Floor, (f, dto) => + { + if (dto.PropertyType is "ИЖС") + { + return null; + } + + return f.Random.Int(1, dto.TotalFloors); + }) + .RuleFor(x => x.CadastralNumber, f => + $"{f.Random.Int(1, 99):D2}:" + + $"{f.Random.Int(1, 99):D2}:" + + $"{f.Random.Int(1, 9999999):D7}:" + + $"{f.Random.Int(1, 9999):D4}") + .RuleFor(x => x.CadastralValue, (f, dto) => + { + var pricePerM2 = f.Random.Double(MinPricePerM2, MaxPricePerM2); + var price = dto.TotalArea * pricePerM2; + return (decimal)Math.Round(price, 2); + }); + + var generatedObject = faker.Generate(); + + logger.LogInformation( + "Residential building generated: Id={Id}, Address='{Address}', PropertyType='{PropertyType}', " + + "BuildYear={BuildYear}, TotalArea={TotalArea}, LivingArea={LivingArea}, Floor={Floor}, " + + "TotalFloors={TotalFloors}, CadastralNumber='{CadastralNumber}', CadastralValue={CadastralValue}", + generatedObject.Id, generatedObject.Address, generatedObject.PropertyType, generatedObject.BuildYear, + generatedObject.TotalArea, generatedObject.LivingArea, generatedObject.Floor, generatedObject.TotalFloors, + generatedObject.CadastralNumber, generatedObject.CadastralValue + ); + + return generatedObject; + } +} \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Program.cs b/ResidentialBuilding.Generator/Program.cs new file mode 100644 index 00000000..760ae158 --- /dev/null +++ b/ResidentialBuilding.Generator/Program.cs @@ -0,0 +1,46 @@ +using Generator.Generator; +using Generator.Service; +using ResidentialBuilding.ServiceDefaults; +using Serilog; +using Serilog.Formatting.Compact; + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseSerilog((hostingContext, services, loggerConfiguration) => +{ + loggerConfiguration + .MinimumLevel.Information() + .ReadFrom.Configuration(hostingContext.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console( + formatter: new CompactJsonFormatter() + ); +}); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("residential-building-cache"); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowLocalDev", policy => + { + policy + .AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseCors("AllowLocalDev"); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Properties/launchSettings.json b/ResidentialBuilding.Generator/Properties/launchSettings.json new file mode 100644 index 00000000..ffb66c15 --- /dev/null +++ b/ResidentialBuilding.Generator/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:39434", + "sslPort": 44329 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5204", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7291;http://localhost:5204", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs new file mode 100644 index 00000000..02476703 --- /dev/null +++ b/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs @@ -0,0 +1,21 @@ +using Generator.DTO; + +namespace Generator.Service; + +/// +/// Сервис получения объектов жилого строительства по идентификатору. +/// Если удалось найти объект в кэше - возвращает его, иначе генерирует, кэширует и возвращает сгенерированный. +/// +public interface IResidentialBuildingService +{ + /// + /// Пытается найти в кэше объект с заданным идентификатором: + /// если удалось, то десериализует объект из JSON-а и возвращает; + /// если не удалось или произошла ошибка в ходе получения/десериализации, то генерирует объект, сохраняет в кэш и + /// возвращает сгенерированный. + /// + /// Идентификатор объекта жилого строительства. + /// Токен отмены. + /// DTO объекта жилого строительства. + public Task GetByIdAsync(int id, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs new file mode 100644 index 00000000..7e3b5fef --- /dev/null +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using Generator.DTO; +using Generator.Generator; +using Microsoft.Extensions.Caching.Distributed; + +namespace Generator.Service; + +public class ResidentialBuildingService( + ILogger logger, + ResidentialBuildingGenerator generator, + IDistributedCache cache, + IConfiguration configuration + ) : IResidentialBuildingService +{ + private const string CacheKeyPrefix = "residential-building:"; + + private const int CacheExpirationTimeMinutesDefault = 15; + + private readonly TimeSpan _cacheExpirationTimeMinutes = TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationTimeMinutes", CacheExpirationTimeMinutesDefault)); + + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) + { + var cacheKey = $"{CacheKeyPrefix}{id}"; + + string? jsonCached = null; + try + { + jsonCached = await cache.GetStringAsync(cacheKey, cancellationToken); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to read from distributed cache for key={cacheKey}. Falling back to generation.", cacheKey); + } + + if (!string.IsNullOrEmpty(jsonCached)) + { + logger.LogInformation("Cache for residential building with Id={} received.", id); + + ResidentialBuildingDto? objCached = null; + try + { + objCached = JsonSerializer.Deserialize(jsonCached); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Invalid JSON in residential building cache for key {cacheKey}.", cacheKey); + } + + if (objCached is null) + { + logger.LogWarning("Cache for residential building with Id={id} returned null.", id); + } + else + { + logger.LogInformation("Cache for residential building with Id={id} is valid, returned", id); + return objCached; + } + } + + var obj = generator.Generate(id); + + try + { + await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), CreateCacheOptions(), cancellationToken); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to write residential building with Id={id} to cache. Still returning generated value.", id); + } + + logger.LogInformation("Generated and cached residential building with Id={id}", id); + + return obj; + } + + /// + /// Создаёт настройки кэша - задаёт время жизни кэша. + /// + private DistributedCacheEntryOptions CreateCacheOptions() + { + return new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheExpirationTimeMinutes + }; + } +} \ No newline at end of file diff --git a/ResidentialBuilding.Generator/appsettings.Development.json b/ResidentialBuilding.Generator/appsettings.Development.json new file mode 100644 index 00000000..af36b864 --- /dev/null +++ b/ResidentialBuilding.Generator/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "CacheSettings": { + "ExpirationTimeMinutes": 5 + } +} diff --git a/ResidentialBuilding.Generator/appsettings.json b/ResidentialBuilding.Generator/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/ResidentialBuilding.Generator/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 9b66e86249b49db7e50de3c9b4d173da3962ea77 Mon Sep 17 00:00:00 2001 From: Donistr Date: Sun, 22 Feb 2026 19:37:29 +0400 Subject: [PATCH 03/37] =?UTF-8?q?fix:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B,=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B=D0=B5=20=D0=B7=D0=B0=D0=B1=D1=8B=D0=BB?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D0=B8=D1=82?= =?UTF-8?q?=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudDevelopment.sln | 18 +++++++++++++++++ .../ResidentialBuilding.Generator.csproj | 20 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..da11d6ee 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -5,6 +5,12 @@ VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.Generator", "ResidentialBuilding.Generator\ResidentialBuilding.Generator.csproj", "{4C3748F7-BE7B-4C97-A656-D0D467E4BC5D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.AppHost", "ResidentialBuilding.AppHost\ResidentialBuilding.AppHost.csproj", "{248C1F9B-F012-4C7C-A458-4E2D0F918A70}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.ServiceDefaults", "ResidentialBuilding.ServiceDefaults\ResidentialBuilding.ServiceDefaults.csproj", "{3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +21,18 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {4C3748F7-BE7B-4C97-A656-D0D467E4BC5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C3748F7-BE7B-4C97-A656-D0D467E4BC5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C3748F7-BE7B-4C97-A656-D0D467E4BC5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C3748F7-BE7B-4C97-A656-D0D467E4BC5D}.Release|Any CPU.Build.0 = Release|Any CPU + {248C1F9B-F012-4C7C-A458-4E2D0F918A70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {248C1F9B-F012-4C7C-A458-4E2D0F918A70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {248C1F9B-F012-4C7C-A458-4E2D0F918A70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {248C1F9B-F012-4C7C-A458-4E2D0F918A70}.Release|Any CPU.Build.0 = Release|Any CPU + {3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj b/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj new file mode 100644 index 00000000..07dcd97c --- /dev/null +++ b/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + Generator + + + + + + + + + + + + + From 4eaaee0d48ba047f9dac95111cc748bf649a7763 Mon Sep 17 00:00:00 2001 From: Donistr Date: Sun, 22 Feb 2026 19:38:09 +0400 Subject: [PATCH 04/37] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20AppHost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.AppHost/AppHost.cs | 14 +++++++++ .../Properties/launchSettings.json | 29 +++++++++++++++++++ .../ResidentialBuilding.AppHost.csproj | 23 +++++++++++++++ .../appsettings.Development.json | 8 +++++ ResidentialBuilding.AppHost/appsettings.json | 9 ++++++ 5 files changed, 83 insertions(+) create mode 100644 ResidentialBuilding.AppHost/AppHost.cs create mode 100644 ResidentialBuilding.AppHost/Properties/launchSettings.json create mode 100644 ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj create mode 100644 ResidentialBuilding.AppHost/appsettings.Development.json create mode 100644 ResidentialBuilding.AppHost/appsettings.json diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs new file mode 100644 index 00000000..2ed01edc --- /dev/null +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -0,0 +1,14 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("residential-building-cache") + .WithRedisInsight(containerName: "residential-building-insight"); + +var generator = builder.AddProject("generator") + .WithReference(cache, "residential-building-cache") + .WaitFor(cache); + +var client = builder.AddProject("client") + .WithReference(generator) + .WaitFor(generator); + +builder.Build().Run(); \ No newline at end of file diff --git a/ResidentialBuilding.AppHost/Properties/launchSettings.json b/ResidentialBuilding.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..330da1c2 --- /dev/null +++ b/ResidentialBuilding.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:17129;http://localhost:15221", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21101", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22255" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15221", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19083", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20274" + } + } + } +} \ No newline at end of file diff --git a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj new file mode 100644 index 00000000..83be43b7 --- /dev/null +++ b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net8.0 + enable + enable + ed7e1e47-dc98-4419-8424-85412466aa9b + + + + + + + + + + + + + diff --git a/ResidentialBuilding.AppHost/appsettings.Development.json b/ResidentialBuilding.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ResidentialBuilding.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ResidentialBuilding.AppHost/appsettings.json b/ResidentialBuilding.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/ResidentialBuilding.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} From 52dc141366e0f9dc5b5280db0373e353adab7856 Mon Sep 17 00:00:00 2001 From: Donistr Date: Sun, 22 Feb 2026 19:39:05 +0400 Subject: [PATCH 05/37] =?UTF-8?q?fix:=20=D0=BF=D0=BE=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20=D0=B2=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D1=83?= =?UTF-8?q?=20=D1=81=D1=82=D1=83=D0=B4=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D1=87=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=20url=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D1=81=D0=BE?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client.Wasm/Components/DataCard.razor | 2 +- Client.Wasm/Components/StudentCard.razor | 8 ++++---- Client.Wasm/wwwroot/appsettings.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index c646a839..307d98ce 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -68,7 +68,7 @@ private async Task RequestNewData() { var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); + Value = await Client.GetFromJsonAsync($"{baseAddress}/{Id}", new JsonSerializerOptions { }); StateHasChanged(); } } diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..eae33b0e 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,10 +4,10 @@ - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №1 «Кэширование» + Вариант №38 "Объект жилого строительства" + Выполнена Елагиным Денисом 6513 + Ссылка на форк diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..47f7d32e 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "http://localhost:5204/api/residential-building" } From 70c225b3b43fdec93342d9de209797ee6b94ac36 Mon Sep 17 00:00:00 2001 From: Donistr Date: Sun, 22 Feb 2026 19:46:44 +0400 Subject: [PATCH 06/37] =?UTF-8?q?style:=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BB=20code=20cleanup=20=D0=B2=D0=BE=20=D0=B2?= =?UTF-8?q?=D1=81=D1=91=D0=BC=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 23 ++++---- Client.Wasm/App.razor | 4 +- Client.Wasm/Client.Wasm.csproj | 30 +++++------ Client.Wasm/Components/DataCard.razor | 31 +++++++---- Client.Wasm/Components/StudentCard.razor | 11 ++-- Client.Wasm/Layout/MainLayout.razor.css | 24 ++++----- Client.Wasm/Program.cs | 2 +- Client.Wasm/wwwroot/css/app.css | 50 +++++++++--------- Client.Wasm/wwwroot/index.html | 45 ++++++++-------- ResidentialBuilding.AppHost/AppHost.cs | 10 ++-- .../ResidentialBuilding.AppHost.csproj | 14 ++--- .../ResidentialBuildingController.cs | 12 +++-- .../DTO/ResidentialBuildingDto.cs | 52 +++++++++---------- .../Generator/ResidentialBuildingGenerator.cs | 40 +++++++------- ResidentialBuilding.Generator/Program.cs | 8 +-- .../ResidentialBuilding.Generator.csproj | 8 +-- .../Service/IResidentialBuildingService.cs | 12 ++--- .../Service/ResidentialBuildingService.cs | 33 +++++++----- .../Extensions.cs | 11 ++-- ...ResidentialBuilding.ServiceDefaults.csproj | 8 +-- 20 files changed, 228 insertions(+), 200 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0f3bba5c..a8a58a0e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -43,6 +43,7 @@ csharp_style_var_for_built_in_types = true:error csharp_style_var_when_type_is_apparent = true:error csharp_style_var_elsewhere = false:silent csharp_space_around_binary_operators = before_and_after + [*.{cs,vb}] #### Naming styles #### @@ -64,31 +65,31 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 diff --git a/Client.Wasm/App.razor b/Client.Wasm/App.razor index 6fd3ed1b..a8cf8177 100644 --- a/Client.Wasm/App.razor +++ b/Client.Wasm/App.razor @@ -1,7 +1,7 @@  - - + + Not found diff --git a/Client.Wasm/Client.Wasm.csproj b/Client.Wasm/Client.Wasm.csproj index 0ba9f90c..292e7634 100644 --- a/Client.Wasm/Client.Wasm.csproj +++ b/Client.Wasm/Client.Wasm.csproj @@ -1,21 +1,21 @@  - - net8.0 - enable - enable - + + net8.0 + enable + enable + - - - + + + - - - - - - - + + + + + + + diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index 307d98ce..ad0ad2fc 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -1,22 +1,25 @@ -@inject IConfiguration Configuration +@inject IConfiguration Configuration @inject HttpClient Client - Характеристики текущего объекта + + + Характеристики текущего объекта + - +
- + # Характеристика Значение - + - @if(Value is null) + @if (Value is null) { 1 @@ -30,7 +33,7 @@ foreach (var property in array) { - @(Array.IndexOf(array, property)+1) + @(Array.IndexOf(array, property) + 1) @property.Key @property.Value?.ToString() @@ -40,10 +43,13 @@
- + - Запросить новый объект + + + Запросить новый объект + @@ -54,7 +60,9 @@ - + @@ -68,7 +76,8 @@ private async Task RequestNewData() { var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}/{Id}", new JsonSerializerOptions { }); + Value = await Client.GetFromJsonAsync($"{baseAddress}/{Id}", new JsonSerializerOptions()); StateHasChanged(); } + } diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index eae33b0e..0613f8c7 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -1,13 +1,18 @@  - Лабораторная работа + + + Лабораторная работа + Номер №1 «Кэширование» Вариант №38 "Объект жилого строительства" - Выполнена Елагиным Денисом 6513 - Ссылка на форк + Выполнена Елагиным Денисом 6513 + + + Ссылка на форк diff --git a/Client.Wasm/Layout/MainLayout.razor.css b/Client.Wasm/Layout/MainLayout.razor.css index ecf25e5b..019d27b2 100644 --- a/Client.Wasm/Layout/MainLayout.razor.css +++ b/Client.Wasm/Layout/MainLayout.razor.css @@ -21,20 +21,20 @@ main { align-items: center; } - .top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; - } +.top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; +} - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } +.top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; +} - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } +.top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; +} @media (max-width: 640.98px) { .top-row { diff --git a/Client.Wasm/Program.cs b/Client.Wasm/Program.cs index a182a920..30bab128 100644 --- a/Client.Wasm/Program.cs +++ b/Client.Wasm/Program.cs @@ -14,4 +14,4 @@ .AddBootstrapProviders() .AddFontAwesomeIcons(); -await builder.Build().RunAsync(); +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Client.Wasm/wwwroot/css/app.css b/Client.Wasm/wwwroot/css/app.css index 54a8aa38..cc1cfb1e 100644 --- a/Client.Wasm/wwwroot/css/app.css +++ b/Client.Wasm/wwwroot/css/app.css @@ -17,7 +17,7 @@ a, .btn-link { } .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; } .content { @@ -48,12 +48,12 @@ a, .btn-link { z-index: 1000; } - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; @@ -61,9 +61,9 @@ a, .btn-link { color: white; } - .blazor-error-boundary::after { - content: "An error has occurred." - } +.blazor-error-boundary::after { + content: "An error has occurred." +} .loading-progress { position: relative; @@ -73,19 +73,19 @@ a, .btn-link { margin: 20vh auto 1rem auto; } - .loading-progress circle { - fill: none; - stroke: #e0e0e0; - stroke-width: 0.6rem; - transform-origin: 50% 50%; - transform: rotate(-90deg); - } +.loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); +} - .loading-progress circle:last-child { - stroke: #1b6ec2; - stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; - transition: stroke-dasharray 0.05s ease-in-out; - } +.loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; +} .loading-progress-text { position: absolute; @@ -94,9 +94,9 @@ a, .btn-link { inset: calc(20vh + 3.25rem) 0 auto 0.2rem; } - .loading-progress-text:after { - content: var(--blazor-load-percentage-text, "Loading"); - } +.loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); +} code { color: #c02d76; diff --git a/Client.Wasm/wwwroot/index.html b/Client.Wasm/wwwroot/index.html index b74ee328..eb7d7c12 100644 --- a/Client.Wasm/wwwroot/index.html +++ b/Client.Wasm/wwwroot/index.html @@ -2,34 +2,35 @@ - - + + Client.Wasm - - - + + + - - - - + + + + -
- - - - -
-
+
+ + + + +
+
-
- An unhandled error has occurred. - Reload - 🗙 -
- +
+ An unhandled error has occurred. + Reload + 🗙 +
+ diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs index 2ed01edc..d7182a3a 100644 --- a/ResidentialBuilding.AppHost/AppHost.cs +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -1,13 +1,15 @@ -var builder = DistributedApplication.CreateBuilder(args); +using Projects; -var cache = builder.AddRedis("residential-building-cache") +IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args); + +IResourceBuilder cache = builder.AddRedis("residential-building-cache") .WithRedisInsight(containerName: "residential-building-insight"); -var generator = builder.AddProject("generator") +IResourceBuilder generator = builder.AddProject("generator") .WithReference(cache, "residential-building-cache") .WaitFor(cache); -var client = builder.AddProject("client") +IResourceBuilder client = builder.AddProject("client") .WithReference(generator) .WaitFor(generator); diff --git a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj index 83be43b7..cf0a5e19 100644 --- a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj +++ b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj @@ -1,7 +1,7 @@ - - + + Exe net8.0 @@ -11,13 +11,13 @@ - - + + - - + + - + diff --git a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs index 5d921d27..1f6ab24f 100644 --- a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs +++ b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs @@ -5,14 +5,16 @@ namespace Generator.Controller; /// -/// Контроллер для объектов жилого строительства. +/// Контроллер для объектов жилого строительства. /// [Route("api/residential-building")] [ApiController] -public class ResidentialBuildingController(ILogger logger, IResidentialBuildingService residentialBuildingService) : ControllerBase +public class ResidentialBuildingController( + ILogger logger, + IResidentialBuildingService residentialBuildingService) : ControllerBase { /// - /// Получение объекта жилого строительства по id. + /// Получение объекта жилого строительства по id. /// [HttpGet("{id:int}")] public async Task> GetResidentialBuilding(int id) @@ -21,9 +23,9 @@ public async Task> GetResidentialBuilding(i { return BadRequest("id must be >= 0"); } - + logger.LogInformation("Getting residential building with Id={id}.", id); - var result = await residentialBuildingService.GetByIdAsync(id); + ResidentialBuildingDto result = await residentialBuildingService.GetByIdAsync(id); logger.LogInformation("Residential building with Id={id} successfully received.", id); return Ok(result); diff --git a/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs b/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs index 2c9eac03..f62c40a1 100644 --- a/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs +++ b/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs @@ -1,57 +1,57 @@ namespace Generator.DTO; /// -/// DTO объекта жилого строительства +/// DTO объекта жилого строительства. /// public class ResidentialBuildingDto { /// - /// Идентификатор в системе + /// Идентификатор в системе. /// public int Id { get; set; } - + /// - /// Адрес + /// Адрес. /// public string Address { get; set; } = string.Empty; - + /// - /// Тип недвижимости + /// Тип недвижимости. /// public string PropertyType { get; set; } = string.Empty; - + /// - /// Год постройки + /// Год постройки. /// - public int BuildYear { get; set; } - + public int BuildYear { get; set; } + /// - /// Общая площадь + /// Общая площадь. /// - public double TotalArea { get; set; } - + public double TotalArea { get; set; } + /// - /// Жилая площадь + /// Жилая площадь. /// - public double LivingArea { get; set; } - + public double LivingArea { get; set; } + /// - /// Этаж + /// Этаж. /// public int? Floor { get; set; } - + /// - /// Этажность + /// Этажность. /// - public int TotalFloors { get; set; } - + public int TotalFloors { get; set; } + /// - /// Кадастровый номер + /// Кадастровый номер. /// - public string CadastralNumber { get; set; } = string.Empty; - + public string CadastralNumber { get; set; } = string.Empty; + /// - /// Кадастровая стоимость + /// Кадастровая стоимость. /// - public decimal CadastralValue { get; set; } + public decimal CadastralValue { get; set; } } \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs index 1d5a534e..7f68d3b6 100644 --- a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs +++ b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs @@ -4,18 +4,10 @@ namespace Generator.Generator; /// -/// Генератор объектов жилого строительства на основе Bogus +/// Генератор объектов жилого строительства на основе Bogus. /// public class ResidentialBuildingGenerator(ILogger logger) { - private static readonly string[] _propertyTypes = - [ - "Квартира", - "ИЖС", - "Апартаменты", - "Офис" - ]; - private const int MinBuildYear = 1900; private const double MinTotalArea = 10.0; @@ -34,16 +26,24 @@ public class ResidentialBuildingGenerator(ILogger private const double MaxPricePerM2 = 200; + private static readonly string[] _propertyTypes = + [ + "Квартира", + "ИЖС", + "Апартаменты", + "Офис" + ]; + /// - /// Генерирует объект жилого строительства для заданного идентификатора + /// Генерирует объект жилого строительства для заданного идентификатора. /// - /// Идентификатор объекта жилого строительства - /// Сгенерированный объект жилого строительства + /// Идентификатор объекта жилого строительства. + /// Сгенерированный объект жилого строительства. public ResidentialBuildingDto Generate(int id) { logger.LogInformation("Generating Residential Building for Id={id}", id); - - var faker = new Faker("ru") + + Faker? faker = new Faker("ru") .RuleFor(x => x.Id, _ => id) .RuleFor(x => x.Address, f => f.Address.FullAddress()) .RuleFor(x => x.PropertyType, f => f.PickRandom(_propertyTypes)) @@ -77,16 +77,16 @@ public ResidentialBuildingDto Generate(int id) return (decimal)Math.Round(price, 2); }); - var generatedObject = faker.Generate(); - + ResidentialBuildingDto? generatedObject = faker.Generate(); + logger.LogInformation( "Residential building generated: Id={Id}, Address='{Address}', PropertyType='{PropertyType}', " + "BuildYear={BuildYear}, TotalArea={TotalArea}, LivingArea={LivingArea}, Floor={Floor}, " + - "TotalFloors={TotalFloors}, CadastralNumber='{CadastralNumber}', CadastralValue={CadastralValue}", - generatedObject.Id, generatedObject.Address, generatedObject.PropertyType, generatedObject.BuildYear, - generatedObject.TotalArea, generatedObject.LivingArea, generatedObject.Floor, generatedObject.TotalFloors, + "TotalFloors={TotalFloors}, CadastralNumber='{CadastralNumber}', CadastralValue={CadastralValue}", + generatedObject.Id, generatedObject.Address, generatedObject.PropertyType, generatedObject.BuildYear, + generatedObject.TotalArea, generatedObject.LivingArea, generatedObject.Floor, generatedObject.TotalFloors, generatedObject.CadastralNumber, generatedObject.CadastralValue - ); + ); return generatedObject; } diff --git a/ResidentialBuilding.Generator/Program.cs b/ResidentialBuilding.Generator/Program.cs index 760ae158..4412dc53 100644 --- a/ResidentialBuilding.Generator/Program.cs +++ b/ResidentialBuilding.Generator/Program.cs @@ -4,7 +4,7 @@ using Serilog; using Serilog.Formatting.Compact; -var builder = WebApplication.CreateBuilder(args); +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog((hostingContext, services, loggerConfiguration) => { @@ -14,7 +14,7 @@ .ReadFrom.Services(services) .Enrich.FromLogContext() .WriteTo.Console( - formatter: new CompactJsonFormatter() + new CompactJsonFormatter() ); }); @@ -33,11 +33,11 @@ }); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddControllers(); -var app = builder.Build(); +WebApplication app = builder.Build(); app.UseCors("AllowLocalDev"); diff --git a/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj b/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj index 07dcd97c..73d8dace 100644 --- a/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj +++ b/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj @@ -8,13 +8,13 @@ - - - + + + - + diff --git a/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs index 02476703..ae30a580 100644 --- a/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs @@ -3,16 +3,16 @@ namespace Generator.Service; /// -/// Сервис получения объектов жилого строительства по идентификатору. -/// Если удалось найти объект в кэше - возвращает его, иначе генерирует, кэширует и возвращает сгенерированный. +/// Сервис получения объектов жилого строительства по идентификатору. +/// Если удалось найти объект в кэше - возвращает его, иначе генерирует, кэширует и возвращает сгенерированный. /// public interface IResidentialBuildingService { /// - /// Пытается найти в кэше объект с заданным идентификатором: - /// если удалось, то десериализует объект из JSON-а и возвращает; - /// если не удалось или произошла ошибка в ходе получения/десериализации, то генерирует объект, сохраняет в кэш и - /// возвращает сгенерированный. + /// Пытается найти в кэше объект с заданным идентификатором: + /// если удалось, то десериализует объект из JSON-а и возвращает; + /// если не удалось или произошла ошибка в ходе получения/десериализации, то генерирует объект, сохраняет в кэш и + /// возвращает сгенерированный. /// /// Идентификатор объекта жилого строительства. /// Токен отмены. diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs index 7e3b5fef..6a669e28 100644 --- a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -10,14 +10,16 @@ public class ResidentialBuildingService( ResidentialBuildingGenerator generator, IDistributedCache cache, IConfiguration configuration - ) : IResidentialBuildingService +) : IResidentialBuildingService { private const string CacheKeyPrefix = "residential-building:"; - + private const int CacheExpirationTimeMinutesDefault = 15; - - private readonly TimeSpan _cacheExpirationTimeMinutes = TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationTimeMinutes", CacheExpirationTimeMinutesDefault)); - + + private readonly TimeSpan _cacheExpirationTimeMinutes = + TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationTimeMinutes", + CacheExpirationTimeMinutesDefault)); + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) { var cacheKey = $"{CacheKeyPrefix}{id}"; @@ -29,7 +31,8 @@ public async Task GetByIdAsync(int id, CancellationToken } catch (Exception ex) { - logger.LogWarning(ex, "Failed to read from distributed cache for key={cacheKey}. Falling back to generation.", cacheKey); + logger.LogWarning(ex, + "Failed to read from distributed cache for key={cacheKey}. Falling back to generation.", cacheKey); } if (!string.IsNullOrEmpty(jsonCached)) @@ -45,7 +48,7 @@ public async Task GetByIdAsync(int id, CancellationToken { logger.LogWarning(ex, "Invalid JSON in residential building cache for key {cacheKey}.", cacheKey); } - + if (objCached is null) { logger.LogWarning("Cache for residential building with Id={id} returned null.", id); @@ -56,25 +59,27 @@ public async Task GetByIdAsync(int id, CancellationToken return objCached; } } - - var obj = generator.Generate(id); + + ResidentialBuildingDto obj = generator.Generate(id); try { - await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), CreateCacheOptions(), cancellationToken); + await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), CreateCacheOptions(), + cancellationToken); } catch (Exception ex) { - logger.LogWarning(ex, "Failed to write residential building with Id={id} to cache. Still returning generated value.", id); + logger.LogWarning(ex, + "Failed to write residential building with Id={id} to cache. Still returning generated value.", id); } - + logger.LogInformation("Generated and cached residential building with Id={id}", id); - + return obj; } /// - /// Создаёт настройки кэша - задаёт время жизни кэша. + /// Создаёт настройки кэша - задаёт время жизни кэша. /// private DistributedCacheEntryOptions CreateCacheOptions() { diff --git a/ResidentialBuilding.ServiceDefaults/Extensions.cs b/ResidentialBuilding.ServiceDefaults/Extensions.cs index 5e59484d..2991eb62 100644 --- a/ResidentialBuilding.ServiceDefaults/Extensions.cs +++ b/ResidentialBuilding.ServiceDefaults/Extensions.cs @@ -44,7 +44,8 @@ public static TBuilder AddServiceDefaults(this TBuilder builder) where return builder; } - public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) + where TBuilder : IHostApplicationBuilder { builder.Logging.AddOpenTelemetry(logging => { @@ -78,7 +79,8 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w return builder; } - private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) + where TBuilder : IHostApplicationBuilder { var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); @@ -97,7 +99,8 @@ private static TBuilder AddOpenTelemetryExporters(this TBuilder builde return builder; } - public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) + where TBuilder : IHostApplicationBuilder { builder.Services.AddHealthChecks() // Add a default liveness check to ensure app is responsive @@ -124,4 +127,4 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) return app; } -} +} \ No newline at end of file diff --git a/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj b/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj index 051b99a2..723047f9 100644 --- a/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj +++ b/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj @@ -17,10 +17,10 @@ - - - - + + + + From ae35609d2d8d51af54c0a301c23f1c49ab7cce14 Mon Sep 17 00:00:00 2001 From: Donistr Date: Sun, 22 Feb 2026 20:27:16 +0400 Subject: [PATCH 07/37] =?UTF-8?q?refactor:=20=D0=B2=D1=8B=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=20faker=20=D0=B2=20private=20static=20readonly=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generator/ResidentialBuildingGenerator.cs | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs index 7f68d3b6..53690474 100644 --- a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs +++ b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs @@ -34,6 +34,39 @@ public class ResidentialBuildingGenerator(ILogger "Офис" ]; + private static readonly Faker? _faker = new Faker("ru") + .RuleFor(x => x.Address, f => f.Address.FullAddress()) + .RuleFor(x => x.PropertyType, f => f.PickRandom(_propertyTypes)) + .RuleFor(x => x.BuildYear, f => f.Random.Int(MinBuildYear, DateTime.Today.Year)) + .RuleFor(x => x.TotalArea, f => Math.Round(f.Random.Double(MinTotalArea, MaxTotalArea), 2)) + .RuleFor(x => x.LivingArea, (f, dto) => + { + var livingAreaPartOfTotalArea = + f.Random.Double(MinLivingAreaPartOfTotalArea, MaxLivingAreaPartOfTotalArea); + return Math.Round(livingAreaPartOfTotalArea * dto.TotalArea, 2); + }) + .RuleFor(x => x.TotalFloors, f => f.Random.Int(MinTotalFloors, MaxTotalFloors)) + .RuleFor(x => x.Floor, (f, dto) => + { + if (dto.PropertyType is "ИЖС") + { + return null; + } + + return f.Random.Int(1, dto.TotalFloors); + }) + .RuleFor(x => x.CadastralNumber, f => + $"{f.Random.Int(1, 99):D2}:" + + $"{f.Random.Int(1, 99):D2}:" + + $"{f.Random.Int(1, 9999999):D7}:" + + $"{f.Random.Int(1, 9999):D4}") + .RuleFor(x => x.CadastralValue, (f, dto) => + { + var pricePerM2 = f.Random.Double(MinPricePerM2, MaxPricePerM2); + var price = dto.TotalArea * pricePerM2; + return (decimal)Math.Round(price, 2); + }); + /// /// Генерирует объект жилого строительства для заданного идентификатора. /// @@ -43,41 +76,8 @@ public ResidentialBuildingDto Generate(int id) { logger.LogInformation("Generating Residential Building for Id={id}", id); - Faker? faker = new Faker("ru") - .RuleFor(x => x.Id, _ => id) - .RuleFor(x => x.Address, f => f.Address.FullAddress()) - .RuleFor(x => x.PropertyType, f => f.PickRandom(_propertyTypes)) - .RuleFor(x => x.BuildYear, f => f.Random.Int(MinBuildYear, DateTime.Today.Year)) - .RuleFor(x => x.TotalArea, f => Math.Round(f.Random.Double(MinTotalArea, MaxTotalArea), 2)) - .RuleFor(x => x.LivingArea, (f, dto) => - { - var livingAreaPartOfTotalArea = - f.Random.Double(MinLivingAreaPartOfTotalArea, MaxLivingAreaPartOfTotalArea); - return Math.Round(livingAreaPartOfTotalArea * dto.TotalArea, 2); - }) - .RuleFor(x => x.TotalFloors, f => f.Random.Int(MinTotalFloors, MaxTotalFloors)) - .RuleFor(x => x.Floor, (f, dto) => - { - if (dto.PropertyType is "ИЖС") - { - return null; - } - - return f.Random.Int(1, dto.TotalFloors); - }) - .RuleFor(x => x.CadastralNumber, f => - $"{f.Random.Int(1, 99):D2}:" + - $"{f.Random.Int(1, 99):D2}:" + - $"{f.Random.Int(1, 9999999):D7}:" + - $"{f.Random.Int(1, 9999):D4}") - .RuleFor(x => x.CadastralValue, (f, dto) => - { - var pricePerM2 = f.Random.Double(MinPricePerM2, MaxPricePerM2); - var price = dto.TotalArea * pricePerM2; - return (decimal)Math.Round(price, 2); - }); - - ResidentialBuildingDto? generatedObject = faker.Generate(); + ResidentialBuildingDto? generatedObject = _faker!.Generate(); + generatedObject.Id = id; logger.LogInformation( "Residential building generated: Id={Id}, Address='{Address}', PropertyType='{PropertyType}', " + From 8724252d82ee5da176c3a8715efaddb0e1b69a39 Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 25 Feb 2026 09:08:33 +0400 Subject: [PATCH 08/37] =?UTF-8?q?style:=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BB=20revert=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D0=B0=20?= =?UTF-8?q?=D1=81=20=D0=BA=D0=BB=D0=B8=D0=BD=D0=B0=D0=BF=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 23 ++++---- Client.Wasm/App.razor | 4 +- Client.Wasm/Client.Wasm.csproj | 30 +++++------ Client.Wasm/Components/DataCard.razor | 31 ++++------- Client.Wasm/Components/StudentCard.razor | 11 ++-- Client.Wasm/Layout/MainLayout.razor.css | 24 ++++----- Client.Wasm/Program.cs | 2 +- Client.Wasm/wwwroot/css/app.css | 50 +++++++++--------- Client.Wasm/wwwroot/index.html | 45 ++++++++-------- ResidentialBuilding.AppHost/AppHost.cs | 10 ++-- .../ResidentialBuilding.AppHost.csproj | 14 ++--- .../ResidentialBuildingController.cs | 12 ++--- .../DTO/ResidentialBuildingDto.cs | 52 +++++++++---------- .../Generator/ResidentialBuildingGenerator.cs | 16 +++--- ResidentialBuilding.Generator/Program.cs | 8 +-- .../ResidentialBuilding.Generator.csproj | 8 +-- .../Service/IResidentialBuildingService.cs | 12 ++--- .../Service/ResidentialBuildingService.cs | 33 +++++------- .../Extensions.cs | 11 ++-- ...ResidentialBuilding.ServiceDefaults.csproj | 8 +-- 20 files changed, 188 insertions(+), 216 deletions(-) diff --git a/.editorconfig b/.editorconfig index a8a58a0e..0f3bba5c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -43,7 +43,6 @@ csharp_style_var_for_built_in_types = true:error csharp_style_var_when_type_is_apparent = true:error csharp_style_var_elsewhere = false:silent csharp_space_around_binary_operators = before_and_after - [*.{cs,vb}] #### Naming styles #### @@ -65,31 +64,31 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 diff --git a/Client.Wasm/App.razor b/Client.Wasm/App.razor index a8cf8177..6fd3ed1b 100644 --- a/Client.Wasm/App.razor +++ b/Client.Wasm/App.razor @@ -1,7 +1,7 @@  - - + + Not found diff --git a/Client.Wasm/Client.Wasm.csproj b/Client.Wasm/Client.Wasm.csproj index 292e7634..0ba9f90c 100644 --- a/Client.Wasm/Client.Wasm.csproj +++ b/Client.Wasm/Client.Wasm.csproj @@ -1,21 +1,21 @@  - - net8.0 - enable - enable - + + net8.0 + enable + enable + - - - + + + - - - - - - - + + + + + + + diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index ad0ad2fc..307d98ce 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -1,25 +1,22 @@ -@inject IConfiguration Configuration +@inject IConfiguration Configuration @inject HttpClient Client - - - Характеристики текущего объекта - + Характеристики текущего объекта - +
- + # Характеристика Значение - + - @if (Value is null) + @if(Value is null) { 1 @@ -33,7 +30,7 @@ foreach (var property in array) { - @(Array.IndexOf(array, property) + 1) + @(Array.IndexOf(array, property)+1) @property.Key @property.Value?.ToString() @@ -43,13 +40,10 @@
- + - - - Запросить новый объект - + Запросить новый объект @@ -60,9 +54,7 @@ - + @@ -76,8 +68,7 @@ private async Task RequestNewData() { var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}/{Id}", new JsonSerializerOptions()); + Value = await Client.GetFromJsonAsync($"{baseAddress}/{Id}", new JsonSerializerOptions { }); StateHasChanged(); } - } diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 0613f8c7..eae33b0e 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -1,18 +1,13 @@  - - - Лабораторная работа - + Лабораторная работа Номер №1 «Кэширование» Вариант №38 "Объект жилого строительства" - Выполнена Елагиным Денисом 6513 - - - Ссылка на форк + Выполнена Елагиным Денисом 6513 + Ссылка на форк diff --git a/Client.Wasm/Layout/MainLayout.razor.css b/Client.Wasm/Layout/MainLayout.razor.css index 019d27b2..ecf25e5b 100644 --- a/Client.Wasm/Layout/MainLayout.razor.css +++ b/Client.Wasm/Layout/MainLayout.razor.css @@ -21,20 +21,20 @@ main { align-items: center; } -.top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; -} + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } -.top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; -} + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } -.top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; -} + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } @media (max-width: 640.98px) { .top-row { diff --git a/Client.Wasm/Program.cs b/Client.Wasm/Program.cs index 30bab128..a182a920 100644 --- a/Client.Wasm/Program.cs +++ b/Client.Wasm/Program.cs @@ -14,4 +14,4 @@ .AddBootstrapProviders() .AddFontAwesomeIcons(); -await builder.Build().RunAsync(); \ No newline at end of file +await builder.Build().RunAsync(); diff --git a/Client.Wasm/wwwroot/css/app.css b/Client.Wasm/wwwroot/css/app.css index cc1cfb1e..54a8aa38 100644 --- a/Client.Wasm/wwwroot/css/app.css +++ b/Client.Wasm/wwwroot/css/app.css @@ -17,7 +17,7 @@ a, .btn-link { } .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; } .content { @@ -48,12 +48,12 @@ a, .btn-link { z-index: 1000; } -#blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; -} + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; @@ -61,9 +61,9 @@ a, .btn-link { color: white; } -.blazor-error-boundary::after { - content: "An error has occurred." -} + .blazor-error-boundary::after { + content: "An error has occurred." + } .loading-progress { position: relative; @@ -73,19 +73,19 @@ a, .btn-link { margin: 20vh auto 1rem auto; } -.loading-progress circle { - fill: none; - stroke: #e0e0e0; - stroke-width: 0.6rem; - transform-origin: 50% 50%; - transform: rotate(-90deg); -} + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } -.loading-progress circle:last-child { - stroke: #1b6ec2; - stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; - transition: stroke-dasharray 0.05s ease-in-out; -} + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } .loading-progress-text { position: absolute; @@ -94,9 +94,9 @@ a, .btn-link { inset: calc(20vh + 3.25rem) 0 auto 0.2rem; } -.loading-progress-text:after { - content: var(--blazor-load-percentage-text, "Loading"); -} + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } code { color: #c02d76; diff --git a/Client.Wasm/wwwroot/index.html b/Client.Wasm/wwwroot/index.html index eb7d7c12..b74ee328 100644 --- a/Client.Wasm/wwwroot/index.html +++ b/Client.Wasm/wwwroot/index.html @@ -2,35 +2,34 @@ - - + + Client.Wasm - - - + + + - - - - + + + + -
- - - - -
-
+
+ + + + +
+
-
- An unhandled error has occurred. - Reload - 🗙 -
- +
+ An unhandled error has occurred. + Reload + 🗙 +
+ diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs index d7182a3a..2ed01edc 100644 --- a/ResidentialBuilding.AppHost/AppHost.cs +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -1,15 +1,13 @@ -using Projects; +var builder = DistributedApplication.CreateBuilder(args); -IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args); - -IResourceBuilder cache = builder.AddRedis("residential-building-cache") +var cache = builder.AddRedis("residential-building-cache") .WithRedisInsight(containerName: "residential-building-insight"); -IResourceBuilder generator = builder.AddProject("generator") +var generator = builder.AddProject("generator") .WithReference(cache, "residential-building-cache") .WaitFor(cache); -IResourceBuilder client = builder.AddProject("client") +var client = builder.AddProject("client") .WithReference(generator) .WaitFor(generator); diff --git a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj index cf0a5e19..83be43b7 100644 --- a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj +++ b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj @@ -1,7 +1,7 @@ - - + + Exe net8.0 @@ -11,13 +11,13 @@ - - + + - - + + - + diff --git a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs index 1f6ab24f..5d921d27 100644 --- a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs +++ b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs @@ -5,16 +5,14 @@ namespace Generator.Controller; /// -/// Контроллер для объектов жилого строительства. +/// Контроллер для объектов жилого строительства. /// [Route("api/residential-building")] [ApiController] -public class ResidentialBuildingController( - ILogger logger, - IResidentialBuildingService residentialBuildingService) : ControllerBase +public class ResidentialBuildingController(ILogger logger, IResidentialBuildingService residentialBuildingService) : ControllerBase { /// - /// Получение объекта жилого строительства по id. + /// Получение объекта жилого строительства по id. /// [HttpGet("{id:int}")] public async Task> GetResidentialBuilding(int id) @@ -23,9 +21,9 @@ public async Task> GetResidentialBuilding(i { return BadRequest("id must be >= 0"); } - + logger.LogInformation("Getting residential building with Id={id}.", id); - ResidentialBuildingDto result = await residentialBuildingService.GetByIdAsync(id); + var result = await residentialBuildingService.GetByIdAsync(id); logger.LogInformation("Residential building with Id={id} successfully received.", id); return Ok(result); diff --git a/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs b/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs index f62c40a1..2c9eac03 100644 --- a/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs +++ b/ResidentialBuilding.Generator/DTO/ResidentialBuildingDto.cs @@ -1,57 +1,57 @@ namespace Generator.DTO; /// -/// DTO объекта жилого строительства. +/// DTO объекта жилого строительства /// public class ResidentialBuildingDto { /// - /// Идентификатор в системе. + /// Идентификатор в системе /// public int Id { get; set; } - + /// - /// Адрес. + /// Адрес /// public string Address { get; set; } = string.Empty; - + /// - /// Тип недвижимости. + /// Тип недвижимости /// public string PropertyType { get; set; } = string.Empty; - + /// - /// Год постройки. + /// Год постройки /// - public int BuildYear { get; set; } - + public int BuildYear { get; set; } + /// - /// Общая площадь. + /// Общая площадь /// - public double TotalArea { get; set; } - + public double TotalArea { get; set; } + /// - /// Жилая площадь. + /// Жилая площадь /// - public double LivingArea { get; set; } - + public double LivingArea { get; set; } + /// - /// Этаж. + /// Этаж /// public int? Floor { get; set; } - + /// - /// Этажность. + /// Этажность /// - public int TotalFloors { get; set; } - + public int TotalFloors { get; set; } + /// - /// Кадастровый номер. + /// Кадастровый номер /// - public string CadastralNumber { get; set; } = string.Empty; - + public string CadastralNumber { get; set; } = string.Empty; + /// - /// Кадастровая стоимость. + /// Кадастровая стоимость /// - public decimal CadastralValue { get; set; } + public decimal CadastralValue { get; set; } } \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs index 53690474..7b5e1d5e 100644 --- a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs +++ b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs @@ -4,7 +4,7 @@ namespace Generator.Generator; /// -/// Генератор объектов жилого строительства на основе Bogus. +/// Генератор объектов жилого строительства на основе Bogus /// public class ResidentialBuildingGenerator(ILogger logger) { @@ -33,8 +33,8 @@ public class ResidentialBuildingGenerator(ILogger "Апартаменты", "Офис" ]; - - private static readonly Faker? _faker = new Faker("ru") + + private static readonly Faker _faker = new Faker("ru") .RuleFor(x => x.Address, f => f.Address.FullAddress()) .RuleFor(x => x.PropertyType, f => f.PickRandom(_propertyTypes)) .RuleFor(x => x.BuildYear, f => f.Random.Int(MinBuildYear, DateTime.Today.Year)) @@ -68,17 +68,17 @@ public class ResidentialBuildingGenerator(ILogger }); /// - /// Генерирует объект жилого строительства для заданного идентификатора. + /// Генерирует объект жилого строительства для заданного идентификатора /// - /// Идентификатор объекта жилого строительства. - /// Сгенерированный объект жилого строительства. + /// Идентификатор объекта жилого строительства + /// Сгенерированный объект жилого строительства public ResidentialBuildingDto Generate(int id) { logger.LogInformation("Generating Residential Building for Id={id}", id); - ResidentialBuildingDto? generatedObject = _faker!.Generate(); + var generatedObject = _faker.Generate(); generatedObject.Id = id; - + logger.LogInformation( "Residential building generated: Id={Id}, Address='{Address}', PropertyType='{PropertyType}', " + "BuildYear={BuildYear}, TotalArea={TotalArea}, LivingArea={LivingArea}, Floor={Floor}, " + diff --git a/ResidentialBuilding.Generator/Program.cs b/ResidentialBuilding.Generator/Program.cs index 4412dc53..760ae158 100644 --- a/ResidentialBuilding.Generator/Program.cs +++ b/ResidentialBuilding.Generator/Program.cs @@ -4,7 +4,7 @@ using Serilog; using Serilog.Formatting.Compact; -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog((hostingContext, services, loggerConfiguration) => { @@ -14,7 +14,7 @@ .ReadFrom.Services(services) .Enrich.FromLogContext() .WriteTo.Console( - new CompactJsonFormatter() + formatter: new CompactJsonFormatter() ); }); @@ -33,11 +33,11 @@ }); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddControllers(); -WebApplication app = builder.Build(); +var app = builder.Build(); app.UseCors("AllowLocalDev"); diff --git a/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj b/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj index 73d8dace..07dcd97c 100644 --- a/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj +++ b/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj @@ -8,13 +8,13 @@ - - - + + + - + diff --git a/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs index ae30a580..02476703 100644 --- a/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/IResidentialBuildingService.cs @@ -3,16 +3,16 @@ namespace Generator.Service; /// -/// Сервис получения объектов жилого строительства по идентификатору. -/// Если удалось найти объект в кэше - возвращает его, иначе генерирует, кэширует и возвращает сгенерированный. +/// Сервис получения объектов жилого строительства по идентификатору. +/// Если удалось найти объект в кэше - возвращает его, иначе генерирует, кэширует и возвращает сгенерированный. /// public interface IResidentialBuildingService { /// - /// Пытается найти в кэше объект с заданным идентификатором: - /// если удалось, то десериализует объект из JSON-а и возвращает; - /// если не удалось или произошла ошибка в ходе получения/десериализации, то генерирует объект, сохраняет в кэш и - /// возвращает сгенерированный. + /// Пытается найти в кэше объект с заданным идентификатором: + /// если удалось, то десериализует объект из JSON-а и возвращает; + /// если не удалось или произошла ошибка в ходе получения/десериализации, то генерирует объект, сохраняет в кэш и + /// возвращает сгенерированный. /// /// Идентификатор объекта жилого строительства. /// Токен отмены. diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs index 6a669e28..7e3b5fef 100644 --- a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -10,16 +10,14 @@ public class ResidentialBuildingService( ResidentialBuildingGenerator generator, IDistributedCache cache, IConfiguration configuration -) : IResidentialBuildingService + ) : IResidentialBuildingService { private const string CacheKeyPrefix = "residential-building:"; - + private const int CacheExpirationTimeMinutesDefault = 15; - - private readonly TimeSpan _cacheExpirationTimeMinutes = - TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationTimeMinutes", - CacheExpirationTimeMinutesDefault)); - + + private readonly TimeSpan _cacheExpirationTimeMinutes = TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationTimeMinutes", CacheExpirationTimeMinutesDefault)); + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) { var cacheKey = $"{CacheKeyPrefix}{id}"; @@ -31,8 +29,7 @@ public async Task GetByIdAsync(int id, CancellationToken } catch (Exception ex) { - logger.LogWarning(ex, - "Failed to read from distributed cache for key={cacheKey}. Falling back to generation.", cacheKey); + logger.LogWarning(ex, "Failed to read from distributed cache for key={cacheKey}. Falling back to generation.", cacheKey); } if (!string.IsNullOrEmpty(jsonCached)) @@ -48,7 +45,7 @@ public async Task GetByIdAsync(int id, CancellationToken { logger.LogWarning(ex, "Invalid JSON in residential building cache for key {cacheKey}.", cacheKey); } - + if (objCached is null) { logger.LogWarning("Cache for residential building with Id={id} returned null.", id); @@ -59,27 +56,25 @@ public async Task GetByIdAsync(int id, CancellationToken return objCached; } } - - ResidentialBuildingDto obj = generator.Generate(id); + + var obj = generator.Generate(id); try { - await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), CreateCacheOptions(), - cancellationToken); + await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), CreateCacheOptions(), cancellationToken); } catch (Exception ex) { - logger.LogWarning(ex, - "Failed to write residential building with Id={id} to cache. Still returning generated value.", id); + logger.LogWarning(ex, "Failed to write residential building with Id={id} to cache. Still returning generated value.", id); } - + logger.LogInformation("Generated and cached residential building with Id={id}", id); - + return obj; } /// - /// Создаёт настройки кэша - задаёт время жизни кэша. + /// Создаёт настройки кэша - задаёт время жизни кэша. /// private DistributedCacheEntryOptions CreateCacheOptions() { diff --git a/ResidentialBuilding.ServiceDefaults/Extensions.cs b/ResidentialBuilding.ServiceDefaults/Extensions.cs index 2991eb62..5e59484d 100644 --- a/ResidentialBuilding.ServiceDefaults/Extensions.cs +++ b/ResidentialBuilding.ServiceDefaults/Extensions.cs @@ -44,8 +44,7 @@ public static TBuilder AddServiceDefaults(this TBuilder builder) where return builder; } - public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) - where TBuilder : IHostApplicationBuilder + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.Logging.AddOpenTelemetry(logging => { @@ -79,8 +78,7 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) return builder; } - private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) - where TBuilder : IHostApplicationBuilder + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder { var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); @@ -99,8 +97,7 @@ private static TBuilder AddOpenTelemetryExporters(this TBuilder builde return builder; } - public static TBuilder AddDefaultHealthChecks(this TBuilder builder) - where TBuilder : IHostApplicationBuilder + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.Services.AddHealthChecks() // Add a default liveness check to ensure app is responsive @@ -127,4 +124,4 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) return app; } -} \ No newline at end of file +} diff --git a/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj b/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj index 723047f9..051b99a2 100644 --- a/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj +++ b/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj @@ -17,10 +17,10 @@ - - - - + + + + From 6cab0cf9c8db81867a4592131a8452bff81a418e Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 25 Feb 2026 10:00:16 +0400 Subject: [PATCH 09/37] =?UTF-8?q?fix:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20s?= =?UTF-8?q?erilog,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D0=BF=D0=BE=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=D0=B8=D1=82=D1=8C=20=D1=81=D1=82=D1=80=D1=83?= =?UTF-8?q?=D0=BA=D1=82=D1=83=D1=80=D0=BD=D0=BE=D0=B5=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.Generator/Program.cs | 14 -------------- .../ResidentialBuilding.ServiceDefaults.csproj | 4 ---- 2 files changed, 18 deletions(-) diff --git a/ResidentialBuilding.Generator/Program.cs b/ResidentialBuilding.Generator/Program.cs index 760ae158..8a3f9736 100644 --- a/ResidentialBuilding.Generator/Program.cs +++ b/ResidentialBuilding.Generator/Program.cs @@ -1,23 +1,9 @@ using Generator.Generator; using Generator.Service; using ResidentialBuilding.ServiceDefaults; -using Serilog; -using Serilog.Formatting.Compact; var builder = WebApplication.CreateBuilder(args); -builder.Host.UseSerilog((hostingContext, services, loggerConfiguration) => -{ - loggerConfiguration - .MinimumLevel.Information() - .ReadFrom.Configuration(hostingContext.Configuration) - .ReadFrom.Services(services) - .Enrich.FromLogContext() - .WriteTo.Console( - formatter: new CompactJsonFormatter() - ); -}); - builder.AddServiceDefaults(); builder.AddRedisDistributedCache("residential-building-cache"); diff --git a/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj b/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj index 051b99a2..9fb1f653 100644 --- a/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj +++ b/ResidentialBuilding.ServiceDefaults/ResidentialBuilding.ServiceDefaults.csproj @@ -17,10 +17,6 @@ - - - - From c6f84a5a74b6df5ee472398204bff9d2d594b882 Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 25 Feb 2026 10:04:05 +0400 Subject: [PATCH 10/37] =?UTF-8?q?refactor:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20CreateCahcheOptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/ResidentialBuildingService.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs index 7e3b5fef..073f7773 100644 --- a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -61,7 +61,11 @@ public async Task GetByIdAsync(int id, CancellationToken try { - await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), CreateCacheOptions(), cancellationToken); + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheExpirationTimeMinutes + }; + await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), cacheOptions, cancellationToken); } catch (Exception ex) { @@ -72,15 +76,4 @@ public async Task GetByIdAsync(int id, CancellationToken return obj; } - - /// - /// Создаёт настройки кэша - задаёт время жизни кэша. - /// - private DistributedCacheEntryOptions CreateCacheOptions() - { - return new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = _cacheExpirationTimeMinutes - }; - } } \ No newline at end of file From 4c1f77197b3ae1ec058b126b233672c9744e36f8 Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 25 Feb 2026 10:28:28 +0400 Subject: [PATCH 11/37] =?UTF-8?q?refactor:=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B8=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B3=D0=B5=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=B0=20ResidentialBuildin?= =?UTF-8?q?gDto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generator/ResidentialBuildingGenerator.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs index 7b5e1d5e..0468a35c 100644 --- a/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs +++ b/ResidentialBuilding.Generator/Generator/ResidentialBuildingGenerator.cs @@ -80,13 +80,20 @@ public ResidentialBuildingDto Generate(int id) generatedObject.Id = id; logger.LogInformation( - "Residential building generated: Id={Id}, Address='{Address}', PropertyType='{PropertyType}', " + - "BuildYear={BuildYear}, TotalArea={TotalArea}, LivingArea={LivingArea}, Floor={Floor}, " + - "TotalFloors={TotalFloors}, CadastralNumber='{CadastralNumber}', CadastralValue={CadastralValue}", - generatedObject.Id, generatedObject.Address, generatedObject.PropertyType, generatedObject.BuildYear, - generatedObject.TotalArea, generatedObject.LivingArea, generatedObject.Floor, generatedObject.TotalFloors, - generatedObject.CadastralNumber, generatedObject.CadastralValue - ); + "Residential building generated: {@Building}", + new + { + generatedObject.Id, + generatedObject.Address, + generatedObject.PropertyType, + generatedObject.BuildYear, + generatedObject.TotalArea, + generatedObject.LivingArea, + generatedObject.Floor, + generatedObject.TotalFloors, + generatedObject.CadastralNumber, + generatedObject.CadastralValue + }); return generatedObject; } From 51fac4e356137e5740ed8b7260b4d42ce17c6259 Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 25 Feb 2026 10:35:16 +0400 Subject: [PATCH 12/37] =?UTF-8?q?fix:=20=D0=BF=D0=BE=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20cors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.Generator/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ResidentialBuilding.Generator/Program.cs b/ResidentialBuilding.Generator/Program.cs index 8a3f9736..fea32e2d 100644 --- a/ResidentialBuilding.Generator/Program.cs +++ b/ResidentialBuilding.Generator/Program.cs @@ -13,8 +13,8 @@ { policy .AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); + .WithHeaders("Content-Type") + .WithMethods("GET"); }); }); From 38f2929442e39caf3657132d43a880d11a438845 Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 25 Feb 2026 10:43:21 +0400 Subject: [PATCH 13/37] =?UTF-8?q?refactor:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B1=D0=B5=D1=81=D0=BF=D0=BE=D0=BB=D0=B5=D0=B7=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20generator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.AppHost/AppHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs index 2ed01edc..abdadfe7 100644 --- a/ResidentialBuilding.AppHost/AppHost.cs +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -7,7 +7,7 @@ .WithReference(cache, "residential-building-cache") .WaitFor(cache); -var client = builder.AddProject("client") +builder.AddProject("client") .WithReference(generator) .WaitFor(generator); From fbc4abf0014e9aa97d277411fefb433276f414a5 Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 26 Feb 2026 12:55:02 +0400 Subject: [PATCH 14/37] =?UTF-8?q?reafactor:=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/ResidentialBuildingService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs index 073f7773..974b1724 100644 --- a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -1,7 +1,7 @@ -using System.Text.Json; -using Generator.DTO; +using Generator.DTO; using Generator.Generator; using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; namespace Generator.Service; From 77121714dd9e2d277cac222faeac63bf7728d940 Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 26 Feb 2026 12:55:39 +0400 Subject: [PATCH 15/37] =?UTF-8?q?refactor:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=20=D0=B0=D1=82=D1=80=D0=B8=D0=B1?= =?UTF-8?q?=D1=83=D1=82=D1=8B=20=D1=81=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80?= =?UTF-8?q?=D0=B0=D1=89=D0=B0=D0=B5=D0=BC=D1=8B=D0=BC=D0=B8=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/ResidentialBuildingController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs index 5d921d27..7df7ada8 100644 --- a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs +++ b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs @@ -14,7 +14,13 @@ public class ResidentialBuildingController(ILogger /// Получение объекта жилого строительства по id. /// + /// Идентификатор объекта жилого строительства. + /// DTO объекта жилого строительства. + /// Успешное получение объекта. + /// Некорректный id. [HttpGet("{id:int}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> GetResidentialBuilding(int id) { if (id <= 0) From 9d3044059b2d1e6f83e129ea3099c1def4c881cb Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 4 Mar 2026 19:32:16 +0400 Subject: [PATCH 16/37] =?UTF-8?q?fix:=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20gateway=20=D0=B8=20=D0=BF=D0=BE=D0=B4=202=20?= =?UTF-8?q?=D0=BB=D0=B0=D0=B1=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client.Wasm/Components/DataCard.razor | 2 +- Client.Wasm/Components/StudentCard.razor | 4 ++-- Client.Wasm/wwwroot/appsettings.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index 307d98ce..c646a839 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -68,7 +68,7 @@ private async Task RequestNewData() { var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}/{Id}", new JsonSerializerOptions { }); + Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); StateHasChanged(); } } diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index eae33b0e..f3d48486 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,8 +4,8 @@ - Номер №1 «Кэширование» - Вариант №38 "Объект жилого строительства" + Номер №2 «Балансировка нагрузки» + Вариант №38 "Query Based" Выполнена Елагиным Денисом 6513 Ссылка на форк diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index 47f7d32e..7f22c193 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "http://localhost:5204/api/residential-building" + "BaseAddress": "http://localhost:5300/residential-building" } From 33692780f9bb84a17d6af65650558929f30d460e Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 4 Mar 2026 19:33:16 +0400 Subject: [PATCH 17/37] =?UTF-8?q?fix:=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20=D0=BF=D0=BE=D0=B4=20query=20=D0=BF=D0=B0=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/ResidentialBuildingController.cs | 4 ++-- ResidentialBuilding.Generator/Properties/launchSettings.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs index 7df7ada8..0fbc4c23 100644 --- a/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs +++ b/ResidentialBuilding.Generator/Controller/ResidentialBuildingController.cs @@ -18,10 +18,10 @@ public class ResidentialBuildingController(ILoggerDTO объекта жилого строительства. /// Успешное получение объекта. /// Некорректный id. - [HttpGet("{id:int}")] + [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> GetResidentialBuilding(int id) + public async Task> GetResidentialBuilding([FromQuery] int id) { if (id <= 0) { diff --git a/ResidentialBuilding.Generator/Properties/launchSettings.json b/ResidentialBuilding.Generator/Properties/launchSettings.json index ffb66c15..a340fa14 100644 --- a/ResidentialBuilding.Generator/Properties/launchSettings.json +++ b/ResidentialBuilding.Generator/Properties/launchSettings.json @@ -13,7 +13,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5204", + "applicationUrl": "http://localhost:5200", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -22,7 +22,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7291;http://localhost:5204", + "applicationUrl": "https://localhost:7291;http://localhost:5200", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } From ad90a894fa0d3abe6665f65c427d5a09d5284113 Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 4 Mar 2026 19:34:09 +0400 Subject: [PATCH 18/37] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20ocelot=20=D1=81=20=D0=BA=D0=B0=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=BC=D0=BD=D1=8B=D0=BC=20quary=20based=20=D0=B1=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D1=89=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=BD=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudDevelopment.sln | 6 ++ .../LoadBalancer/QueryBasedLoadBalancer.cs | 66 +++++++++++++++++++ ResidentialBuilding.Gateway/Program.cs | 35 ++++++++++ .../Properties/launchSettings.json | 38 +++++++++++ .../ResidentialBuilding.Gateway.csproj | 17 +++++ .../appsettings.Development.json | 8 +++ ResidentialBuilding.Gateway/appsettings.json | 9 +++ ResidentialBuilding.Gateway/ocelot.json | 20 ++++++ 8 files changed, 199 insertions(+) create mode 100644 ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs create mode 100644 ResidentialBuilding.Gateway/Program.cs create mode 100644 ResidentialBuilding.Gateway/Properties/launchSettings.json create mode 100644 ResidentialBuilding.Gateway/ResidentialBuilding.Gateway.csproj create mode 100644 ResidentialBuilding.Gateway/appsettings.Development.json create mode 100644 ResidentialBuilding.Gateway/appsettings.json create mode 100644 ResidentialBuilding.Gateway/ocelot.json diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index da11d6ee..09f502a1 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.AppHost EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.ServiceDefaults", "ResidentialBuilding.ServiceDefaults\ResidentialBuilding.ServiceDefaults.csproj", "{3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.Gateway", "ResidentialBuilding.Gateway\ResidentialBuilding.Gateway.csproj", "{2CC59865-F962-47FF-9C2F-1CB6F535316B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AEE6EF1-603F-411D-89C2-5CE78EBCAEBA}.Release|Any CPU.Build.0 = Release|Any CPU + {2CC59865-F962-47FF-9C2F-1CB6F535316B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CC59865-F962-47FF-9C2F-1CB6F535316B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CC59865-F962-47FF-9C2F-1CB6F535316B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CC59865-F962-47FF-9C2F-1CB6F535316B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs b/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs new file mode 100644 index 00000000..f0d17247 --- /dev/null +++ b/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs @@ -0,0 +1,66 @@ +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.Responses; +using Ocelot.Values; + +namespace ResidentialBuilding.Gateway.LoadBalancer; + +/// +/// Query-based балансировщик нагрузки для Ocelot. +/// Выбирает downstream-сервис на основе хэша от отсортированных query-параметров запроса. +/// +/// Логгер. +/// Асинхронная функция, возвращающая актуальный список доступных downstream-сервисов. +public class QueryBasedLoadBalancer(ILogger logger, Func>> services) + : ILoadBalancer +{ + private static readonly object _lock = new(); + + public string Type => nameof(QueryBasedLoadBalancer); + + /// + /// Метод вызывается Ocelot после завершения запроса к downstream-сервису. + /// В текущей реализации ничего не делает (не требуется для query-based подхода). + /// + public void Release(ServiceHostAndPort hostAndPort) { } + + /// + /// Основной метод выбора downstream-сервиса на основе query-параметров текущего запроса. + /// + /// Контекст HTTP-запроса (используется для доступа к Query string). + /// + /// OkResponse с выбранным адресом сервиса в зависимости от query-параметров, либо случайно выбранный адрес + /// сервиса, если query-параметры не заданы. + /// + public async Task> LeaseAsync(HttpContext httpContext) + { + var currentServices = await services.Invoke(); + lock (_lock) + { + var query = httpContext.Request.Query; + + if (query.Count <= 0) + { + var randomIndex = Random.Shared.Next(currentServices.Count); + logger.LogWarning("Query doesn't contain any query parameters, index={randomIndex} selected by random.", randomIndex); + + return new OkResponse(currentServices[randomIndex].HostAndPort); + } + + var queryParams = query + .OrderBy(kvp => kvp.Key, StringComparer.Ordinal) + .Select(kvp => $"{kvp.Key}={string.Join(",", kvp.Value.OrderBy(v => v))}") + .ToList(); + + var hashKey = string.Join("&", queryParams); + var hash = hashKey.GetHashCode(); + if (hash < 0) + { + hash = -hash; + } + var index = (hash % currentServices.Count); + logger.LogInformation("Query based selected index={index} for hash={hash} calculated for string='{hashKey}'", index, hash, hashKey); + + return new OkResponse(currentServices[index].HostAndPort); + } + } +} \ No newline at end of file diff --git a/ResidentialBuilding.Gateway/Program.cs b/ResidentialBuilding.Gateway/Program.cs new file mode 100644 index 00000000..3df66775 --- /dev/null +++ b/ResidentialBuilding.Gateway/Program.cs @@ -0,0 +1,35 @@ +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using ResidentialBuilding.Gateway.LoadBalancer; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowLocalDev", policy => + { + policy + .AllowAnyOrigin() + .WithHeaders("Content-Type") + .WithMethods("GET"); + }); +}); + +builder.Configuration + .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true) + .AddOcelot(); + +builder.Services + .AddOcelot(builder.Configuration) + .AddCustomLoadBalancer((sp, _, discoveryProvider) => + { + var logger = sp.GetRequiredService>(); + return new QueryBasedLoadBalancer(logger, discoveryProvider.GetAsync); + }); + +var app = builder.Build(); + +app.UseCors("AllowLocalDev"); +await app.UseOcelot(); + +await app.RunAsync(); diff --git a/ResidentialBuilding.Gateway/Properties/launchSettings.json b/ResidentialBuilding.Gateway/Properties/launchSettings.json new file mode 100644 index 00000000..e210075f --- /dev/null +++ b/ResidentialBuilding.Gateway/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:22127", + "sslPort": 44310 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5298", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7014;http://localhost:5298", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ResidentialBuilding.Gateway/ResidentialBuilding.Gateway.csproj b/ResidentialBuilding.Gateway/ResidentialBuilding.Gateway.csproj new file mode 100644 index 00000000..8123f6ca --- /dev/null +++ b/ResidentialBuilding.Gateway/ResidentialBuilding.Gateway.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/ResidentialBuilding.Gateway/appsettings.Development.json b/ResidentialBuilding.Gateway/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ResidentialBuilding.Gateway/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ResidentialBuilding.Gateway/appsettings.json b/ResidentialBuilding.Gateway/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/ResidentialBuilding.Gateway/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ResidentialBuilding.Gateway/ocelot.json b/ResidentialBuilding.Gateway/ocelot.json new file mode 100644 index 00000000..7c0439d5 --- /dev/null +++ b/ResidentialBuilding.Gateway/ocelot.json @@ -0,0 +1,20 @@ +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 5201 }, + { "Host": "localhost", "Port": 5202 }, + { "Host": "localhost", "Port": 5203 }, + { "Host": "localhost", "Port": 5204 }, + { "Host": "localhost", "Port": 5205 } + ], + "UpstreamPathTemplate": "/{everything}", + "UpstreamHttpMethod": [ "GET" ], + "LoadBalancerOptions": { + "Type": "QueryBasedLoadBalancer" + } + } + ] +} \ No newline at end of file From b900ae7b82fed9d75a8589f6567519c0874c4689 Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 4 Mar 2026 19:35:00 +0400 Subject: [PATCH 19/37] =?UTF-8?q?fix:=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=BB=20AppHost=20=D0=BF=D0=BE=D0=B4=20ocelot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.AppHost/AppHost.cs | 41 +++++++++++++++++-- .../ResidentialBuilding.AppHost.csproj | 1 + 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs index abdadfe7..16ac9f32 100644 --- a/ResidentialBuilding.AppHost/AppHost.cs +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -3,12 +3,47 @@ var cache = builder.AddRedis("residential-building-cache") .WithRedisInsight(containerName: "residential-building-insight"); -var generator = builder.AddProject("generator") +var generator1 = builder.AddProject("generator-1") .WithReference(cache, "residential-building-cache") + .WithEndpoint("http", endpoint => endpoint.Port = 5201) .WaitFor(cache); +var generator2 = builder.AddProject("generator-2") + .WithReference(cache, "residential-building-cache") + .WithEndpoint("http", endpoint => endpoint.Port = 5202) + .WaitFor(cache); + +var generator3 = builder.AddProject("generator-3") + .WithReference(cache, "residential-building-cache") + .WithEndpoint("http", endpoint => endpoint.Port = 5203) + .WaitFor(cache); + +var generator4 = builder.AddProject("generator-4") + .WithReference(cache, "residential-building-cache") + .WithEndpoint("http", endpoint => endpoint.Port = 5204) + .WaitFor(cache); + +var generator5 = builder.AddProject("generator-5") + .WithReference(cache, "residential-building-cache") + .WithEndpoint("http", endpoint => endpoint.Port = 5205) + .WaitFor(cache); + +var gateway = builder.AddProject("gateway") + .WithEndpoint("http", endpoint => endpoint.Port = 5300) + .WithReference(generator1) + .WithReference(generator2) + .WithReference(generator3) + .WithReference(generator4) + .WithReference(generator5) + .WithExternalHttpEndpoints() + .WaitFor(generator1) + .WaitFor(generator2) + .WaitFor(generator3) + .WaitFor(generator4) + .WaitFor(generator5); + builder.AddProject("client") - .WithReference(generator) - .WaitFor(generator); + .WithReference(gateway) + .WaitFor(gateway); builder.Build().Run(); \ No newline at end of file diff --git a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj index 83be43b7..9c517909 100644 --- a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj +++ b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj @@ -18,6 +18,7 @@ + From 42081709df2054f1375d57c78695773999e8c945 Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 5 Mar 2026 17:13:34 +0400 Subject: [PATCH 20/37] =?UTF-8?q?fix:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B1=D0=B5=D1=81=D0=BF=D0=BE=D0=BB=D0=B5=D0=B7=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20lock=20=D0=B8=D0=B7=20=D0=B1=D0=B0=D0=BB=D0=B0=D0=BD?= =?UTF-8?q?=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D1=89=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LoadBalancer/QueryBasedLoadBalancer.cs | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs b/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs index f0d17247..22f74d8b 100644 --- a/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs +++ b/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs @@ -13,8 +13,6 @@ namespace ResidentialBuilding.Gateway.LoadBalancer; public class QueryBasedLoadBalancer(ILogger logger, Func>> services) : ILoadBalancer { - private static readonly object _lock = new(); - public string Type => nameof(QueryBasedLoadBalancer); /// @@ -34,33 +32,30 @@ public void Release(ServiceHostAndPort hostAndPort) { } public async Task> LeaseAsync(HttpContext httpContext) { var currentServices = await services.Invoke(); - lock (_lock) + var query = httpContext.Request.Query; + + if (query.Count <= 0) { - var query = httpContext.Request.Query; - - if (query.Count <= 0) - { - var randomIndex = Random.Shared.Next(currentServices.Count); - logger.LogWarning("Query doesn't contain any query parameters, index={randomIndex} selected by random.", randomIndex); - - return new OkResponse(currentServices[randomIndex].HostAndPort); - } - - var queryParams = query - .OrderBy(kvp => kvp.Key, StringComparer.Ordinal) - .Select(kvp => $"{kvp.Key}={string.Join(",", kvp.Value.OrderBy(v => v))}") - .ToList(); + var randomIndex = Random.Shared.Next(currentServices.Count); + logger.LogWarning("Query doesn't contain any query parameters, index={randomIndex} selected by random.", randomIndex); - var hashKey = string.Join("&", queryParams); - var hash = hashKey.GetHashCode(); - if (hash < 0) - { - hash = -hash; - } - var index = (hash % currentServices.Count); - logger.LogInformation("Query based selected index={index} for hash={hash} calculated for string='{hashKey}'", index, hash, hashKey); - - return new OkResponse(currentServices[index].HostAndPort); + return new OkResponse(currentServices[randomIndex].HostAndPort); + } + + var queryParams = query + .OrderBy(kvp => kvp.Key, StringComparer.Ordinal) + .Select(kvp => $"{kvp.Key}={string.Join(",", kvp.Value.OrderBy(v => v))}") + .ToList(); + + var hashKey = string.Join("&", queryParams); + var hash = hashKey.GetHashCode(); + if (hash < 0) + { + hash = -hash; } + var index = (hash % currentServices.Count); + logger.LogInformation("Query based selected index={index} for hash={hash} calculated for string='{hashKey}'", index, hash, hashKey); + + return new OkResponse(currentServices[index].HostAndPort); } } \ No newline at end of file From 3191a830e3e591891fbd26cc9f742f3a83778127 Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 5 Mar 2026 17:30:46 +0400 Subject: [PATCH 21/37] =?UTF-8?q?refactor:=20=D1=81=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=B2=20AppHost.cs=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BE=D0=B2=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20=D1=86=D0=B8=D0=BA=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.AppHost/AppHost.cs | 49 +++++++------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs index 16ac9f32..0faccc47 100644 --- a/ResidentialBuilding.AppHost/AppHost.cs +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -3,44 +3,21 @@ var cache = builder.AddRedis("residential-building-cache") .WithRedisInsight(containerName: "residential-building-insight"); -var generator1 = builder.AddProject("generator-1") - .WithReference(cache, "residential-building-cache") - .WithEndpoint("http", endpoint => endpoint.Port = 5201) - .WaitFor(cache); - -var generator2 = builder.AddProject("generator-2") - .WithReference(cache, "residential-building-cache") - .WithEndpoint("http", endpoint => endpoint.Port = 5202) - .WaitFor(cache); - -var generator3 = builder.AddProject("generator-3") - .WithReference(cache, "residential-building-cache") - .WithEndpoint("http", endpoint => endpoint.Port = 5203) - .WaitFor(cache); - -var generator4 = builder.AddProject("generator-4") - .WithReference(cache, "residential-building-cache") - .WithEndpoint("http", endpoint => endpoint.Port = 5204) - .WaitFor(cache); - -var generator5 = builder.AddProject("generator-5") - .WithReference(cache, "residential-building-cache") - .WithEndpoint("http", endpoint => endpoint.Port = 5205) - .WaitFor(cache); - var gateway = builder.AddProject("gateway") .WithEndpoint("http", endpoint => endpoint.Port = 5300) - .WithReference(generator1) - .WithReference(generator2) - .WithReference(generator3) - .WithReference(generator4) - .WithReference(generator5) - .WithExternalHttpEndpoints() - .WaitFor(generator1) - .WaitFor(generator2) - .WaitFor(generator3) - .WaitFor(generator4) - .WaitFor(generator5); + .WithExternalHttpEndpoints(); + +const int generatorPortBase = 5200; +for (var i = 1; i <= 5; ++i) +{ + var i1 = i; + var generator = builder.AddProject($"generator-{i}") + .WithReference(cache, "residential-building-cache") + .WithEndpoint("http", endpoint => endpoint.Port = generatorPortBase + i1) + .WaitFor(cache); + + gateway.WaitFor(generator); +} builder.AddProject("client") .WithReference(gateway) From 1c6352f24a3c1611416b73e8933bf3b88a6c160f Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 5 Mar 2026 17:33:24 +0400 Subject: [PATCH 22/37] =?UTF-8?q?refactor:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=B1=D0=B5=D1=81=D0=BF=D0=BE=D0=BB=D0=B5=D0=B7=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20cors=20=D1=83=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=D0=B0-=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.Generator/Program.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ResidentialBuilding.Generator/Program.cs b/ResidentialBuilding.Generator/Program.cs index fea32e2d..a3e90dc1 100644 --- a/ResidentialBuilding.Generator/Program.cs +++ b/ResidentialBuilding.Generator/Program.cs @@ -7,17 +7,6 @@ builder.AddServiceDefaults(); builder.AddRedisDistributedCache("residential-building-cache"); -builder.Services.AddCors(options => -{ - options.AddPolicy("AllowLocalDev", policy => - { - policy - .AllowAnyOrigin() - .WithHeaders("Content-Type") - .WithMethods("GET"); - }); -}); - builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -25,8 +14,6 @@ var app = builder.Build(); -app.UseCors("AllowLocalDev"); - app.MapControllers(); app.Run(); \ No newline at end of file From 8f9d89b27677c750b58ae67d9b05183407979922 Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 5 Mar 2026 17:40:42 +0400 Subject: [PATCH 23/37] =?UTF-8?q?refactor:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D0=B0=20=D0=BE=D1=86=D0=B5=D0=BB=D0=BE=D1=82=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=D1=81=D1=85=D0=BE=D0=BB=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D1=8B=20=D0=B8=D0=B7=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83?= =?UTF-8?q?=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.Gateway/ocelot.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ResidentialBuilding.Gateway/ocelot.json b/ResidentialBuilding.Gateway/ocelot.json index 7c0439d5..54ac5e1c 100644 --- a/ResidentialBuilding.Gateway/ocelot.json +++ b/ResidentialBuilding.Gateway/ocelot.json @@ -1,7 +1,7 @@ { "Routes": [ { - "DownstreamPathTemplate": "/api/{everything}", + "DownstreamPathTemplate": "/api/residential-building", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5201 }, @@ -10,7 +10,7 @@ { "Host": "localhost", "Port": 5204 }, { "Host": "localhost", "Port": 5205 } ], - "UpstreamPathTemplate": "/{everything}", + "UpstreamPathTemplate": "/residential-building", "UpstreamHttpMethod": [ "GET" ], "LoadBalancerOptions": { "Type": "QueryBasedLoadBalancer" From 472fee8bb6e05b8837d755613e394d3ffa1f3126 Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 5 Mar 2026 17:58:58 +0400 Subject: [PATCH 24/37] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20=D0=B1=D0=B0=D0=BB=D0=B0=D0=BD=D1=81=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D1=89=D0=B8=D0=BA,=20=D1=87=D1=82=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=20=D0=BE=D0=BD=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=BE=D0=B2=D0=B0=D0=BB=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LoadBalancer/QueryBasedLoadBalancer.cs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs b/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs index 22f74d8b..89678583 100644 --- a/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs +++ b/ResidentialBuilding.Gateway/LoadBalancer/QueryBasedLoadBalancer.cs @@ -6,13 +6,15 @@ namespace ResidentialBuilding.Gateway.LoadBalancer; /// /// Query-based балансировщик нагрузки для Ocelot. -/// Выбирает downstream-сервис на основе хэша от отсортированных query-параметров запроса. +/// Выбирает downstream-сервис на основе query-параметра id запроса. /// /// Логгер. /// Асинхронная функция, возвращающая актуальный список доступных downstream-сервисов. public class QueryBasedLoadBalancer(ILogger logger, Func>> services) : ILoadBalancer { + private const string IdQueryParamName = "id"; + public string Type => nameof(QueryBasedLoadBalancer); /// @@ -22,40 +24,41 @@ public class QueryBasedLoadBalancer(ILogger logger, Func public void Release(ServiceHostAndPort hostAndPort) { } /// - /// Основной метод выбора downstream-сервиса на основе query-параметров текущего запроса. + /// Основной метод выбора downstream-сервиса на основе query-параметра id текущего запроса. /// /// Контекст HTTP-запроса (используется для доступа к Query string). /// - /// OkResponse с выбранным адресом сервиса в зависимости от query-параметров, либо случайно выбранный адрес - /// сервиса, если query-параметры не заданы. + /// OkResponse с выбранным адресом сервиса в зависимости от query-параметра id, либо случайно выбранный адрес + /// сервиса, если query-параметр не задан. /// public async Task> LeaseAsync(HttpContext httpContext) { var currentServices = await services.Invoke(); var query = httpContext.Request.Query; - if (query.Count <= 0) + if (!query.TryGetValue(IdQueryParamName, out var idValues) || idValues.Count <= 0) { - var randomIndex = Random.Shared.Next(currentServices.Count); - logger.LogWarning("Query doesn't contain any query parameters, index={randomIndex} selected by random.", randomIndex); - - return new OkResponse(currentServices[randomIndex].HostAndPort); + return SelectRandomService(currentServices); } - var queryParams = query - .OrderBy(kvp => kvp.Key, StringComparer.Ordinal) - .Select(kvp => $"{kvp.Key}={string.Join(",", kvp.Value.OrderBy(v => v))}") - .ToList(); + var idStr = idValues.First(); - var hashKey = string.Join("&", queryParams); - var hash = hashKey.GetHashCode(); - if (hash < 0) + if (string.IsNullOrWhiteSpace(idStr) || !int.TryParse(idStr, out var id) || id < 0) { - hash = -hash; + return SelectRandomService(currentServices); } - var index = (hash % currentServices.Count); - logger.LogInformation("Query based selected index={index} for hash={hash} calculated for string='{hashKey}'", index, hash, hashKey); + + var index = id % currentServices.Count; + logger.LogInformation("Query based selected index={index}.", index); return new OkResponse(currentServices[index].HostAndPort); } + + private OkResponse SelectRandomService(List currentServices) + { + var randomIndex = Random.Shared.Next(currentServices.Count); + logger.LogWarning("Query doesn't contain correct id parameter, index={randomIndex} selected by random.", randomIndex); + + return new OkResponse(currentServices[randomIndex].HostAndPort); + } } \ No newline at end of file From 8d77860f93703afbed70caf71e9a0c78fd0e25e7 Mon Sep 17 00:00:00 2001 From: Donistr Date: Tue, 10 Mar 2026 15:19:51 +0400 Subject: [PATCH 25/37] =?UTF-8?q?refactor:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BB=20i1=20=D0=B8=D0=B7=20AppHost.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.AppHost/AppHost.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs index 0faccc47..6c1a8750 100644 --- a/ResidentialBuilding.AppHost/AppHost.cs +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -10,10 +10,9 @@ const int generatorPortBase = 5200; for (var i = 1; i <= 5; ++i) { - var i1 = i; var generator = builder.AddProject($"generator-{i}") .WithReference(cache, "residential-building-cache") - .WithEndpoint("http", endpoint => endpoint.Port = generatorPortBase + i1) + .WithEndpoint("http", endpoint => endpoint.Port = generatorPortBase + i) .WaitFor(cache); gateway.WaitFor(generator); From d8f4141fad344e661cebdf8dd371a9593b65ce0b Mon Sep 17 00:00:00 2001 From: Donistr Date: Tue, 14 Apr 2026 13:23:51 +0400 Subject: [PATCH 26/37] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D1=83?= =?UTF-8?q?=20=D1=81=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B2=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.AppHost/AppHost.cs | 33 ++++++++++- .../residential-building-template-sns-s3.yaml | 59 +++++++++++++++++++ .../ResidentialBuilding.AppHost.csproj | 1 + ResidentialBuilding.AppHost/appsettings.json | 3 + .../Messaging/IProducerService.cs | 15 +++++ .../Messaging/SnsPublisherService.cs | 43 ++++++++++++++ ResidentialBuilding.Generator/Program.cs | 7 +++ .../ResidentialBuilding.Generator.csproj | 3 + .../Service/ResidentialBuildingService.cs | 3 + 9 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 ResidentialBuilding.AppHost/CloudFormation/residential-building-template-sns-s3.yaml create mode 100644 ResidentialBuilding.Generator/Messaging/IProducerService.cs create mode 100644 ResidentialBuilding.Generator/Messaging/SnsPublisherService.cs diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs index 6c1a8750..d05055b2 100644 --- a/ResidentialBuilding.AppHost/AppHost.cs +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -1,3 +1,6 @@ +using Amazon; +using Aspire.Hosting.LocalStack.Container; + var builder = DistributedApplication.CreateBuilder(args); var cache = builder.AddRedis("residential-building-cache") @@ -7,14 +10,38 @@ .WithEndpoint("http", endpoint => endpoint.Port = 5300) .WithExternalHttpEndpoints(); +var awsConfig = builder.AddAWSSDKConfig() + .WithProfile("default") + .WithRegion(RegionEndpoint.EUCentral1); + +var localstack = builder + .AddLocalStack("residential-building-localstack", awsConfig: awsConfig, configureContainer: container => + { + container.Lifetime = ContainerLifetime.Session; + container.DebugLevel = 1; + container.LogLevel = LocalStackLogLevel.Debug; + container.Port = 4566; + container.AdditionalEnvironmentVariables + .Add("DEBUG", "1"); + container.AdditionalEnvironmentVariables + .Add("SNS_CERT_URL_HOST", "sns.eu-central-1.amazonaws.com"); + }); + +var awsResources = builder + .AddAWSCloudFormationTemplate("resources", "CloudFormation/residential-building-template-sns-s3.yaml", "residential-building") + .WithReference(awsConfig); + const int generatorPortBase = 5200; for (var i = 1; i <= 5; ++i) { var generator = builder.AddProject($"generator-{i}") .WithReference(cache, "residential-building-cache") .WithEndpoint("http", endpoint => endpoint.Port = generatorPortBase + i) - .WaitFor(cache); - + .WithReference(awsResources) + .WithEnvironment("Settings__MessageBroker", "SNS") + .WaitFor(cache) + .WaitFor(awsResources); + gateway.WaitFor(generator); } @@ -22,4 +49,6 @@ .WithReference(gateway) .WaitFor(gateway); +builder.UseLocalStack(localstack); + builder.Build().Run(); \ No newline at end of file diff --git a/ResidentialBuilding.AppHost/CloudFormation/residential-building-template-sns-s3.yaml b/ResidentialBuilding.AppHost/CloudFormation/residential-building-template-sns-s3.yaml new file mode 100644 index 00000000..07f33505 --- /dev/null +++ b/ResidentialBuilding.AppHost/CloudFormation/residential-building-template-sns-s3.yaml @@ -0,0 +1,59 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Cloud formation template for residential building project' + +Parameters: + BucketName: + Type: String + Description: Name for the S3 bucket + Default: 'residential-building-bucket' + + TopicName: + Type: String + Description: Name for the SNS topic + Default: 'residential-building-topic' + +Resources: + ResidentialBuildingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref BucketName + VersioningConfiguration: + Status: Suspended + Tags: + - Key: Name + Value: !Ref BucketName + - Key: Environment + Value: Sample + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + + ResidentialBuildingTopic: + Type: AWS::SNS::Topic + Properties: + TopicName: !Ref TopicName + DisplayName: !Ref TopicName + Tags: + - Key: Name + Value: !Ref TopicName + - Key: Environment + Value: Sample + +Outputs: + S3BucketName: + Description: Name of the S3 bucket + Value: !Ref ResidentialBuildingBucket + + S3BucketArn: + Description: ARN of the S3 bucket + Value: !GetAtt ResidentialBuildingBucket.Arn + + SNSTopicName: + Description: Name of the SNS topic + Value: !GetAtt ResidentialBuildingTopic.TopicName + + SNSTopicArn: + Description: ARN of the SNS topic + Value: !Ref ResidentialBuildingTopic \ No newline at end of file diff --git a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj index 9c517909..d44d2f6f 100644 --- a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj +++ b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj @@ -13,6 +13,7 @@ + diff --git a/ResidentialBuilding.AppHost/appsettings.json b/ResidentialBuilding.AppHost/appsettings.json index 31c092aa..a6b256bb 100644 --- a/ResidentialBuilding.AppHost/appsettings.json +++ b/ResidentialBuilding.AppHost/appsettings.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning", "Aspire.Hosting.Dcp": "Warning" } + }, + "LocalStack": { + "UseLocalStack": true } } diff --git a/ResidentialBuilding.Generator/Messaging/IProducerService.cs b/ResidentialBuilding.Generator/Messaging/IProducerService.cs new file mode 100644 index 00000000..aa9a594a --- /dev/null +++ b/ResidentialBuilding.Generator/Messaging/IProducerService.cs @@ -0,0 +1,15 @@ +using Generator.DTO; + +namespace Generator.Messaging; + +/// +/// Интерфейс службы для отправки генерируемых ЗУ в брокер сообщений +/// +public interface IProducerService +{ + /// + /// Отправляет сообщение в брокер + /// + /// Объект жилого строительства + public Task SendMessage(ResidentialBuildingDto residentialBuilding); +} \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Messaging/SnsPublisherService.cs b/ResidentialBuilding.Generator/Messaging/SnsPublisherService.cs new file mode 100644 index 00000000..dc7a3f27 --- /dev/null +++ b/ResidentialBuilding.Generator/Messaging/SnsPublisherService.cs @@ -0,0 +1,43 @@ +using System.Net; +using System.Text.Json; +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using Generator.DTO; + +namespace Generator.Messaging; + +/// +/// Служба для отправки сообщений в SNS +/// +/// Клиент SNS +/// Конфигурация +/// Логгер +public class SnsPublisherService(IAmazonSimpleNotificationService client, IConfiguration configuration, ILogger logger) : IProducerService +{ + private readonly string _topicArn = configuration["AWS:Resources:SNSTopicArn"] + ?? throw new KeyNotFoundException("SNS topic link was not found in configuration"); + + /// + public async Task SendMessage(ResidentialBuildingDto residentialBuilding) + { + try + { + var json = JsonSerializer.Serialize(residentialBuilding); + var request = new PublishRequest + { + Message = json, + TopicArn = _topicArn + }; + var response = await client.PublishAsync(request); + if (response.HttpStatusCode == HttpStatusCode.OK) + logger.LogInformation("Residential building {id} was sent to sink via SNS", residentialBuilding.Id); + else + throw new Exception($"SNS returned {response.HttpStatusCode}"); + } + catch (Exception ex) + { + logger.LogError(ex, "Unable to send residential building through SNS topic"); + } + + } +} \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Program.cs b/ResidentialBuilding.Generator/Program.cs index a3e90dc1..2b02c719 100644 --- a/ResidentialBuilding.Generator/Program.cs +++ b/ResidentialBuilding.Generator/Program.cs @@ -1,5 +1,8 @@ +using Amazon.SimpleNotificationService; using Generator.Generator; +using Generator.Messaging; using Generator.Service; +using LocalStack.Client.Extensions; using ResidentialBuilding.ServiceDefaults; var builder = WebApplication.CreateBuilder(args); @@ -10,6 +13,10 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddLocalStack(builder.Configuration); +builder.Services.AddSingleton(); +builder.Services.AddAwsService(); + builder.Services.AddControllers(); var app = builder.Build(); diff --git a/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj b/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj index 07dcd97c..eeaf2a82 100644 --- a/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj +++ b/ResidentialBuilding.Generator/ResidentialBuilding.Generator.csproj @@ -11,6 +11,9 @@ + + + diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs index 974b1724..c261219f 100644 --- a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -2,6 +2,7 @@ using Generator.Generator; using Microsoft.Extensions.Caching.Distributed; using System.Text.Json; +using Generator.Messaging; namespace Generator.Service; @@ -9,6 +10,7 @@ public class ResidentialBuildingService( ILogger logger, ResidentialBuildingGenerator generator, IDistributedCache cache, + IProducerService messagingService, IConfiguration configuration ) : IResidentialBuildingService { @@ -65,6 +67,7 @@ public async Task GetByIdAsync(int id, CancellationToken { AbsoluteExpirationRelativeToNow = _cacheExpirationTimeMinutes }; + await messagingService.SendMessage(obj); await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), cacheOptions, cancellationToken); } catch (Exception ex) From da0458eeaf20502a39d6dcf06acf0e0a6a1f18c1 Mon Sep 17 00:00:00 2001 From: Donistr Date: Tue, 14 Apr 2026 16:53:49 +0400 Subject: [PATCH 27/37] =?UTF-8?q?refactor:=20=D1=80=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20=D0=B3=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.Generator/Program.cs | 6 +- .../Messaging/IPublisherService.cs} | 6 +- .../Messaging/SnsPublisherService.cs | 19 ++--- .../Service/ResidentialBuildingService.cs | 72 +++---------------- 4 files changed, 29 insertions(+), 74 deletions(-) rename ResidentialBuilding.Generator/{Messaging/IProducerService.cs => Service/Messaging/IPublisherService.cs} (73%) rename ResidentialBuilding.Generator/{ => Service}/Messaging/SnsPublisherService.cs (69%) diff --git a/ResidentialBuilding.Generator/Program.cs b/ResidentialBuilding.Generator/Program.cs index 2b02c719..481ddf1f 100644 --- a/ResidentialBuilding.Generator/Program.cs +++ b/ResidentialBuilding.Generator/Program.cs @@ -1,7 +1,8 @@ using Amazon.SimpleNotificationService; using Generator.Generator; -using Generator.Messaging; using Generator.Service; +using Generator.Service.Cache; +using Generator.Service.Messaging; using LocalStack.Client.Extensions; using ResidentialBuilding.ServiceDefaults; @@ -11,10 +12,11 @@ builder.AddRedisDistributedCache("residential-building-cache"); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddLocalStack(builder.Configuration); -builder.Services.AddSingleton(); builder.Services.AddAwsService(); builder.Services.AddControllers(); diff --git a/ResidentialBuilding.Generator/Messaging/IProducerService.cs b/ResidentialBuilding.Generator/Service/Messaging/IPublisherService.cs similarity index 73% rename from ResidentialBuilding.Generator/Messaging/IProducerService.cs rename to ResidentialBuilding.Generator/Service/Messaging/IPublisherService.cs index aa9a594a..4c1fcc10 100644 --- a/ResidentialBuilding.Generator/Messaging/IProducerService.cs +++ b/ResidentialBuilding.Generator/Service/Messaging/IPublisherService.cs @@ -1,11 +1,11 @@ using Generator.DTO; -namespace Generator.Messaging; +namespace Generator.Service.Messaging; /// -/// Интерфейс службы для отправки генерируемых ЗУ в брокер сообщений +/// Интерфейс службы для отправки генерируемых объектов в брокер сообщений /// -public interface IProducerService +public interface IPublisherService { /// /// Отправляет сообщение в брокер diff --git a/ResidentialBuilding.Generator/Messaging/SnsPublisherService.cs b/ResidentialBuilding.Generator/Service/Messaging/SnsPublisherService.cs similarity index 69% rename from ResidentialBuilding.Generator/Messaging/SnsPublisherService.cs rename to ResidentialBuilding.Generator/Service/Messaging/SnsPublisherService.cs index dc7a3f27..a0b339a2 100644 --- a/ResidentialBuilding.Generator/Messaging/SnsPublisherService.cs +++ b/ResidentialBuilding.Generator/Service/Messaging/SnsPublisherService.cs @@ -4,15 +4,15 @@ using Amazon.SimpleNotificationService.Model; using Generator.DTO; -namespace Generator.Messaging; +namespace Generator.Service.Messaging; /// -/// Служба для отправки сообщений в SNS +/// Сервис для отправки сообщений в SNS /// /// Клиент SNS /// Конфигурация /// Логгер -public class SnsPublisherService(IAmazonSimpleNotificationService client, IConfiguration configuration, ILogger logger) : IProducerService +public class SnsPublisherService(IAmazonSimpleNotificationService client, IConfiguration configuration, ILogger logger) : IPublisherService { private readonly string _topicArn = configuration["AWS:Resources:SNSTopicArn"] ?? throw new KeyNotFoundException("SNS topic link was not found in configuration"); @@ -28,11 +28,14 @@ public async Task SendMessage(ResidentialBuildingDto residentialBuilding) Message = json, TopicArn = _topicArn }; - var response = await client.PublishAsync(request); - if (response.HttpStatusCode == HttpStatusCode.OK) - logger.LogInformation("Residential building {id} was sent to sink via SNS", residentialBuilding.Id); - else - throw new Exception($"SNS returned {response.HttpStatusCode}"); + + var statusCode = (await client.PublishAsync(request)).HttpStatusCode; + if (statusCode != HttpStatusCode.OK) + { + throw new Exception($"SNS returned status code {statusCode}"); + } + + logger.LogInformation("Residential building with id={Id} was sent to file service via SNS", residentialBuilding.Id); } catch (Exception ex) { diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs index c261219f..fb8083f0 100644 --- a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -1,82 +1,32 @@ using Generator.DTO; using Generator.Generator; -using Microsoft.Extensions.Caching.Distributed; -using System.Text.Json; -using Generator.Messaging; +using Generator.Service.Cache; +using Generator.Service.Messaging; namespace Generator.Service; public class ResidentialBuildingService( ILogger logger, ResidentialBuildingGenerator generator, - IDistributedCache cache, - IProducerService messagingService, - IConfiguration configuration + ICacheService cacheService, + IPublisherService messagingService ) : IResidentialBuildingService { - private const string CacheKeyPrefix = "residential-building:"; - - private const int CacheExpirationTimeMinutesDefault = 15; - - private readonly TimeSpan _cacheExpirationTimeMinutes = TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationTimeMinutes", CacheExpirationTimeMinutesDefault)); public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) { - var cacheKey = $"{CacheKeyPrefix}{id}"; + var obj = await cacheService.GetCache(id, cancellationToken); - string? jsonCached = null; - try - { - jsonCached = await cache.GetStringAsync(cacheKey, cancellationToken); - } - catch (Exception ex) + if (obj is not null) { - logger.LogWarning(ex, "Failed to read from distributed cache for key={cacheKey}. Falling back to generation.", cacheKey); - } - - if (!string.IsNullOrEmpty(jsonCached)) - { - logger.LogInformation("Cache for residential building with Id={} received.", id); - - ResidentialBuildingDto? objCached = null; - try - { - objCached = JsonSerializer.Deserialize(jsonCached); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Invalid JSON in residential building cache for key {cacheKey}.", cacheKey); - } - - if (objCached is null) - { - logger.LogWarning("Cache for residential building with Id={id} returned null.", id); - } - else - { - logger.LogInformation("Cache for residential building with Id={id} is valid, returned", id); - return objCached; - } + return obj; } - var obj = generator.Generate(id); - - try - { - var cacheOptions = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = _cacheExpirationTimeMinutes - }; - await messagingService.SendMessage(obj); - await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), cacheOptions, cancellationToken); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to write residential building with Id={id} to cache. Still returning generated value.", id); - } + obj = generator.Generate(id); + await cacheService.SetCache(id, obj, cancellationToken); + await messagingService.SendMessage(obj); - logger.LogInformation("Generated and cached residential building with Id={id}", id); - + logger.LogInformation("Generated, cached and sent to SNS residential building with Id={id}", id); return obj; } } \ No newline at end of file From 07bfa04a97ff50778462ba4dda519ca3d8b831d8 Mon Sep 17 00:00:00 2001 From: Donistr Date: Tue, 14 Apr 2026 16:55:12 +0400 Subject: [PATCH 28/37] =?UTF-8?q?fix:=20=D0=B7=D0=B0=D0=B1=D1=8B=D0=BB=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/Cache/CacheService.cs | 81 +++++++++++++++++++ .../Service/Cache/ICacheService.cs | 8 ++ 2 files changed, 89 insertions(+) create mode 100644 ResidentialBuilding.Generator/Service/Cache/CacheService.cs create mode 100644 ResidentialBuilding.Generator/Service/Cache/ICacheService.cs diff --git a/ResidentialBuilding.Generator/Service/Cache/CacheService.cs b/ResidentialBuilding.Generator/Service/Cache/CacheService.cs new file mode 100644 index 00000000..2f06ba39 --- /dev/null +++ b/ResidentialBuilding.Generator/Service/Cache/CacheService.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace Generator.Service.Cache; + +public class CacheService( + ILogger logger, + IDistributedCache cache, + IConfiguration configuration) : ICacheService +{ + private const string CacheKeyPrefix = "residential-building:"; + + private const int CacheExpirationTimeMinutesDefault = 15; + + private readonly TimeSpan _cacheExpirationTimeMinutes = + TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationTimeMinutes", + CacheExpirationTimeMinutesDefault)); + + public async Task GetCache(int id, CancellationToken cancellationToken = default) + { + var cacheKey = $"{CacheKeyPrefix}{id}"; + + string? jsonCached; + try + { + jsonCached = await cache.GetStringAsync(cacheKey, cancellationToken); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to read from distributed cache for key={cacheKey}.", cacheKey); + return default; + } + + if (string.IsNullOrEmpty(jsonCached)) + { + logger.LogWarning("Received cache for key={cacheKey} is null or empty.", cacheKey); + return default; + } + + T? objCached; + try + { + objCached = JsonSerializer.Deserialize(jsonCached); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Invalid JSON in cache for key {cacheKey}.", cacheKey); + return default; + } + + if (objCached is null) + { + logger.LogWarning("Cache for key {cacheKey} returned null.", cacheKey); + return default; + } + + logger.LogInformation("Cache for cache key {cacheKey} is valid, returned", cacheKey); + return objCached; + } + + public async Task SetCache(int id, T obj, CancellationToken cancellationToken = default) + { + var cacheKey = $"{CacheKeyPrefix}{id}"; + + try + { + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheExpirationTimeMinutes + }; + + await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(obj), cacheOptions, cancellationToken); + return true; + } + catch(Exception ex) + { + logger.LogWarning(ex, "Failed to write object for cache key {cacheKey}.", cacheKey); + return false; + } + } +} \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Service/Cache/ICacheService.cs b/ResidentialBuilding.Generator/Service/Cache/ICacheService.cs new file mode 100644 index 00000000..58c0c66d --- /dev/null +++ b/ResidentialBuilding.Generator/Service/Cache/ICacheService.cs @@ -0,0 +1,8 @@ +namespace Generator.Service.Cache; + +public interface ICacheService +{ + public Task GetCache(int id, CancellationToken cancellationToken = default); + + public Task SetCache(int id, T obj, CancellationToken cancellationToken = default); +} \ No newline at end of file From 701551ab3a898d1df183d14f655f1b5b0510232d Mon Sep 17 00:00:00 2001 From: Donistr Date: Tue, 14 Apr 2026 17:40:34 +0400 Subject: [PATCH 29/37] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D1=8B=D0=B9?= =?UTF-8?q?=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 25 ++++ CloudDevelopment.sln | 6 + ResidentialBuilding.AppHost/AppHost.cs | 9 +- .../ResidentialBuilding.AppHost.csproj | 1 + .../Controller/S3StorageController.cs | 68 +++++++++ .../Controller/SnsSubscriberController.cs | 78 ++++++++++ ResidentialBuilding.FileService/Dockerfile | 23 +++ ResidentialBuilding.FileService/Program.cs | 34 +++++ .../Properties/launchSettings.json | 40 ++++++ .../ResidentialBuilding.FileService.csproj | 27 ++++ .../Service/Messaging/ISubscriptionService.cs | 9 ++ .../Messaging/SnsSubscriptionService.cs | 51 +++++++ .../Service/Storage/AwsFileService.cs | 134 ++++++++++++++++++ .../Service/Storage/IFileService.cs | 33 +++++ .../appsettings.Development.json | 8 ++ .../appsettings.json | 8 ++ .../Service/Messaging/SnsPublisherService.cs | 22 +-- 17 files changed, 566 insertions(+), 10 deletions(-) create mode 100644 .dockerignore create mode 100644 ResidentialBuilding.FileService/Controller/S3StorageController.cs create mode 100644 ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs create mode 100644 ResidentialBuilding.FileService/Dockerfile create mode 100644 ResidentialBuilding.FileService/Program.cs create mode 100644 ResidentialBuilding.FileService/Properties/launchSettings.json create mode 100644 ResidentialBuilding.FileService/ResidentialBuilding.FileService.csproj create mode 100644 ResidentialBuilding.FileService/Service/Messaging/ISubscriptionService.cs create mode 100644 ResidentialBuilding.FileService/Service/Messaging/SnsSubscriptionService.cs create mode 100644 ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs create mode 100644 ResidentialBuilding.FileService/Service/Storage/IFileService.cs create mode 100644 ResidentialBuilding.FileService/appsettings.Development.json create mode 100644 ResidentialBuilding.FileService/appsettings.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index 09f502a1..bb769e3e 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.Service EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.Gateway", "ResidentialBuilding.Gateway\ResidentialBuilding.Gateway.csproj", "{2CC59865-F962-47FF-9C2F-1CB6F535316B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.FileService", "ResidentialBuilding.FileService\ResidentialBuilding.FileService.csproj", "{B5797E57-88BC-41C8-97A8-D226E4596503}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {2CC59865-F962-47FF-9C2F-1CB6F535316B}.Debug|Any CPU.Build.0 = Debug|Any CPU {2CC59865-F962-47FF-9C2F-1CB6F535316B}.Release|Any CPU.ActiveCfg = Release|Any CPU {2CC59865-F962-47FF-9C2F-1CB6F535316B}.Release|Any CPU.Build.0 = Release|Any CPU + {B5797E57-88BC-41C8-97A8-D226E4596503}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5797E57-88BC-41C8-97A8-D226E4596503}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5797E57-88BC-41C8-97A8-D226E4596503}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5797E57-88BC-41C8-97A8-D226E4596503}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ResidentialBuilding.AppHost/AppHost.cs b/ResidentialBuilding.AppHost/AppHost.cs index d05055b2..14517138 100644 --- a/ResidentialBuilding.AppHost/AppHost.cs +++ b/ResidentialBuilding.AppHost/AppHost.cs @@ -28,7 +28,7 @@ }); var awsResources = builder - .AddAWSCloudFormationTemplate("resources", "CloudFormation/residential-building-template-sns-s3.yaml", "residential-building") + .AddAWSCloudFormationTemplate("files", "CloudFormation/residential-building-template-sns-s3.yaml", "residential-building") .WithReference(awsConfig); const int generatorPortBase = 5200; @@ -49,6 +49,13 @@ .WithReference(gateway) .WaitFor(gateway); +var fileService = builder.AddProject("residential-building-file-service") + .WithReference(awsResources) + .WithEnvironment("Settings__MessageBroker", "SNS") + .WithEnvironment("Settings__S3Hosting", "Localstack") + .WaitFor(awsResources); +fileService.WithEnvironment("AWS__Resources__SNSUrl", "http://host.docker.internal:5280/api/sns"); + builder.UseLocalStack(localstack); builder.Build().Run(); \ No newline at end of file diff --git a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj index d44d2f6f..677ac305 100644 --- a/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj +++ b/ResidentialBuilding.AppHost/ResidentialBuilding.AppHost.csproj @@ -20,6 +20,7 @@ + diff --git a/ResidentialBuilding.FileService/Controller/S3StorageController.cs b/ResidentialBuilding.FileService/Controller/S3StorageController.cs new file mode 100644 index 00000000..e9291a5b --- /dev/null +++ b/ResidentialBuilding.FileService/Controller/S3StorageController.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc; +using ResidentialBuilding.EventSink.Service.Storage; +using System.Text; +using System.Text.Json.Nodes; + +namespace ResidentialBuilding.EventSink.Controller; + +/// +/// Контроллер для взаимодейсвия с S3 +/// +/// Служба для работы с S3 +/// Логгер +[ApiController] +[Route("api/s3")] +public class S3StorageController(IFileService fileService, ILogger logger) : ControllerBase +{ + /// + /// Метод для получения списка хранящихся в S3 файлов + /// + /// Список с ключами файлов + [HttpGet] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> ListFiles() + { + logger.LogInformation("Method {method} of {controller} was called.", nameof(ListFiles), + nameof(S3StorageController)); + try + { + var list = await fileService.GetFilesList(); + + logger.LogInformation("Got a list of {count} files from bucket.", list.Count); + return Ok(list); + } + catch (Exception ex) + { + logger.LogError(ex, "Exception occured during {method} of {controller}.", nameof(ListFiles), + nameof(S3StorageController)); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// Получает строковое представление хранящегося в S3 документа + /// + /// Ключ файла + /// Строковое представление файла + [HttpGet("{key}")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task> GetFile(string key) + { + logger.LogInformation("Method {method} of {controller} was called.", nameof(GetFile), + nameof(S3StorageController)); + try + { + var node = await fileService.DownloadFile(key); + logger.LogInformation("Received json of {size} bytes.", Encoding.UTF8.GetByteCount(node.ToJsonString())); + return Ok(node); + } + catch (Exception ex) + { + logger.LogError(ex, "Exception occured during {method} of {controller}.", nameof(GetFile), + nameof(S3StorageController)); + return StatusCode(500, new { message = ex.Message }); + } + } +} \ No newline at end of file diff --git a/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs b/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs new file mode 100644 index 00000000..513bb04a --- /dev/null +++ b/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs @@ -0,0 +1,78 @@ +using Amazon.SimpleNotificationService.Util; +using Microsoft.AspNetCore.Mvc; +using ResidentialBuilding.EventSink.Service.Storage; +using System.Text; + +namespace ResidentialBuilding.EventSink.Controller; + +/// +/// Контроллер для приема сообщений от SNS +/// +/// Служба для работы с S3 +/// Логгер +[ApiController] +[Route("api/sns")] +public class SnsSubscriberController(IFileService fileService, ILogger logger) : ControllerBase +{ + /// + /// Вебхук, который получает оповещения из SNS топика + /// + /// + /// Используется не только, чтобы получать оповещения, + /// но и для того, чтобы подтвердить подписку при + /// инициализации информационного обмена. + /// В любом случае должен возвращать 200 + /// + [HttpPost] + [ProducesResponseType(200)] + public async Task ReceiveMessage() + { + logger.LogInformation("SNS webhook was called."); + try + { + using var reader = new StreamReader(Request.Body, Encoding.UTF8); + var jsonContent = await reader.ReadToEndAsync(); + + var snsMessage = Message.ParseMessage(jsonContent); + + switch (snsMessage.Type) + { + case "SubscriptionConfirmation": + { + logger.LogInformation("SubscriptionConfirmation was received."); + + using var httpClient = new HttpClient(); + var builder = new UriBuilder(new Uri(snsMessage.SubscribeURL)) + { + Scheme = "http", + Host = "localhost", + Port = 4566 + }; + var response = await httpClient.GetAsync(builder.Uri); + + if (!response.IsSuccessStatusCode) + { + var body = await response.Content.ReadAsStringAsync(); + throw new Exception($"SubscriptionConfirmation returned {response.StatusCode}: {body}."); + } + + logger.LogInformation("Subscription was successfully confirmed."); + return Ok(); + } + case "Notification": + { + await fileService.UploadFile(snsMessage.MessageText); + + logger.LogInformation("Notification was successfully processed."); + break; + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Exception occurred while processing SNS notifications."); + } + + return Ok(); + } +} \ No newline at end of file diff --git a/ResidentialBuilding.FileService/Dockerfile b/ResidentialBuilding.FileService/Dockerfile new file mode 100644 index 00000000..08c071bf --- /dev/null +++ b/ResidentialBuilding.FileService/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["ResidentialBuilding.EventSink/ResidentialBuilding.EventSink.csproj", "ResidentialBuilding.EventSink/"] +RUN dotnet restore "ResidentialBuilding.EventSink/ResidentialBuilding.EventSink.csproj" +COPY . . +WORKDIR "/src/ResidentialBuilding.EventSink" +RUN dotnet build "./ResidentialBuilding.EventSink.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./ResidentialBuilding.EventSink.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ResidentialBuilding.EventSink.dll"] diff --git a/ResidentialBuilding.FileService/Program.cs b/ResidentialBuilding.FileService/Program.cs new file mode 100644 index 00000000..f27f5d49 --- /dev/null +++ b/ResidentialBuilding.FileService/Program.cs @@ -0,0 +1,34 @@ +using Amazon.S3; +using Amazon.SimpleNotificationService; +using LocalStack.Client.Extensions; +using Microsoft.AspNetCore.Builder; +using ResidentialBuilding.ServiceDefaults; +using ResidentialBuilding.EventSink.Service.Messaging; +using ResidentialBuilding.EventSink.Service.Storage; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddControllers(); + +builder.Services.AddLocalStack(builder.Configuration); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddAwsService(); +builder.Services.AddAwsService(); + +var app = builder.Build(); + +var scope = app.Services.CreateScope(); +var subscriptionService = scope.ServiceProvider.GetRequiredService(); +await subscriptionService.SubscribeEndpoint(); + +scope = app.Services.CreateScope(); +var s3Service = scope.ServiceProvider.GetRequiredService(); +await s3Service.EnsureBucketExists(); + +app.MapControllers(); +app.Run(); \ No newline at end of file diff --git a/ResidentialBuilding.FileService/Properties/launchSettings.json b/ResidentialBuilding.FileService/Properties/launchSettings.json new file mode 100644 index 00000000..f68ddb41 --- /dev/null +++ b/ResidentialBuilding.FileService/Properties/launchSettings.json @@ -0,0 +1,40 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5280" + }, + "https": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7099;http://localhost:5280" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": false, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/ResidentialBuilding.FileService/ResidentialBuilding.FileService.csproj b/ResidentialBuilding.FileService/ResidentialBuilding.FileService.csproj new file mode 100644 index 00000000..06b40de5 --- /dev/null +++ b/ResidentialBuilding.FileService/ResidentialBuilding.FileService.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + dotnet-ResidentialBuilding.EventSink-3fc8b533-d1b8-4ada-b0c8-745b4c7331f7 + Linux + ResidentialBuilding.EventSink + + + + + + + + + + + + + + + .dockerignore + + + diff --git a/ResidentialBuilding.FileService/Service/Messaging/ISubscriptionService.cs b/ResidentialBuilding.FileService/Service/Messaging/ISubscriptionService.cs new file mode 100644 index 00000000..64a6c30a --- /dev/null +++ b/ResidentialBuilding.FileService/Service/Messaging/ISubscriptionService.cs @@ -0,0 +1,9 @@ +namespace ResidentialBuilding.EventSink.Service.Messaging; + +public interface ISubscriptionService +{ + /// + /// Делает попытку подписаться на топик SNS + /// + public Task SubscribeEndpoint(); +} \ No newline at end of file diff --git a/ResidentialBuilding.FileService/Service/Messaging/SnsSubscriptionService.cs b/ResidentialBuilding.FileService/Service/Messaging/SnsSubscriptionService.cs new file mode 100644 index 00000000..468c49d2 --- /dev/null +++ b/ResidentialBuilding.FileService/Service/Messaging/SnsSubscriptionService.cs @@ -0,0 +1,51 @@ +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using System.Net; + +namespace ResidentialBuilding.EventSink.Service.Messaging; + +/// +/// Служба для подписки на SNS на старте приложения +/// +/// Клиент SNS +/// Конфигурация +/// Логгер +public class SnsSubscriptionService( + IAmazonSimpleNotificationService snsClient, + IConfiguration configuration, + ILogger logger) : ISubscriptionService +{ + /// + /// Уникальный идентификатор топика SNS + /// + private readonly string _topicArn = configuration["AWS:Resources:SNSTopicArn"] ?? + throw new KeyNotFoundException("SNS topic link was not found in configuration"); + + /// + /// Делает попытку подписаться на топик SNS + /// + public async Task SubscribeEndpoint() + { + logger.LogInformation("Sending subscribe request for topic {topic}", _topicArn); + var endpoint = configuration["AWS:Resources:SNSUrl"]; + + var request = new SubscribeRequest + { + TopicArn = _topicArn, + Protocol = "http", + Endpoint = endpoint, + ReturnSubscriptionArn = true + }; + var response = await snsClient.SubscribeAsync(request); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + logger.LogError("Failed to subscribe to topic {topic}", _topicArn); + } + else + { + logger.LogInformation("Subscription request for topic {topic} is successfully, waiting for confirmation", + _topicArn); + } + } +} \ No newline at end of file diff --git a/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs b/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs new file mode 100644 index 00000000..5dd74a88 --- /dev/null +++ b/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs @@ -0,0 +1,134 @@ +using Amazon.S3; +using Amazon.S3.Model; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace ResidentialBuilding.EventSink.Service.Storage; + +public class AwsFileService(IAmazonS3 client, IConfiguration configuration, ILogger logger) + : IFileService +{ + private readonly string _bucketName = configuration["AWS:Resources:S3BucketName"] + ?? throw new KeyNotFoundException( + "S3 bucket name was not found in configuration"); + + /// + public async Task> GetFilesList() + { + var list = new List(); + + var request = new ListObjectsV2Request + { + BucketName = _bucketName, + Prefix = "", + Delimiter = "," + }; + var paginator = client.Paginators.ListObjectsV2(request); + + logger.LogInformation("Began listing files in bucket {bucket}.", _bucketName); + await foreach (var response in paginator.Responses) + { + if (response?.S3Objects == null) + { + logger.LogWarning("Received null response from bucket {bucket}.", _bucketName); + continue; + } + + foreach (var obj in response.S3Objects) + { + if (obj == null) + { + logger.LogWarning("Received null object from bucket {bucket}.", _bucketName); + continue; + } + + list.Add(obj.Key); + } + } + + return list; + } + + /// + public async Task UploadFile(string fileData) + { + logger.LogInformation("Uploading file {file}.", fileData); + + var rootNode = JsonNode.Parse(fileData) ?? throw new ArgumentException("Passed string is not a valid JSON"); + var id = rootNode["Id"]?.GetValue() ?? throw new ArgumentException("Passed JSON has invalid structure"); + + using var stream = new MemoryStream(); + await JsonSerializer.SerializeAsync(stream, rootNode); + stream.Seek(0, SeekOrigin.Begin); + + logger.LogInformation("Began uploading residential building with id={Id} onto bucket {bucket}.", id, + _bucketName); + var request = new PutObjectRequest + { + BucketName = _bucketName, + Key = $"residential_building_{id}.json", + InputStream = stream + }; + + var response = await client.PutObjectAsync(request); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + logger.LogError("Failed to upload residential building {file}: {code}.", id, response.HttpStatusCode); + return false; + } + + logger.LogInformation("Finished uploading residential building {file} to {bucket}.", id, _bucketName); + return true; + } + + /// + public async Task DownloadFile(string key) + { + logger.LogInformation("Began downloading file {file} from bucket {bucket}.", key, _bucketName); + + try + { + var request = new GetObjectRequest + { + BucketName = _bucketName, + Key = key + }; + using var response = await client.GetObjectAsync(request); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + logger.LogError("Failed to download file {file}, code: {code}.", key, response.HttpStatusCode); + throw new InvalidOperationException( + $"Error occurred downloading file {key} - {response.HttpStatusCode}."); + } + + using var reader = new StreamReader(response.ResponseStream, Encoding.UTF8); + return JsonNode.Parse(await reader.ReadToEndAsync()) ?? + throw new InvalidOperationException($"Downloaded document is not a valid JSON."); + } + catch (Exception ex) + { + logger.LogError(ex, "Exception occurred during downloading file {file}.", key); + throw; + } + } + + /// + public async Task EnsureBucketExists() + { + logger.LogInformation("Checking whether bucket {bucket} exists.", _bucketName); + try + { + await client.EnsureBucketExistsAsync(_bucketName); + logger.LogInformation("bucket {bucket} existence ensured.", _bucketName); + } + catch (Exception ex) + { + logger.LogError(ex, "Unhandled exception occurred during checking bucket {bucket}.", _bucketName); + throw; + } + } +} \ No newline at end of file diff --git a/ResidentialBuilding.FileService/Service/Storage/IFileService.cs b/ResidentialBuilding.FileService/Service/Storage/IFileService.cs new file mode 100644 index 00000000..751266d0 --- /dev/null +++ b/ResidentialBuilding.FileService/Service/Storage/IFileService.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Nodes; + +namespace ResidentialBuilding.EventSink.Service.Storage; + +/// +/// Интерфейс службы для манипуляции файлами в объектном хранилище +/// +public interface IFileService +{ + /// + /// Отправляет файл в хранилище + /// + /// Строковая репрезентация сохраняемого файла + public Task UploadFile(string fileData); + + /// + /// Получает список всех файлов из хранилища + /// + /// Список путей к файлам + public Task> GetFilesList(); + + /// + /// Получает строковую репрезентацию файла из хранилища + /// + /// Путь к файлу в бакете + /// Строковая репрезентация прочтенного файла + public Task DownloadFile(string filePath); + + /// + /// Создает S3 бакет при необходимости + /// + public Task EnsureBucketExists(); +} \ No newline at end of file diff --git a/ResidentialBuilding.FileService/appsettings.Development.json b/ResidentialBuilding.FileService/appsettings.Development.json new file mode 100644 index 00000000..b2dcdb67 --- /dev/null +++ b/ResidentialBuilding.FileService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/ResidentialBuilding.FileService/appsettings.json b/ResidentialBuilding.FileService/appsettings.json new file mode 100644 index 00000000..b2dcdb67 --- /dev/null +++ b/ResidentialBuilding.FileService/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/ResidentialBuilding.Generator/Service/Messaging/SnsPublisherService.cs b/ResidentialBuilding.Generator/Service/Messaging/SnsPublisherService.cs index a0b339a2..308604f6 100644 --- a/ResidentialBuilding.Generator/Service/Messaging/SnsPublisherService.cs +++ b/ResidentialBuilding.Generator/Service/Messaging/SnsPublisherService.cs @@ -1,8 +1,8 @@ -using System.Net; -using System.Text.Json; -using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService; using Amazon.SimpleNotificationService.Model; using Generator.DTO; +using System.Net; +using System.Text.Json; namespace Generator.Service.Messaging; @@ -12,10 +12,14 @@ namespace Generator.Service.Messaging; /// Клиент SNS /// Конфигурация /// Логгер -public class SnsPublisherService(IAmazonSimpleNotificationService client, IConfiguration configuration, ILogger logger) : IPublisherService +public class SnsPublisherService( + IAmazonSimpleNotificationService client, + IConfiguration configuration, + ILogger logger) : IPublisherService { private readonly string _topicArn = configuration["AWS:Resources:SNSTopicArn"] - ?? throw new KeyNotFoundException("SNS topic link was not found in configuration"); + ?? throw new KeyNotFoundException( + "SNS topic link was not found in configuration"); /// public async Task SendMessage(ResidentialBuildingDto residentialBuilding) @@ -28,19 +32,19 @@ public async Task SendMessage(ResidentialBuildingDto residentialBuilding) Message = json, TopicArn = _topicArn }; - + var statusCode = (await client.PublishAsync(request)).HttpStatusCode; if (statusCode != HttpStatusCode.OK) { throw new Exception($"SNS returned status code {statusCode}"); } - - logger.LogInformation("Residential building with id={Id} was sent to file service via SNS", residentialBuilding.Id); + + logger.LogInformation("Residential building with id={Id} was sent to file service via SNS", + residentialBuilding.Id); } catch (Exception ex) { logger.LogError(ex, "Unable to send residential building through SNS topic"); } - } } \ No newline at end of file From b25b77919e589eb969fcca28aca93b381f1ade2b Mon Sep 17 00:00:00 2001 From: Donistr Date: Tue, 14 Apr 2026 20:39:53 +0400 Subject: [PATCH 30/37] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=BE=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudDevelopment.sln | 6 + ResidentialBuilding.Tests/AppFixture.cs | 42 ++++ ResidentialBuilding.Tests/IntegrationTests.cs | 226 ++++++++++++++++++ .../ResidentialBuilding.Tests.csproj | 42 ++++ 4 files changed, 316 insertions(+) create mode 100644 ResidentialBuilding.Tests/AppFixture.cs create mode 100644 ResidentialBuilding.Tests/IntegrationTests.cs create mode 100644 ResidentialBuilding.Tests/ResidentialBuilding.Tests.csproj diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index bb769e3e..30762b2c 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.Gateway EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.FileService", "ResidentialBuilding.FileService\ResidentialBuilding.FileService.csproj", "{B5797E57-88BC-41C8-97A8-D226E4596503}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialBuilding.Tests", "ResidentialBuilding.Tests\ResidentialBuilding.Tests.csproj", "{ADBF06EE-2008-4D96-8337-078A1C1E80DF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {B5797E57-88BC-41C8-97A8-D226E4596503}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5797E57-88BC-41C8-97A8-D226E4596503}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5797E57-88BC-41C8-97A8-D226E4596503}.Release|Any CPU.Build.0 = Release|Any CPU + {ADBF06EE-2008-4D96-8337-078A1C1E80DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADBF06EE-2008-4D96-8337-078A1C1E80DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADBF06EE-2008-4D96-8337-078A1C1E80DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADBF06EE-2008-4D96-8337-078A1C1E80DF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ResidentialBuilding.Tests/AppFixture.cs b/ResidentialBuilding.Tests/AppFixture.cs new file mode 100644 index 00000000..e8a6e06d --- /dev/null +++ b/ResidentialBuilding.Tests/AppFixture.cs @@ -0,0 +1,42 @@ +using Aspire.Hosting; + +namespace ResidentialBuilding.Tests; + +/// +/// Фикстура для интеграционных тестов, использующая .NET Aspire DistributedApplicationTesting. +/// Обеспечивает запуск полного стека приложения (AppHost) один раз для всех тестов класса, +/// использующих . +/// +public class AppFixture : IAsyncLifetime +{ + /// + /// Экземпляр запущенного распределённого приложения Aspire. + /// Доступен всем тестам через внедрение зависимости. + /// + public DistributedApplication App { get; private set; } = null!; + + private IDistributedApplicationTestingBuilder? _builder; + + /// + /// Инициализирует и запускает все сервисы приложения перед выполнением тестов. + /// + public async Task InitializeAsync() + { + _builder = await DistributedApplicationTestingBuilder + .CreateAsync(); + _builder.Configuration["DcpPublisher:RandomizePorts"] = "false"; + + App = await _builder.BuildAsync(); + await App.StartAsync(); + } + + /// + /// Корректно останавливает все сервисы и освобождает ресурсы после завершения тестов. + /// + public async Task DisposeAsync() + { + await App.StopAsync(); + await App.DisposeAsync(); + await _builder!.DisposeAsync(); + } +} \ No newline at end of file diff --git a/ResidentialBuilding.Tests/IntegrationTests.cs b/ResidentialBuilding.Tests/IntegrationTests.cs new file mode 100644 index 00000000..7d6b1955 --- /dev/null +++ b/ResidentialBuilding.Tests/IntegrationTests.cs @@ -0,0 +1,226 @@ +using Aspire.Hosting; +using Generator.DTO; +using System.Text.Json; + +namespace ResidentialBuilding.Tests; + +/// +/// Интеграционные тесты для проверки микросервисного пайплайна. +/// +/// Фикстура, чтобы не поднимать Aspire для каждого теста отдельно. +public class IntegrationTests(AppFixture fixture) : IClassFixture +{ + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private static T? Deserialize(string json) => JsonSerializer.Deserialize(json, _jsonOptions); + + private readonly DistributedApplication _app = fixture.App; + + /// + /// Проверяет основной положительный сценарий: + /// Запрос через Gateway → генерация объекта → сохранение в S3 → данные идентичны. + /// + [Fact] + public async Task GetFromGateway_SavesToS3_AndDataIsIdentical() + { + var gatewayClient = _app.CreateHttpClient("gateway", "http"); + var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); + + var id = Random.Shared.Next(1, 10); + + var response = await gatewayClient.GetAsync($"/residential-building?id={id}"); + response.EnsureSuccessStatusCode(); + + var apiBuilding = Deserialize(await response.Content.ReadAsStringAsync()); + + await WaitForFileInS3Async(fileClient, id, TimeSpan.FromSeconds(10)); + + var s3Building = await GetBuildingFromS3Async(fileClient, id); + + Assert.NotNull(apiBuilding); + Assert.Equal(id, apiBuilding.Id); + Assert.NotNull(s3Building); + Assert.Equal(id, s3Building.Id); + Assert.Equivalent(apiBuilding, s3Building, strict: true); + } + + /// + /// Проверяет корректность сохранения объектов с разными идентификаторами. + /// + [Theory] + [InlineData(11)] + [InlineData(12)] + [InlineData(13)] + public async Task DifferentIds_AreCorrectlySavedToS3(int id) + { + var gatewayClient = _app.CreateHttpClient("gateway", "http"); + var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); + + var response = await gatewayClient.GetAsync($"/residential-building?id={id}"); + response.EnsureSuccessStatusCode(); + + var apiBuilding = Deserialize(await response.Content.ReadAsStringAsync()); + + await WaitForFileInS3Async(fileClient, id, TimeSpan.FromSeconds(10)); + + var s3Building = await GetBuildingFromS3Async(fileClient, id); + + Assert.NotNull(s3Building); + Assert.Equal(id, s3Building.Id); + Assert.Equivalent(apiBuilding, s3Building, strict: true); + } + + /// + /// Проверяет работу кэширования в Generator: + /// При повторных запросах одного и того же id возвращается идентичный объект, + /// и S3 тоже возвращает один и тот же объект. + /// + [Fact] + public async Task SameIds_GivingSameObjects() + { + var gatewayClient = _app.CreateHttpClient("gateway", "http"); + var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); + + var id = Random.Shared.Next(20, 30); + + var response = await gatewayClient.GetAsync($"/residential-building?id={id}"); + response.EnsureSuccessStatusCode(); + + var apiBuilding = Deserialize(await response.Content.ReadAsStringAsync()); + + await WaitForFileInS3Async(fileClient, id, TimeSpan.FromSeconds(10)); + + var s3Building = await GetBuildingFromS3Async(fileClient, id); + + var response1 = await gatewayClient.GetAsync($"/residential-building?id={id}"); + response.EnsureSuccessStatusCode(); + + var apiBuilding1 = Deserialize(await response1.Content.ReadAsStringAsync()); + + await WaitForFileInS3Async(fileClient, id, TimeSpan.FromSeconds(10)); + + var s3Building1 = await GetBuildingFromS3Async(fileClient, id); + + Assert.NotNull(apiBuilding); + Assert.Equal(id, apiBuilding.Id); + Assert.NotNull(s3Building); + Assert.Equal(id, s3Building.Id); + Assert.Equivalent(apiBuilding, s3Building, strict: true); + + Assert.NotNull(apiBuilding1); + Assert.Equal(id, apiBuilding1.Id); + Assert.NotNull(s3Building1); + Assert.Equal(id, s3Building1.Id); + Assert.Equivalent(apiBuilding1, s3Building1, strict: true); + + Assert.Equivalent(apiBuilding, apiBuilding1, strict: true); + Assert.Equivalent(s3Building, s3Building1, strict: true); + } + + /// + /// Проверяет обработку нескольких разных объектов в одном тесте. + /// Убеждается, что все сгенерированные объекты корректно сохраняются в S3. + /// + [Fact] + public async Task MultipleDifferentIds_AllSavedCorrectly() + { + var gatewayClient = _app.CreateHttpClient("gateway", "http"); + var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); + + var ids = Enumerable.Range(0, 5).Select(_ => Random.Shared.Next(40, 50)).ToList(); + + var apiBuildings = new List(); + + foreach (var id in ids) + { + var response = await gatewayClient.GetAsync($"/residential-building?id={id}"); + response.EnsureSuccessStatusCode(); + + var building = Deserialize(await response.Content.ReadAsStringAsync()); + Assert.NotNull(building); + apiBuildings.Add(building!); + + await WaitForFileInS3Async(fileClient, id, TimeSpan.FromSeconds(8)); + } + + foreach (var id in ids) + { + var s3Building = await GetBuildingFromS3Async(fileClient, id); + var original = apiBuildings.First(b => b.Id == id); + + Assert.Equal(id, s3Building.Id); + Assert.Equivalent(original, s3Building, strict: true); + } + } + + /// + /// Проверяет эндпоинт получения списка файлов в S3 и уникальность по Id. + /// Специально используется повторяющийся id (1001), чтобы убедиться, + /// что для одного идентификатора создаётся только один файл. + /// + [Fact] + public async Task S3_ReturnsCorrectListOfFiles() + { + var gatewayClient = _app.CreateHttpClient("gateway", "http"); + var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); + + var ids = new[] { 1001, 1002, 1003, 1001 }; + + foreach (var id in ids) + { + await gatewayClient.GetAsync($"/residential-building?id={id}"); + await WaitForFileInS3Async(fileClient, id, TimeSpan.FromSeconds(10)); + } + + var listResponse = await fileClient.GetAsync("/api/s3"); + listResponse.EnsureSuccessStatusCode(); + + var fileList = Deserialize>(await listResponse.Content.ReadAsStringAsync()); + + Assert.NotNull(fileList); + + foreach (var id in ids) + { + Assert.Contains($"residential_building_{id}.json", fileList); + } + var addedFiles = fileList.Where(f => f.StartsWith("residential_building_100")).ToList(); + Assert.Equal(3, addedFiles.Count); + } + + /// + /// Ожидает появления файла в S3 с указанным id. + /// Поллинг с таймаутом — необходим из-за асинхронной природы связи SNS и FileService. + /// + private static async Task WaitForFileInS3Async(HttpClient fileClient, int id, TimeSpan timeout) + { + var fileName = $"residential_building_{id}.json"; + + var deadline = DateTime.UtcNow + timeout; + + while (DateTime.UtcNow < deadline) + { + var probe = await fileClient.GetAsync($"/api/s3/{fileName}"); + if (probe.IsSuccessStatusCode) + { + return; + } + + await Task.Delay(500); + } + + throw new TimeoutException($"File {fileName} did not appear in S3 within {timeout.TotalSeconds}s"); + } + + /// + /// Скачивает и десериализует объект ResidentialBuildingDto из S3 через FileService. + /// + private static async Task GetBuildingFromS3Async(HttpClient fileClient, int id) + { + var fileServiceResponse = await fileClient.GetAsync($"/api/s3/residential_building_{id}.json"); + Console.WriteLine(fileServiceResponse.Content.ReadAsStringAsync()); + return Deserialize(await fileServiceResponse.Content.ReadAsStringAsync()); + } +} \ No newline at end of file diff --git a/ResidentialBuilding.Tests/ResidentialBuilding.Tests.csproj b/ResidentialBuilding.Tests/ResidentialBuilding.Tests.csproj new file mode 100644 index 00000000..1e922405 --- /dev/null +++ b/ResidentialBuilding.Tests/ResidentialBuilding.Tests.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + CloudFormation\residential-building-template-sns-s3.yaml + + + + From 4c57cc3efb1d2f2a353f3b55e0b7900a72c74739 Mon Sep 17 00:00:00 2001 From: Donistr Date: Tue, 14 Apr 2026 20:43:32 +0400 Subject: [PATCH 31/37] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D1=82=D0=B0=D0=BC,=20=D0=B3=D0=B4?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B5=20=D1=85=D0=B2=D0=B0=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/Cache/CacheService.cs | 5 ++++ .../Service/Cache/ICacheService.cs | 24 +++++++++++++++++++ .../Service/ResidentialBuildingService.cs | 1 + 3 files changed, 30 insertions(+) diff --git a/ResidentialBuilding.Generator/Service/Cache/CacheService.cs b/ResidentialBuilding.Generator/Service/Cache/CacheService.cs index 2f06ba39..2fc2cdc5 100644 --- a/ResidentialBuilding.Generator/Service/Cache/CacheService.cs +++ b/ResidentialBuilding.Generator/Service/Cache/CacheService.cs @@ -3,6 +3,9 @@ namespace Generator.Service.Cache; +/// +/// Реализация на базе (Redis). +/// public class CacheService( ILogger logger, IDistributedCache cache, @@ -16,6 +19,7 @@ public class CacheService( TimeSpan.FromMinutes(configuration.GetValue("CacheSettings:ExpirationTimeMinutes", CacheExpirationTimeMinutesDefault)); + /// public async Task GetCache(int id, CancellationToken cancellationToken = default) { var cacheKey = $"{CacheKeyPrefix}{id}"; @@ -58,6 +62,7 @@ public class CacheService( return objCached; } + /// public async Task SetCache(int id, T obj, CancellationToken cancellationToken = default) { var cacheKey = $"{CacheKeyPrefix}{id}"; diff --git a/ResidentialBuilding.Generator/Service/Cache/ICacheService.cs b/ResidentialBuilding.Generator/Service/Cache/ICacheService.cs index 58c0c66d..1eb98c6c 100644 --- a/ResidentialBuilding.Generator/Service/Cache/ICacheService.cs +++ b/ResidentialBuilding.Generator/Service/Cache/ICacheService.cs @@ -1,8 +1,32 @@ namespace Generator.Service.Cache; +/// +/// Интерфейс сервиса кэширования, абстрагирующий работу с распределённым кэшем (Redis). +/// public interface ICacheService { + /// + /// Получает объект из кэша по идентификатору. + /// + /// Тип десериализуемого объекта. + /// Идентификатор жилого здания (ключ кэша). + /// Токен отмены операции. + /// + /// Десериализованный объект типа , + /// или default, если объект отсутствует или произошла ошибка. + /// public Task GetCache(int id, CancellationToken cancellationToken = default); + /// + /// Сохраняет объект в распределённый кэш с заданным временем жизни. + /// + /// Тип сохраняемого объекта. + /// Идентификатор жилого здания (ключ кэша). + /// Объект для сохранения в кэше. + /// Токен отмены операции. + /// + /// true — если объект успешно сохранён, + /// false — если произошла ошибка при записи. + /// public Task SetCache(int id, T obj, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs index fb8083f0..b942db13 100644 --- a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -13,6 +13,7 @@ IPublisherService messagingService ) : IResidentialBuildingService { + /// public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) { var obj = await cacheService.GetCache(id, cancellationToken); From f7e948be5aea4965bfcb2b1135b46721b57a1dfd Mon Sep 17 00:00:00 2001 From: Donistr Date: Tue, 14 Apr 2026 20:44:19 +0400 Subject: [PATCH 32/37] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D1=82=D0=B0=D0=BC,=20=D0=B3=D0=B4?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B5=20=D1=85=D0=B2=D0=B0=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/ResidentialBuildingService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs index b942db13..07d43f78 100644 --- a/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs +++ b/ResidentialBuilding.Generator/Service/ResidentialBuildingService.cs @@ -5,6 +5,9 @@ namespace Generator.Service; +/// +/// Реализация . +/// public class ResidentialBuildingService( ILogger logger, ResidentialBuildingGenerator generator, From e361b72ec341d19d31fdda865f319be2c6140150 Mon Sep 17 00:00:00 2001 From: Donistr Date: Wed, 15 Apr 2026 12:28:41 +0400 Subject: [PATCH 33/37] =?UTF-8?q?refactor:=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BD=D0=B5=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20Dockerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.FileService/Dockerfile | 23 ---------------------- 1 file changed, 23 deletions(-) delete mode 100644 ResidentialBuilding.FileService/Dockerfile diff --git a/ResidentialBuilding.FileService/Dockerfile b/ResidentialBuilding.FileService/Dockerfile deleted file mode 100644 index 08c071bf..00000000 --- a/ResidentialBuilding.FileService/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base -USER $APP_UID -WORKDIR /app -EXPOSE 8080 -EXPOSE 8081 - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["ResidentialBuilding.EventSink/ResidentialBuilding.EventSink.csproj", "ResidentialBuilding.EventSink/"] -RUN dotnet restore "ResidentialBuilding.EventSink/ResidentialBuilding.EventSink.csproj" -COPY . . -WORKDIR "/src/ResidentialBuilding.EventSink" -RUN dotnet build "./ResidentialBuilding.EventSink.csproj" -c $BUILD_CONFIGURATION -o /app/build - -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./ResidentialBuilding.EventSink.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "ResidentialBuilding.EventSink.dll"] From e3b4472cdce067c3a36c271f65792b35cc86e4ef Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 16 Apr 2026 14:21:27 +0400 Subject: [PATCH 34/37] =?UTF-8?q?refactor:=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB=20namespace=20=D0=B2=D0=B5=D0=B7=D0=B4=D0=B5?= =?UTF-8?q?=20=D0=B2=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B5=20FileSe?= =?UTF-8?q?rvice=20=D0=BD=D0=B0=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/S3StorageController.cs | 4 ++-- .../Controller/SnsSubscriberController.cs | 4 ++-- ResidentialBuilding.FileService/Program.cs | 4 ++-- .../ResidentialBuilding.FileService.csproj | 4 ++-- .../Service/Messaging/ISubscriptionService.cs | 2 +- .../Service/Messaging/SnsSubscriptionService.cs | 2 +- .../Service/Storage/AwsFileService.cs | 2 +- .../Service/Storage/IFileService.cs | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ResidentialBuilding.FileService/Controller/S3StorageController.cs b/ResidentialBuilding.FileService/Controller/S3StorageController.cs index e9291a5b..a28d4b72 100644 --- a/ResidentialBuilding.FileService/Controller/S3StorageController.cs +++ b/ResidentialBuilding.FileService/Controller/S3StorageController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Mvc; -using ResidentialBuilding.EventSink.Service.Storage; using System.Text; using System.Text.Json.Nodes; +using ResidentialBuilding.FileService.Service.Storage; -namespace ResidentialBuilding.EventSink.Controller; +namespace ResidentialBuilding.FileService.Controller; /// /// Контроллер для взаимодейсвия с S3 diff --git a/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs b/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs index 513bb04a..e2bd2bc1 100644 --- a/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs +++ b/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs @@ -1,9 +1,9 @@ using Amazon.SimpleNotificationService.Util; using Microsoft.AspNetCore.Mvc; -using ResidentialBuilding.EventSink.Service.Storage; using System.Text; +using ResidentialBuilding.FileService.Service.Storage; -namespace ResidentialBuilding.EventSink.Controller; +namespace ResidentialBuilding.FileService.Controller; /// /// Контроллер для приема сообщений от SNS diff --git a/ResidentialBuilding.FileService/Program.cs b/ResidentialBuilding.FileService/Program.cs index f27f5d49..df74a07d 100644 --- a/ResidentialBuilding.FileService/Program.cs +++ b/ResidentialBuilding.FileService/Program.cs @@ -3,8 +3,8 @@ using LocalStack.Client.Extensions; using Microsoft.AspNetCore.Builder; using ResidentialBuilding.ServiceDefaults; -using ResidentialBuilding.EventSink.Service.Messaging; -using ResidentialBuilding.EventSink.Service.Storage; +using ResidentialBuilding.FileService.Service.Messaging; +using ResidentialBuilding.FileService.Service.Storage; var builder = WebApplication.CreateBuilder(args); diff --git a/ResidentialBuilding.FileService/ResidentialBuilding.FileService.csproj b/ResidentialBuilding.FileService/ResidentialBuilding.FileService.csproj index 06b40de5..25d51140 100644 --- a/ResidentialBuilding.FileService/ResidentialBuilding.FileService.csproj +++ b/ResidentialBuilding.FileService/ResidentialBuilding.FileService.csproj @@ -4,9 +4,9 @@ net8.0 enable enable - dotnet-ResidentialBuilding.EventSink-3fc8b533-d1b8-4ada-b0c8-745b4c7331f7 + dotnet-ResidentialBuilding.FileService-3fc8b533-d1b8-4ada-b0c8-745b4c7331f7 Linux - ResidentialBuilding.EventSink + ResidentialBuilding.FileService diff --git a/ResidentialBuilding.FileService/Service/Messaging/ISubscriptionService.cs b/ResidentialBuilding.FileService/Service/Messaging/ISubscriptionService.cs index 64a6c30a..900b8cc3 100644 --- a/ResidentialBuilding.FileService/Service/Messaging/ISubscriptionService.cs +++ b/ResidentialBuilding.FileService/Service/Messaging/ISubscriptionService.cs @@ -1,4 +1,4 @@ -namespace ResidentialBuilding.EventSink.Service.Messaging; +namespace ResidentialBuilding.FileService.Service.Messaging; public interface ISubscriptionService { diff --git a/ResidentialBuilding.FileService/Service/Messaging/SnsSubscriptionService.cs b/ResidentialBuilding.FileService/Service/Messaging/SnsSubscriptionService.cs index 468c49d2..70182c9a 100644 --- a/ResidentialBuilding.FileService/Service/Messaging/SnsSubscriptionService.cs +++ b/ResidentialBuilding.FileService/Service/Messaging/SnsSubscriptionService.cs @@ -2,7 +2,7 @@ using Amazon.SimpleNotificationService.Model; using System.Net; -namespace ResidentialBuilding.EventSink.Service.Messaging; +namespace ResidentialBuilding.FileService.Service.Messaging; /// /// Служба для подписки на SNS на старте приложения diff --git a/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs b/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs index 5dd74a88..1a41eeff 100644 --- a/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs +++ b/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.Json.Nodes; -namespace ResidentialBuilding.EventSink.Service.Storage; +namespace ResidentialBuilding.FileService.Service.Storage; public class AwsFileService(IAmazonS3 client, IConfiguration configuration, ILogger logger) : IFileService diff --git a/ResidentialBuilding.FileService/Service/Storage/IFileService.cs b/ResidentialBuilding.FileService/Service/Storage/IFileService.cs index 751266d0..706751a2 100644 --- a/ResidentialBuilding.FileService/Service/Storage/IFileService.cs +++ b/ResidentialBuilding.FileService/Service/Storage/IFileService.cs @@ -1,6 +1,6 @@ using System.Text.Json.Nodes; -namespace ResidentialBuilding.EventSink.Service.Storage; +namespace ResidentialBuilding.FileService.Service.Storage; /// /// Интерфейс службы для манипуляции файлами в объектном хранилище From a2de00a38ec4e278548f31349713948692cb2e29 Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 16 Apr 2026 14:38:07 +0400 Subject: [PATCH 35/37] =?UTF-8?q?refactor:=20=D0=B2=20SnsSubscriberControl?= =?UTF-8?q?ler.ReceiveMessage=20=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=20return=20Ok()=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=86?= =?UTF-8?q?=D0=B5=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=B0;=20=D0=B2=20AwsFi?= =?UTF-8?q?leService=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20summar?= =?UTF-8?q?y=20=D0=B8=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D1=87?= =?UTF-8?q?=D0=B5=D0=BB=D0=BE=D0=B2=D0=B5=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/SnsSubscriberController.cs | 2 +- .../Service/Storage/AwsFileService.cs | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs b/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs index e2bd2bc1..f9be6fb8 100644 --- a/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs +++ b/ResidentialBuilding.FileService/Controller/SnsSubscriberController.cs @@ -57,7 +57,7 @@ public async Task ReceiveMessage() } logger.LogInformation("Subscription was successfully confirmed."); - return Ok(); + break; } case "Notification": { diff --git a/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs b/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs index 1a41eeff..cb89d11f 100644 --- a/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs +++ b/ResidentialBuilding.FileService/Service/Storage/AwsFileService.cs @@ -7,12 +7,17 @@ namespace ResidentialBuilding.FileService.Service.Storage; +/// +/// Реализация интерфейса IFileService, отвечающая за работу с хранилищем Amazon S3 +/// +/// Клиент AWS SDK для работы с Amazon S3 +/// Конфигурация приложения, из которой извлекается имя S3-бакета +/// Логгер public class AwsFileService(IAmazonS3 client, IConfiguration configuration, ILogger logger) : IFileService { - private readonly string _bucketName = configuration["AWS:Resources:S3BucketName"] - ?? throw new KeyNotFoundException( - "S3 bucket name was not found in configuration"); + private readonly string _bucketName = configuration["AWS:Resources:S3BucketName"] + ?? throw new KeyNotFoundException("S3 bucket name was not found in configuration"); /// public async Task> GetFilesList() From 93c9ab66a17e2dc7af0547c6a2d40ed2380a56aa Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 16 Apr 2026 14:55:14 +0400 Subject: [PATCH 36/37] =?UTF-8?q?tests:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BD=D0=B5=D0=B3=D0=B0=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=BE=D0=BD=D0=BD=D0=BE=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.Tests/IntegrationTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/ResidentialBuilding.Tests/IntegrationTests.cs b/ResidentialBuilding.Tests/IntegrationTests.cs index 7d6b1955..7f408325 100644 --- a/ResidentialBuilding.Tests/IntegrationTests.cs +++ b/ResidentialBuilding.Tests/IntegrationTests.cs @@ -190,6 +190,59 @@ public async Task S3_ReturnsCorrectListOfFiles() Assert.Equal(3, addedFiles.Count); } + /// + /// Проверяет обработку некорректных значений id в Gateway. + /// + [Theory] + [InlineData("0")] + [InlineData("-1")] + [InlineData("asd")] + public async Task InvalidId_ReturnsBadRequest(string invalidId) + { + var gatewayClient = _app.CreateHttpClient("gateway", "http"); + + var response = await gatewayClient.GetAsync($"/residential-building?id={invalidId}"); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + /// + /// Проверяет поведение при отсутствии параметра id. + /// + [Fact] + public async Task MissingIdParameter_ReturnsBadRequest() + { + var gatewayClient = _app.CreateHttpClient("gateway", "http"); + + var response = await gatewayClient.GetAsync("/residential-building"); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + /// + /// Проверяет, что при некорректном id файл в S3 НЕ создаётся. + /// + [Theory] + [InlineData("0")] + [InlineData("-1")] + [InlineData("asd")] + public async Task InvalidId_DoesNotCreateFileInS3(string invalidId) + { + var gatewayClient = _app.CreateHttpClient("gateway", "http"); + var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); + + var response = await gatewayClient.GetAsync($"/residential-building?id={invalidId}"); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + await Task.Delay(5000); + + var fileName = $"residential_building_{invalidId}.json"; + var probe = await fileClient.GetAsync($"/api/s3/{fileName}"); + + Assert.False(probe.IsSuccessStatusCode, "Файл не должен был появиться в S3 при невалидном id"); + } + /// /// Ожидает появления файла в S3 с указанным id. /// Поллинг с таймаутом — необходим из-за асинхронной природы связи SNS и FileService. From b149b4f5602eb7f8a4ef81678148489fdd000452 Mon Sep 17 00:00:00 2001 From: Donistr Date: Thu, 16 Apr 2026 15:00:54 +0400 Subject: [PATCH 37/37] =?UTF-8?q?tests:=20=D0=B7=D0=B0=D0=B1=D1=8B=D0=BB?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ResidentialBuilding.Tests/IntegrationTests.cs | 69 ++----------------- 1 file changed, 4 insertions(+), 65 deletions(-) diff --git a/ResidentialBuilding.Tests/IntegrationTests.cs b/ResidentialBuilding.Tests/IntegrationTests.cs index 7f408325..bf4d20d7 100644 --- a/ResidentialBuilding.Tests/IntegrationTests.cs +++ b/ResidentialBuilding.Tests/IntegrationTests.cs @@ -23,42 +23,15 @@ public class IntegrationTests(AppFixture fixture) : IClassFixture /// Проверяет основной положительный сценарий: /// Запрос через Gateway → генерация объекта → сохранение в S3 → данные идентичны. /// - [Fact] - public async Task GetFromGateway_SavesToS3_AndDataIsIdentical() - { - var gatewayClient = _app.CreateHttpClient("gateway", "http"); - var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); - - var id = Random.Shared.Next(1, 10); - - var response = await gatewayClient.GetAsync($"/residential-building?id={id}"); - response.EnsureSuccessStatusCode(); - - var apiBuilding = Deserialize(await response.Content.ReadAsStringAsync()); - - await WaitForFileInS3Async(fileClient, id, TimeSpan.FromSeconds(10)); - - var s3Building = await GetBuildingFromS3Async(fileClient, id); - - Assert.NotNull(apiBuilding); - Assert.Equal(id, apiBuilding.Id); - Assert.NotNull(s3Building); - Assert.Equal(id, s3Building.Id); - Assert.Equivalent(apiBuilding, s3Building, strict: true); - } - - /// - /// Проверяет корректность сохранения объектов с разными идентификаторами. - /// [Theory] [InlineData(11)] [InlineData(12)] [InlineData(13)] - public async Task DifferentIds_AreCorrectlySavedToS3(int id) + public async Task GetFromGateway_SavesToS3_AndDataIsIdentical(int id) { var gatewayClient = _app.CreateHttpClient("gateway", "http"); var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); - + var response = await gatewayClient.GetAsync($"/residential-building?id={id}"); response.EnsureSuccessStatusCode(); @@ -68,6 +41,8 @@ public async Task DifferentIds_AreCorrectlySavedToS3(int id) var s3Building = await GetBuildingFromS3Async(fileClient, id); + Assert.NotNull(apiBuilding); + Assert.Equal(id, apiBuilding.Id); Assert.NotNull(s3Building); Assert.Equal(id, s3Building.Id); Assert.Equivalent(apiBuilding, s3Building, strict: true); @@ -119,42 +94,6 @@ public async Task SameIds_GivingSameObjects() Assert.Equivalent(apiBuilding, apiBuilding1, strict: true); Assert.Equivalent(s3Building, s3Building1, strict: true); } - - /// - /// Проверяет обработку нескольких разных объектов в одном тесте. - /// Убеждается, что все сгенерированные объекты корректно сохраняются в S3. - /// - [Fact] - public async Task MultipleDifferentIds_AllSavedCorrectly() - { - var gatewayClient = _app.CreateHttpClient("gateway", "http"); - var fileClient = _app.CreateHttpClient("residential-building-file-service", "http"); - - var ids = Enumerable.Range(0, 5).Select(_ => Random.Shared.Next(40, 50)).ToList(); - - var apiBuildings = new List(); - - foreach (var id in ids) - { - var response = await gatewayClient.GetAsync($"/residential-building?id={id}"); - response.EnsureSuccessStatusCode(); - - var building = Deserialize(await response.Content.ReadAsStringAsync()); - Assert.NotNull(building); - apiBuildings.Add(building!); - - await WaitForFileInS3Async(fileClient, id, TimeSpan.FromSeconds(8)); - } - - foreach (var id in ids) - { - var s3Building = await GetBuildingFromS3Async(fileClient, id); - var original = apiBuildings.First(b => b.Id == id); - - Assert.Equal(id, s3Building.Id); - Assert.Equivalent(original, s3Building, strict: true); - } - } /// /// Проверяет эндпоинт получения списка файлов в S3 и уникальность по Id.