diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f118..5d02510 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -1,13 +1,17 @@  - Лабораторная работа + + Лабораторная работа + - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №1 "Кэширование" + Вариант №19 "Объект жилого строительства" + Выполнена Абаниным Иваном 6512 + + Ссылка на форк + - + \ No newline at end of file diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab..73144ed 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,6 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + + "BaseAddress": "https://localhost:7159/api/ResidentialProperty" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241..b5f3a98 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -1,10 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36811.4 +# Visual Studio Version 18 +VisualStudioVersion = 18.4.11605.240 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}") = "ResidentialProperty.AppHost", "ResidentialProperty.AppHost\ResidentialProperty.AppHost.csproj", "{C15C9641-651E-4929-A9AC-F91BA292ED4F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialProperty.Domain", "ResidentialProperty.Domain\ResidentialProperty.Domain.csproj", "{BCCEEEA7-39DD-7F63-CC8A-99C00D0CDD05}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialProperty.ServiceDefaults", "ResidentialProperty.ServiceDefaults\ResidentialProperty.ServiceDefaults.csproj", "{603B5FBD-0AA1-02F5-BFAB-CB290AEFE7C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResidentialProperty.Api", "ResidentialProperty.Api\ResidentialProperty.Api.csproj", "{02A8658C-5DF4-D5DF-1B14-34C6D1A25FFE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +23,22 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {C15C9641-651E-4929-A9AC-F91BA292ED4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C15C9641-651E-4929-A9AC-F91BA292ED4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C15C9641-651E-4929-A9AC-F91BA292ED4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C15C9641-651E-4929-A9AC-F91BA292ED4F}.Release|Any CPU.Build.0 = Release|Any CPU + {BCCEEEA7-39DD-7F63-CC8A-99C00D0CDD05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCCEEEA7-39DD-7F63-CC8A-99C00D0CDD05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCCEEEA7-39DD-7F63-CC8A-99C00D0CDD05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCCEEEA7-39DD-7F63-CC8A-99C00D0CDD05}.Release|Any CPU.Build.0 = Release|Any CPU + {603B5FBD-0AA1-02F5-BFAB-CB290AEFE7C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {603B5FBD-0AA1-02F5-BFAB-CB290AEFE7C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {603B5FBD-0AA1-02F5-BFAB-CB290AEFE7C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {603B5FBD-0AA1-02F5-BFAB-CB290AEFE7C0}.Release|Any CPU.Build.0 = Release|Any CPU + {02A8658C-5DF4-D5DF-1B14-34C6D1A25FFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02A8658C-5DF4-D5DF-1B14-34C6D1A25FFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02A8658C-5DF4-D5DF-1B14-34C6D1A25FFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02A8658C-5DF4-D5DF-1B14-34C6D1A25FFE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ResidentialProperty.Api/Controllers/ResidentialPropertyController.cs b/ResidentialProperty.Api/Controllers/ResidentialPropertyController.cs new file mode 100644 index 0000000..a287bad --- /dev/null +++ b/ResidentialProperty.Api/Controllers/ResidentialPropertyController.cs @@ -0,0 +1,28 @@ +using ResidentialProperty.Api.Services.ResidentialPropertyGeneratorService; +using ResidentialProperty.Domain.Entities; +using Microsoft.AspNetCore.Mvc; + +namespace ResidentialProperty.Api.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class ResidentialPropertyController( + IResidentialPropertyGeneratorService generatorService, + ILogger logger) : ControllerBase +{ + /// + /// Получить объект жилого строительства по ID, если не найден в кэше — сгенерировать новый + /// + /// ID объекта + /// Токен отмены операции + /// Объект жилого строительства + [HttpGet] + public async Task> GetById([FromQuery] int id, CancellationToken cancellationToken) + { + logger.LogInformation("Received request to retrieve/generate property {Id}", id); + + var property = await generatorService.GetByIdAsync(id, cancellationToken); + + return Ok(property); + } +} \ No newline at end of file diff --git a/ResidentialProperty.Api/Program.cs b/ResidentialProperty.Api/Program.cs new file mode 100644 index 0000000..67b3157 --- /dev/null +++ b/ResidentialProperty.Api/Program.cs @@ -0,0 +1,63 @@ +using ResidentialProperty.Api.Services.ResidentialPropertyGeneratorService; +using ResidentialProperty.ServiceDefaults; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("redis"); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.SetIsOriginAllowed(origin => + new Uri(origin).Host == "localhost") + .WithMethods("GET") + .WithHeaders("Content-Type") + .AllowCredentials(); + }); +}); + +// Регистрация сервисов +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new Microsoft.OpenApi.OpenApiInfo + { + Title = "Residential Property Generator API", + Description = "API для генерации объектов жилого строительства", + Version = "v1" + }); + + var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename); + if (File.Exists(xmlPath)) + { + options.IncludeXmlComments(xmlPath); + } + + var domainXmlPath = Path.Combine(AppContext.BaseDirectory, "ResidentialProperty.Domain.xml"); + if (File.Exists(domainXmlPath)) + { + options.IncludeXmlComments(domainXmlPath); + } +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseCors(); +app.MapControllers(); +app.MapDefaultEndpoints(); + +app.Run(); \ No newline at end of file diff --git a/ResidentialProperty.Api/Properties/launchSettings.json b/ResidentialProperty.Api/Properties/launchSettings.json new file mode 100644 index 0000000..d9a257f --- /dev/null +++ b/ResidentialProperty.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:32342", + "sslPort": 44362 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5187", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7159;http://localhost:5187", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ResidentialProperty.Api/ResidentialProperty.Api.csproj b/ResidentialProperty.Api/ResidentialProperty.Api.csproj new file mode 100644 index 0000000..65e726d --- /dev/null +++ b/ResidentialProperty.Api/ResidentialProperty.Api.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + + + + + + + + diff --git a/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/IResidentialPropertyGeneratorService.cs b/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/IResidentialPropertyGeneratorService.cs new file mode 100644 index 0000000..ee19e92 --- /dev/null +++ b/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/IResidentialPropertyGeneratorService.cs @@ -0,0 +1,11 @@ +using ResidentialProperty.Domain.Entities; + +namespace ResidentialProperty.Api.Services.ResidentialPropertyGeneratorService; + +/// +/// Сервис получения объекта жилого строительства +/// +public interface IResidentialPropertyGeneratorService +{ + public Task GetByIdAsync(int id, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/ResidentialPropertyGenerator.cs b/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/ResidentialPropertyGenerator.cs new file mode 100644 index 0000000..9f550fa --- /dev/null +++ b/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/ResidentialPropertyGenerator.cs @@ -0,0 +1,39 @@ +using Bogus; +using ResidentialProperty.Domain.Entities; + +namespace ResidentialProperty.Api.Services.ResidentialPropertyGeneratorService; + +/// +/// Генератор случайных объектов жилого строительства с использованием Bogus +/// +public class ResidentialPropertyGenerator +{ + private readonly Faker _faker; + private int _idCounter = 1; + + public ResidentialPropertyGenerator() + { + var propertyTypes = new[] { "Квартира", "ИЖС", "Апартаменты", "Офис", "Коммерческая" }; + var currentYear = DateTime.Now.Year; + + _faker = new Faker("ru") + .RuleFor(p => p.Id, f => _idCounter++) + .RuleFor(p => p.Address, f => $"{f.Address.City()}, ул. {f.Address.StreetName()}, д. {f.Random.Number(1, 100)}") + .RuleFor(p => p.PropertyType, f => f.PickRandom(propertyTypes)) + .RuleFor(p => p.YearBuilt, f => f.Random.Number(1950, currentYear)) + .RuleFor(p => p.TotalArea, f => Math.Round(f.Random.Double(30, 200), 2)) + .RuleFor(p => p.LivingArea, (f, p) => Math.Round(p.TotalArea * f.Random.Double(0.5, 0.9), 2)) + .RuleFor(p => p.TotalFloors, f => f.Random.Number(1, 25)) + .RuleFor(p => p.Floor, (f, p) => + p.PropertyType == "ИЖС" ? null : f.Random.Number(1, p.TotalFloors)) + .RuleFor(p => p.CadastralNumber, f => + $"{f.Random.Number(1, 99):D2}.{f.Random.Number(1, 99):D2}.{f.Random.Number(1, 99):D2}.{f.Random.Number(1, 999999):D6}.{f.Random.Number(1, 9999):D4}") + .RuleFor(p => p.CadastralValue, (f, p) => + Math.Round((decimal)(p.TotalArea * f.Random.Double(50000, 150000)), 2)); + } + + /// + /// Генерирует один случайный объект жилого строительства + /// + public ResidentialPropertyEntity Generate() => _faker.Generate(); +} diff --git a/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/ResidentialPropertyGeneratorService.cs b/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/ResidentialPropertyGeneratorService.cs new file mode 100644 index 0000000..136154e --- /dev/null +++ b/ResidentialProperty.Api/Services/ResidentialPropertyGeneratorService/ResidentialPropertyGeneratorService.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.Caching.Distributed; +using ResidentialProperty.Api.Services.ResidentialPropertyGeneratorService; +using ResidentialProperty.Domain.Entities; +using System.Text.Json; + +namespace ResidentialProperty.Api.Services.ResidentialPropertyGeneratorService; + +/// +/// Сервис получения объекта жилого строительства: сначала ищет в кэше, при промахе — генерирует новый и сохраняет +/// +public class ResidentialPropertyGeneratorService( + IDistributedCache cache, + ResidentialPropertyGenerator generator, + IConfiguration configuration, + ILogger logger) : IResidentialPropertyGeneratorService +{ + private readonly int _expirationMinutes = configuration.GetValue("CacheSettings:ExpirationMinutes", 10); + + /// + /// Возвращает объект жилого строительства по идентификатору. + /// Если объект найден в кэше — возвращается из него; иначе генерируется, сохраняется в кэш и возвращается. + /// + /// Идентификатор объекта + /// Токен отмены операции + /// Объект жилого строительства + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) + { + logger.LogInformation("Attempting to retrieve residential property {Id} from cache", id); + + var cacheKey = $"residential-property-{id}"; + + // Получаем объект из кэша + ResidentialPropertyEntity? property = null; + try + { + var cachedData = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (!string.IsNullOrEmpty(cachedData)) + { + property = JsonSerializer.Deserialize(cachedData); + + if (property != null) + { + logger.LogInformation("Residential property {Id} found in cache", id); + return property; + } + + logger.LogWarning("Property {Id} was found in cache but could not be deserialized. Generating a new one", id); + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to retrieve property {Id} from cache (error ignored)", id); + } + + // Если в кэше нет или ошибка — генерируем новый объект + logger.LogInformation("Property {Id} not found in cache or cache unavailable, generating a new one", id); + property = generator.Generate(); + property.Id = id; + + // Попытка сохранить в кэш + try + { + logger.LogInformation("Saving property {Id} to cache", id); + + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_expirationMinutes) + }; + + await cache.SetStringAsync( + cacheKey, + JsonSerializer.Serialize(property), + cacheOptions, + cancellationToken); + + logger.LogInformation( + "Residential property generated and cached: Id={Id}, Address={Address}, Type={PropertyType}, TotalArea={TotalArea}, CadastralValue={CadastralValue}", + property.Id, + property.Address, + property.PropertyType, + property.TotalArea, + property.CadastralValue); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to save property {Id} to cache (error ignored)", id); + } + + return property; + } +} \ No newline at end of file diff --git a/ResidentialProperty.Api/appsettings.Development.json b/ResidentialProperty.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ResidentialProperty.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ResidentialProperty.Api/appsettings.json b/ResidentialProperty.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/ResidentialProperty.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ResidentialProperty.AppHost/AppHost.cs b/ResidentialProperty.AppHost/AppHost.cs new file mode 100644 index 0000000..1aea5e1 --- /dev/null +++ b/ResidentialProperty.AppHost/AppHost.cs @@ -0,0 +1,17 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// Добавляем Redis +var redis = builder.AddRedis("redis") + .WithRedisCommander(); + +// Добавляем API проект с ссылкой на Redis +var api = builder.AddProject("residentialproperty-api") + .WithReference(redis) + .WaitFor(redis); // ждем запуска Redis + +// Добавляем клиентский проект с ссылкой на API +builder.AddProject("client-wasm") + .WithReference(api) + .WaitFor(api); // ждем запуска API + +builder.Build().Run(); \ No newline at end of file diff --git a/ResidentialProperty.AppHost/Properties/launchSettings.json b/ResidentialProperty.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..ccfb9c9 --- /dev/null +++ b/ResidentialProperty.AppHost/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17085;http://localhost:15162", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21046", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23227", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22078" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15162", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19155", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18234", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20241" + } + } + } +} diff --git a/ResidentialProperty.AppHost/ResidentialProperty.AppHost.csproj b/ResidentialProperty.AppHost/ResidentialProperty.AppHost.csproj new file mode 100644 index 0000000..cbcf79b --- /dev/null +++ b/ResidentialProperty.AppHost/ResidentialProperty.AppHost.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + c0442ce5-33ff-46f9-8634-132dc4b6abb8 + + + + + + + + + + + + diff --git a/ResidentialProperty.AppHost/appsettings.Development.json b/ResidentialProperty.AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ResidentialProperty.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ResidentialProperty.AppHost/appsettings.json b/ResidentialProperty.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/ResidentialProperty.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/ResidentialProperty.Domain/Entities/ResidentialProperty.cs b/ResidentialProperty.Domain/Entities/ResidentialProperty.cs new file mode 100644 index 0000000..237542a --- /dev/null +++ b/ResidentialProperty.Domain/Entities/ResidentialProperty.cs @@ -0,0 +1,57 @@ +namespace ResidentialProperty.Domain.Entities; + +/// +/// Объект жилого строительства +/// +public class ResidentialPropertyEntity +{ + /// + /// Идентификатор в системе + /// + public required int Id { get; set; } + + /// + /// Адрес + /// + public required string Address { get; set; } + + /// + /// Тип недвижимости (Квартира, ИЖС, Апартаменты, Офис и т.д.) + /// + public required string PropertyType { get; set; } + + /// + /// Год постройки (не может быть позже текущего) + /// + public int YearBuilt { get; set; } + + /// + /// Общая площадь (округляется до двух знаков после запятой) + /// + public double TotalArea { get; set; } + + /// + /// Жилая площадь (не может быть больше общей, округляется до двух знаков) + /// + public double LivingArea { get; set; } + + /// + /// Этаж (не указывается для ИЖС, не может быть больше этажности) + /// + public int? Floor { get; set; } + + /// + /// Этажность здания (не может быть меньше 1) + /// + public int TotalFloors { get; set; } + + /// + /// Кадастровый номер (формат: **.**.**.******.****) + /// + public required string CadastralNumber { get; set; } + + /// + /// Кадастровая стоимость (округляется до двух знаков, пропорциональна площади) + /// + public decimal CadastralValue { get; set; } +} \ No newline at end of file diff --git a/ResidentialProperty.Domain/ResidentialProperty.Domain.csproj b/ResidentialProperty.Domain/ResidentialProperty.Domain.csproj new file mode 100644 index 0000000..32cc1e0 --- /dev/null +++ b/ResidentialProperty.Domain/ResidentialProperty.Domain.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + diff --git a/ResidentialProperty.ServiceDefaults/Extensions.cs b/ResidentialProperty.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..f1e201a --- /dev/null +++ b/ResidentialProperty.ServiceDefaults/Extensions.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace ResidentialProperty.ServiceDefaults; + +/// +/// Расширения для настройки сервисов Aspire (структурное логирование, телеметрия, health checks) +/// +public static class Extensions +{ + /// + /// Добавляет стандартные настройки Aspire для сервисов + /// + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + /// + /// Настройка OpenTelemetry для структурного логирования и телеметрии + /// + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + /// + /// Добавляет стандартные health checks + /// + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + /// + /// Настройка стандартных endpoints (health checks) + /// + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/ResidentialProperty.ServiceDefaults/ResidentialProperty.ServiceDefaults.csproj b/ResidentialProperty.ServiceDefaults/ResidentialProperty.ServiceDefaults.csproj new file mode 100644 index 0000000..2b50d0b --- /dev/null +++ b/ResidentialProperty.ServiceDefaults/ResidentialProperty.ServiceDefaults.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + +