From ba028b3135c09cdb04e35d9e309f9e97e5418cbb Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Thu, 29 Jan 2026 17:31:06 +0300 Subject: [PATCH 01/17] initialization --- 351004/Kuchko/st.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 351004/Kuchko/st.txt diff --git a/351004/Kuchko/st.txt b/351004/Kuchko/st.txt new file mode 100644 index 000000000..e69de29bb From 9c2407f28755adba67ca9b26de2aadfb4ae71668 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 9 Feb 2026 17:33:20 +0300 Subject: [PATCH 02/17] init --- 351004/Kuchko/Distcomp/Distcomp.slnx | 3 +++ .../Controllers/WeatherForecastController.cs | 26 +++++++++++++++++++ .../Kuchko/Distcomp/Distcomp/Distcomp.csproj | 13 ++++++++++ 351004/Kuchko/Distcomp/Distcomp/Distcomp.http | 4 +++ 351004/Kuchko/Distcomp/Distcomp/Program.cs | 23 ++++++++++++++++ .../Distcomp/Properties/launchSettings.json | 23 ++++++++++++++++ .../Distcomp/Distcomp/WeatherForecast.cs | 13 ++++++++++ .../Distcomp/appsettings.Development.json | 8 ++++++ .../Kuchko/Distcomp/Distcomp/appsettings.json | 9 +++++++ 9 files changed, 122 insertions(+) create mode 100644 351004/Kuchko/Distcomp/Distcomp.slnx create mode 100644 351004/Kuchko/Distcomp/Distcomp/Controllers/WeatherForecastController.cs create mode 100644 351004/Kuchko/Distcomp/Distcomp/Distcomp.csproj create mode 100644 351004/Kuchko/Distcomp/Distcomp/Distcomp.http create mode 100644 351004/Kuchko/Distcomp/Distcomp/Program.cs create mode 100644 351004/Kuchko/Distcomp/Distcomp/Properties/launchSettings.json create mode 100644 351004/Kuchko/Distcomp/Distcomp/WeatherForecast.cs create mode 100644 351004/Kuchko/Distcomp/Distcomp/appsettings.Development.json create mode 100644 351004/Kuchko/Distcomp/Distcomp/appsettings.json diff --git a/351004/Kuchko/Distcomp/Distcomp.slnx b/351004/Kuchko/Distcomp/Distcomp.slnx new file mode 100644 index 000000000..522261157 --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp.slnx @@ -0,0 +1,3 @@ + + + diff --git a/351004/Kuchko/Distcomp/Distcomp/Controllers/WeatherForecastController.cs b/351004/Kuchko/Distcomp/Distcomp/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..28e45ac01 --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp/Controllers/WeatherForecastController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Distcomp.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + [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/351004/Kuchko/Distcomp/Distcomp/Distcomp.csproj b/351004/Kuchko/Distcomp/Distcomp/Distcomp.csproj new file mode 100644 index 000000000..530dd4e7d --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp/Distcomp.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/351004/Kuchko/Distcomp/Distcomp/Distcomp.http b/351004/Kuchko/Distcomp/Distcomp/Distcomp.http new file mode 100644 index 000000000..3a1162b74 --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp/Distcomp.http @@ -0,0 +1,4 @@ +@Distcomp_HostAddress = http://localhost:5028 + +GET {{Distcomp_HostAddress}}/weatherforecast/ +Accept: application/json diff --git a/351004/Kuchko/Distcomp/Distcomp/Program.cs b/351004/Kuchko/Distcomp/Distcomp/Program.cs new file mode 100644 index 000000000..666a9c543 --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp/Program.cs @@ -0,0 +1,23 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/351004/Kuchko/Distcomp/Distcomp/Properties/launchSettings.json b/351004/Kuchko/Distcomp/Distcomp/Properties/launchSettings.json new file mode 100644 index 000000000..fc378b3c2 --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5028", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7044;http://localhost:5028", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/351004/Kuchko/Distcomp/Distcomp/WeatherForecast.cs b/351004/Kuchko/Distcomp/Distcomp/WeatherForecast.cs new file mode 100644 index 000000000..808222132 --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace Distcomp +{ + 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/351004/Kuchko/Distcomp/Distcomp/appsettings.Development.json b/351004/Kuchko/Distcomp/Distcomp/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/351004/Kuchko/Distcomp/Distcomp/appsettings.json b/351004/Kuchko/Distcomp/Distcomp/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/351004/Kuchko/Distcomp/Distcomp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 9e2d8b87dac9715aa81698c2a9301ddda8406745 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 13 Apr 2026 02:33:02 +0300 Subject: [PATCH 03/17] delete examples --- 351004/Kuchko/Distcomp/Distcomp.slnx | 3 - .../Controllers/WeatherForecastController.cs | 26 ------- .../Kuchko/Distcomp/Distcomp/Distcomp.csproj | 13 ---- 351004/Kuchko/Distcomp/Distcomp/Distcomp.http | 4 -- 351004/Kuchko/Distcomp/Distcomp/Program.cs | 23 ------ .../Distcomp/Distcomp/WeatherForecast.cs | 13 ---- .../Distcomp/appsettings.Development.json | 8 --- 351004/Kuchko/ServerApp/.dockerignore | 25 +++++++ 351004/Kuchko/ServerApp/.gitattributes | 2 + 351004/Kuchko/ServerApp/.gitignore | 71 +++++++++++++++++++ 351004/Kuchko/ServerApp/ServerApp.sln | 16 +++++ 351004/Kuchko/ServerApp/ServerApp/Dockerfile | 23 ++++++ 351004/Kuchko/ServerApp/ServerApp/Program.cs | 41 +++++++++++ .../ServerApp}/Properties/launchSettings.json | 4 +- .../ServerApp/ServerApp/ServerApp.csproj | 20 ++++++ .../Kuchko/ServerApp/ServerApp/ServerApp.http | 6 ++ .../ServerApp}/appsettings.json | 0 351004/Kuchko/ServerApp/global.json | 7 ++ 351004/Kuchko/st.txt | 0 19 files changed, 213 insertions(+), 92 deletions(-) delete mode 100644 351004/Kuchko/Distcomp/Distcomp.slnx delete mode 100644 351004/Kuchko/Distcomp/Distcomp/Controllers/WeatherForecastController.cs delete mode 100644 351004/Kuchko/Distcomp/Distcomp/Distcomp.csproj delete mode 100644 351004/Kuchko/Distcomp/Distcomp/Distcomp.http delete mode 100644 351004/Kuchko/Distcomp/Distcomp/Program.cs delete mode 100644 351004/Kuchko/Distcomp/Distcomp/WeatherForecast.cs delete mode 100644 351004/Kuchko/Distcomp/Distcomp/appsettings.Development.json create mode 100644 351004/Kuchko/ServerApp/.dockerignore create mode 100644 351004/Kuchko/ServerApp/.gitattributes create mode 100644 351004/Kuchko/ServerApp/.gitignore create mode 100644 351004/Kuchko/ServerApp/ServerApp.sln create mode 100644 351004/Kuchko/ServerApp/ServerApp/Dockerfile create mode 100644 351004/Kuchko/ServerApp/ServerApp/Program.cs rename 351004/Kuchko/{Distcomp/Distcomp => ServerApp/ServerApp}/Properties/launchSettings.json (80%) create mode 100644 351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj create mode 100644 351004/Kuchko/ServerApp/ServerApp/ServerApp.http rename 351004/Kuchko/{Distcomp/Distcomp => ServerApp/ServerApp}/appsettings.json (100%) create mode 100644 351004/Kuchko/ServerApp/global.json delete mode 100644 351004/Kuchko/st.txt diff --git a/351004/Kuchko/Distcomp/Distcomp.slnx b/351004/Kuchko/Distcomp/Distcomp.slnx deleted file mode 100644 index 522261157..000000000 --- a/351004/Kuchko/Distcomp/Distcomp.slnx +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/351004/Kuchko/Distcomp/Distcomp/Controllers/WeatherForecastController.cs b/351004/Kuchko/Distcomp/Distcomp/Controllers/WeatherForecastController.cs deleted file mode 100644 index 28e45ac01..000000000 --- a/351004/Kuchko/Distcomp/Distcomp/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Distcomp.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = - [ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - ]; - - [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/351004/Kuchko/Distcomp/Distcomp/Distcomp.csproj b/351004/Kuchko/Distcomp/Distcomp/Distcomp.csproj deleted file mode 100644 index 530dd4e7d..000000000 --- a/351004/Kuchko/Distcomp/Distcomp/Distcomp.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net10.0 - enable - enable - - - - - - - diff --git a/351004/Kuchko/Distcomp/Distcomp/Distcomp.http b/351004/Kuchko/Distcomp/Distcomp/Distcomp.http deleted file mode 100644 index 3a1162b74..000000000 --- a/351004/Kuchko/Distcomp/Distcomp/Distcomp.http +++ /dev/null @@ -1,4 +0,0 @@ -@Distcomp_HostAddress = http://localhost:5028 - -GET {{Distcomp_HostAddress}}/weatherforecast/ -Accept: application/json diff --git a/351004/Kuchko/Distcomp/Distcomp/Program.cs b/351004/Kuchko/Distcomp/Distcomp/Program.cs deleted file mode 100644 index 666a9c543..000000000 --- a/351004/Kuchko/Distcomp/Distcomp/Program.cs +++ /dev/null @@ -1,23 +0,0 @@ -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. - -builder.Services.AddControllers(); -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); -} - -app.UseHttpsRedirection(); - -app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); diff --git a/351004/Kuchko/Distcomp/Distcomp/WeatherForecast.cs b/351004/Kuchko/Distcomp/Distcomp/WeatherForecast.cs deleted file mode 100644 index 808222132..000000000 --- a/351004/Kuchko/Distcomp/Distcomp/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Distcomp -{ - 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/351004/Kuchko/Distcomp/Distcomp/appsettings.Development.json b/351004/Kuchko/Distcomp/Distcomp/appsettings.Development.json deleted file mode 100644 index 0c208ae91..000000000 --- a/351004/Kuchko/Distcomp/Distcomp/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/351004/Kuchko/ServerApp/.dockerignore b/351004/Kuchko/ServerApp/.dockerignore new file mode 100644 index 000000000..cd967fc3a --- /dev/null +++ b/351004/Kuchko/ServerApp/.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/351004/Kuchko/ServerApp/.gitattributes b/351004/Kuchko/ServerApp/.gitattributes new file mode 100644 index 000000000..d5df80166 --- /dev/null +++ b/351004/Kuchko/ServerApp/.gitattributes @@ -0,0 +1,2 @@ +*.sh text eol=lf +*.cmd text eol=crlf \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/.gitignore b/351004/Kuchko/ServerApp/.gitignore new file mode 100644 index 000000000..82700fa31 --- /dev/null +++ b/351004/Kuchko/ServerApp/.gitignore @@ -0,0 +1,71 @@ +## ===================== +## Build results +## ===================== +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ +artifacts/ + +## ===================== +## .NET Core +## ===================== +project.lock.json +project.fragment.lock.json + +## ===================== +## ASP.NET Scaffolding +## ===================== +ScaffoldingReadMe.txt + +## ===================== +## NuGet Packages +## ===================== +*.nupkg +*.snupkg +*.symbols.nupkg + +## ===================== +## Environment / secrets +## ===================== +# dotenv +.env + +# Local appsettings with secrets (Cloudinary keys, DB passwords) +appsettings.Development.json +secrets.json + +# User Secrets folder (optional, depending on setup) +%APPDATA%\Microsoft\UserSecrets/ +~/.microsoft/usersecrets/ + +## ===================== +## Others +## ===================== +~$* +*~ +CodeCoverage/ + +## ===================== +## Logs and test results +## ===================== +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.VisualState.xml +TestResult.xml +nunit-*.xml + +## ===================== +## MSBuild Binary and Structured Log +## ===================== +*.binlog \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp.sln b/351004/Kuchko/ServerApp/ServerApp.sln new file mode 100644 index 000000000..cff7f910a --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerApp", "ServerApp\ServerApp.csproj", "{F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/351004/Kuchko/ServerApp/ServerApp/Dockerfile b/351004/Kuchko/ServerApp/ServerApp/Dockerfile new file mode 100644 index 000000000..f31d3b03d --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["ServerApp/ServerApp.csproj", "ServerApp/"] +RUN dotnet restore "ServerApp/ServerApp.csproj" +COPY . . +WORKDIR "/src/ServerApp" +RUN dotnet build "./ServerApp.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./ServerApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ServerApp.dll"] diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs new file mode 100644 index 000000000..d5e0ef3a2 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -0,0 +1,41 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast"); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} \ No newline at end of file diff --git a/351004/Kuchko/Distcomp/Distcomp/Properties/launchSettings.json b/351004/Kuchko/ServerApp/ServerApp/Properties/launchSettings.json similarity index 80% rename from 351004/Kuchko/Distcomp/Distcomp/Properties/launchSettings.json rename to 351004/Kuchko/ServerApp/ServerApp/Properties/launchSettings.json index fc378b3c2..b17a53886 100644 --- a/351004/Kuchko/Distcomp/Distcomp/Properties/launchSettings.json +++ b/351004/Kuchko/ServerApp/ServerApp/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5028", + "applicationUrl": "http://localhost:5070", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -14,7 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7044;http://localhost:5028", + "applicationUrl": "https://localhost:7045;http://localhost:5070", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj new file mode 100644 index 000000000..1ceb629f4 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + enable + Linux + + + + + + + + + .dockerignore + + + + diff --git a/351004/Kuchko/ServerApp/ServerApp/ServerApp.http b/351004/Kuchko/ServerApp/ServerApp/ServerApp.http new file mode 100644 index 000000000..dd904e8c0 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/ServerApp.http @@ -0,0 +1,6 @@ +@ServerApp_HostAddress = http://localhost:5070 + +GET {{ServerApp_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/351004/Kuchko/Distcomp/Distcomp/appsettings.json b/351004/Kuchko/ServerApp/ServerApp/appsettings.json similarity index 100% rename from 351004/Kuchko/Distcomp/Distcomp/appsettings.json rename to 351004/Kuchko/ServerApp/ServerApp/appsettings.json diff --git a/351004/Kuchko/ServerApp/global.json b/351004/Kuchko/ServerApp/global.json new file mode 100644 index 000000000..93681ff86 --- /dev/null +++ b/351004/Kuchko/ServerApp/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/351004/Kuchko/st.txt b/351004/Kuchko/st.txt deleted file mode 100644 index e69de29bb..000000000 From 9dd3aaccff5f9364670717b72970bdc0c0f8f304 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 13 Apr 2026 04:39:14 +0300 Subject: [PATCH 04/17] add structure --- .../ServerApp/Controllers/TestController.cs | 19 +++++++ 351004/Kuchko/ServerApp/ServerApp/Dockerfile | 32 +++++++---- .../Infrastructure/RoutePrefixConvention.cs | 24 ++++++++ .../ServerApp/Models/MyApiResponse.cs | 8 +++ 351004/Kuchko/ServerApp/ServerApp/Program.cs | 56 +++++++++---------- .../ServerApp/Properties/launchSettings.json | 5 +- .../ServerApp/ServerApp/ServerApp.csproj | 15 ++++- 7 files changed, 114 insertions(+), 45 deletions(-) create mode 100644 351004/Kuchko/ServerApp/ServerApp/Controllers/TestController.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Infrastructure/RoutePrefixConvention.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/MyApiResponse.cs diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/TestController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/TestController.cs new file mode 100644 index 000000000..291a32602 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/TestController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ServerApp.Controllers; + +[ApiController] +[Route("test")] // Итоговый путь будет: /api/v1.0/test +public class TestController : ControllerBase +{ + [HttpGet] + public IActionResult GetStatus() + { + return Ok(new + { + Message = "Приложение работает!", + Version = "1.0", + Time = DateTime.Now + }); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Dockerfile b/351004/Kuchko/ServerApp/ServerApp/Dockerfile index f31d3b03d..509a8dd6c 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Dockerfile +++ b/351004/Kuchko/ServerApp/ServerApp/Dockerfile @@ -1,23 +1,33 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base -USER $APP_UID +# 1. Слой выполнения (Runtime) +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app -EXPOSE 8080 -EXPOSE 8081 +# ВАЖНО: Порт из ТЗ +EXPOSE 24110 +# Настройка портов и среды внутри контейнера +ENV ASPNETCORE_URLS=http://+:24110 +ENV ASPNETCORE_ENVIRONMENT=Development + +# 2. Слой сборки (SDK) FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -COPY ["ServerApp/ServerApp.csproj", "ServerApp/"] -RUN dotnet restore "ServerApp/ServerApp.csproj" + +# Копируем файл проекта и восстанавливаем зависимости +COPY ["ServerApp.csproj", "."] +RUN dotnet restore "./ServerApp.csproj" + +# Копируем все файлы и собираем проект COPY . . -WORKDIR "/src/ServerApp" -RUN dotnet build "./ServerApp.csproj" -c $BUILD_CONFIGURATION -o /app/build +WORKDIR "/src/." +RUN dotnet build "ServerApp.csproj" -c $BUILD_CONFIGURATION -o /app/build +# 3. Слой публикации FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./ServerApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +RUN dotnet publish "ServerApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +# 4. Финальный образ FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "ServerApp.dll"] +ENTRYPOINT ["dotnet", "ServerApp.dll"] \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/RoutePrefixConvention.cs b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/RoutePrefixConvention.cs new file mode 100644 index 000000000..7dbfd452b --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/RoutePrefixConvention.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace ServerApp.Infrastructure; + +public class ApiPrefixConvention : IApplicationModelConvention +{ + private readonly AttributeRouteModel _routePrefix; + + public ApiPrefixConvention(IRouteTemplateProvider routeTemplateProvider) + { + _routePrefix = new AttributeRouteModel(routeTemplateProvider); + } + + public void Apply(ApplicationModel application) + { + foreach (var selector in application.Controllers.SelectMany(c => c.Selectors)) + if (selector.AttributeRouteModel != null) + selector.AttributeRouteModel = + AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel); + else + selector.AttributeRouteModel = _routePrefix; + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/MyApiResponse.cs b/351004/Kuchko/ServerApp/ServerApp/Models/MyApiResponse.cs new file mode 100644 index 000000000..829f15df8 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/MyApiResponse.cs @@ -0,0 +1,8 @@ +namespace ServerApp.Models; + +public class ApiResponse +{ + public string Message { get; set; } = string.Empty; + public string Version { get; set; } = "1.0"; + public DateTime ServerTime { get; set; } = DateTime.Now; +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index d5e0ef3a2..e5b948b5b 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -1,41 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using Scalar.AspNetCore; +using ServerApp; +using ServerApp.Infrastructure; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); +// 1. Настройка контроллеров с нашим префиксом +builder.Services.AddControllers(options => +{ + options.Conventions.Add(new ApiPrefixConvention(new RouteAttribute("api/v1.0"))); +}); + +builder.Services.AddOpenApi(); var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.MapOpenApi(); -} - -app.UseHttpsRedirection(); - -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => + // Генерирует эндпоинт с JSON описанием API (по умолчанию /openapi/v1.json) + app.MapOpenApi(); + + app.MapScalarApiReference(options => { - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; - }) - .WithName("GetWeatherForecast"); + options + .WithTitle("My Project API v1.0") + .WithTheme(ScalarTheme.DeepSpace); + }); +} -app.Run(); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} \ No newline at end of file +// app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); +app.Run(); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Properties/launchSettings.json b/351004/Kuchko/ServerApp/ServerApp/Properties/launchSettings.json index b17a53886..5d8337171 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Properties/launchSettings.json +++ b/351004/Kuchko/ServerApp/ServerApp/Properties/launchSettings.json @@ -5,7 +5,8 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5070", + "launchUrl": "scalar/v1", + "applicationUrl": "http://localhost:24110", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -14,7 +15,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7045;http://localhost:5070", + "applicationUrl": "https://localhost:7045;http://localhost:24110", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj index 1ceb629f4..c6001adf0 100644 --- a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj +++ b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj @@ -8,7 +8,11 @@ - + + + + + @@ -17,4 +21,13 @@ + + + + + + + + + From ce9643f74b90c687281e1f8ff20577908fdf2d99 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 13 Apr 2026 11:11:12 +0300 Subject: [PATCH 05/17] added entities & DTOs --- .../Models/DTOs/Requests/ArticleRequestTo.cs | 13 +++++++++++++ .../Models/DTOs/Requests/AuthorRequestTo.cs | 18 ++++++++++++++++++ .../Models/DTOs/Requests/MessageRequestTo.cs | 10 ++++++++++ .../Models/DTOs/Requests/StickerRequestTo.cs | 9 +++++++++ .../Models/DTOs/Responses/ArticleResponseTo.cs | 10 ++++++++++ .../Models/DTOs/Responses/AuthorResponseTo.cs | 8 ++++++++ .../Models/DTOs/Responses/MessageResponseTo.cs | 7 +++++++ .../Models/DTOs/Responses/StickerResponseTo.cs | 6 ++++++ .../ServerApp/Models/Entities/Article.cs | 10 ++++++++++ .../ServerApp/Models/Entities/Author.cs | 9 +++++++++ .../ServerApp/Models/Entities/BaseEntity.cs | 6 ++++++ .../ServerApp/Models/Entities/Message.cs | 7 +++++++ .../ServerApp/Models/Entities/Sticker.cs | 6 ++++++ .../ServerApp/ServerApp/ServerApp.csproj | 4 +--- 14 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/AuthorRequestTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/MessageRequestTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/StickerRequestTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/AuthorResponseTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/MessageResponseTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/StickerResponseTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/Entities/BaseEntity.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs new file mode 100644 index 000000000..7bf6c92e4 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace ServerApp.Models.DTOs.Requests; + +public record ArticleRequestTo( + [Required] long AuthorId, + [Required] + [StringLength(64, MinimumLength = 2)] + string Title, + [Required] + [StringLength(2048, MinimumLength = 4)] + string Content +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/AuthorRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/AuthorRequestTo.cs new file mode 100644 index 000000000..b81331142 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/AuthorRequestTo.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace ServerApp.Models.DTOs.Requests; + +public record AuthorRequestTo( + [Required] + [StringLength(64, MinimumLength = 2)] + string Login, + [Required] + [StringLength(128, MinimumLength = 8)] + string Password, + [Required] + [StringLength(64, MinimumLength = 2)] + string Firstname, + [Required] + [StringLength(64, MinimumLength = 2)] + string Lastname +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/MessageRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/MessageRequestTo.cs new file mode 100644 index 000000000..82c5f21f2 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/MessageRequestTo.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace ServerApp.Models.DTOs.Requests; + +public record MessageRequestTo( + [Required] long ArticleId, + [Required] + [StringLength(2048, MinimumLength = 4)] + string Content +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/StickerRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/StickerRequestTo.cs new file mode 100644 index 000000000..71d76f132 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/StickerRequestTo.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace ServerApp.Models.DTOs.Requests; + +public record StickerRequestTo( + [Required] + [StringLength(32, MinimumLength = 2, ErrorMessage = "Название стикера должно быть от 2 до 32 символов")] + string Name +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs new file mode 100644 index 000000000..2bde6576e --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs @@ -0,0 +1,10 @@ +namespace ServerApp.Models.DTOs.Responses; + +public record ArticleResponseTo( + long Id, + long AuthorId, + string Title, + string Content, + DateTime Created, + DateTime Modified +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/AuthorResponseTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/AuthorResponseTo.cs new file mode 100644 index 000000000..12967b87a --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/AuthorResponseTo.cs @@ -0,0 +1,8 @@ +namespace ServerApp.Models.DTOs.Responses; + +public record AuthorResponseTo( + long Id, + string Login, + string Firstname, + string Lastname +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/MessageResponseTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/MessageResponseTo.cs new file mode 100644 index 000000000..f27c5c994 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/MessageResponseTo.cs @@ -0,0 +1,7 @@ +namespace ServerApp.Models.DTOs.Responses; + +public record MessageResponseTo( + long Id, + long ArticleId, + string Content +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/StickerResponseTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/StickerResponseTo.cs new file mode 100644 index 000000000..7a4fbb75a --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/StickerResponseTo.cs @@ -0,0 +1,6 @@ +namespace ServerApp.Models.DTOs.Responses; + +public record StickerResponseTo( + long Id, + string Name +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs new file mode 100644 index 000000000..cc537ab6e --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs @@ -0,0 +1,10 @@ +namespace ServerApp.Models.Entities; + +public class Article : BaseEntity +{ + public long AuthorId { get; set; } + public string Title { get; set; } = null!; + public string Content { get; set; } = null!; + public DateTime Created { get; set; } + public DateTime Modified { get; set; } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs new file mode 100644 index 000000000..34eb0b188 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs @@ -0,0 +1,9 @@ +namespace ServerApp.Models.Entities; + +public class Author : BaseEntity +{ + public string Login { get; set; } = null!; + public string Password { get; set; } = null!; + public string Firstname { get; set; } = null!; + public string Lastname { get; set; } = null!; +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/BaseEntity.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/BaseEntity.cs new file mode 100644 index 000000000..454327034 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/BaseEntity.cs @@ -0,0 +1,6 @@ +namespace ServerApp.Models.Entities; + +public class BaseEntity +{ + public long Id { get; set; } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs new file mode 100644 index 000000000..75c7d0cf3 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs @@ -0,0 +1,7 @@ +namespace ServerApp.Models.Entities; + +public class Message : BaseEntity +{ + public long ArticleId { get; set; } + public string Content { get; set; } = null!; +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs new file mode 100644 index 000000000..47e5578b4 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs @@ -0,0 +1,6 @@ +namespace ServerApp.Models.Entities; + +public class Sticker : BaseEntity +{ + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj index c6001adf0..b7eea7708 100644 --- a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj +++ b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj @@ -10,6 +10,7 @@ + @@ -22,9 +23,6 @@ - - - From f5b05ecfa235ae6a05023e33fb1874a6ff54e603 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 13 Apr 2026 11:23:59 +0300 Subject: [PATCH 06/17] add generate errorcode --- .../Infrastructure/GlobalExceptionFilter.cs | 30 +++++++++++++++++++ .../ServerApp/Models/DTOs/ErrorResponse.cs | 6 ++++ 351004/Kuchko/ServerApp/ServerApp/Program.cs | 14 +++++++++ 3 files changed, 50 insertions(+) create mode 100644 351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/ErrorResponse.cs diff --git a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs new file mode 100644 index 000000000..a3788cff1 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using ServerApp.Models.DTOs; + +namespace ServerApp.Infrastructure; + +public class GlobalExceptionFilter : IExceptionFilter +{ + public void OnException(ExceptionContext context) + { + // Логика определения кодов + var (statusCode, customSubCode) = context.Exception switch + { + KeyNotFoundException => (StatusCodes.Status404NotFound, 01), + ArgumentException => (StatusCodes.Status400BadRequest, 01), + + _ => (StatusCodes.Status500InternalServerError, 01) + }; + + var finalErrorCode = statusCode * 100 + customSubCode; + var response = new ErrorResponse(context.Exception.Message, finalErrorCode); + + context.Result = new ObjectResult(response) + { + StatusCode = statusCode + }; + + context.ExceptionHandled = true; + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/ErrorResponse.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/ErrorResponse.cs new file mode 100644 index 000000000..466adaaf9 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/ErrorResponse.cs @@ -0,0 +1,6 @@ +namespace ServerApp.Models.DTOs; + +public record ErrorResponse( + string ErrorMessage, + int ErrorCode +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index e5b948b5b..c6ca18769 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -2,6 +2,7 @@ using Scalar.AspNetCore; using ServerApp; using ServerApp.Infrastructure; +using ServerApp.Models.DTOs; var builder = WebApplication.CreateBuilder(args); @@ -9,6 +10,19 @@ builder.Services.AddControllers(options => { options.Conventions.Add(new ApiPrefixConvention(new RouteAttribute("api/v1.0"))); + options.Filters.Add(); +}); + +builder.Services.Configure(options => +{ + options.InvalidModelStateResponseFactory = context => + { + var errorMsg = string.Join(" | ", context.ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage)); + + return new BadRequestObjectResult(new ErrorResponse(errorMsg, 40001)); + }; }); builder.Services.AddOpenApi(); From 3372d1de4e2c3ba16f6fdbb570032c961def973b Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 13 Apr 2026 11:38:44 +0300 Subject: [PATCH 07/17] added InMemory Repository --- .../ServerApp/Repository/IRepository.cs | 12 ++++++ .../Repository/InMemoryRepository.cs | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 351004/Kuchko/ServerApp/ServerApp/Repository/IRepository.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/IRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/IRepository.cs new file mode 100644 index 000000000..26bc7a946 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Repository/IRepository.cs @@ -0,0 +1,12 @@ +using ServerApp.Models.Entities; + +namespace ServerApp.Repository; + +public interface IRepository where T : BaseEntity +{ + IEnumerable GetAll(); + T? GetById(long id); + T Create(T entity); + T Update(T entity); + bool Delete(long id); +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs new file mode 100644 index 000000000..122b1ffcd --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs @@ -0,0 +1,39 @@ +using System.Collections.Concurrent; +using ServerApp.Models.Entities; + +namespace ServerApp.Repository; + +public class InMemoryRepository : IRepository where T : BaseEntity +{ + protected readonly ConcurrentDictionary _storage = new(); + private long _currentId; + + public IEnumerable GetAll() + { + return _storage.Values; + } + + public T? GetById(long id) + { + return _storage.TryGetValue(id, out var entity) ? entity : null; + } + + public T Create(T entity) + { + entity.Id = Interlocked.Increment(ref _currentId); + _storage[entity.Id] = entity; + return entity; + } + + public T Update(T entity) + { + if (!_storage.ContainsKey(entity.Id)) throw new KeyNotFoundException(); + _storage[entity.Id] = entity; + return entity; + } + + public bool Delete(long id) + { + return _storage.TryRemove(id, out _); + } +} \ No newline at end of file From 4cbba54f29bd9b7317260ce67d91f6752e8b1a6d Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 13 Apr 2026 12:14:28 +0300 Subject: [PATCH 08/17] added services --- 351004/Kuchko/ServerApp/ServerApp/Program.cs | 69 ++++++++++++++----- .../ServerApp/ServerApp/ServerApp.csproj | 6 -- .../Implementations/ArticleService.cs | 55 +++++++++++++++ .../Services/Implementations/AuthorService.cs | 41 +++++++++++ .../Implementations/MessageService.cs | 48 +++++++++++++ .../Implementations/StickerService.cs | 40 +++++++++++ .../Services/Interfaces/IArticleService.cs | 13 ++++ .../Services/Interfaces/IAuthorService.cs | 13 ++++ .../Services/Interfaces/IMessageService.cs | 13 ++++ .../Services/Interfaces/IStickerService.cs | 13 ++++ .../Services/Mapping/MappingConfig.cs | 36 ++++++++++ 11 files changed, 322 insertions(+), 25 deletions(-) create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Implementations/StickerService.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IArticleService.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IAuthorService.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IStickerService.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index c6ca18769..0b5e2564b 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -1,39 +1,55 @@ +using System.Reflection; +using Mapster; using Microsoft.AspNetCore.Mvc; using Scalar.AspNetCore; -using ServerApp; using ServerApp.Infrastructure; using ServerApp.Models.DTOs; +using ServerApp.Models.Entities; +using ServerApp.Repository; +using ServerApp.Services.Implementations; +using ServerApp.Services.Interfaces; var builder = WebApplication.CreateBuilder(args); -// 1. Настройка контроллеров с нашим префиксом -builder.Services.AddControllers(options => -{ - options.Conventions.Add(new ApiPrefixConvention(new RouteAttribute("api/v1.0"))); - options.Filters.Add(); -}); +var config = TypeAdapterConfig.GlobalSettings; +config.Scan(Assembly.GetExecutingAssembly()); -builder.Services.Configure(options => -{ - options.InvalidModelStateResponseFactory = context => +builder.Services.AddSingleton, InMemoryRepository>(); +builder.Services.AddSingleton, InMemoryRepository
>(); +builder.Services.AddSingleton, InMemoryRepository>(); +builder.Services.AddSingleton, InMemoryRepository>(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(options => + { + options.Conventions.Add(new ApiPrefixConvention(new RouteAttribute("api/v1.0"))); + options.Filters.Add(); + }) + .ConfigureApiBehaviorOptions(options => { - var errorMsg = string.Join(" | ", context.ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage)); + options.InvalidModelStateResponseFactory = context => + { + var errorMsg = string.Join(" | ", context.ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage)); - return new BadRequestObjectResult(new ErrorResponse(errorMsg, 40001)); - }; -}); + return new BadRequestObjectResult(new ErrorResponse(errorMsg, 40001)); + }; + }); -builder.Services.AddOpenApi(); +builder.Services.AddOpenApi(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { // Генерирует эндпоинт с JSON описанием API (по умолчанию /openapi/v1.json) - app.MapOpenApi(); - + app.MapOpenApi(); + app.MapScalarApiReference(options => { options @@ -46,4 +62,19 @@ // app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); + +using (var scope = app.Services.CreateScope()) +{ + var authorRepo = scope.ServiceProvider.GetRequiredService>(); + + if (!authorRepo.GetAll().Any()) + authorRepo.Create(new Author + { + Login = "kuchkomaxim2527@gmail.com", + Password = "password123", + Firstname = "Максим", + Lastname = "Кучко" + }); +} + app.Run(); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj index b7eea7708..77bce4e99 100644 --- a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj +++ b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj @@ -22,10 +22,4 @@ - - - - - - diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs new file mode 100644 index 000000000..785862631 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs @@ -0,0 +1,55 @@ +using Mapster; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Models.Entities; +using ServerApp.Repository; +using ServerApp.Services.Interfaces; + +namespace ServerApp.Services.Implementations; + +public class ArticleService( + IRepository
articleRepo, + IRepository authorRepo) : IArticleService +{ + public IEnumerable GetAll() => + articleRepo.GetAll().Adapt>(); + + public ArticleResponseTo GetById(long id) + { + var article = articleRepo.GetById(id) ?? throw new KeyNotFoundException($"Article {id} not found"); + return article.Adapt(); + } + + public ArticleResponseTo Create(ArticleRequestTo request) + { + // Валидация связи: существует ли автор? + if (authorRepo.GetById(request.AuthorId) == null) + throw new ArgumentException($"Author with ID {request.AuthorId} not found"); + + var article = request.Adapt
(); + article.Created = article.Modified = DateTime.UtcNow; // Установка меток времени + + var created = articleRepo.Create(article); + return created.Adapt(); + } + + public ArticleResponseTo Update(long id, ArticleRequestTo request) + { + var existing = articleRepo.GetById(id) ?? throw new KeyNotFoundException($"Article {id} not found"); + + if (authorRepo.GetById(request.AuthorId) == null) + throw new ArgumentException($"Author {request.AuthorId} not found"); + + request.Adapt(existing); + existing.Id = id; + existing.Modified = DateTime.UtcNow; // Обновляем только дату изменения + + articleRepo.Update(existing); + return existing.Adapt(); + } + + public void Delete(long id) + { + if (!articleRepo.Delete(id)) throw new KeyNotFoundException($"Article {id} not found"); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs new file mode 100644 index 000000000..66cf4fa8a --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs @@ -0,0 +1,41 @@ +using Mapster; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Models.Entities; +using ServerApp.Repository; +using ServerApp.Services.Interfaces; + +namespace ServerApp.Services.Implementations; + +public class AuthorService(IRepository repository) : IAuthorService +{ + public IEnumerable GetAll() => + repository.GetAll().Adapt>(); + + public AuthorResponseTo GetById(long id) + { + var author = repository.GetById(id) ?? throw new KeyNotFoundException($"Author with ID {id} not found"); + return author.Adapt(); + } + + public AuthorResponseTo Create(AuthorRequestTo request) + { + var author = request.Adapt(); + var created = repository.Create(author); + return created.Adapt(); + } + + public AuthorResponseTo Update(long id, AuthorRequestTo request) + { + var existing = repository.GetById(id) ?? throw new KeyNotFoundException($"Author {id} not found"); + request.Adapt(existing); + existing.Id = id; + repository.Update(existing); + return existing.Adapt(); + } + + public void Delete(long id) + { + if (!repository.Delete(id)) throw new KeyNotFoundException($"Author {id} not found"); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs new file mode 100644 index 000000000..62873d53a --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs @@ -0,0 +1,48 @@ +using Mapster; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Models.Entities; +using ServerApp.Repository; +using ServerApp.Services.Interfaces; + +namespace ServerApp.Services.Implementations; + +public class MessageService( + IRepository messageRepo, + IRepository
articleRepo) : IMessageService +{ + public IEnumerable GetAll() => messageRepo.GetAll().Adapt>(); + + public MessageResponseTo GetById(long id) + { + var msg = messageRepo.GetById(id) ?? throw new KeyNotFoundException($"Message {id} not found"); + return msg.Adapt(); + } + + public MessageResponseTo Create(MessageRequestTo request) + { + if (articleRepo.GetById(request.ArticleId) == null) + throw new ArgumentException($"Article {request.ArticleId} not found"); + + var message = request.Adapt(); + var created = messageRepo.Create(message); + return created.Adapt(); + } + + public MessageResponseTo Update(long id, MessageRequestTo request) + { + var existing = messageRepo.GetById(id) ?? throw new KeyNotFoundException($"Message {id} not found"); + if (articleRepo.GetById(request.ArticleId) == null) + throw new ArgumentException($"Article {request.ArticleId} not found"); + + request.Adapt(existing); + existing.Id = id; + messageRepo.Update(existing); + return existing.Adapt(); + } + + public void Delete(long id) + { + if (!messageRepo.Delete(id)) throw new KeyNotFoundException($"Message {id} not found"); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/StickerService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/StickerService.cs new file mode 100644 index 000000000..2d40db95e --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/StickerService.cs @@ -0,0 +1,40 @@ +using Mapster; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Models.Entities; +using ServerApp.Repository; +using ServerApp.Services.Interfaces; + +namespace ServerApp.Services.Implementations; + +public class StickerService(IRepository repository) : IStickerService +{ + public IEnumerable GetAll() => repository.GetAll().Adapt>(); + + public StickerResponseTo GetById(long id) + { + var sticker = repository.GetById(id) ?? throw new KeyNotFoundException($"Sticker {id} not found"); + return sticker.Adapt(); + } + + public StickerResponseTo Create(StickerRequestTo request) + { + var sticker = request.Adapt(); + var created = repository.Create(sticker); + return created.Adapt(); + } + + public StickerResponseTo Update(long id, StickerRequestTo request) + { + var existing = repository.GetById(id) ?? throw new KeyNotFoundException($"Sticker {id} not found"); + request.Adapt(existing); + existing.Id = id; + repository.Update(existing); + return existing.Adapt(); + } + + public void Delete(long id) + { + if (!repository.Delete(id)) throw new KeyNotFoundException($"Sticker {id} not found"); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IArticleService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IArticleService.cs new file mode 100644 index 000000000..4d80164c1 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IArticleService.cs @@ -0,0 +1,13 @@ +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; + +namespace ServerApp.Services.Interfaces; + +public interface IArticleService +{ + IEnumerable GetAll(); + ArticleResponseTo GetById(long id); + ArticleResponseTo Create(ArticleRequestTo request); + ArticleResponseTo Update(long id, ArticleRequestTo request); + void Delete(long id); +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IAuthorService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IAuthorService.cs new file mode 100644 index 000000000..baea7a8bf --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IAuthorService.cs @@ -0,0 +1,13 @@ +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; + +namespace ServerApp.Services.Interfaces; + +public interface IAuthorService +{ + IEnumerable GetAll(); + AuthorResponseTo GetById(long id); + AuthorResponseTo Create(AuthorRequestTo request); + AuthorResponseTo Update(long id, AuthorRequestTo request); + void Delete(long id); +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs new file mode 100644 index 000000000..67977361f --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs @@ -0,0 +1,13 @@ +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; + +namespace ServerApp.Services.Interfaces; + +public interface IMessageService +{ + IEnumerable GetAll(); + MessageResponseTo GetById(long id); + MessageResponseTo Create(MessageRequestTo request); + MessageResponseTo Update(long id, MessageRequestTo request); + void Delete(long id); +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IStickerService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IStickerService.cs new file mode 100644 index 000000000..48422f730 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IStickerService.cs @@ -0,0 +1,13 @@ +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; + +namespace ServerApp.Services.Interfaces; + +public interface IStickerService +{ + IEnumerable GetAll(); + StickerResponseTo GetById(long id); + StickerResponseTo Create(StickerRequestTo request); + StickerResponseTo Update(long id, StickerRequestTo request); + void Delete(long id); +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs new file mode 100644 index 000000000..d0cee3e36 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs @@ -0,0 +1,36 @@ +using Mapster; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Models.Entities; + +namespace ServerApp.Services.Mapping; + +public class MappingConfig : IRegister +{ + public void Register(TypeAdapterConfig config) + { + // 1. Маппинг для Author + config.NewConfig(); + config.NewConfig(); + + // 2. Маппинг для Article + config.NewConfig() + .AfterMapping(dest => + { + // При создании статьи устанавливаем даты + var now = DateTime.UtcNow; + dest.Created = now; + dest.Modified = now; + }); + + config.NewConfig(); + + // 3. Маппинг для Message + config.NewConfig(); + config.NewConfig(); + + // 4. Маппинг для Sticker + config.NewConfig(); + config.NewConfig(); + } +} \ No newline at end of file From da24d64a3f697204929f4957ad9b2650d35edc26 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 13 Apr 2026 13:17:01 +0300 Subject: [PATCH 09/17] added controllers & finished Task310 REST --- .../Controllers/ArticleController.cs | 46 ++++++++++++++++ .../ServerApp/Controllers/AuthorController.cs | 54 +++++++++++++++++++ .../Controllers/MessageController.cs | 37 +++++++++++++ .../Controllers/StickerController.cs | 37 +++++++++++++ .../Models/DTOs/Requests/ArticleRequestTo.cs | 1 + .../Models/DTOs/Requests/AuthorRequestTo.cs | 1 + 351004/Kuchko/ServerApp/ServerApp/Program.cs | 22 +++++++- 7 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Controllers/StickerController.cs diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs new file mode 100644 index 000000000..4f9b963c3 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using ServerApp.Models.DTOs; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Services.Interfaces; + +namespace ServerApp.Controllers; + +[ApiController] +[Route("articles")] +public class ArticleController(IArticleService articleService) : ControllerBase +{ + [HttpGet] + public ActionResult> GetAll() => Ok(articleService.GetAll()); + + [HttpGet("{id:long}")] + public ActionResult GetById(long id) => Ok(articleService.GetById(id)); + + [HttpPost] + public ActionResult Create([FromBody] ArticleRequestTo request) + { + var response = articleService.Create(request); + return CreatedAtAction(nameof(GetById), new { id = response.Id }, response); + } + + [HttpPut("{id:long}")] + [HttpPut] + public ActionResult Update(long? id, [FromBody] ArticleRequestTo request) + { + long finalId = id ?? (request.Id ?? 0); + + if (finalId == 0) + { + return BadRequest(new ErrorResponse("ID must be provided in URL or body", 40002)); + } + + return Ok(articleService.Update(finalId, request)); + } + + [HttpDelete("{id:long}")] + public IActionResult Delete(long id) + { + articleService.Delete(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs new file mode 100644 index 000000000..e503f0de8 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Mvc; +using ServerApp.Models.DTOs; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Services.Interfaces; + +namespace ServerApp.Controllers; + +[ApiController] +[Route("authors")] // Итоговый путь: /api/v1.0/authors +public class AuthorController(IAuthorService authorService) : ControllerBase +{ + [HttpGet] + public ActionResult> GetAll() + { + return Ok(authorService.GetAll()); + } + + [HttpGet("{id:long}")] + public ActionResult GetById(long id) + { + return Ok(authorService.GetById(id)); + } + + [HttpPost] + public ActionResult Create([FromBody] AuthorRequestTo request) + { + var response = authorService.Create(request); + // Возвращаем статус 201 Created и ссылку на получение созданного объекта + return CreatedAtAction(nameof(GetById), new { id = response.Id }, response); + } + + [HttpPut("{id:long}")] // Поддержка /api/v1.0/authors/{id} + [HttpPut] // Поддержка /api/v1.0/authors (ID внутри JSON) + public ActionResult Update(long? id, [FromBody] AuthorRequestTo request) + { + long finalId = id ?? (request.Id ?? 0); + + if (finalId == 0) + { + return BadRequest(new ErrorResponse("ID must be provided in URL or body", 40002)); + } + + // Вызываем сервис с найденным ID + return Ok(authorService.Update(finalId, request)); + } + + [HttpDelete("{id:long}")] + public IActionResult Delete(long id) + { + authorService.Delete(id); + return NoContent(); // Статус 204 No Content по ТЗ + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs new file mode 100644 index 000000000..adc37370f --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Services.Interfaces; + +namespace ServerApp.Controllers; + +[ApiController] +[Route("messages")] +public class MessageController(IMessageService messageService) : ControllerBase +{ + [HttpGet] + public ActionResult> GetAll() => Ok(messageService.GetAll()); + + [HttpGet("{id:long}")] + public ActionResult GetById(long id) => Ok(messageService.GetById(id)); + + [HttpPost] + public ActionResult Create([FromBody] MessageRequestTo request) + { + var response = messageService.Create(request); + return CreatedAtAction(nameof(GetById), new { id = response.Id }, response); + } + + [HttpPut("{id:long}")] + public ActionResult Update(long id, [FromBody] MessageRequestTo request) + { + return Ok(messageService.Update(id, request)); + } + + [HttpDelete("{id:long}")] + public IActionResult Delete(long id) + { + messageService.Delete(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/StickerController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/StickerController.cs new file mode 100644 index 000000000..3598146aa --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/StickerController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using ServerApp.Models.DTOs.Requests; +using ServerApp.Models.DTOs.Responses; +using ServerApp.Services.Interfaces; + +namespace ServerApp.Controllers; + +[ApiController] +[Route("stickers")] +public class StickerController(IStickerService stickerService) : ControllerBase +{ + [HttpGet] + public ActionResult> GetAll() => Ok(stickerService.GetAll()); + + [HttpGet("{id:long}")] + public ActionResult GetById(long id) => Ok(stickerService.GetById(id)); + + [HttpPost] + public ActionResult Create([FromBody] StickerRequestTo request) + { + var response = stickerService.Create(request); + return CreatedAtAction(nameof(GetById), new { id = response.Id }, response); + } + + [HttpPut("{id:long}")] + public ActionResult Update(long id, [FromBody] StickerRequestTo request) + { + return Ok(stickerService.Update(id, request)); + } + + [HttpDelete("{id:long}")] + public IActionResult Delete(long id) + { + stickerService.Delete(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs index 7bf6c92e4..2a4f80bc4 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs @@ -3,6 +3,7 @@ namespace ServerApp.Models.DTOs.Requests; public record ArticleRequestTo( + long? Id, [Required] long AuthorId, [Required] [StringLength(64, MinimumLength = 2)] diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/AuthorRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/AuthorRequestTo.cs index b81331142..90109ff4e 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/AuthorRequestTo.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/AuthorRequestTo.cs @@ -3,6 +3,7 @@ namespace ServerApp.Models.DTOs.Requests; public record AuthorRequestTo( + long? Id, [Required] [StringLength(64, MinimumLength = 2)] string Login, diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index 0b5e2564b..4ae572cb8 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -41,9 +41,29 @@ }; }); -builder.Services.AddOpenApi(); +builder.Services.AddOpenApi(options => +{ + options.AddDocumentTransformer((document, context, cancellationToken) => + { + // Просто удаляем все предопределенные сервера + // Браузер сам подставит текущий адрес (localhost:24110) + document.Servers.Clear(); + return Task.CompletedTask; + }); +}); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); var app = builder.Build(); +app.UseCors(); if (app.Environment.IsDevelopment()) { From 75b30dba545020503295a2726a50361b2716dcd9 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Sun, 3 May 2026 20:17:21 +0300 Subject: [PATCH 10/17] add Dependences --- .../ServerApp/Controllers/TestController.cs | 19 -------------- 351004/Kuchko/ServerApp/ServerApp/Dockerfile | 13 +++------- .../ServerApp/Infrastructure/AppDbContext.cs | 26 +++++++++++++++++++ .../ServerApp/Models/Entities/Article.cs | 20 +++++++++++++- .../ServerApp/Models/Entities/Author.cs | 15 ++++++++++- .../ServerApp/Models/Entities/BaseEntity.cs | 8 +++++- .../ServerApp/Models/Entities/Message.cs | 11 +++++++- .../ServerApp/Models/Entities/Sticker.cs | 8 +++++- .../ServerApp/ServerApp/Models/QueryParams.cs | 8 ++++++ .../ServerApp/Repository/EfRepository.cs | 6 +++++ .../ServerApp/Repository/IRepository.cs | 4 ++- .../Repository/InMemoryRepository.cs | 6 +++++ .../ServerApp/ServerApp/ServerApp.csproj | 14 ++++++++-- .../ServerApp/ServerApp/appsettings.json | 3 +++ 351004/Kuchko/ServerApp/docker-compose.yml | 12 +++++++++ 15 files changed, 137 insertions(+), 36 deletions(-) delete mode 100644 351004/Kuchko/ServerApp/ServerApp/Controllers/TestController.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/QueryParams.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs create mode 100644 351004/Kuchko/ServerApp/docker-compose.yml diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/TestController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/TestController.cs deleted file mode 100644 index 291a32602..000000000 --- a/351004/Kuchko/ServerApp/ServerApp/Controllers/TestController.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace ServerApp.Controllers; - -[ApiController] -[Route("test")] // Итоговый путь будет: /api/v1.0/test -public class TestController : ControllerBase -{ - [HttpGet] - public IActionResult GetStatus() - { - return Ok(new - { - Message = "Приложение работает!", - Version = "1.0", - Time = DateTime.Now - }); - } -} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Dockerfile b/351004/Kuchko/ServerApp/ServerApp/Dockerfile index 509a8dd6c..b0480195e 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Dockerfile +++ b/351004/Kuchko/ServerApp/ServerApp/Dockerfile @@ -2,24 +2,19 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app # ВАЖНО: Порт из ТЗ -EXPOSE 24110 +EXPOSE 8080 # Настройка портов и среды внутри контейнера -ENV ASPNETCORE_URLS=http://+:24110 +ENV ASPNETCORE_URLS=http://+:8080 ENV ASPNETCORE_ENVIRONMENT=Development # 2. Слой сборки (SDK) FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src - -# Копируем файл проекта и восстанавливаем зависимости -COPY ["ServerApp.csproj", "."] -RUN dotnet restore "./ServerApp.csproj" - -# Копируем все файлы и собираем проект +COPY ["ServerApp.csproj", "./"] +RUN dotnet restore "ServerApp.csproj" COPY . . -WORKDIR "/src/." RUN dotnet build "ServerApp.csproj" -c $BUILD_CONFIGURATION -o /app/build # 3. Слой публикации diff --git a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs new file mode 100644 index 000000000..806200d8e --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using ServerApp.Models.Entities; + +namespace ServerApp.Infrastructure; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Authors { get; set; } + public DbSet
Articles { get; set; } + public DbSet Messages { get; set; } + public DbSet Stickers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity
() + .HasMany(s => s.Stickers) + .WithMany(a => a.Articles) + .UsingEntity>( + "tbl_article_sticker", + j => j.HasOne().WithMany().HasForeignKey("sticker_id"), + j => j.HasOne
().WithMany().HasForeignKey("article_id") + ); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs index cc537ab6e..6c527c663 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs @@ -1,10 +1,28 @@ -namespace ServerApp.Models.Entities; +using System.ComponentModel.DataAnnotations.Schema; +namespace ServerApp.Models.Entities; + +[Table("tbl_article")] public class Article : BaseEntity { + [Column("author_id")] public long AuthorId { get; set; } + + [ForeignKey("AuthorId")] + public virtual Author Author { get; set; } = null!; + + [Column("title")] public string Title { get; set; } = null!; + + [Column("content")] public string Content { get; set; } = null!; + + [Column("created")] public DateTime Created { get; set; } + + [Column("modified")] public DateTime Modified { get; set; } + + public virtual ICollection Messages { get; set; } = new List(); + public virtual ICollection Stickers { get; set; } = new List(); } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs index 34eb0b188..b182d98a7 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs @@ -1,9 +1,22 @@ -namespace ServerApp.Models.Entities; +using System.ComponentModel.DataAnnotations.Schema; +namespace ServerApp.Models.Entities; + +[Table("tbl_author")] public class Author : BaseEntity { + [Column("login")] public string Login { get; set; } = null!; + + [Column("password")] public string Password { get; set; } = null!; + + [Column("firstname")] public string Firstname { get; set; } = null!; + + [Column("lastname")] public string Lastname { get; set; } = null!; + + // связь 1 ко многим + public virtual ICollection
Articles { get; set; } = new List
(); } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/BaseEntity.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/BaseEntity.cs index 454327034..8ba05d204 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/BaseEntity.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/BaseEntity.cs @@ -1,6 +1,12 @@ -namespace ServerApp.Models.Entities; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ServerApp.Models.Entities; public class BaseEntity { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Column("id")] public long Id { get; set; } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs index 75c7d0cf3..69f71b09b 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs @@ -1,7 +1,16 @@ -namespace ServerApp.Models.Entities; +using System.ComponentModel.DataAnnotations.Schema; +namespace ServerApp.Models.Entities; + +[Table("tbl_message")] public class Message : BaseEntity { + [Column("article_id")] public long ArticleId { get; set; } + + [ForeignKey("ArticleId")] // Указываем связь по внешнему ключу + public virtual Article Article { get; set; } = null!; + + [Column("content")] public string Content { get; set; } = null!; } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs index 47e5578b4..c0150fb59 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs @@ -1,6 +1,12 @@ -namespace ServerApp.Models.Entities; +using System.ComponentModel.DataAnnotations.Schema; +namespace ServerApp.Models.Entities; + +[Table("tbl_sticker")] public class Sticker : BaseEntity { + [Column("name")] public string Name { get; set; } = null!; + + public virtual ICollection
Articles { get; set; } = new List
(); } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/QueryParams.cs b/351004/Kuchko/ServerApp/ServerApp/Models/QueryParams.cs new file mode 100644 index 000000000..c55bca25d --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Models/QueryParams.cs @@ -0,0 +1,8 @@ +namespace ServerApp.Models; + +public record QueryParams ( + int PageNumber = 1, + int PageSize = 10, + string? SortBy = "Id", + string? SortOrder = "asc" +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs new file mode 100644 index 000000000..5c55d018e --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs @@ -0,0 +1,6 @@ +namespace ServerApp.Repository; + +public class EfRepository +{ + +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/IRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/IRepository.cs index 26bc7a946..8afd02f18 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Repository/IRepository.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Repository/IRepository.cs @@ -1,9 +1,11 @@ -using ServerApp.Models.Entities; +using ServerApp.Models; +using ServerApp.Models.Entities; namespace ServerApp.Repository; public interface IRepository where T : BaseEntity { + IEnumerable GetPaged(QueryParams @params); IEnumerable GetAll(); T? GetById(long id); T Create(T entity); diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs index 122b1ffcd..ac0407b50 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using ServerApp.Models; using ServerApp.Models.Entities; namespace ServerApp.Repository; @@ -8,6 +9,11 @@ public class InMemoryRepository : IRepository where T : BaseEntity protected readonly ConcurrentDictionary _storage = new(); private long _currentId; + public IEnumerable GetPaged(QueryParams @params) + { + throw new NotImplementedException(); + } + public IEnumerable GetAll() { return _storage.Values; diff --git a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj index 77bce4e99..699011994 100644 --- a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj +++ b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj @@ -8,12 +8,22 @@ - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/351004/Kuchko/ServerApp/ServerApp/appsettings.json b/351004/Kuchko/ServerApp/ServerApp/appsettings.json index 10f68b8c8..4c9f0efb3 100644 --- a/351004/Kuchko/ServerApp/ServerApp/appsettings.json +++ b/351004/Kuchko/ServerApp/ServerApp/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionString":{ + "DefaultConnection": "Host=localhost;Port=5432;Database=distcomp;Username=postgres;Password=postgres" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/351004/Kuchko/ServerApp/docker-compose.yml b/351004/Kuchko/ServerApp/docker-compose.yml new file mode 100644 index 000000000..140192346 --- /dev/null +++ b/351004/Kuchko/ServerApp/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.9' + +services: + api: + build: + context: ./ServerApp + dockerfile: Dockerfile + container_name: distcomp_api + ports: + - "24110:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Development From e258466c1f2a656c19352f941b4ea5af091bd96b Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 4 May 2026 13:06:28 +0300 Subject: [PATCH 11/17] add database methods --- .../Controllers/ArticleController.cs | 5 +- .../ServerApp/Controllers/AuthorController.cs | 10 +- .../Infrastructure/GlobalExceptionFilter.cs | 1 + .../20260503235416_InitialCreate.Designer.cs | 208 ++++++++++++++++++ .../20260503235416_InitialCreate.cs | 146 ++++++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 205 +++++++++++++++++ 351004/Kuchko/ServerApp/ServerApp/Program.cs | 49 +++-- .../ServerApp/Repository/EfRepository.cs | 56 ++++- .../Repository/InMemoryRepository.cs | 45 ---- .../Implementations/ArticleService.cs | 13 +- .../Services/Implementations/AuthorService.cs | 13 +- .../Services/Interfaces/IArticleService.cs | 5 +- .../Services/Interfaces/IAuthorService.cs | 5 +- 351004/Kuchko/ServerApp/docker-compose.yml | 29 ++- 14 files changed, 711 insertions(+), 79 deletions(-) create mode 100644 351004/Kuchko/ServerApp/ServerApp/Migrations/20260503235416_InitialCreate.Designer.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Migrations/20260503235416_InitialCreate.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Migrations/AppDbContextModelSnapshot.cs delete mode 100644 351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs index 4f9b963c3..fe6622f06 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using ServerApp.Models; using ServerApp.Models.DTOs; using ServerApp.Models.DTOs.Requests; using ServerApp.Models.DTOs.Responses; @@ -11,7 +12,9 @@ namespace ServerApp.Controllers; public class ArticleController(IArticleService articleService) : ControllerBase { [HttpGet] - public ActionResult> GetAll() => Ok(articleService.GetAll()); + [HttpGet("{parameters}")] + public ActionResult> GetPaged([FromQuery] QueryParams parameters) => + Ok(articleService.GetPaged(parameters)); [HttpGet("{id:long}")] public ActionResult GetById(long id) => Ok(articleService.GetById(id)); diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs index e503f0de8..91b4c7e95 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using ServerApp.Models; using ServerApp.Models.DTOs; using ServerApp.Models.DTOs.Requests; using ServerApp.Models.DTOs.Responses; @@ -7,14 +8,13 @@ namespace ServerApp.Controllers; [ApiController] -[Route("authors")] // Итоговый путь: /api/v1.0/authors +[Route("authors")] public class AuthorController(IAuthorService authorService) : ControllerBase { [HttpGet] - public ActionResult> GetAll() - { - return Ok(authorService.GetAll()); - } + [HttpGet("{parameters}")] + public ActionResult> GetPaged([FromQuery] QueryParams parameters) => + Ok(authorService.GetPaged(parameters)); [HttpGet("{id:long}")] public ActionResult GetById(long id) diff --git a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs index a3788cff1..dc3ffa58c 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs @@ -13,6 +13,7 @@ public void OnException(ExceptionContext context) { KeyNotFoundException => (StatusCodes.Status404NotFound, 01), ArgumentException => (StatusCodes.Status400BadRequest, 01), + InvalidOperationException => (StatusCodes.Status403Forbidden, 01), _ => (StatusCodes.Status500InternalServerError, 01) }; diff --git a/351004/Kuchko/ServerApp/ServerApp/Migrations/20260503235416_InitialCreate.Designer.cs b/351004/Kuchko/ServerApp/ServerApp/Migrations/20260503235416_InitialCreate.Designer.cs new file mode 100644 index 000000000..d359b8b37 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Migrations/20260503235416_InitialCreate.Designer.cs @@ -0,0 +1,208 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using ServerApp.Infrastructure; + +#nullable disable + +namespace ServerApp.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260503235416_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint") + .HasColumnName("author_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("tbl_article"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Firstname") + .IsRequired() + .HasColumnType("text") + .HasColumnName("firstname"); + + b.Property("Lastname") + .IsRequired() + .HasColumnType("text") + .HasColumnName("lastname"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text") + .HasColumnName("login"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.HasKey("Id"); + + b.ToTable("tbl_author"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArticleId") + .HasColumnType("bigint") + .HasColumnName("article_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.ToTable("tbl_message"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Sticker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("tbl_sticker"); + }); + + modelBuilder.Entity("tbl_article_sticker", b => + { + b.Property("article_id") + .HasColumnType("bigint"); + + b.Property("sticker_id") + .HasColumnType("bigint"); + + b.HasKey("article_id", "sticker_id"); + + b.HasIndex("sticker_id"); + + b.ToTable("tbl_article_sticker"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.HasOne("ServerApp.Models.Entities.Author", "Author") + .WithMany("Articles") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Message", b => + { + b.HasOne("ServerApp.Models.Entities.Article", "Article") + .WithMany("Messages") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + }); + + modelBuilder.Entity("tbl_article_sticker", b => + { + b.HasOne("ServerApp.Models.Entities.Article", null) + .WithMany() + .HasForeignKey("article_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ServerApp.Models.Entities.Sticker", null) + .WithMany() + .HasForeignKey("sticker_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Author", b => + { + b.Navigation("Articles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/351004/Kuchko/ServerApp/ServerApp/Migrations/20260503235416_InitialCreate.cs b/351004/Kuchko/ServerApp/ServerApp/Migrations/20260503235416_InitialCreate.cs new file mode 100644 index 000000000..62de10519 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Migrations/20260503235416_InitialCreate.cs @@ -0,0 +1,146 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ServerApp.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "tbl_author", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + login = table.Column(type: "text", nullable: false), + password = table.Column(type: "text", nullable: false), + firstname = table.Column(type: "text", nullable: false), + lastname = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tbl_author", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "tbl_sticker", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tbl_sticker", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "tbl_article", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + author_id = table.Column(type: "bigint", nullable: false), + title = table.Column(type: "text", nullable: false), + content = table.Column(type: "text", nullable: false), + created = table.Column(type: "timestamp with time zone", nullable: false), + modified = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tbl_article", x => x.id); + table.ForeignKey( + name: "FK_tbl_article_tbl_author_author_id", + column: x => x.author_id, + principalTable: "tbl_author", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tbl_article_sticker", + columns: table => new + { + article_id = table.Column(type: "bigint", nullable: false), + sticker_id = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tbl_article_sticker", x => new { x.article_id, x.sticker_id }); + table.ForeignKey( + name: "FK_tbl_article_sticker_tbl_article_article_id", + column: x => x.article_id, + principalTable: "tbl_article", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_tbl_article_sticker_tbl_sticker_sticker_id", + column: x => x.sticker_id, + principalTable: "tbl_sticker", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tbl_message", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + article_id = table.Column(type: "bigint", nullable: false), + content = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tbl_message", x => x.id); + table.ForeignKey( + name: "FK_tbl_message_tbl_article_article_id", + column: x => x.article_id, + principalTable: "tbl_article", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_tbl_article_author_id", + table: "tbl_article", + column: "author_id"); + + migrationBuilder.CreateIndex( + name: "IX_tbl_article_sticker_sticker_id", + table: "tbl_article_sticker", + column: "sticker_id"); + + migrationBuilder.CreateIndex( + name: "IX_tbl_message_article_id", + table: "tbl_message", + column: "article_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "tbl_article_sticker"); + + migrationBuilder.DropTable( + name: "tbl_message"); + + migrationBuilder.DropTable( + name: "tbl_sticker"); + + migrationBuilder.DropTable( + name: "tbl_article"); + + migrationBuilder.DropTable( + name: "tbl_author"); + } + } +} diff --git a/351004/Kuchko/ServerApp/ServerApp/Migrations/AppDbContextModelSnapshot.cs b/351004/Kuchko/ServerApp/ServerApp/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 000000000..b21fd8e29 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,205 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using ServerApp.Infrastructure; + +#nullable disable + +namespace ServerApp.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint") + .HasColumnName("author_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("tbl_article"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Firstname") + .IsRequired() + .HasColumnType("text") + .HasColumnName("firstname"); + + b.Property("Lastname") + .IsRequired() + .HasColumnType("text") + .HasColumnName("lastname"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text") + .HasColumnName("login"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.HasKey("Id"); + + b.ToTable("tbl_author"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArticleId") + .HasColumnType("bigint") + .HasColumnName("article_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.ToTable("tbl_message"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Sticker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("tbl_sticker"); + }); + + modelBuilder.Entity("tbl_article_sticker", b => + { + b.Property("article_id") + .HasColumnType("bigint"); + + b.Property("sticker_id") + .HasColumnType("bigint"); + + b.HasKey("article_id", "sticker_id"); + + b.HasIndex("sticker_id"); + + b.ToTable("tbl_article_sticker"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.HasOne("ServerApp.Models.Entities.Author", "Author") + .WithMany("Articles") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Message", b => + { + b.HasOne("ServerApp.Models.Entities.Article", "Article") + .WithMany("Messages") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + }); + + modelBuilder.Entity("tbl_article_sticker", b => + { + b.HasOne("ServerApp.Models.Entities.Article", null) + .WithMany() + .HasForeignKey("article_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ServerApp.Models.Entities.Sticker", null) + .WithMany() + .HasForeignKey("sticker_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Author", b => + { + b.Navigation("Articles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index 4ae572cb8..584f7377a 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -1,6 +1,7 @@ using System.Reflection; using Mapster; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Scalar.AspNetCore; using ServerApp.Infrastructure; using ServerApp.Models.DTOs; @@ -14,10 +15,16 @@ var config = TypeAdapterConfig.GlobalSettings; config.Scan(Assembly.GetExecutingAssembly()); -builder.Services.AddSingleton, InMemoryRepository>(); -builder.Services.AddSingleton, InMemoryRepository
>(); -builder.Services.AddSingleton, InMemoryRepository>(); -builder.Services.AddSingleton, InMemoryRepository>(); +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); + +builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); + +// builder.Services.AddSingleton, InMemoryRepository>(); +// builder.Services.AddSingleton, InMemoryRepository
>(); +// builder.Services.AddSingleton, InMemoryRepository>(); +// builder.Services.AddSingleton, InMemoryRepository>(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -45,8 +52,6 @@ { options.AddDocumentTransformer((document, context, cancellationToken) => { - // Просто удаляем все предопределенные сервера - // Браузер сам подставит текущий адрес (localhost:24110) document.Servers.Clear(); return Task.CompletedTask; }); @@ -67,7 +72,6 @@ if (app.Environment.IsDevelopment()) { - // Генерирует эндпоинт с JSON описанием API (по умолчанию /openapi/v1.json) app.MapOpenApi(); app.MapScalarApiReference(options => @@ -85,16 +89,31 @@ using (var scope = app.Services.CreateScope()) { - var authorRepo = scope.ServiceProvider.GetRequiredService>(); + var context = scope.ServiceProvider.GetRequiredService(); - if (!authorRepo.GetAll().Any()) - authorRepo.Create(new Author + try + { + // 1. Применяет все не примененные миграции (создает таблицы tbl_...) + context.Database.Migrate(); + + // 2. Добавляет начальную запись по ТЗ, если база пустая + if (!context.Authors.Any()) { - Login = "kuchkomaxim2527@gmail.com", - Password = "password123", - Firstname = "Максим", - Lastname = "Кучко" - }); + context.Authors.Add(new Author + { + Login = "kuchkomaxim2527@gmail.com", + Password = "password123", + Firstname = "Максим", + Lastname = "Кучко" + }); + context.SaveChanges(); + } + } + catch (Exception ex) + { + // Выведет ошибку в консоль Docker, если БД недоступна + Console.WriteLine($"Ошибка при инициализации БД: {ex.Message}"); + } } app.Run(); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs index 5c55d018e..c68a4734f 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs @@ -1,6 +1,56 @@ -namespace ServerApp.Repository; +using System.Linq.Dynamic.Core; +using Microsoft.EntityFrameworkCore; +using ServerApp.Infrastructure; +using ServerApp.Models; +using ServerApp.Models.Entities; -public class EfRepository +namespace ServerApp.Repository; + +public class EfRepository(AppDbContext context) : IRepository where T : BaseEntity { - + public IEnumerable GetPaged(QueryParams p) + { + var query = context.Set().AsNoTracking(); + + // Сортировка ("Id asc" или "Id desc") + var sortExpression = $"{p.SortBy} {p.SortOrder}"; + query = query.OrderBy(sortExpression); + + // Пагинация + return query + .Skip((p.PageNumber - 1) * p.PageSize) + .Take(p.PageSize) + .ToList(); + } + + public IEnumerable GetAll() + { + return context.Set().AsNoTracking().ToList(); + } + + public T? GetById(long id) => context.Set().Find(id); + + public T Create(T entity) + { + context.Set().Add(entity); + context.SaveChanges(); + return entity; + } + + public T Update(T entity) + { + context.Set().Update(entity); + context.SaveChanges(); + return entity; + } + + public bool Delete(long id) + { + var entity = GetById(id); + if (entity == null) return false; + + context.Set().Remove(entity); + context.SaveChanges(); + return true; + } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs deleted file mode 100644 index ac0407b50..000000000 --- a/351004/Kuchko/ServerApp/ServerApp/Repository/InMemoryRepository.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Concurrent; -using ServerApp.Models; -using ServerApp.Models.Entities; - -namespace ServerApp.Repository; - -public class InMemoryRepository : IRepository where T : BaseEntity -{ - protected readonly ConcurrentDictionary _storage = new(); - private long _currentId; - - public IEnumerable GetPaged(QueryParams @params) - { - throw new NotImplementedException(); - } - - public IEnumerable GetAll() - { - return _storage.Values; - } - - public T? GetById(long id) - { - return _storage.TryGetValue(id, out var entity) ? entity : null; - } - - public T Create(T entity) - { - entity.Id = Interlocked.Increment(ref _currentId); - _storage[entity.Id] = entity; - return entity; - } - - public T Update(T entity) - { - if (!_storage.ContainsKey(entity.Id)) throw new KeyNotFoundException(); - _storage[entity.Id] = entity; - return entity; - } - - public bool Delete(long id) - { - return _storage.TryRemove(id, out _); - } -} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs index 785862631..8907a2d97 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs @@ -1,4 +1,5 @@ using Mapster; +using ServerApp.Models; using ServerApp.Models.DTOs.Requests; using ServerApp.Models.DTOs.Responses; using ServerApp.Models.Entities; @@ -11,8 +12,8 @@ public class ArticleService( IRepository
articleRepo, IRepository authorRepo) : IArticleService { - public IEnumerable GetAll() => - articleRepo.GetAll().Adapt>(); + public IEnumerable GetPaged(QueryParams parametrs) => + articleRepo.GetPaged(parametrs).Adapt>(); public ArticleResponseTo GetById(long id) { @@ -25,6 +26,14 @@ public ArticleResponseTo Create(ArticleRequestTo request) // Валидация связи: существует ли автор? if (authorRepo.GetById(request.AuthorId) == null) throw new ArgumentException($"Author with ID {request.AuthorId} not found"); + + bool titleExists = articleRepo.GetAll() + .Any(a => a.Title.Equals(request.Title, StringComparison.OrdinalIgnoreCase)); + + if (titleExists) + { + throw new InvalidOperationException($"Login '{request.Title}' is already taken"); + } var article = request.Adapt
(); article.Created = article.Modified = DateTime.UtcNow; // Установка меток времени diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs index 66cf4fa8a..6cf0afd95 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs @@ -1,4 +1,5 @@ using Mapster; +using ServerApp.Models; using ServerApp.Models.DTOs.Requests; using ServerApp.Models.DTOs.Responses; using ServerApp.Models.Entities; @@ -9,8 +10,8 @@ namespace ServerApp.Services.Implementations; public class AuthorService(IRepository repository) : IAuthorService { - public IEnumerable GetAll() => - repository.GetAll().Adapt>(); + public IEnumerable GetPaged(QueryParams parameters) => + repository.GetPaged(parameters).Adapt>(); public AuthorResponseTo GetById(long id) { @@ -20,6 +21,14 @@ public AuthorResponseTo GetById(long id) public AuthorResponseTo Create(AuthorRequestTo request) { + bool loginExists = repository.GetAll() + .Any(a => a.Login.Equals(request.Login, StringComparison.OrdinalIgnoreCase)); + + if (loginExists) + { + throw new InvalidOperationException($"Login '{request.Login}' is already taken"); + } + var author = request.Adapt(); var created = repository.Create(author); return created.Adapt(); diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IArticleService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IArticleService.cs index 4d80164c1..75a9cb1bd 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IArticleService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IArticleService.cs @@ -1,11 +1,12 @@ -using ServerApp.Models.DTOs.Requests; +using ServerApp.Models; +using ServerApp.Models.DTOs.Requests; using ServerApp.Models.DTOs.Responses; namespace ServerApp.Services.Interfaces; public interface IArticleService { - IEnumerable GetAll(); + IEnumerable GetPaged(QueryParams parameters); ArticleResponseTo GetById(long id); ArticleResponseTo Create(ArticleRequestTo request); ArticleResponseTo Update(long id, ArticleRequestTo request); diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IAuthorService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IAuthorService.cs index baea7a8bf..889285f28 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IAuthorService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IAuthorService.cs @@ -1,11 +1,12 @@ -using ServerApp.Models.DTOs.Requests; +using ServerApp.Models; +using ServerApp.Models.DTOs.Requests; using ServerApp.Models.DTOs.Responses; namespace ServerApp.Services.Interfaces; public interface IAuthorService { - IEnumerable GetAll(); + IEnumerable GetPaged(QueryParams parameters); AuthorResponseTo GetById(long id); AuthorResponseTo Create(AuthorRequestTo request); AuthorResponseTo Update(long id, AuthorRequestTo request); diff --git a/351004/Kuchko/ServerApp/docker-compose.yml b/351004/Kuchko/ServerApp/docker-compose.yml index 140192346..30a81de4a 100644 --- a/351004/Kuchko/ServerApp/docker-compose.yml +++ b/351004/Kuchko/ServerApp/docker-compose.yml @@ -1,12 +1,37 @@ version: '3.9' services: + db: + image: postgres:latest + container_name: postgres_db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: distcomp + ports: + - "5432:5432" + volumes: + - db_data:/var/lib/postgresql + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 10s + timeout: 5s + retries: 15 + start_period: 10s + api: build: context: ./ServerApp dockerfile: Dockerfile container_name: distcomp_api - ports: - - "24110:8080" + depends_on: + db: + condition: service_healthy environment: - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database=distcomp;Username=postgres;Password=postgres + ports: + - "24110:8080" + +volumes: + db_data: \ No newline at end of file From 6eb69efafaedff05d9187dc2a8ae43e6c770c766 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 4 May 2026 13:42:42 +0300 Subject: [PATCH 12/17] Fix Deleting & Adding m2m articles & stickers --- .../ServerApp/Infrastructure/AppDbContext.cs | 21 ++ .../20260504104157_fixUpdating.Designer.cs | 211 ++++++++++++++++++ .../Migrations/20260504104157_fixUpdating.cs | 28 +++ .../Migrations/AppDbContextModelSnapshot.cs | 3 + .../Models/DTOs/Requests/ArticleRequestTo.cs | 4 +- .../DTOs/Responses/ArticleResponseTo.cs | 4 +- .../ServerApp/Repository/EfRepository.cs | 20 +- .../Implementations/ArticleService.cs | 64 +++++- .../Services/Mapping/MappingConfig.cs | 2 + 9 files changed, 349 insertions(+), 8 deletions(-) create mode 100644 351004/Kuchko/ServerApp/ServerApp/Migrations/20260504104157_fixUpdating.Designer.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Migrations/20260504104157_fixUpdating.cs diff --git a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs index 806200d8e..33d2ee6f3 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs @@ -13,6 +13,25 @@ public class AppDbContext(DbContextOptions options) : DbContext(op protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); + + // 1. Уникальный индекс для логина автора + modelBuilder.Entity() + .HasIndex(a => a.Login) + .IsUnique(); + + // 2. КАСКАДНОЕ УДАЛЕНИЕ: Author -> Articles + modelBuilder.Entity
() + .HasOne(a => a.Author) + .WithMany(a => a.Articles) + .HasForeignKey(a => a.AuthorId) + .OnDelete(DeleteBehavior.Cascade); // Если удален автор, удаляются его статьи + + // 3. КАСКАДНОЕ УДАЛЕНИЕ: Article -> Messages + modelBuilder.Entity() + .HasOne(m => m.Article) + .WithMany(a => a.Messages) + .HasForeignKey(m => m.ArticleId) + .OnDelete(DeleteBehavior.Cascade); // Если удалена статья, удаляются ее сообщения modelBuilder.Entity
() .HasMany(s => s.Stickers) @@ -22,5 +41,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) j => j.HasOne().WithMany().HasForeignKey("sticker_id"), j => j.HasOne
().WithMany().HasForeignKey("article_id") ); + + } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Migrations/20260504104157_fixUpdating.Designer.cs b/351004/Kuchko/ServerApp/ServerApp/Migrations/20260504104157_fixUpdating.Designer.cs new file mode 100644 index 000000000..c5214e447 --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Migrations/20260504104157_fixUpdating.Designer.cs @@ -0,0 +1,211 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using ServerApp.Infrastructure; + +#nullable disable + +namespace ServerApp.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260504104157_fixUpdating")] + partial class fixUpdating + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint") + .HasColumnName("author_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("tbl_article"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Firstname") + .IsRequired() + .HasColumnType("text") + .HasColumnName("firstname"); + + b.Property("Lastname") + .IsRequired() + .HasColumnType("text") + .HasColumnName("lastname"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text") + .HasColumnName("login"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("tbl_author"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArticleId") + .HasColumnType("bigint") + .HasColumnName("article_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.ToTable("tbl_message"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Sticker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("tbl_sticker"); + }); + + modelBuilder.Entity("tbl_article_sticker", b => + { + b.Property("article_id") + .HasColumnType("bigint"); + + b.Property("sticker_id") + .HasColumnType("bigint"); + + b.HasKey("article_id", "sticker_id"); + + b.HasIndex("sticker_id"); + + b.ToTable("tbl_article_sticker"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.HasOne("ServerApp.Models.Entities.Author", "Author") + .WithMany("Articles") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Message", b => + { + b.HasOne("ServerApp.Models.Entities.Article", "Article") + .WithMany("Messages") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + }); + + modelBuilder.Entity("tbl_article_sticker", b => + { + b.HasOne("ServerApp.Models.Entities.Article", null) + .WithMany() + .HasForeignKey("article_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ServerApp.Models.Entities.Sticker", null) + .WithMany() + .HasForeignKey("sticker_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Article", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("ServerApp.Models.Entities.Author", b => + { + b.Navigation("Articles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/351004/Kuchko/ServerApp/ServerApp/Migrations/20260504104157_fixUpdating.cs b/351004/Kuchko/ServerApp/ServerApp/Migrations/20260504104157_fixUpdating.cs new file mode 100644 index 000000000..76d5635ed --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Migrations/20260504104157_fixUpdating.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ServerApp.Migrations +{ + /// + public partial class fixUpdating : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_tbl_author_login", + table: "tbl_author", + column: "login", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_tbl_author_login", + table: "tbl_author"); + } + } +} diff --git a/351004/Kuchko/ServerApp/ServerApp/Migrations/AppDbContextModelSnapshot.cs b/351004/Kuchko/ServerApp/ServerApp/Migrations/AppDbContextModelSnapshot.cs index b21fd8e29..81cf756bd 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Migrations/AppDbContextModelSnapshot.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Migrations/AppDbContextModelSnapshot.cs @@ -91,6 +91,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("Login") + .IsUnique(); + b.ToTable("tbl_author"); }); diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs index 2a4f80bc4..f98bfc904 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs @@ -10,5 +10,7 @@ public record ArticleRequestTo( string Title, [Required] [StringLength(2048, MinimumLength = 4)] - string Content + string Content, + + ICollection? Stickers ); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs index 2bde6576e..500fc50c3 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs @@ -6,5 +6,7 @@ public record ArticleResponseTo( string Title, string Content, DateTime Created, - DateTime Modified + DateTime Modified, + + ICollection? Stickers ); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs index c68a4734f..57d99c1fe 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs @@ -28,7 +28,25 @@ public IEnumerable GetAll() return context.Set().AsNoTracking().ToList(); } - public T? GetById(long id) => context.Set().Find(id); + public T? GetById(long id) + { + var query = context.Set().AsQueryable(); + + if (typeof(T) == typeof(Article)) + { + query = query.Include("Stickers"); + } + else if (typeof(T) == typeof(Sticker)) + { + query = query.Include("Articles"); + } + else if (typeof(T) == typeof(Author)) + { + query = query.Include("Articles").Include("Articles.Stickers"); + } + + return query.FirstOrDefault(e => e.Id == id); + } public T Create(T entity) { diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs index 8907a2d97..591cf93c3 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs @@ -9,7 +9,8 @@ namespace ServerApp.Services.Implementations; public class ArticleService( - IRepository
articleRepo, + IRepository stickerRepo, + IRepository
articleRepo, IRepository authorRepo) : IArticleService { public IEnumerable GetPaged(QueryParams parametrs) => @@ -32,12 +33,32 @@ public ArticleResponseTo Create(ArticleRequestTo request) if (titleExists) { - throw new InvalidOperationException($"Login '{request.Title}' is already taken"); - } + throw new InvalidOperationException($"Title '{request.Title}' is already taken"); + } var article = request.Adapt
(); - article.Created = article.Modified = DateTime.UtcNow; // Установка меток времени + article.Created = article.Modified = DateTime.UtcNow; + + article.Stickers = new List(); + if (request.Stickers != null && request.Stickers.Any()) + { + var existingStickers = stickerRepo.GetAll().ToList(); + + foreach (string stickerName in request.Stickers) + { + var found = existingStickers.FirstOrDefault(s => s.Name == stickerName); + + if (found != null) + { + article.Stickers.Add(found); + } + else + { + article.Stickers.Add(new Sticker { Name = stickerName }); + } + } + } var created = articleRepo.Create(article); return created.Adapt(); } @@ -53,12 +74,45 @@ public ArticleResponseTo Update(long id, ArticleRequestTo request) existing.Id = id; existing.Modified = DateTime.UtcNow; // Обновляем только дату изменения + if (request.Stickers != null) + { + var allStickersInDb = stickerRepo.GetAll().ToList(); + existing.Stickers.Clear(); + foreach (var stickerName in request.Stickers) + { + var foundSticker = allStickersInDb.FirstOrDefault(s => s.Name == stickerName); + + if (foundSticker != null) + { + existing.Stickers.Add(foundSticker); + } + else + { + var newSticker = new Sticker { Name = stickerName }; + existing.Stickers.Add(newSticker); + + allStickersInDb.Add(newSticker); + } + } + } + articleRepo.Update(existing); return existing.Adapt(); } public void Delete(long id) { - if (!articleRepo.Delete(id)) throw new KeyNotFoundException($"Article {id} not found"); + var article = articleRepo.GetById(id) ?? throw new KeyNotFoundException($"Article {id} not found"); + var stickersToCheck = article.Stickers.ToList(); + articleRepo.Delete(id); + + foreach (var sticker in stickersToCheck) + { + var currentSticker = stickerRepo.GetById(sticker.Id); + if (currentSticker != null && currentSticker.Articles.Count == 0) + { + stickerRepo.Delete(currentSticker.Id); + } + } } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs index d0cee3e36..f44e1674f 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs @@ -15,10 +15,12 @@ public void Register(TypeAdapterConfig config) // 2. Маппинг для Article config.NewConfig() + .Ignore(dest => dest.Stickers) .AfterMapping(dest => { // При создании статьи устанавливаем даты var now = DateTime.UtcNow; + dest.Stickers = new List(); dest.Created = now; dest.Modified = now; }); From be2fd825d4e9be441199d499cb283522a061f51d Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 11 May 2026 09:53:55 +0300 Subject: [PATCH 13/17] Added Cassandra & DisscusionApp --- .../DiscussionApp/DiscussionApp.csproj | 21 ++++++++++ .../DiscussionApp/DiscussionApp.http | 6 +++ .../Kuchko/ServerApp/DiscussionApp/Dockerfile | 23 +++++++++++ .../Kuchko/ServerApp/DiscussionApp/Program.cs | 41 +++++++++++++++++++ .../Properties/launchSettings.json | 23 +++++++++++ .../ServerApp/DiscussionApp/appsettings.json | 9 ++++ 351004/Kuchko/ServerApp/ServerApp.sln | 6 +++ 351004/Kuchko/ServerApp/docker-compose.yml | 34 +++++++++++++-- 8 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.http create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/Dockerfile create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/Program.cs create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/Properties/launchSettings.json create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/appsettings.json diff --git a/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj b/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj new file mode 100644 index 000000000..815cae332 --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + enable + enable + Linux + + + + + + + + + + .dockerignore + + + + diff --git a/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.http b/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.http new file mode 100644 index 000000000..a54718c04 --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.http @@ -0,0 +1,6 @@ +@DiscussionApp_HostAddress = http://localhost:5206 + +GET {{DiscussionApp_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile b/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile new file mode 100644 index 000000000..093e1f0d5 --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 + +ENV ASPNETCORE_URLS=http://+:8080 +ENV ASPNETCORE_ENVIRONMENT=Development + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["DiscussionApp.csproj", "./"] +RUN dotnet restore "DiscussionApp.csproj" +COPY . . +RUN dotnet build "DiscussionApp.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +RUN dotnet publish "./DiscussionApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "DiscussionApp.dll"] diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Program.cs b/351004/Kuchko/ServerApp/DiscussionApp/Program.cs new file mode 100644 index 000000000..d5e0ef3a2 --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/Program.cs @@ -0,0 +1,41 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast"); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Properties/launchSettings.json b/351004/Kuchko/ServerApp/DiscussionApp/Properties/launchSettings.json new file mode 100644 index 000000000..79539f788 --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5206", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7293;http://localhost:5206", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/351004/Kuchko/ServerApp/DiscussionApp/appsettings.json b/351004/Kuchko/ServerApp/DiscussionApp/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/351004/Kuchko/ServerApp/ServerApp.sln b/351004/Kuchko/ServerApp/ServerApp.sln index cff7f910a..98f51621e 100644 --- a/351004/Kuchko/ServerApp/ServerApp.sln +++ b/351004/Kuchko/ServerApp/ServerApp.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerApp", "ServerApp\ServerApp.csproj", "{F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscussionApp", "DiscussionApp\DiscussionApp.csproj", "{46462AFC-56FE-480A-84FC-488D4DB0C32E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +14,9 @@ Global {F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2EFEDB0-911A-4C64-8CE1-CEC4D2887887}.Release|Any CPU.Build.0 = Release|Any CPU + {46462AFC-56FE-480A-84FC-488D4DB0C32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46462AFC-56FE-480A-84FC-488D4DB0C32E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46462AFC-56FE-480A-84FC-488D4DB0C32E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46462AFC-56FE-480A-84FC-488D4DB0C32E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/351004/Kuchko/ServerApp/docker-compose.yml b/351004/Kuchko/ServerApp/docker-compose.yml index 30a81de4a..8f1c18a08 100644 --- a/351004/Kuchko/ServerApp/docker-compose.yml +++ b/351004/Kuchko/ServerApp/docker-compose.yml @@ -19,19 +19,45 @@ services: retries: 15 start_period: 10s - api: + cassandra_db: + image: cassandra:latest + container_name: cassandra_db + ports: [ "9042:9042" ] + environment: + - CASSANDRA_CLUSTER_NAME=DistcompCluster + healthcheck: + test: [ "CMD-SHELL", "cqlsh -e 'DESCRIBE KEYSPACES'" ] + interval: 15s + + publisher_api: build: - context: ./ServerApp - dockerfile: Dockerfile - container_name: distcomp_api + context: . + dockerfile: ServerApp/Dockerfile + container_name: publisher_api depends_on: db: condition: service_healthy environment: - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:24110 - ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database=distcomp;Username=postgres;Password=postgres + + - DiscussionServiceUrl=http://discussion_api:24130 ports: - "24110:8080" + discussion_api: # Новый микросервис (порт 24130) + build: + context: . + dockerfile: DiscussionApp/Dockerfile + ports: [ "24130:24130" ] + depends_on: + cassandra_db: + condition: service_healthy + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:24130 + - Cassandra__ContactPoint=cassandra_db + volumes: db_data: \ No newline at end of file From 0b155f2f03433353c265b7317729ac1ed63a33f6 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 11 May 2026 10:25:37 +0300 Subject: [PATCH 14/17] Added messaging controllers --- .../Controllers/DiscussionController.cs | 26 +++++++ .../Kuchko/ServerApp/DiscussionApp/Dockerfile | 26 ++++--- .../Kuchko/ServerApp/DiscussionApp/Program.cs | 68 ++++++++++++------- .../Repositories/MessageRepository.cs | 57 ++++++++++++++++ 351004/Kuchko/ServerApp/ServerApp/Dockerfile | 17 +++-- 351004/Kuchko/ServerApp/ServerApp/Program.cs | 8 +-- 351004/Kuchko/ServerApp/docker-compose.yml | 15 ++-- 7 files changed, 163 insertions(+), 54 deletions(-) create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs b/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs new file mode 100644 index 000000000..fcbdb8da0 --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs @@ -0,0 +1,26 @@ +using DiscussionApp.Repositories; +using Microsoft.AspNetCore.Mvc; + +namespace DiscussionApp.Controllers; + +[ApiController] +[Route("messages")] +public class DiscussionController(MessageRepository repo) : ControllerBase +{ + [HttpPost] + public IActionResult Create([FromBody] MessageRequest request) + { + // В реальном проекте ID генерирует Cassandra (uuid), но для совместимости с ТЗ генерим тут + long newId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + repo.Create(request.ArticleId, newId, request.Content); + return Ok(new { Id = newId, ArticleId = request.ArticleId, Content = request.Content }); + } + + [HttpGet("by-article/{articleId}")] + public IActionResult GetByArticle(long articleId) + { + return Ok(repo.GetByArticle(articleId)); + } +} + +public record MessageRequest(long ArticleId, string Content); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile b/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile index 093e1f0d5..55413355f 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile +++ b/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile @@ -1,23 +1,31 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base -USER $APP_UID +# 1. Базовый образ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app -EXPOSE 8080 - -ENV ASPNETCORE_URLS=http://+:8080 +# ИСПРАВЛЕНО: порт по ТЗ +EXPOSE 24130 +ENV ASPNETCORE_URLS=http://+:24130 ENV ASPNETCORE_ENVIRONMENT=Development +# 2. Сборка FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -COPY ["DiscussionApp.csproj", "./"] -RUN dotnet restore "DiscussionApp.csproj" + +# ИСПРАВЛЕНО: Правильные пути с учетом контекста "." (корень решения) +COPY ["DiscussionApp/DiscussionApp.csproj", "DiscussionApp/"] +RUN dotnet restore "DiscussionApp/DiscussionApp.csproj" + COPY . . +# Переходим в папку проекта перед сборкой +WORKDIR "/src/DiscussionApp" RUN dotnet build "DiscussionApp.csproj" -c $BUILD_CONFIGURATION -o /app/build +# 3. Публикация FROM build AS publish -RUN dotnet publish "./DiscussionApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +RUN dotnet publish "DiscussionApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +# 4. Финальный запуск FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "DiscussionApp.dll"] +ENTRYPOINT ["dotnet", "DiscussionApp.dll"] \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Program.cs b/351004/Kuchko/ServerApp/DiscussionApp/Program.cs index d5e0ef3a2..52baee4a6 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Program.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Program.cs @@ -1,41 +1,59 @@ +using DiscussionApp.Repositories; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Routing; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddSingleton(); + +builder.Services.AddControllers(options => +{ + // Обязательное требование ТЗ: префикс /api/v1.0/ + + options.Conventions.Add(new ApiPrefixConvention(new RouteAttribute("api/v1.0"))); +}); + +// --- 3. НАСТРОЙКА СВОБОДНОГО CORS (Для локального тестирования) --- +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); + }); +}); + builder.Services.AddOpenApi(); var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } -app.UseHttpsRedirection(); - -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => - { - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; - }) - .WithName("GetWeatherForecast"); +app.UseCors(); +app.UseAuthorization(); +app.MapControllers(); app.Run(); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +public class ApiPrefixConvention(IRouteTemplateProvider route) : IApplicationModelConvention { - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + private readonly AttributeRouteModel _routePrefix = new(route); + + public void Apply(ApplicationModel application) + { + foreach (var selector in application.Controllers.SelectMany(c => c.Selectors)) + { + if (selector.AttributeRouteModel != null) + { + selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel); + } + else + { + selector.AttributeRouteModel = _routePrefix; + } + } + } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs b/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs new file mode 100644 index 000000000..088ac07b5 --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs @@ -0,0 +1,57 @@ +using Cassandra; +using ISession = Cassandra.ISession; + +namespace DiscussionApp.Repositories; + +public class MessageRepository +{ + private readonly ISession _session; + + public MessageRepository(IConfiguration config) + { + var contactPoint = config["Cassandra:ContactPoint"] ?? "localhost"; + var cluster = Cluster.Builder().AddContactPoint(contactPoint).Build(); + _session = cluster.Connect(); + + InitializeDatabase(); + } + + private void InitializeDatabase() + { + // Создаем Keyspace (Схему distcomp) + _session.Execute(@" + CREATE KEYSPACE IF NOT EXISTS distcomp + WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};"); + + _session.ChangeKeyspace("distcomp"); + + // Создаем таблицу. РЕШАЕМ ПРОБЛЕМУ ПЕРЕКОСА ДАННЫХ: + // PRIMARY KEY ((article_id), id) - article_id это partition key! + _session.Execute(@" + CREATE TABLE IF NOT EXISTS tbl_message ( + article_id bigint, + id bigint, + country text, + content text, + PRIMARY KEY ((article_id), id) + );"); + } + + public void Create(long articleId, long id, string content, string country = "Unknown") + { + var ps = _session.Prepare("INSERT INTO tbl_message (article_id, id, country, content) VALUES (?, ?, ?, ?)"); + _session.Execute(ps.Bind(articleId, id, country, content)); + } + + // Для внешнего API + public IEnumerable GetByArticle(long articleId) + { + var ps = _session.Prepare("SELECT * FROM tbl_message WHERE article_id = ?"); + var rows = _session.Execute(ps.Bind(articleId)); + return rows.Select(r => new { + Id = r.GetValue("id"), + ArticleId = r.GetValue("article_id"), + Content = r.GetValue("content") + }); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Dockerfile b/351004/Kuchko/ServerApp/ServerApp/Dockerfile index b0480195e..6f190932c 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Dockerfile +++ b/351004/Kuchko/ServerApp/ServerApp/Dockerfile @@ -1,20 +1,27 @@ # 1. Слой выполнения (Runtime) FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app -# ВАЖНО: Порт из ТЗ -EXPOSE 8080 +# ИСПРАВЛЕНО: Порт из ТЗ для Publisher +EXPOSE 24110 # Настройка портов и среды внутри контейнера -ENV ASPNETCORE_URLS=http://+:8080 +ENV ASPNETCORE_URLS=http://+:24110 ENV ASPNETCORE_ENVIRONMENT=Development # 2. Слой сборки (SDK) FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -COPY ["ServerApp.csproj", "./"] -RUN dotnet restore "ServerApp.csproj" + +# ИСПРАВЛЕНО: Правильный путь к файлу проекта из корня решения +COPY ["ServerApp/ServerApp.csproj", "ServerApp/"] +RUN dotnet restore "ServerApp/ServerApp.csproj" + +# Копируем всё остальное COPY . . + +# ИСПРАВЛЕНО: Переходим в папку ServerApp перед сборкой +WORKDIR "/src/ServerApp" RUN dotnet build "ServerApp.csproj" -c $BUILD_CONFIGURATION -o /app/build # 3. Слой публикации diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index 584f7377a..7e2b173f1 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -19,13 +19,7 @@ builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); -builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); - -// builder.Services.AddSingleton, InMemoryRepository>(); -// builder.Services.AddSingleton, InMemoryRepository
>(); -// builder.Services.AddSingleton, InMemoryRepository>(); -// builder.Services.AddSingleton, InMemoryRepository>(); - +builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/351004/Kuchko/ServerApp/docker-compose.yml b/351004/Kuchko/ServerApp/docker-compose.yml index 8f1c18a08..48bec9884 100644 --- a/351004/Kuchko/ServerApp/docker-compose.yml +++ b/351004/Kuchko/ServerApp/docker-compose.yml @@ -1,8 +1,6 @@ -version: '3.9' - services: db: - image: postgres:latest + image: postgres:17 container_name: postgres_db environment: POSTGRES_USER: postgres @@ -41,16 +39,15 @@ services: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://+:24110 - ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database=distcomp;Username=postgres;Password=postgres - - DiscussionServiceUrl=http://discussion_api:24130 ports: - - "24110:8080" - - discussion_api: # Новый микросервис (порт 24130) + - "24110:24110" + + discussion_api: build: context: . dockerfile: DiscussionApp/Dockerfile - ports: [ "24130:24130" ] + container_name: discussion_api depends_on: cassandra_db: condition: service_healthy @@ -58,6 +55,8 @@ services: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://+:24130 - Cassandra__ContactPoint=cassandra_db + ports: + - "24130:24130" volumes: db_data: \ No newline at end of file From 3301799f368076d4b09e5939b0196f17da3c4db1 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 11 May 2026 10:53:11 +0300 Subject: [PATCH 15/17] Methods migration in microservice --- .../Controllers/DiscussionController.cs | 41 ++++++++++- .../Repositories/MessageRepository.cs | 46 +++++++++++++ 351004/Kuchko/ServerApp/ServerApp/Program.cs | 7 ++ .../Implementations/MessageService.cs | 69 +++++++++++++++---- 4 files changed, 147 insertions(+), 16 deletions(-) diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs b/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs index fcbdb8da0..8d27acd3b 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs @@ -16,11 +16,50 @@ public IActionResult Create([FromBody] MessageRequest request) return Ok(new { Id = newId, ArticleId = request.ArticleId, Content = request.Content }); } - [HttpGet("by-article/{articleId}")] + [HttpGet("{articleId}")] public IActionResult GetByArticle(long articleId) { return Ok(repo.GetByArticle(articleId)); } + + [HttpGet] + public IActionResult GetAll() + { + return Ok(repo.GetAll()); + } + [HttpGet("{id:long}")] + public IActionResult GetById(long id) + { + var msg = repo.GetById(id); + if (msg == null) return NotFound(); + return Ok(msg); + } + + [HttpPut("{id:long}")] + public IActionResult Update(long id, [FromBody] MessageRequest request) + { + var existing = repo.GetById(id); + if (existing == null) return NotFound(); + + // По логике ТЗ, articleId привязан к сообщению, поэтому мы передаем его из запроса + repo.Update(id, request.ArticleId, request.Content); + + return Ok(new { Id = id, ArticleId = request.ArticleId, Content = request.Content }); + } + + [HttpDelete("{id:long}")] + public IActionResult Delete(long id) + { + var existing = repo.GetById(id); + if (existing == null) return NotFound(); + + // Достаем articleId из существующего сообщения, так как он нужен для удаления в Cassandra + long articleId = (long)existing.GetType().GetProperty("ArticleId").GetValue(existing, null); + + repo.Delete(id, articleId); + return NoContent(); + } + } public record MessageRequest(long ArticleId, string Content); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs b/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs index 088ac07b5..3034afda3 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs @@ -54,4 +54,50 @@ public IEnumerable GetByArticle(long articleId) Content = r.GetValue("content") }); } + + public IEnumerable GetAll() + { + // Внимание: В Cassandra ALLOW FILTERING или SELECT * без ключа + // считается плохой практикой на больших объемах данных, но для тестов ТЗ это нужно. + var ps = _session.Prepare("SELECT * FROM tbl_message"); + var rows = _session.Execute(ps.Bind()); + + return rows.Select(r => new { + Id = r.GetValue("id"), + ArticleId = r.GetValue("article_id"), + // Добавляем Country, так как ТЗ (схема) требует его возвращать + Country = r.GetValue("country"), + Content = r.GetValue("content") + }); + } + + public dynamic? GetById(long id) + { + // ВАЖНО: В Cassandra фильтрация только по clustering key (id) без partition key (article_id) + // требует ALLOW FILTERING. Это плохо для продакшена, но допустимо для учебного ТЗ. + var ps = _session.Prepare("SELECT * FROM tbl_message WHERE id = ? ALLOW FILTERING"); + var row = _session.Execute(ps.Bind(id)).FirstOrDefault(); + + if (row == null) return null; + + return new { + Id = row.GetValue("id"), + ArticleId = row.GetValue("article_id"), + Country = row.GetValue("country"), + Content = row.GetValue("content") + }; + } + + public void Update(long id, long articleId, string content, string country = "Unknown") + { + // Для UPDATE в Cassandra обязательно нужен весь PRIMARY KEY (article_id, id) + var ps = _session.Prepare("UPDATE tbl_message SET content = ?, country = ? WHERE article_id = ? AND id = ?"); + _session.Execute(ps.Bind(content, country, articleId, id)); + } + + public void Delete(long id, long articleId) + { + var ps = _session.Prepare("DELETE FROM tbl_message WHERE article_id = ? AND id = ?"); + _session.Execute(ps.Bind(articleId, id)); + } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index 7e2b173f1..3118eb284 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -25,6 +25,13 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddHttpClient(client => +{ + // Берем URL из docker-compose + var discussionUrl = builder.Configuration["DiscussionServiceUrl"] ?? "http://localhost:24130"; + client.BaseAddress = new Uri(discussionUrl); +}); + builder.Services.AddControllers(options => { options.Conventions.Add(new ApiPrefixConvention(new RouteAttribute("api/v1.0"))); diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs index 62873d53a..789f399bb 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs @@ -1,4 +1,6 @@ -using Mapster; +using System.Net; +using System.Text; +using System.Text.Json; using ServerApp.Models.DTOs.Requests; using ServerApp.Models.DTOs.Responses; using ServerApp.Models.Entities; @@ -8,41 +10,78 @@ namespace ServerApp.Services.Implementations; public class MessageService( - IRepository messageRepo, - IRepository
articleRepo) : IMessageService + IRepository
articleRepo, // Оставляем ArticleRepo для валидации + HttpClient httpClient) : IMessageService { - public IEnumerable GetAll() => messageRepo.GetAll().Adapt>(); + private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true }; + // 1. Получить все сообщения (GET /api/v1.0/messages) + public IEnumerable GetAll() + { + var response = httpClient.GetAsync("/api/v1.0/messages").Result; + EnsureSuccess(response); + + var content = response.Content.ReadAsStringAsync().Result; + return JsonSerializer.Deserialize>(content, _jsonOptions)!; + } + + // 2. Получить сообщение по ID (GET /api/v1.0/messages/{id}) public MessageResponseTo GetById(long id) { - var msg = messageRepo.GetById(id) ?? throw new KeyNotFoundException($"Message {id} not found"); - return msg.Adapt(); + var response = httpClient.GetAsync($"/api/v1.0/messages/{id}").Result; + EnsureSuccess(response, id); + + var content = response.Content.ReadAsStringAsync().Result; + return JsonSerializer.Deserialize(content, _jsonOptions)!; } + // 3. Создать сообщение (POST /api/v1.0/messages) public MessageResponseTo Create(MessageRequestTo request) { + // Бизнес-логика остается в Publisher: проверяем, существует ли статья в Postgres if (articleRepo.GetById(request.ArticleId) == null) throw new ArgumentException($"Article {request.ArticleId} not found"); - var message = request.Adapt(); - var created = messageRepo.Create(message); - return created.Adapt(); + var response = httpClient.PostAsJsonAsync("/api/v1.0/messages", request).Result; + EnsureSuccess(response); + + var content = response.Content.ReadAsStringAsync().Result; + return JsonSerializer.Deserialize(content, _jsonOptions)!; } + // 4. Обновить сообщение (PUT /api/v1.0/messages/{id}) public MessageResponseTo Update(long id, MessageRequestTo request) { - var existing = messageRepo.GetById(id) ?? throw new KeyNotFoundException($"Message {id} not found"); if (articleRepo.GetById(request.ArticleId) == null) throw new ArgumentException($"Article {request.ArticleId} not found"); - request.Adapt(existing); - existing.Id = id; - messageRepo.Update(existing); - return existing.Adapt(); + // Отправляем PUT запрос в DiscussionApp + var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); + var response = httpClient.PutAsync($"/api/v1.0/messages/{id}", jsonContent).Result; + EnsureSuccess(response, id); + + var content = response.Content.ReadAsStringAsync().Result; + return JsonSerializer.Deserialize(content, _jsonOptions)!; } + // 5. Удалить сообщение (DELETE /api/v1.0/messages/{id}) public void Delete(long id) { - if (!messageRepo.Delete(id)) throw new KeyNotFoundException($"Message {id} not found"); + var response = httpClient.DeleteAsync($"/api/v1.0/messages/{id}").Result; + EnsureSuccess(response, id); + } + + // --- Вспомогательный метод для обработки ошибок от DiscussionApp --- + private void EnsureSuccess(HttpResponseMessage response, long? id = null) + { + if (response.IsSuccessStatusCode) return; + + if (response.StatusCode == HttpStatusCode.NotFound) + throw new KeyNotFoundException($"Message {id?.ToString() ?? "data"} not found in Discussion Service"); + + if (response.StatusCode == HttpStatusCode.BadRequest) + throw new ArgumentException("Invalid data sent to Discussion Service"); + + throw new Exception($"Discussion Service error: {response.StatusCode}"); } } \ No newline at end of file From 4263e05dea5ec1075b888582cbee8828dc36337f Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Mon, 11 May 2026 15:06:13 +0300 Subject: [PATCH 16/17] Errors --- .../Controllers/DiscussionController.cs | 47 +++---- .../DiscussionApp/DiscussionApp.csproj | 5 + .../Kuchko/ServerApp/DiscussionApp/Dockerfile | 2 + .../Repositories/MessageRepository.cs | 85 ++++--------- .../DiscussionApp/Services/KafkaWorker.cs | 58 +++++++++ 351004/Kuchko/ServerApp/ServerApp.sln | 6 + .../Controllers/MessageController.cs | 3 +- 351004/Kuchko/ServerApp/ServerApp/Dockerfile | 3 +- .../Models/DTOs/Requests/MessageRequestTo.cs | 10 -- .../DTOs/Responses/MessageResponseTo.cs | 7 -- 351004/Kuchko/ServerApp/ServerApp/Program.cs | 5 + .../ServerApp/ServerApp/ServerApp.csproj | 5 + .../Implementations/MessageService.cs | 115 +++++++++--------- .../Services/Interfaces/IMessageService.cs | 4 +- .../ServerApp/Services/KafkaRequestManager.cs | 16 +++ .../Services/KafkaResponseListener.cs | 32 +++++ .../Services/Mapping/MappingConfig.cs | 1 + .../ServerApp/SharedModels/KafkaEvent.cs | 17 +++ .../ServerApp/SharedModels/MessagesDtos.cs | 16 +++ .../SharedModels/SharedModels.csproj | 9 ++ 351004/Kuchko/ServerApp/docker-compose.yml | 43 +++++++ 21 files changed, 312 insertions(+), 177 deletions(-) create mode 100644 351004/Kuchko/ServerApp/DiscussionApp/Services/KafkaWorker.cs delete mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/MessageRequestTo.cs delete mode 100644 351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/MessageResponseTo.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/KafkaRequestManager.cs create mode 100644 351004/Kuchko/ServerApp/ServerApp/Services/KafkaResponseListener.cs create mode 100644 351004/Kuchko/ServerApp/SharedModels/KafkaEvent.cs create mode 100644 351004/Kuchko/ServerApp/SharedModels/MessagesDtos.cs create mode 100644 351004/Kuchko/ServerApp/SharedModels/SharedModels.csproj diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs b/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs index 8d27acd3b..2368368b0 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs @@ -1,5 +1,6 @@ using DiscussionApp.Repositories; using Microsoft.AspNetCore.Mvc; +using SharedModels; namespace DiscussionApp.Controllers; @@ -8,43 +9,30 @@ namespace DiscussionApp.Controllers; public class DiscussionController(MessageRepository repo) : ControllerBase { [HttpPost] - public IActionResult Create([FromBody] MessageRequest request) + public IActionResult Create([FromBody] MessageRequestTo request) { - // В реальном проекте ID генерирует Cassandra (uuid), но для совместимости с ТЗ генерим тут long newId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - repo.Create(request.ArticleId, newId, request.Content); - return Ok(new { Id = newId, ArticleId = request.ArticleId, Content = request.Content }); - } - - [HttpGet("{articleId}")] - public IActionResult GetByArticle(long articleId) - { - return Ok(repo.GetByArticle(articleId)); + var msg = new MessageResponseTo(newId, request.ArticleId, request.Content, MessageState.Approve); + repo.Create(msg); + return Ok(msg); } [HttpGet] - public IActionResult GetAll() - { - return Ok(repo.GetAll()); - } + public IActionResult GetAll() => Ok(repo.GetAll()); + [HttpGet("{id:long}")] public IActionResult GetById(long id) { var msg = repo.GetById(id); - if (msg == null) return NotFound(); - return Ok(msg); + return msg == null ? NotFound() : Ok(msg); } [HttpPut("{id:long}")] - public IActionResult Update(long id, [FromBody] MessageRequest request) + public IActionResult Update(long id, [FromBody] MessageRequestTo request) { - var existing = repo.GetById(id); - if (existing == null) return NotFound(); - - // По логике ТЗ, articleId привязан к сообщению, поэтому мы передаем его из запроса - repo.Update(id, request.ArticleId, request.Content); - - return Ok(new { Id = id, ArticleId = request.ArticleId, Content = request.Content }); + var msg = new MessageResponseTo(id, request.ArticleId, request.Content, MessageState.Approve); + repo.Update(msg); + return Ok(msg); } [HttpDelete("{id:long}")] @@ -52,14 +40,7 @@ public IActionResult Delete(long id) { var existing = repo.GetById(id); if (existing == null) return NotFound(); - - // Достаем articleId из существующего сообщения, так как он нужен для удаления в Cassandra - long articleId = (long)existing.GetType().GetProperty("ArticleId").GetValue(existing, null); - - repo.Delete(id, articleId); + repo.Delete(id, existing.ArticleId); return NoContent(); } - -} - -public record MessageRequest(long ArticleId, string Content); \ No newline at end of file +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj b/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj index 815cae332..937cce3e7 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj +++ b/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj @@ -9,6 +9,7 @@ + @@ -18,4 +19,8 @@ + + + + diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile b/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile index 55413355f..7a85e9850 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile +++ b/351004/Kuchko/ServerApp/DiscussionApp/Dockerfile @@ -11,6 +11,8 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src +# ВАЖНО: Сначала копируем проекты +COPY ["SharedModels/SharedModels.csproj", "SharedModels/"] # ИСПРАВЛЕНО: Правильные пути с учетом контекста "." (корень решения) COPY ["DiscussionApp/DiscussionApp.csproj", "DiscussionApp/"] RUN dotnet restore "DiscussionApp/DiscussionApp.csproj" diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs b/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs index 3034afda3..30314cb65 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs @@ -1,4 +1,5 @@ using Cassandra; +using SharedModels; using ISession = Cassandra.ISession; namespace DiscussionApp.Repositories; @@ -12,87 +13,41 @@ public MessageRepository(IConfiguration config) var contactPoint = config["Cassandra:ContactPoint"] ?? "localhost"; var cluster = Cluster.Builder().AddContactPoint(contactPoint).Build(); _session = cluster.Connect(); - InitializeDatabase(); } private void InitializeDatabase() { - // Создаем Keyspace (Схему distcomp) - _session.Execute(@" - CREATE KEYSPACE IF NOT EXISTS distcomp - WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};"); - + _session.Execute("CREATE KEYSPACE IF NOT EXISTS distcomp WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};"); _session.ChangeKeyspace("distcomp"); - - // Создаем таблицу. РЕШАЕМ ПРОБЛЕМУ ПЕРЕКОСА ДАННЫХ: - // PRIMARY KEY ((article_id), id) - article_id это partition key! - _session.Execute(@" - CREATE TABLE IF NOT EXISTS tbl_message ( - article_id bigint, - id bigint, - country text, - content text, - PRIMARY KEY ((article_id), id) - );"); - } - - public void Create(long articleId, long id, string content, string country = "Unknown") - { - var ps = _session.Prepare("INSERT INTO tbl_message (article_id, id, country, content) VALUES (?, ?, ?, ?)"); - _session.Execute(ps.Bind(articleId, id, country, content)); + _session.Execute(@"CREATE TABLE IF NOT EXISTS tbl_message ( + article_id bigint, id bigint, content text, state text, country text, + PRIMARY KEY ((article_id), id));"); } - // Для внешнего API - public IEnumerable GetByArticle(long articleId) + public IEnumerable GetAll() { - var ps = _session.Prepare("SELECT * FROM tbl_message WHERE article_id = ?"); - var rows = _session.Execute(ps.Bind(articleId)); - return rows.Select(r => new { - Id = r.GetValue("id"), - ArticleId = r.GetValue("article_id"), - Content = r.GetValue("content") - }); + var rows = _session.Execute("SELECT * FROM tbl_message ALLOW FILTERING"); + return rows.Select(MapRow); } - public IEnumerable GetAll() + public MessageResponseTo? GetById(long id) { - // Внимание: В Cassandra ALLOW FILTERING или SELECT * без ключа - // считается плохой практикой на больших объемах данных, но для тестов ТЗ это нужно. - var ps = _session.Prepare("SELECT * FROM tbl_message"); - var rows = _session.Execute(ps.Bind()); - - return rows.Select(r => new { - Id = r.GetValue("id"), - ArticleId = r.GetValue("article_id"), - // Добавляем Country, так как ТЗ (схема) требует его возвращать - Country = r.GetValue("country"), - Content = r.GetValue("content") - }); - } - - public dynamic? GetById(long id) - { - // ВАЖНО: В Cassandra фильтрация только по clustering key (id) без partition key (article_id) - // требует ALLOW FILTERING. Это плохо для продакшена, но допустимо для учебного ТЗ. var ps = _session.Prepare("SELECT * FROM tbl_message WHERE id = ? ALLOW FILTERING"); var row = _session.Execute(ps.Bind(id)).FirstOrDefault(); - - if (row == null) return null; + return row != null ? MapRow(row) : null; + } - return new { - Id = row.GetValue("id"), - ArticleId = row.GetValue("article_id"), - Country = row.GetValue("country"), - Content = row.GetValue("content") - }; + public void Create(MessageResponseTo msg) + { + var ps = _session.Prepare("INSERT INTO tbl_message (article_id, id, content, state, country) VALUES (?, ?, ?, ?, ?)"); + _session.Execute(ps.Bind(msg.ArticleId, msg.Id, msg.Content, msg.State, "Unknown")); } - public void Update(long id, long articleId, string content, string country = "Unknown") + public void Update(MessageResponseTo msg) { - // Для UPDATE в Cassandra обязательно нужен весь PRIMARY KEY (article_id, id) - var ps = _session.Prepare("UPDATE tbl_message SET content = ?, country = ? WHERE article_id = ? AND id = ?"); - _session.Execute(ps.Bind(content, country, articleId, id)); + var ps = _session.Prepare("UPDATE tbl_message SET content = ?, state = ? WHERE article_id = ? AND id = ?"); + _session.Execute(ps.Bind(msg.Content, msg.State, msg.ArticleId, msg.Id)); } public void Delete(long id, long articleId) @@ -100,4 +55,8 @@ public void Delete(long id, long articleId) var ps = _session.Prepare("DELETE FROM tbl_message WHERE article_id = ? AND id = ?"); _session.Execute(ps.Bind(articleId, id)); } + + private MessageResponseTo MapRow(Row r) => new( + r.GetValue("id"), r.GetValue("article_id"), + r.GetValue("content"), r.GetValue("state")); } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Services/KafkaWorker.cs b/351004/Kuchko/ServerApp/DiscussionApp/Services/KafkaWorker.cs new file mode 100644 index 000000000..e3e48bbbf --- /dev/null +++ b/351004/Kuchko/ServerApp/DiscussionApp/Services/KafkaWorker.cs @@ -0,0 +1,58 @@ +using System.Text.Json; +using Confluent.Kafka; +using DiscussionApp.Repositories; +using SharedModels; + +namespace DiscussionApp.Services; + +public class KafkaWorker(MessageRepository repo, IConfiguration config) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var bootstrap = config["Kafka:BootstrapServers"] ?? "kafka:29092"; + var cConf = new ConsumerConfig { BootstrapServers = bootstrap, GroupId = "dis-group", AutoOffsetReset = AutoOffsetReset.Earliest }; + var pConf = new ProducerConfig { BootstrapServers = bootstrap }; + + using var consumer = new ConsumerBuilder(cConf).Build(); + using var producer = new ProducerBuilder(pConf).Build(); + consumer.Subscribe("InTopic"); + + while (!stoppingToken.IsCancellationRequested) + { + var result = consumer.Consume(stoppingToken); + var @event = JsonSerializer.Deserialize(result.Message.Value); + if (@event == null) continue; + + try { + switch (@event.Action) { + case "GET_ALL": + @event.Payload = JsonSerializer.Serialize(repo.GetAll()); + break; + case "GET": + var gMsg = repo.GetById(long.Parse(@event.Payload)); + @event.Payload = gMsg != null ? JsonSerializer.Serialize(gMsg) : ""; + if (gMsg == null) @event.ErrorMessage = "Not Found"; + break; + case "CREATE": + var cDto = JsonSerializer.Deserialize(@event.Payload)!; + string state = cDto.Content.Contains("spam") ? MessageState.Decline : MessageState.Approve; + var finalMsg = cDto with { State = state }; + repo.Create(finalMsg); + @event.Payload = JsonSerializer.Serialize(finalMsg); + break; + case "UPDATE": + var uDto = JsonSerializer.Deserialize(@event.Payload)!; + repo.Update(uDto); + break; + case "DELETE": + repo.Delete(long.Parse(@event.Payload), @event.ArticleId); + break; + } + } catch (Exception ex) { @event.ErrorMessage = ex.Message; } + + await producer.ProduceAsync("OutTopic", new Message { + Key = @event.ArticleId.ToString(), Value = JsonSerializer.Serialize(@event) + }); + } + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp.sln b/351004/Kuchko/ServerApp/ServerApp.sln index 98f51621e..6d55bf5b8 100644 --- a/351004/Kuchko/ServerApp/ServerApp.sln +++ b/351004/Kuchko/ServerApp/ServerApp.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerApp", "ServerApp\Serv EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscussionApp", "DiscussionApp\DiscussionApp.csproj", "{46462AFC-56FE-480A-84FC-488D4DB0C32E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedModels", "SharedModels\SharedModels.csproj", "{6DAA6601-8D50-45D6-A710-55448D8DCF0E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {46462AFC-56FE-480A-84FC-488D4DB0C32E}.Debug|Any CPU.Build.0 = Debug|Any CPU {46462AFC-56FE-480A-84FC-488D4DB0C32E}.Release|Any CPU.ActiveCfg = Release|Any CPU {46462AFC-56FE-480A-84FC-488D4DB0C32E}.Release|Any CPU.Build.0 = Release|Any CPU + {6DAA6601-8D50-45D6-A710-55448D8DCF0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DAA6601-8D50-45D6-A710-55448D8DCF0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DAA6601-8D50-45D6-A710-55448D8DCF0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DAA6601-8D50-45D6-A710-55448D8DCF0E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs index adc37370f..3027ebb85 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; -using ServerApp.Models.DTOs.Requests; -using ServerApp.Models.DTOs.Responses; using ServerApp.Services.Interfaces; +using SharedModels; namespace ServerApp.Controllers; diff --git a/351004/Kuchko/ServerApp/ServerApp/Dockerfile b/351004/Kuchko/ServerApp/ServerApp/Dockerfile index 6f190932c..4e6b18058 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Dockerfile +++ b/351004/Kuchko/ServerApp/ServerApp/Dockerfile @@ -13,7 +13,8 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -# ИСПРАВЛЕНО: Правильный путь к файлу проекта из корня решения +# ВАЖНО: Сначала копируем проекты +COPY ["SharedModels/SharedModels.csproj", "SharedModels/"] COPY ["ServerApp/ServerApp.csproj", "ServerApp/"] RUN dotnet restore "ServerApp/ServerApp.csproj" diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/MessageRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/MessageRequestTo.cs deleted file mode 100644 index 82c5f21f2..000000000 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/MessageRequestTo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ServerApp.Models.DTOs.Requests; - -public record MessageRequestTo( - [Required] long ArticleId, - [Required] - [StringLength(2048, MinimumLength = 4)] - string Content -); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/MessageResponseTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/MessageResponseTo.cs deleted file mode 100644 index f27c5c994..000000000 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/MessageResponseTo.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ServerApp.Models.DTOs.Responses; - -public record MessageResponseTo( - long Id, - long ArticleId, - string Content -); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index 3118eb284..288dcf04b 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -7,6 +7,7 @@ using ServerApp.Models.DTOs; using ServerApp.Models.Entities; using ServerApp.Repository; +using ServerApp.Services; using ServerApp.Services.Implementations; using ServerApp.Services.Interfaces; @@ -25,6 +26,10 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); // Код из предыдущего ответа +builder.Services.AddScoped(); + builder.Services.AddHttpClient(client => { // Берем URL из docker-compose diff --git a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj index 699011994..11da0c8e5 100644 --- a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj +++ b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj @@ -8,6 +8,7 @@ + @@ -32,4 +33,8 @@ + + + + diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs index 789f399bb..0706b2e72 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs @@ -1,87 +1,86 @@ -using System.Net; -using System.Text; -using System.Text.Json; -using ServerApp.Models.DTOs.Requests; -using ServerApp.Models.DTOs.Responses; -using ServerApp.Models.Entities; -using ServerApp.Repository; +using System.Text.Json; +using Confluent.Kafka; +using SharedModels; using ServerApp.Services.Interfaces; +using ServerApp.Repository; namespace ServerApp.Services.Implementations; public class MessageService( - IRepository
articleRepo, // Оставляем ArticleRepo для валидации - HttpClient httpClient) : IMessageService + KafkaRequestManager rms, + IConfiguration config, + IRepository articleRepo) : IMessageService { - private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true }; - - // 1. Получить все сообщения (GET /api/v1.0/messages) - public IEnumerable GetAll() + private readonly ProducerConfig _pConf = new() { BootstrapServers = config["Kafka:BootstrapServers"] ?? "kafka:29092" }; + private readonly JsonSerializerOptions _options = new() { PropertyNameCaseInsensitive = true }; + private async Task RequestReply(KafkaEvent ev) { - var response = httpClient.GetAsync("/api/v1.0/messages").Result; - EnsureSuccess(response); + var tcs = new TaskCompletionSource(); + rms.Add(ev.CorrelationId, tcs); + + using var p = new ProducerBuilder(_pConf).Build(); + await p.ProduceAsync("InTopic", new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); - var content = response.Content.ReadAsStringAsync().Result; - return JsonSerializer.Deserialize>(content, _jsonOptions)!; + if (await Task.WhenAny(tcs.Task, Task.Delay(1000)) == tcs.Task) return await tcs.Task; + throw new TimeoutException("Discussion service timeout"); } - // 2. Получить сообщение по ID (GET /api/v1.0/messages/{id}) - public MessageResponseTo GetById(long id) + public MessageResponseTo Create(MessageRequestTo request) { - var response = httpClient.GetAsync($"/api/v1.0/messages/{id}").Result; - EnsureSuccess(response, id); - - var content = response.Content.ReadAsStringAsync().Result; - return JsonSerializer.Deserialize(content, _jsonOptions)!; + if (articleRepo.GetById(request.ArticleId) == null) throw new ArgumentException("Article not found"); + + var msg = new MessageResponseTo(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), request.ArticleId, request.Content, MessageState.Pending); + var ev = new KafkaEvent { Action = "CREATE", ArticleId = request.ArticleId, Payload = JsonSerializer.Serialize(msg) }; + + using var p = new ProducerBuilder(_pConf).Build(); + p.Produce("InTopic", new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); + return msg; // Сразу возвращаем PENDING } - // 3. Создать сообщение (POST /api/v1.0/messages) - public MessageResponseTo Create(MessageRequestTo request) + private async Task SendRequestReply(KafkaEvent ev) { - // Бизнес-логика остается в Publisher: проверяем, существует ли статья в Postgres - if (articleRepo.GetById(request.ArticleId) == null) - throw new ArgumentException($"Article {request.ArticleId} not found"); + var tcs = new TaskCompletionSource(); + rms.Add(ev.CorrelationId, tcs); - var response = httpClient.PostAsJsonAsync("/api/v1.0/messages", request).Result; - EnsureSuccess(response); + using var p = new ProducerBuilder(_pConf).Build(); + await p.ProduceAsync("InTopic", new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); - var content = response.Content.ReadAsStringAsync().Result; - return JsonSerializer.Deserialize(content, _jsonOptions)!; + if (await Task.WhenAny(tcs.Task, Task.Delay(1000)) == tcs.Task) return await tcs.Task; + throw new TimeoutException("Discussion service timeout (1s)"); } - // 4. Обновить сообщение (PUT /api/v1.0/messages/{id}) public MessageResponseTo Update(long id, MessageRequestTo request) { - if (articleRepo.GetById(request.ArticleId) == null) - throw new ArgumentException($"Article {request.ArticleId} not found"); - - // Отправляем PUT запрос в DiscussionApp - var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); - var response = httpClient.PutAsync($"/api/v1.0/messages/{id}", jsonContent).Result; - EnsureSuccess(response, id); - - var content = response.Content.ReadAsStringAsync().Result; - return JsonSerializer.Deserialize(content, _jsonOptions)!; + var msg = new MessageResponseTo(id, request.ArticleId, request.Content, MessageState.Pending); + var ev = new KafkaEvent { + Action = "UPDATE", + ArticleId = request.ArticleId, + Payload = JsonSerializer.Serialize(msg), + CorrelationId = Guid.NewGuid().ToString() + }; + + var result = SendRequestReply(ev).Result; + return JsonSerializer.Deserialize(result.Payload, _options)!; } - // 5. Удалить сообщение (DELETE /api/v1.0/messages/{id}) - public void Delete(long id) + public MessageResponseTo GetById(long id) { - var response = httpClient.DeleteAsync($"/api/v1.0/messages/{id}").Result; - EnsureSuccess(response, id); + // Для упрощения в этом задании ищем по ID (с ALLOW FILTERING в Cassandra) + var res = RequestReply(new KafkaEvent { Action = "GET", Payload = id.ToString() }).Result; + return JsonSerializer.Deserialize(res.Payload)!; } - // --- Вспомогательный метод для обработки ошибок от DiscussionApp --- - private void EnsureSuccess(HttpResponseMessage response, long? id = null) + public IEnumerable GetAll() { - if (response.IsSuccessStatusCode) return; - - if (response.StatusCode == HttpStatusCode.NotFound) - throw new KeyNotFoundException($"Message {id?.ToString() ?? "data"} not found in Discussion Service"); - - if (response.StatusCode == HttpStatusCode.BadRequest) - throw new ArgumentException("Invalid data sent to Discussion Service"); + var res = RequestReply(new KafkaEvent { Action = "GET_ALL" }).Result; + return JsonSerializer.Deserialize>(res.Payload)!; + } - throw new Exception($"Discussion Service error: {response.StatusCode}"); + public void Delete(long id) + { + // Нам нужно знать articleId для удаления в Cassandra. + // В реальном API он должен передаваться, тут получим его через GET + var msg = GetById(id); + _ = RequestReply(new KafkaEvent { Action = "DELETE", ArticleId = msg.ArticleId, Payload = id.ToString() }).Result; } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs index 67977361f..70e18b2fe 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs @@ -1,6 +1,4 @@ -using ServerApp.Models.DTOs.Requests; -using ServerApp.Models.DTOs.Responses; - +using SharedModels; namespace ServerApp.Services.Interfaces; public interface IMessageService diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/KafkaRequestManager.cs b/351004/Kuchko/ServerApp/ServerApp/Services/KafkaRequestManager.cs new file mode 100644 index 000000000..6c86deaae --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/KafkaRequestManager.cs @@ -0,0 +1,16 @@ +using System.Collections.Concurrent; +using SharedModels; + +namespace ServerApp.Services; + +public class KafkaRequestManager +{ + private readonly ConcurrentDictionary> _requests = new(); + + public void Add(string id, TaskCompletionSource tcs) => _requests.TryAdd(id, tcs); + + public void Resolve(KafkaEvent ev) + { + if (_requests.TryRemove(ev.CorrelationId, out var tcs)) tcs.TrySetResult(ev); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/KafkaResponseListener.cs b/351004/Kuchko/ServerApp/ServerApp/Services/KafkaResponseListener.cs new file mode 100644 index 000000000..b8f1dc1bb --- /dev/null +++ b/351004/Kuchko/ServerApp/ServerApp/Services/KafkaResponseListener.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using Confluent.Kafka; +using SharedModels; + +namespace ServerApp.Services; + +public class KafkaResponseListener(KafkaRequestManager requestManager, IConfiguration config) : BackgroundService +{ + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + return Task.Run(() => + { + var conf = new ConsumerConfig { + BootstrapServers = config["Kafka:BootstrapServers"] ?? "kafka:29092", + GroupId = "publisher-response-group", + AutoOffsetReset = AutoOffsetReset.Earliest + }; + + using var consumer = new ConsumerBuilder(conf).Build(); + consumer.Subscribe("OutTopic"); + + while (!stoppingToken.IsCancellationRequested) + { + try { + var result = consumer.Consume(stoppingToken); + var @event = JsonSerializer.Deserialize(result.Message.Value); + if (@event != null) requestManager.Resolve(@event); + } catch { /* log error */ } + } + }, stoppingToken); + } +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs index f44e1674f..8974b1b7a 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs @@ -2,6 +2,7 @@ using ServerApp.Models.DTOs.Requests; using ServerApp.Models.DTOs.Responses; using ServerApp.Models.Entities; +using SharedModels; namespace ServerApp.Services.Mapping; diff --git a/351004/Kuchko/ServerApp/SharedModels/KafkaEvent.cs b/351004/Kuchko/ServerApp/SharedModels/KafkaEvent.cs new file mode 100644 index 000000000..596f1b6b0 --- /dev/null +++ b/351004/Kuchko/ServerApp/SharedModels/KafkaEvent.cs @@ -0,0 +1,17 @@ +namespace SharedModels; + +public class KafkaEvent +{ + public string CorrelationId { get; set; } = Guid.NewGuid().ToString(); // Уникальный ID для Request-Reply + public string Action { get; set; } = string.Empty; // CREATE, GET, UPDATE, DELETE + public long ArticleId { get; set; } // Ключ для партиционирования + public string Payload { get; set; } = string.Empty; // Сами данные (JSON) + public string ErrorMessage { get; set; } = string.Empty; +} + +public static class MessageState +{ + public const string Pending = "PENDING"; + public const string Approve = "APPROVE"; + public const string Decline = "DECLINE"; +} \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/SharedModels/MessagesDtos.cs b/351004/Kuchko/ServerApp/SharedModels/MessagesDtos.cs new file mode 100644 index 000000000..af381ad00 --- /dev/null +++ b/351004/Kuchko/ServerApp/SharedModels/MessagesDtos.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels; + +public record MessageRequestTo( + [Required] long ArticleId, + [Required][StringLength(2048, MinimumLength = 2)] string Content +); + +// Добавили поле State, как требует ТЗ +public record MessageResponseTo( + long Id, + long ArticleId, + string Content, + string State +); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/SharedModels/SharedModels.csproj b/351004/Kuchko/ServerApp/SharedModels/SharedModels.csproj new file mode 100644 index 000000000..17b910f6d --- /dev/null +++ b/351004/Kuchko/ServerApp/SharedModels/SharedModels.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/351004/Kuchko/ServerApp/docker-compose.yml b/351004/Kuchko/ServerApp/docker-compose.yml index 48bec9884..36571e4ab 100644 --- a/351004/Kuchko/ServerApp/docker-compose.yml +++ b/351004/Kuchko/ServerApp/docker-compose.yml @@ -33,6 +33,8 @@ services: dockerfile: ServerApp/Dockerfile container_name: publisher_api depends_on: + kafka: + condition: service_healthy db: condition: service_healthy environment: @@ -49,6 +51,8 @@ services: dockerfile: DiscussionApp/Dockerfile container_name: discussion_api depends_on: + kafka: + condition: service_healthy cassandra_db: condition: service_healthy environment: @@ -57,6 +61,45 @@ services: - Cassandra__ContactPoint=cassandra_db ports: - "24130:24130" + + zookeeper: + image: confluentinc/cp-zookeeper:7.4.4 + container_name: zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + healthcheck: + test: [ "CMD", "nc", "-z", "localhost", "2181" ] + interval: 10s + timeout: 5s + retries: 5 + + kafka: + # Зафиксирована версия 7.4.4 + image: confluentinc/cp-kafka:7.4.4 + container_name: kafka + depends_on: + zookeeper: + condition: service_healthy + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + # 29092 для внутреннего Docker, 9092 для тестов с хоста (localhost) + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + # Автосоздание топиков, чтобы не создавать их вручную (ТЗ это разрешает) + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + healthcheck: + test: [ "CMD", "nc", "-z", "localhost", "9092" ] + interval: 10s + timeout: 5s + retries: 5 volumes: db_data: \ No newline at end of file From 2368df97a8d685cb8109b3bb5ac58aab3050e294 Mon Sep 17 00:00:00 2001 From: Maksim Kuchko Date: Tue, 12 May 2026 17:38:56 +0300 Subject: [PATCH 17/17] fix --- .../Controllers/DiscussionController.cs | 7 +- .../DiscussionApp/DiscussionApp.csproj | 12 +-- .../Kuchko/ServerApp/DiscussionApp/Program.cs | 23 ++--- .../Repositories/MessageRepository.cs | 15 ++-- .../DiscussionApp/Services/KafkaWorker.cs | 22 +++-- .../Controllers/ArticleController.cs | 22 ++--- .../ServerApp/Controllers/AuthorController.cs | 17 ++-- .../Controllers/MessageController.cs | 10 ++- .../Controllers/StickerController.cs | 10 ++- .../ServerApp/Infrastructure/AppDbContext.cs | 4 +- .../Infrastructure/GlobalExceptionFilter.cs | 2 +- .../ServerApp/Models/DTOs/ErrorResponse.cs | 2 +- .../Models/DTOs/Requests/ArticleRequestTo.cs | 3 +- .../DTOs/Responses/ArticleResponseTo.cs | 11 ++- .../Models/DTOs/Responses/AuthorResponseTo.cs | 6 +- .../ServerApp/Models/Entities/Article.cs | 28 +++--- .../ServerApp/Models/Entities/Author.cs | 20 ++--- .../ServerApp/Models/Entities/Message.cs | 10 +-- .../ServerApp/Models/Entities/Sticker.cs | 5 +- .../ServerApp/ServerApp/Models/QueryParams.cs | 2 +- 351004/Kuchko/ServerApp/ServerApp/Program.cs | 2 +- .../ServerApp/Repository/EfRepository.cs | 15 +--- .../ServerApp/ServerApp/ServerApp.csproj | 32 +++---- .../Implementations/ArticleService.cs | 51 +++++------ .../Services/Implementations/AuthorService.cs | 19 ++--- .../Implementations/MessageService.cs | 85 +++++++++++-------- .../Implementations/StickerService.cs | 5 +- .../Services/Interfaces/IMessageService.cs | 1 + .../ServerApp/Services/KafkaRequestManager.cs | 5 +- .../Services/KafkaResponseListener.cs | 14 +-- .../Services/Mapping/MappingConfig.cs | 4 +- .../ServerApp/ServerApp/appsettings.json | 2 +- .../ServerApp/SharedModels/KafkaEvent.cs | 2 +- .../ServerApp/SharedModels/MessagesDtos.cs | 10 ++- 34 files changed, 244 insertions(+), 234 deletions(-) diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs b/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs index 2368368b0..f46ed9865 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Controllers/DiscussionController.cs @@ -11,14 +11,17 @@ public class DiscussionController(MessageRepository repo) : ControllerBase [HttpPost] public IActionResult Create([FromBody] MessageRequestTo request) { - long newId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var newId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var msg = new MessageResponseTo(newId, request.ArticleId, request.Content, MessageState.Approve); repo.Create(msg); return Ok(msg); } [HttpGet] - public IActionResult GetAll() => Ok(repo.GetAll()); + public IActionResult GetAll() + { + return Ok(repo.GetAll()); + } [HttpGet("{id:long}")] public IActionResult GetById(long id) diff --git a/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj b/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj index 937cce3e7..518bf075a 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj +++ b/351004/Kuchko/ServerApp/DiscussionApp/DiscussionApp.csproj @@ -8,19 +8,19 @@ - - + + - - .dockerignore - + + .dockerignore + - + diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Program.cs b/351004/Kuchko/ServerApp/DiscussionApp/Program.cs index 52baee4a6..09bf35add 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Program.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Program.cs @@ -10,31 +10,25 @@ builder.Services.AddControllers(options => { // Обязательное требование ТЗ: префикс /api/v1.0/ - + options.Conventions.Add(new ApiPrefixConvention(new RouteAttribute("api/v1.0"))); }); // --- 3. НАСТРОЙКА СВОБОДНОГО CORS (Для локального тестирования) --- builder.Services.AddCors(options => { - options.AddDefaultPolicy(policy => - { - policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); - }); + options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); }); }); builder.Services.AddOpenApi(); var app = builder.Build(); -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); -} +if (app.Environment.IsDevelopment()) app.MapOpenApi(); app.UseCors(); app.UseAuthorization(); -app.MapControllers(); +app.MapControllers(); app.Run(); @@ -45,15 +39,10 @@ public class ApiPrefixConvention(IRouteTemplateProvider route) : IApplicationMod public void Apply(ApplicationModel application) { foreach (var selector in application.Controllers.SelectMany(c => c.Selectors)) - { if (selector.AttributeRouteModel != null) - { - selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel); - } + selector.AttributeRouteModel = + AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel); else - { selector.AttributeRouteModel = _routePrefix; - } - } } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs b/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs index 30314cb65..b993f4400 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Repositories/MessageRepository.cs @@ -18,7 +18,8 @@ public MessageRepository(IConfiguration config) private void InitializeDatabase() { - _session.Execute("CREATE KEYSPACE IF NOT EXISTS distcomp WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};"); + _session.Execute( + "CREATE KEYSPACE IF NOT EXISTS distcomp WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};"); _session.ChangeKeyspace("distcomp"); _session.Execute(@"CREATE TABLE IF NOT EXISTS tbl_message ( article_id bigint, id bigint, content text, state text, country text, @@ -40,7 +41,8 @@ public IEnumerable GetAll() public void Create(MessageResponseTo msg) { - var ps = _session.Prepare("INSERT INTO tbl_message (article_id, id, content, state, country) VALUES (?, ?, ?, ?, ?)"); + var ps = _session.Prepare( + "INSERT INTO tbl_message (article_id, id, content, state, country) VALUES (?, ?, ?, ?, ?)"); _session.Execute(ps.Bind(msg.ArticleId, msg.Id, msg.Content, msg.State, "Unknown")); } @@ -56,7 +58,10 @@ public void Delete(long id, long articleId) _session.Execute(ps.Bind(articleId, id)); } - private MessageResponseTo MapRow(Row r) => new( - r.GetValue("id"), r.GetValue("article_id"), - r.GetValue("content"), r.GetValue("state")); + private MessageResponseTo MapRow(Row r) + { + return new MessageResponseTo( + r.GetValue("id"), r.GetValue("article_id"), + r.GetValue("content"), r.GetValue("state")); + } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/DiscussionApp/Services/KafkaWorker.cs b/351004/Kuchko/ServerApp/DiscussionApp/Services/KafkaWorker.cs index e3e48bbbf..4f80ed403 100644 --- a/351004/Kuchko/ServerApp/DiscussionApp/Services/KafkaWorker.cs +++ b/351004/Kuchko/ServerApp/DiscussionApp/Services/KafkaWorker.cs @@ -10,7 +10,8 @@ public class KafkaWorker(MessageRepository repo, IConfiguration config) : Backgr protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var bootstrap = config["Kafka:BootstrapServers"] ?? "kafka:29092"; - var cConf = new ConsumerConfig { BootstrapServers = bootstrap, GroupId = "dis-group", AutoOffsetReset = AutoOffsetReset.Earliest }; + var cConf = new ConsumerConfig + { BootstrapServers = bootstrap, GroupId = "dis-group", AutoOffsetReset = AutoOffsetReset.Earliest }; var pConf = new ProducerConfig { BootstrapServers = bootstrap }; using var consumer = new ConsumerBuilder(cConf).Build(); @@ -23,8 +24,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var @event = JsonSerializer.Deserialize(result.Message.Value); if (@event == null) continue; - try { - switch (@event.Action) { + try + { + switch (@event.Action) + { case "GET_ALL": @event.Payload = JsonSerializer.Serialize(repo.GetAll()); break; @@ -35,7 +38,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) break; case "CREATE": var cDto = JsonSerializer.Deserialize(@event.Payload)!; - string state = cDto.Content.Contains("spam") ? MessageState.Decline : MessageState.Approve; + var state = cDto.Content.Contains("spam") ? MessageState.Decline : MessageState.Approve; var finalMsg = cDto with { State = state }; repo.Create(finalMsg); @event.Payload = JsonSerializer.Serialize(finalMsg); @@ -48,10 +51,15 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) repo.Delete(long.Parse(@event.Payload), @event.ArticleId); break; } - } catch (Exception ex) { @event.ErrorMessage = ex.Message; } + } + catch (Exception ex) + { + @event.ErrorMessage = ex.Message; + } - await producer.ProduceAsync("OutTopic", new Message { - Key = @event.ArticleId.ToString(), Value = JsonSerializer.Serialize(@event) + await producer.ProduceAsync("OutTopic", new Message + { + Key = @event.ArticleId.ToString(), Value = JsonSerializer.Serialize(@event) }); } } diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs index fe6622f06..77a504178 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/ArticleController.cs @@ -13,11 +13,16 @@ public class ArticleController(IArticleService articleService) : ControllerBase { [HttpGet] [HttpGet("{parameters}")] - public ActionResult> GetPaged([FromQuery] QueryParams parameters) => - Ok(articleService.GetPaged(parameters)); + public ActionResult> GetPaged([FromQuery] QueryParams parameters) + { + return Ok(articleService.GetPaged(parameters)); + } [HttpGet("{id:long}")] - public ActionResult GetById(long id) => Ok(articleService.GetById(id)); + public ActionResult GetById(long id) + { + return Ok(articleService.GetById(id)); + } [HttpPost] public ActionResult Create([FromBody] ArticleRequestTo request) @@ -30,13 +35,10 @@ public ActionResult Create([FromBody] ArticleRequestTo reques [HttpPut] public ActionResult Update(long? id, [FromBody] ArticleRequestTo request) { - long finalId = id ?? (request.Id ?? 0); - - if (finalId == 0) - { - return BadRequest(new ErrorResponse("ID must be provided in URL or body", 40002)); - } - + var finalId = id ?? (request.Id ?? 0); + + if (finalId == 0) return BadRequest(new ErrorResponse("ID must be provided in URL or body", 40002)); + return Ok(articleService.Update(finalId, request)); } diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs index 91b4c7e95..5ff98e260 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/AuthorController.cs @@ -8,13 +8,15 @@ namespace ServerApp.Controllers; [ApiController] -[Route("authors")] +[Route("authors")] public class AuthorController(IAuthorService authorService) : ControllerBase { [HttpGet] [HttpGet("{parameters}")] - public ActionResult> GetPaged([FromQuery] QueryParams parameters) => - Ok(authorService.GetPaged(parameters)); + public ActionResult> GetPaged([FromQuery] QueryParams parameters) + { + return Ok(authorService.GetPaged(parameters)); + } [HttpGet("{id:long}")] public ActionResult GetById(long id) @@ -31,15 +33,12 @@ public ActionResult Create([FromBody] AuthorRequestTo request) } [HttpPut("{id:long}")] // Поддержка /api/v1.0/authors/{id} - [HttpPut] // Поддержка /api/v1.0/authors (ID внутри JSON) + [HttpPut] // Поддержка /api/v1.0/authors (ID внутри JSON) public ActionResult Update(long? id, [FromBody] AuthorRequestTo request) { - long finalId = id ?? (request.Id ?? 0); + var finalId = id ?? (request.Id ?? 0); - if (finalId == 0) - { - return BadRequest(new ErrorResponse("ID must be provided in URL or body", 40002)); - } + if (finalId == 0) return BadRequest(new ErrorResponse("ID must be provided in URL or body", 40002)); // Вызываем сервис с найденным ID return Ok(authorService.Update(finalId, request)); diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs index 3027ebb85..7fcab4181 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/MessageController.cs @@ -9,10 +9,16 @@ namespace ServerApp.Controllers; public class MessageController(IMessageService messageService) : ControllerBase { [HttpGet] - public ActionResult> GetAll() => Ok(messageService.GetAll()); + public ActionResult> GetAll() + { + return Ok(messageService.GetAll()); + } [HttpGet("{id:long}")] - public ActionResult GetById(long id) => Ok(messageService.GetById(id)); + public ActionResult GetById(long id) + { + return Ok(messageService.GetById(id)); + } [HttpPost] public ActionResult Create([FromBody] MessageRequestTo request) diff --git a/351004/Kuchko/ServerApp/ServerApp/Controllers/StickerController.cs b/351004/Kuchko/ServerApp/ServerApp/Controllers/StickerController.cs index 3598146aa..c72bad80e 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Controllers/StickerController.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Controllers/StickerController.cs @@ -10,10 +10,16 @@ namespace ServerApp.Controllers; public class StickerController(IStickerService stickerService) : ControllerBase { [HttpGet] - public ActionResult> GetAll() => Ok(stickerService.GetAll()); + public ActionResult> GetAll() + { + return Ok(stickerService.GetAll()); + } [HttpGet("{id:long}")] - public ActionResult GetById(long id) => Ok(stickerService.GetById(id)); + public ActionResult GetById(long id) + { + return Ok(stickerService.GetById(id)); + } [HttpPost] public ActionResult Create([FromBody] StickerRequestTo request) diff --git a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs index 33d2ee6f3..53aa6c6cb 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/AppDbContext.cs @@ -13,7 +13,7 @@ public class AppDbContext(DbContextOptions options) : DbContext(op protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - + // 1. Уникальный индекс для логина автора modelBuilder.Entity() .HasIndex(a => a.Login) @@ -41,7 +41,5 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) j => j.HasOne().WithMany().HasForeignKey("sticker_id"), j => j.HasOne
().WithMany().HasForeignKey("article_id") ); - - } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs index dc3ffa58c..c37708b66 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Infrastructure/GlobalExceptionFilter.cs @@ -17,7 +17,7 @@ public void OnException(ExceptionContext context) _ => (StatusCodes.Status500InternalServerError, 01) }; - + var finalErrorCode = statusCode * 100 + customSubCode; var response = new ErrorResponse(context.Exception.Message, finalErrorCode); diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/ErrorResponse.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/ErrorResponse.cs index 466adaaf9..5ba7d9a49 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/ErrorResponse.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/ErrorResponse.cs @@ -1,6 +1,6 @@ namespace ServerApp.Models.DTOs; public record ErrorResponse( - string ErrorMessage, + string ErrorMessage, int ErrorCode ); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs index f98bfc904..70d65fd44 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Requests/ArticleRequestTo.cs @@ -11,6 +11,5 @@ public record ArticleRequestTo( [Required] [StringLength(2048, MinimumLength = 4)] string Content, - - ICollection? Stickers + ICollection? Stickers ); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs index 500fc50c3..b52295988 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/ArticleResponseTo.cs @@ -1,12 +1,11 @@ namespace ServerApp.Models.DTOs.Responses; public record ArticleResponseTo( - long Id, - long AuthorId, - string Title, - string Content, - DateTime Created, + long Id, + long AuthorId, + string Title, + string Content, + DateTime Created, DateTime Modified, - ICollection? Stickers ); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/AuthorResponseTo.cs b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/AuthorResponseTo.cs index 12967b87a..500c28018 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/AuthorResponseTo.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/DTOs/Responses/AuthorResponseTo.cs @@ -1,8 +1,8 @@ namespace ServerApp.Models.DTOs.Responses; public record AuthorResponseTo( - long Id, - string Login, - string Firstname, + long Id, + string Login, + string Firstname, string Lastname ); \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs index 6c527c663..43d8de6d4 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Article.cs @@ -5,23 +5,17 @@ namespace ServerApp.Models.Entities; [Table("tbl_article")] public class Article : BaseEntity { - [Column("author_id")] - public long AuthorId { get; set; } - - [ForeignKey("AuthorId")] - public virtual Author Author { get; set; } = null!; - - [Column("title")] - public string Title { get; set; } = null!; - - [Column("content")] - public string Content { get; set; } = null!; - - [Column("created")] - public DateTime Created { get; set; } - - [Column("modified")] - public DateTime Modified { get; set; } + [Column("author_id")] public long AuthorId { get; set; } + + [ForeignKey("AuthorId")] public virtual Author Author { get; set; } = null!; + + [Column("title")] public string Title { get; set; } = null!; + + [Column("content")] public string Content { get; set; } = null!; + + [Column("created")] public DateTime Created { get; set; } + + [Column("modified")] public DateTime Modified { get; set; } public virtual ICollection Messages { get; set; } = new List(); public virtual ICollection Stickers { get; set; } = new List(); diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs index b182d98a7..c0703a18f 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Author.cs @@ -5,18 +5,14 @@ namespace ServerApp.Models.Entities; [Table("tbl_author")] public class Author : BaseEntity { - [Column("login")] - public string Login { get; set; } = null!; - - [Column("password")] - public string Password { get; set; } = null!; - - [Column("firstname")] - public string Firstname { get; set; } = null!; - - [Column("lastname")] - public string Lastname { get; set; } = null!; - + [Column("login")] public string Login { get; set; } = null!; + + [Column("password")] public string Password { get; set; } = null!; + + [Column("firstname")] public string Firstname { get; set; } = null!; + + [Column("lastname")] public string Lastname { get; set; } = null!; + // связь 1 ко многим public virtual ICollection
Articles { get; set; } = new List
(); } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs index 69f71b09b..775ffc96e 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Message.cs @@ -5,12 +5,10 @@ namespace ServerApp.Models.Entities; [Table("tbl_message")] public class Message : BaseEntity { - [Column("article_id")] - public long ArticleId { get; set; } - + [Column("article_id")] public long ArticleId { get; set; } + [ForeignKey("ArticleId")] // Указываем связь по внешнему ключу public virtual Article Article { get; set; } = null!; - - [Column("content")] - public string Content { get; set; } = null!; + + [Column("content")] public string Content { get; set; } = null!; } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs index c0150fb59..3eefec1e0 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/Entities/Sticker.cs @@ -5,8 +5,7 @@ namespace ServerApp.Models.Entities; [Table("tbl_sticker")] public class Sticker : BaseEntity { - [Column("name")] - public string Name { get; set; } = null!; - + [Column("name")] public string Name { get; set; } = null!; + public virtual ICollection
Articles { get; set; } = new List
(); } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Models/QueryParams.cs b/351004/Kuchko/ServerApp/ServerApp/Models/QueryParams.cs index c55bca25d..dc550bee5 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Models/QueryParams.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Models/QueryParams.cs @@ -1,6 +1,6 @@ namespace ServerApp.Models; -public record QueryParams ( +public record QueryParams( int PageNumber = 1, int PageSize = 10, string? SortBy = "Id", diff --git a/351004/Kuchko/ServerApp/ServerApp/Program.cs b/351004/Kuchko/ServerApp/ServerApp/Program.cs index 288dcf04b..7e124cacf 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Program.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Program.cs @@ -20,7 +20,7 @@ builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); -builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); +builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs b/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs index 57d99c1fe..8b5b62816 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Repository/EfRepository.cs @@ -33,18 +33,11 @@ public IEnumerable GetAll() var query = context.Set().AsQueryable(); if (typeof(T) == typeof(Article)) - { query = query.Include("Stickers"); - } - else if (typeof(T) == typeof(Sticker)) - { + else if (typeof(T) == typeof(Sticker)) query = query.Include("Articles"); - } - else if (typeof(T) == typeof(Author)) - { - query = query.Include("Articles").Include("Articles.Stickers"); - } - + else if (typeof(T) == typeof(Author)) query = query.Include("Articles").Include("Articles.Stickers"); + return query.FirstOrDefault(e => e.Id == id); } @@ -66,7 +59,7 @@ public bool Delete(long id) { var entity = GetById(id); if (entity == null) return false; - + context.Set().Remove(entity); context.SaveChanges(); return true; diff --git a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj index 11da0c8e5..b7ba96ce6 100644 --- a/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj +++ b/351004/Kuchko/ServerApp/ServerApp/ServerApp.csproj @@ -8,33 +8,33 @@ - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - - + + + - - .dockerignore - + + .dockerignore + - + diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs index 591cf93c3..f0fd8c2d9 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/ArticleService.cs @@ -13,8 +13,10 @@ public class ArticleService( IRepository
articleRepo, IRepository authorRepo) : IArticleService { - public IEnumerable GetPaged(QueryParams parametrs) => - articleRepo.GetPaged(parametrs).Adapt>(); + public IEnumerable GetPaged(QueryParams parametrs) + { + return articleRepo.GetPaged(parametrs).Adapt>(); + } public ArticleResponseTo GetById(long id) { @@ -27,38 +29,32 @@ public ArticleResponseTo Create(ArticleRequestTo request) // Валидация связи: существует ли автор? if (authorRepo.GetById(request.AuthorId) == null) throw new ArgumentException($"Author with ID {request.AuthorId} not found"); - - bool titleExists = articleRepo.GetAll() + + var titleExists = articleRepo.GetAll() .Any(a => a.Title.Equals(request.Title, StringComparison.OrdinalIgnoreCase)); - - if (titleExists) - { - throw new InvalidOperationException($"Title '{request.Title}' is already taken"); - } + + if (titleExists) throw new InvalidOperationException($"Title '{request.Title}' is already taken"); var article = request.Adapt
(); article.Created = article.Modified = DateTime.UtcNow; - + article.Stickers = new List(); - + if (request.Stickers != null && request.Stickers.Any()) { - var existingStickers = stickerRepo.GetAll().ToList(); + var existingStickers = stickerRepo.GetAll().ToList(); - foreach (string stickerName in request.Stickers) + foreach (var stickerName in request.Stickers) { var found = existingStickers.FirstOrDefault(s => s.Name == stickerName); - + if (found != null) - { article.Stickers.Add(found); - } else - { article.Stickers.Add(new Sticker { Name = stickerName }); - } } } + var created = articleRepo.Create(article); return created.Adapt(); } @@ -66,14 +62,14 @@ public ArticleResponseTo Create(ArticleRequestTo request) public ArticleResponseTo Update(long id, ArticleRequestTo request) { var existing = articleRepo.GetById(id) ?? throw new KeyNotFoundException($"Article {id} not found"); - + if (authorRepo.GetById(request.AuthorId) == null) throw new ArgumentException($"Author {request.AuthorId} not found"); request.Adapt(existing); existing.Id = id; existing.Modified = DateTime.UtcNow; // Обновляем только дату изменения - + if (request.Stickers != null) { var allStickersInDb = stickerRepo.GetAll().ToList(); @@ -81,7 +77,7 @@ public ArticleResponseTo Update(long id, ArticleRequestTo request) foreach (var stickerName in request.Stickers) { var foundSticker = allStickersInDb.FirstOrDefault(s => s.Name == stickerName); - + if (foundSticker != null) { existing.Stickers.Add(foundSticker); @@ -90,12 +86,12 @@ public ArticleResponseTo Update(long id, ArticleRequestTo request) { var newSticker = new Sticker { Name = stickerName }; existing.Stickers.Add(newSticker); - - allStickersInDb.Add(newSticker); + + allStickersInDb.Add(newSticker); } } } - + articleRepo.Update(existing); return existing.Adapt(); } @@ -105,14 +101,11 @@ public void Delete(long id) var article = articleRepo.GetById(id) ?? throw new KeyNotFoundException($"Article {id} not found"); var stickersToCheck = article.Stickers.ToList(); articleRepo.Delete(id); - + foreach (var sticker in stickersToCheck) { var currentSticker = stickerRepo.GetById(sticker.Id); - if (currentSticker != null && currentSticker.Articles.Count == 0) - { - stickerRepo.Delete(currentSticker.Id); - } + if (currentSticker != null && currentSticker.Articles.Count == 0) stickerRepo.Delete(currentSticker.Id); } } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs index 6cf0afd95..0fa6318e1 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/AuthorService.cs @@ -10,8 +10,10 @@ namespace ServerApp.Services.Implementations; public class AuthorService(IRepository repository) : IAuthorService { - public IEnumerable GetPaged(QueryParams parameters) => - repository.GetPaged(parameters).Adapt>(); + public IEnumerable GetPaged(QueryParams parameters) + { + return repository.GetPaged(parameters).Adapt>(); + } public AuthorResponseTo GetById(long id) { @@ -21,14 +23,11 @@ public AuthorResponseTo GetById(long id) public AuthorResponseTo Create(AuthorRequestTo request) { - bool loginExists = repository.GetAll() + var loginExists = repository.GetAll() .Any(a => a.Login.Equals(request.Login, StringComparison.OrdinalIgnoreCase)); - - if (loginExists) - { - throw new InvalidOperationException($"Login '{request.Login}' is already taken"); - } - + + if (loginExists) throw new InvalidOperationException($"Login '{request.Login}' is already taken"); + var author = request.Adapt(); var created = repository.Create(author); return created.Adapt(); @@ -37,7 +36,7 @@ public AuthorResponseTo Create(AuthorRequestTo request) public AuthorResponseTo Update(long id, AuthorRequestTo request) { var existing = repository.GetById(id) ?? throw new KeyNotFoundException($"Author {id} not found"); - request.Adapt(existing); + request.Adapt(existing); existing.Id = id; repository.Update(existing); return existing.Adapt(); diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs index 0706b2e72..346234324 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/MessageService.cs @@ -1,62 +1,46 @@ using System.Text.Json; using Confluent.Kafka; -using SharedModels; -using ServerApp.Services.Interfaces; +using ServerApp.Models.Entities; using ServerApp.Repository; +using ServerApp.Services.Interfaces; +using SharedModels; namespace ServerApp.Services.Implementations; public class MessageService( - KafkaRequestManager rms, - IConfiguration config, - IRepository articleRepo) : IMessageService + KafkaRequestManager rms, + IConfiguration config, + IRepository
articleRepo) : IMessageService { - private readonly ProducerConfig _pConf = new() { BootstrapServers = config["Kafka:BootstrapServers"] ?? "kafka:29092" }; private readonly JsonSerializerOptions _options = new() { PropertyNameCaseInsensitive = true }; - private async Task RequestReply(KafkaEvent ev) - { - var tcs = new TaskCompletionSource(); - rms.Add(ev.CorrelationId, tcs); - - using var p = new ProducerBuilder(_pConf).Build(); - await p.ProduceAsync("InTopic", new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); - if (await Task.WhenAny(tcs.Task, Task.Delay(1000)) == tcs.Task) return await tcs.Task; - throw new TimeoutException("Discussion service timeout"); - } + private readonly ProducerConfig _pConf = new() + { BootstrapServers = config["Kafka:BootstrapServers"] ?? "kafka:29092" }; public MessageResponseTo Create(MessageRequestTo request) { if (articleRepo.GetById(request.ArticleId) == null) throw new ArgumentException("Article not found"); - - var msg = new MessageResponseTo(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), request.ArticleId, request.Content, MessageState.Pending); - var ev = new KafkaEvent { Action = "CREATE", ArticleId = request.ArticleId, Payload = JsonSerializer.Serialize(msg) }; - - using var p = new ProducerBuilder(_pConf).Build(); - p.Produce("InTopic", new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); - return msg; // Сразу возвращаем PENDING - } - private async Task SendRequestReply(KafkaEvent ev) - { - var tcs = new TaskCompletionSource(); - rms.Add(ev.CorrelationId, tcs); + var msg = new MessageResponseTo(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), request.ArticleId, + request.Content, MessageState.Pending); + var ev = new KafkaEvent + { Action = "CREATE", ArticleId = request.ArticleId, Payload = JsonSerializer.Serialize(msg) }; using var p = new ProducerBuilder(_pConf).Build(); - await p.ProduceAsync("InTopic", new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); - - if (await Task.WhenAny(tcs.Task, Task.Delay(1000)) == tcs.Task) return await tcs.Task; - throw new TimeoutException("Discussion service timeout (1s)"); + p.Produce("InTopic", + new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); + return msg; // Сразу возвращаем PENDING } public MessageResponseTo Update(long id, MessageRequestTo request) { var msg = new MessageResponseTo(id, request.ArticleId, request.Content, MessageState.Pending); - var ev = new KafkaEvent { - Action = "UPDATE", - ArticleId = request.ArticleId, + var ev = new KafkaEvent + { + Action = "UPDATE", + ArticleId = request.ArticleId, Payload = JsonSerializer.Serialize(msg), - CorrelationId = Guid.NewGuid().ToString() + CorrelationId = Guid.NewGuid().ToString() }; var result = SendRequestReply(ev).Result; @@ -81,6 +65,33 @@ public void Delete(long id) // Нам нужно знать articleId для удаления в Cassandra. // В реальном API он должен передаваться, тут получим его через GET var msg = GetById(id); - _ = RequestReply(new KafkaEvent { Action = "DELETE", ArticleId = msg.ArticleId, Payload = id.ToString() }).Result; + _ = RequestReply(new KafkaEvent { Action = "DELETE", ArticleId = msg.ArticleId, Payload = id.ToString() }) + .Result; + } + + private async Task RequestReply(KafkaEvent ev) + { + var tcs = new TaskCompletionSource(); + rms.Add(ev.CorrelationId, tcs); + + using var p = new ProducerBuilder(_pConf).Build(); + await p.ProduceAsync("InTopic", + new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); + + if (await Task.WhenAny(tcs.Task, Task.Delay(1000)) == tcs.Task) return await tcs.Task; + throw new TimeoutException("Discussion service timeout"); + } + + private async Task SendRequestReply(KafkaEvent ev) + { + var tcs = new TaskCompletionSource(); + rms.Add(ev.CorrelationId, tcs); + + using var p = new ProducerBuilder(_pConf).Build(); + await p.ProduceAsync("InTopic", + new Message { Key = ev.ArticleId.ToString(), Value = JsonSerializer.Serialize(ev) }); + + if (await Task.WhenAny(tcs.Task, Task.Delay(1000)) == tcs.Task) return await tcs.Task; + throw new TimeoutException("Discussion service timeout (1s)"); } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/StickerService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/StickerService.cs index 2d40db95e..43e18f271 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/StickerService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Implementations/StickerService.cs @@ -9,7 +9,10 @@ namespace ServerApp.Services.Implementations; public class StickerService(IRepository repository) : IStickerService { - public IEnumerable GetAll() => repository.GetAll().Adapt>(); + public IEnumerable GetAll() + { + return repository.GetAll().Adapt>(); + } public StickerResponseTo GetById(long id) { diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs index 70e18b2fe..fa7f1f800 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Interfaces/IMessageService.cs @@ -1,4 +1,5 @@ using SharedModels; + namespace ServerApp.Services.Interfaces; public interface IMessageService diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/KafkaRequestManager.cs b/351004/Kuchko/ServerApp/ServerApp/Services/KafkaRequestManager.cs index 6c86deaae..ce0f680f2 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/KafkaRequestManager.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/KafkaRequestManager.cs @@ -7,7 +7,10 @@ public class KafkaRequestManager { private readonly ConcurrentDictionary> _requests = new(); - public void Add(string id, TaskCompletionSource tcs) => _requests.TryAdd(id, tcs); + public void Add(string id, TaskCompletionSource tcs) + { + _requests.TryAdd(id, tcs); + } public void Resolve(KafkaEvent ev) { diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/KafkaResponseListener.cs b/351004/Kuchko/ServerApp/ServerApp/Services/KafkaResponseListener.cs index b8f1dc1bb..603c470ac 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/KafkaResponseListener.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/KafkaResponseListener.cs @@ -10,7 +10,8 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) { return Task.Run(() => { - var conf = new ConsumerConfig { + var conf = new ConsumerConfig + { BootstrapServers = config["Kafka:BootstrapServers"] ?? "kafka:29092", GroupId = "publisher-response-group", AutoOffsetReset = AutoOffsetReset.Earliest @@ -20,13 +21,16 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) consumer.Subscribe("OutTopic"); while (!stoppingToken.IsCancellationRequested) - { - try { + try + { var result = consumer.Consume(stoppingToken); var @event = JsonSerializer.Deserialize(result.Message.Value); if (@event != null) requestManager.Resolve(@event); - } catch { /* log error */ } - } + } + catch + { + /* log error */ + } }, stoppingToken); } } \ No newline at end of file diff --git a/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs b/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs index 8974b1b7a..248d715d9 100644 --- a/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs +++ b/351004/Kuchko/ServerApp/ServerApp/Services/Mapping/MappingConfig.cs @@ -21,11 +21,11 @@ public void Register(TypeAdapterConfig config) { // При создании статьи устанавливаем даты var now = DateTime.UtcNow; - dest.Stickers = new List(); + dest.Stickers = new List(); dest.Created = now; dest.Modified = now; }); - + config.NewConfig(); // 3. Маппинг для Message diff --git a/351004/Kuchko/ServerApp/ServerApp/appsettings.json b/351004/Kuchko/ServerApp/ServerApp/appsettings.json index 4c9f0efb3..dd4408856 100644 --- a/351004/Kuchko/ServerApp/ServerApp/appsettings.json +++ b/351004/Kuchko/ServerApp/ServerApp/appsettings.json @@ -1,5 +1,5 @@ { - "ConnectionString":{ + "ConnectionString": { "DefaultConnection": "Host=localhost;Port=5432;Database=distcomp;Username=postgres;Password=postgres" }, "Logging": { diff --git a/351004/Kuchko/ServerApp/SharedModels/KafkaEvent.cs b/351004/Kuchko/ServerApp/SharedModels/KafkaEvent.cs index 596f1b6b0..e4c872778 100644 --- a/351004/Kuchko/ServerApp/SharedModels/KafkaEvent.cs +++ b/351004/Kuchko/ServerApp/SharedModels/KafkaEvent.cs @@ -6,7 +6,7 @@ public class KafkaEvent public string Action { get; set; } = string.Empty; // CREATE, GET, UPDATE, DELETE public long ArticleId { get; set; } // Ключ для партиционирования public string Payload { get; set; } = string.Empty; // Сами данные (JSON) - public string ErrorMessage { get; set; } = string.Empty; + public string ErrorMessage { get; set; } = string.Empty; } public static class MessageState diff --git a/351004/Kuchko/ServerApp/SharedModels/MessagesDtos.cs b/351004/Kuchko/ServerApp/SharedModels/MessagesDtos.cs index af381ad00..4d3f306f8 100644 --- a/351004/Kuchko/ServerApp/SharedModels/MessagesDtos.cs +++ b/351004/Kuchko/ServerApp/SharedModels/MessagesDtos.cs @@ -4,13 +4,15 @@ namespace SharedModels; public record MessageRequestTo( [Required] long ArticleId, - [Required][StringLength(2048, MinimumLength = 2)] string Content + [Required] + [StringLength(2048, MinimumLength = 2)] + string Content ); // Добавили поле State, как требует ТЗ public record MessageResponseTo( - long Id, - long ArticleId, - string Content, + long Id, + long ArticleId, + string Content, string State ); \ No newline at end of file