diff --git a/ApiGateway/ApiGateway.csproj b/ApiGateway/ApiGateway.csproj new file mode 100644 index 00000000..68f85ae2 --- /dev/null +++ b/ApiGateway/ApiGateway.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + \ No newline at end of file diff --git a/ApiGateway/ApiGateway.http b/ApiGateway/ApiGateway.http new file mode 100644 index 00000000..027416f8 --- /dev/null +++ b/ApiGateway/ApiGateway.http @@ -0,0 +1,6 @@ +@ApiGateway_HostAddress = http://localhost:5248 + +GET {{ApiGateway_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs b/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs new file mode 100644 index 00000000..57d5385d --- /dev/null +++ b/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs @@ -0,0 +1,64 @@ +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; + +namespace ApiGateway.LoadBalancing; + +public class WeightedRoundRobinBalancer : ILoadBalancer +{ + public string Type => nameof(WeightedRoundRobinBalancer); + + private readonly List _expandedHosts; + private readonly object _lock = new(); + private int _currentIndex = 0; + + public WeightedRoundRobinBalancer(IConfiguration configuration) + { + var hosts = new List<(string host, int port, int weight)>(); + + AddHostFromConfig(configuration, "generation-service-1", 3, hosts); + AddHostFromConfig(configuration, "generation-service-2", 2, hosts); + AddHostFromConfig(configuration, "generation-service-3", 1, hosts); + + _expandedHosts = new List(); + foreach (var (host, port, weight) in hosts) + { + for (var i = 0; i < weight; i++) + { + _expandedHosts.Add(new ServiceHostAndPort(host, port)); + } + } + + if (_expandedHosts.Count == 0) + { + _expandedHosts.Add(new ServiceHostAndPort("localhost", 5229)); + } + } + + private static void AddHostFromConfig( + IConfiguration config, + string serviceName, + int weight, + List<(string, int, int)> hosts) + { + var url = config[$"services:{serviceName}:http:0"]; + if (string.IsNullOrEmpty(url)) return; + + var uri = new Uri(url); + hosts.Add((uri.Host, uri.Port, weight)); + } + + public Task> LeaseAsync(HttpContext httpContext) + { + lock (_lock) + { + var host = _expandedHosts[_currentIndex]; + _currentIndex = (_currentIndex + 1) % _expandedHosts.Count; + + return Task.FromResult>( + new OkResponse(host)); + } + } + + public void Release(ServiceHostAndPort hostAndPort) { } +} \ No newline at end of file diff --git a/ApiGateway/Program.cs b/ApiGateway/Program.cs new file mode 100644 index 00000000..fb176aae --- /dev/null +++ b/ApiGateway/Program.cs @@ -0,0 +1,23 @@ +using ApiGateway.LoadBalancing; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); + +builder.Services + .AddOcelot(builder.Configuration) + .AddCustomLoadBalancer( + (route, serviceDiscoveryProvider) => + new WeightedRoundRobinBalancer(builder.Configuration)); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +await app.UseOcelot(); + +app.Run(); \ No newline at end of file diff --git a/ApiGateway/Properties/launchSettings.json b/ApiGateway/Properties/launchSettings.json new file mode 100644 index 00000000..57c326d7 --- /dev/null +++ b/ApiGateway/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:35782", + "sslPort": 44338 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5248", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7243;http://localhost:5248", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ApiGateway/appsettings.Development.json b/ApiGateway/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ApiGateway/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ApiGateway/appsettings.json b/ApiGateway/appsettings.json new file mode 100644 index 00000000..ce70a5d9 --- /dev/null +++ b/ApiGateway/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://localhost:5555" + } + } + } +} \ No newline at end of file diff --git a/ApiGateway/ocelot.json b/ApiGateway/ocelot.json new file mode 100644 index 00000000..2afb6e8a --- /dev/null +++ b/ApiGateway/ocelot.json @@ -0,0 +1,35 @@ +{ + "Routes": [ + { + "UpstreamPathTemplate": "/contracts/{everything}", + "UpstreamHttpMethod": [ "Get" ], + "DownstreamPathTemplate": "/contracts/{everything}", + "DownstreamScheme": "http", + "LoadBalancerOptions": { + "Type": "WeightedRoundRobinBalancer" + }, + "DownstreamHostAndPorts": [ + { + "Host": "placeholder", + "Port": 80 + } + ] + }, + { + "UpstreamPathTemplate": "/contracts", + "UpstreamHttpMethod": [ "Get" ], + "DownstreamPathTemplate": "/contracts", + "DownstreamScheme": "http", + "LoadBalancerOptions": { + "Type": "WeightedRoundRobinBalancer" + }, + "DownstreamHostAndPorts": [ + { + "Host": "placeholder", + "Port": 80 + } + ] + } + ], + "GlobalConfiguration": {} +} \ No newline at end of file diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..b5cd718f 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.11626.88 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}") = "CloudDevelopment.AppHost", "CloudDevelopment\CloudDevelopment.AppHost\CloudDevelopment.AppHost.csproj", "{3505653B-9833-43DD-8E51-1D359D201914}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudDevelopment.ServiceDefaults", "CloudDevelopment\CloudDevelopment.ServiceDefaults\CloudDevelopment.ServiceDefaults.csproj", "{18868854-E44C-A570-242C-63484B3BC379}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenerationService", "GenerationService\GenerationService.csproj", "{34140EB6-F1B4-5A29-B45A-DBC1635966E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiGateway", "ApiGateway\ApiGateway.csproj", "{ABE66B30-BD47-A7B2-B17B-5BA67959B026}" +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 + {3505653B-9833-43DD-8E51-1D359D201914}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3505653B-9833-43DD-8E51-1D359D201914}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3505653B-9833-43DD-8E51-1D359D201914}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3505653B-9833-43DD-8E51-1D359D201914}.Release|Any CPU.Build.0 = Release|Any CPU + {18868854-E44C-A570-242C-63484B3BC379}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18868854-E44C-A570-242C-63484B3BC379}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18868854-E44C-A570-242C-63484B3BC379}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18868854-E44C-A570-242C-63484B3BC379}.Release|Any CPU.Build.0 = Release|Any CPU + {34140EB6-F1B4-5A29-B45A-DBC1635966E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34140EB6-F1B4-5A29-B45A-DBC1635966E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34140EB6-F1B4-5A29-B45A-DBC1635966E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34140EB6-F1B4-5A29-B45A-DBC1635966E5}.Release|Any CPU.Build.0 = Release|Any CPU + {ABE66B30-BD47-A7B2-B17B-5BA67959B026}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABE66B30-BD47-A7B2-B17B-5BA67959B026}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABE66B30-BD47-A7B2-B17B-5BA67959B026}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABE66B30-BD47-A7B2-B17B-5BA67959B026}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CloudDevelopment/CloudDevelopment.AppHost/AppHost.cs b/CloudDevelopment/CloudDevelopment.AppHost/AppHost.cs new file mode 100644 index 00000000..e819eaec --- /dev/null +++ b/CloudDevelopment/CloudDevelopment.AppHost/AppHost.cs @@ -0,0 +1,28 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var redis = builder.AddRedis("redis") + .WithRedisInsight(); + +builder.AddProject("client-wasm"); + +var generation1 = builder.AddProject("generation-service-1") + .WithReference(redis) + .WaitFor(redis); + +var generation2 = builder.AddProject("generation-service-2") + .WithReference(redis) + .WaitFor(redis); + +var generation3 = builder.AddProject("generation-service-3") + .WithReference(redis) + .WaitFor(redis); + +builder.AddProject("api-gateway") + .WithReference(generation1) + .WithReference(generation2) + .WithReference(generation3) + .WaitFor(generation1) + .WaitFor(generation2) + .WaitFor(generation3); + +builder.Build().Run(); \ No newline at end of file diff --git a/CloudDevelopment/CloudDevelopment.AppHost/CloudDevelopment.AppHost.csproj b/CloudDevelopment/CloudDevelopment.AppHost/CloudDevelopment.AppHost.csproj new file mode 100644 index 00000000..0801fda3 --- /dev/null +++ b/CloudDevelopment/CloudDevelopment.AppHost/CloudDevelopment.AppHost.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + 40038ea2-2aa4-401d-a7e5-218c78b6c0c5 + + + + + + + + + + + + + + diff --git a/CloudDevelopment/CloudDevelopment.AppHost/Properties/launchSettings.json b/CloudDevelopment/CloudDevelopment.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..cae9441e --- /dev/null +++ b/CloudDevelopment/CloudDevelopment.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:17149;http://localhost:15072", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21077", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23195", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22234" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15072", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19047", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18197", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20205" + } + } + } +} diff --git a/CloudDevelopment/CloudDevelopment.AppHost/appsettings.Development.json b/CloudDevelopment/CloudDevelopment.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/CloudDevelopment/CloudDevelopment.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CloudDevelopment/CloudDevelopment.AppHost/appsettings.json b/CloudDevelopment/CloudDevelopment.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/CloudDevelopment/CloudDevelopment.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/CloudDevelopment/CloudDevelopment.ServiceDefaults/CloudDevelopment.ServiceDefaults.csproj b/CloudDevelopment/CloudDevelopment.ServiceDefaults/CloudDevelopment.ServiceDefaults.csproj new file mode 100644 index 00000000..b7140ed1 --- /dev/null +++ b/CloudDevelopment/CloudDevelopment.ServiceDefaults/CloudDevelopment.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/CloudDevelopment/CloudDevelopment.ServiceDefaults/Extensions.cs b/CloudDevelopment/CloudDevelopment.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..b72c8753 --- /dev/null +++ b/CloudDevelopment/CloudDevelopment.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.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// 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/GenerationService/GenerationService.csproj b/GenerationService/GenerationService.csproj new file mode 100644 index 00000000..91e25d86 --- /dev/null +++ b/GenerationService/GenerationService.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/GenerationService/GenerationService.http b/GenerationService/GenerationService.http new file mode 100644 index 00000000..6c35122f --- /dev/null +++ b/GenerationService/GenerationService.http @@ -0,0 +1,6 @@ +@GenerationService_HostAddress = http://localhost:5229 + +GET {{GenerationService_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/GenerationService/Models/SoftwareProjectContract.cs b/GenerationService/Models/SoftwareProjectContract.cs new file mode 100644 index 00000000..ead29255 --- /dev/null +++ b/GenerationService/Models/SoftwareProjectContract.cs @@ -0,0 +1,14 @@ +namespace GenerationService.Models; + +public record SoftwareProjectContract( + int Id, + string ProjectName, + string ClientCompany, + string ProjectManager, + DateOnly StartDate, + DateOnly PlannedEndDate, + DateOnly ActualEndDate, + decimal Budget, + decimal ActualCost, + int CompletionPercentage +); \ No newline at end of file diff --git a/GenerationService/Program.cs b/GenerationService/Program.cs new file mode 100644 index 00000000..e1e95fec --- /dev/null +++ b/GenerationService/Program.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using GenerationService.Models; +using GenerationService.Services; +using Microsoft.Extensions.Caching.Distributed; +using Serilog; +using Serilog.Formatting.Compact; + +var builder = WebApplication.CreateBuilder(args); + + +builder.AddServiceDefaults(); + + +// 2. Настраиваем Serilog — структурное логирование +builder.Host.UseSerilog((context, configuration) => + configuration + .ReadFrom.Configuration(context.Configuration) + .WriteTo.Console(new CompactJsonFormatter())); // JSON формат в консоль + + +// 3. Подключаем Redis для кэширования +builder.AddRedisDistributedCache("redis"); + + +// 4. Регистрируем наш генератор как сервис +builder.Services.AddSingleton(); + + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.UseSwagger(); +app.UseSwaggerUI(); + + +// 5. Эндпоинт GET /contracts/{id} +app.MapGet("/contracts/{id}", async ( + string id, + IDistributedCache cache, + ContractGeneratorService generator, + ILogger logger) => +{ + var cacheKey = $"contract:{id}"; + + // Пробуем достать из кэша + var cached = await cache.GetStringAsync(cacheKey); + + if (cached is not null) + { + logger.LogInformation("Cache HIT для ключа {CacheKey}", cacheKey); + var cachedContract = JsonSerializer.Deserialize(cached); + return Results.Ok(cachedContract); + } + + + logger.LogInformation("Cache MISS для ключа {CacheKey}. Генерация нового контракта...", cacheKey); + var contract = generator.Generate(); + + + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) + }; + await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(contract), options); + + logger.LogInformation("Контракт {ContractId} сохранён в кэш", contract.Id); + return Results.Ok(contract); +}); + + +app.MapGet("/contracts", ( + ContractGeneratorService generator, + ILogger logger) => +{ + logger.LogInformation("Генерация нового контракта по запросу"); + var contract = generator.Generate(); + return Results.Ok(contract); +}); + +app.MapDefaultEndpoints(); + +app.Run(); \ No newline at end of file diff --git a/GenerationService/Properties/launchSettings.json b/GenerationService/Properties/launchSettings.json new file mode 100644 index 00000000..073f933e --- /dev/null +++ b/GenerationService/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8827", + "sslPort": 44304 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7130;http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/GenerationService/Services/ContractGeneratorService.cs b/GenerationService/Services/ContractGeneratorService.cs new file mode 100644 index 00000000..2256002b --- /dev/null +++ b/GenerationService/Services/ContractGeneratorService.cs @@ -0,0 +1,58 @@ +using Bogus; +using GenerationService.Models; + +namespace GenerationService.Services; + +public class ContractGeneratorService +{ + private readonly Faker _faker; + + public ContractGeneratorService() + { + _faker = new Faker("ru") + .CustomInstantiator(f => + { + // Дата начала — в пределах последних 2 лет + var startDate = DateOnly.FromDateTime(f.Date.Past(2)); + + // Плановая дата завершения — позже даты начала + var plannedEnd = startDate.AddDays(f.Random.Int(30, 365)); + + // Фактическая дата завершения — позже даты начала + var actualEnd = startDate.AddDays(f.Random.Int(30, 400)); + + // Бюджет округлён до 2 знаков + var budget = Math.Round(f.Finance.Amount(500_000, 10_000_000), 2); + + // Фактические затраты пропорциональны бюджету (от 50% до 130%) + var ratio = f.Random.Decimal(0.5m, 1.3m); + var actualCost = Math.Round(budget * ratio, 2); + + // Если есть фактическая дата завершения — процент 100 + // (фактическая дата всегда есть в нашей модели, поэтому всегда 100) + var completion = 100; + + return new SoftwareProjectContract( + Id: f.Random.Int(1, 100000), + ProjectName: f.Commerce.ProductName() + " " + + f.Hacker.Noun() + " " + + f.Finance.Currency().Description, + ClientCompany: f.Company.CompanyName(), + ProjectManager: f.Name.LastName() + " " + + f.Name.FirstName() + " " + + f.Name.FirstName(), + StartDate: startDate, + PlannedEndDate: plannedEnd, + ActualEndDate: actualEnd, + Budget: budget, + ActualCost: actualCost, + CompletionPercentage: completion + ); + }); + } + + public SoftwareProjectContract Generate() + { + return _faker.Generate(); + } +} \ No newline at end of file diff --git a/GenerationService/appsettings.Development.json b/GenerationService/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/GenerationService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/GenerationService/appsettings.json b/GenerationService/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/GenerationService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Services.Contracts/Controllers/WeatherForecastController.cs b/Services.Contracts/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..d7131464 --- /dev/null +++ b/Services.Contracts/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Services.Contracts.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/Services.Contracts/Models/Contract.cs b/Services.Contracts/Models/Contract.cs new file mode 100644 index 00000000..13fbb225 --- /dev/null +++ b/Services.Contracts/Models/Contract.cs @@ -0,0 +1,14 @@ +namespace Services.Contracts.Models; + +public class Contract +{ + public Guid Id { get; set; } + + public string ProjectName { get; set; } = ""; + + public string Customer { get; set; } = ""; + + public decimal Budget { get; set; } + + public DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/Services.Contracts/Program.cs b/Services.Contracts/Program.cs new file mode 100644 index 00000000..48863a6d --- /dev/null +++ b/Services.Contracts/Program.cs @@ -0,0 +1,25 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Services.Contracts/Properties/launchSettings.json b/Services.Contracts/Properties/launchSettings.json new file mode 100644 index 00000000..1a0f998a --- /dev/null +++ b/Services.Contracts/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:43579", + "sslPort": 44304 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5162", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7255;http://localhost:5162", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Services.Contracts/Services.Contracts.csproj b/Services.Contracts/Services.Contracts.csproj new file mode 100644 index 00000000..5419ef0c --- /dev/null +++ b/Services.Contracts/Services.Contracts.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Services.Contracts/Services.Contracts.http b/Services.Contracts/Services.Contracts.http new file mode 100644 index 00000000..5b34ba27 --- /dev/null +++ b/Services.Contracts/Services.Contracts.http @@ -0,0 +1,6 @@ +@Services.Contracts_HostAddress = http://localhost:5162 + +GET {{Services.Contracts_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Services.Contracts/Services/ContractGenerator.cs b/Services.Contracts/Services/ContractGenerator.cs new file mode 100644 index 00000000..07f904b4 --- /dev/null +++ b/Services.Contracts/Services/ContractGenerator.cs @@ -0,0 +1,16 @@ +using Bogus; + +public class ContractGenerator +{ + public Contract Generate() + { + var faker = new Faker() + .RuleFor(c => c.Id, f => Guid.NewGuid()) + .RuleFor(c => c.ProjectName, f => f.Company.CompanyName()) + .RuleFor(c => c.Customer, f => f.Person.FullName) + .RuleFor(c => c.Budget, f => f.Random.Decimal(10000, 500000)) + .RuleFor(c => c.CreatedAt, f => DateTime.UtcNow); + + return faker.Generate(); + } +} \ No newline at end of file diff --git a/Services.Contracts/Services/IContractGenerator.cs b/Services.Contracts/Services/IContractGenerator.cs new file mode 100644 index 00000000..76e5e24a --- /dev/null +++ b/Services.Contracts/Services/IContractGenerator.cs @@ -0,0 +1,4 @@ +public interface IContractGenerator +{ + Contract Generate(); +} \ No newline at end of file diff --git a/Services.Contracts/WeatherForecast.cs b/Services.Contracts/WeatherForecast.cs new file mode 100644 index 00000000..488c10de --- /dev/null +++ b/Services.Contracts/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Services.Contracts; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/Services.Contracts/appsettings.Development.json b/Services.Contracts/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Services.Contracts/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Services.Contracts/appsettings.json b/Services.Contracts/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Services.Contracts/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/screenshots_cloud/all_run.png b/screenshots_cloud/all_run.png new file mode 100644 index 00000000..11cf6b0b Binary files /dev/null and b/screenshots_cloud/all_run.png differ diff --git a/screenshots_cloud/localhost_run.png b/screenshots_cloud/localhost_run.png new file mode 100644 index 00000000..adacedbb Binary files /dev/null and b/screenshots_cloud/localhost_run.png differ diff --git a/screenshots_cloud/many_services.png b/screenshots_cloud/many_services.png new file mode 100644 index 00000000..377f73a6 Binary files /dev/null and b/screenshots_cloud/many_services.png differ diff --git a/screenshots_cloud/rediska.png b/screenshots_cloud/rediska.png new file mode 100644 index 00000000..1d428cd6 Binary files /dev/null and b/screenshots_cloud/rediska.png differ