commit b64f71f7ec1067c227cec74a5d540efb18b361a4 Author: Павел Горбенко Date: Wed Jul 22 14:25:58 2026 +0500 Создан бэкенд-боилерплейт по образцу tnsu-inventory 4-слойная архитектура (Api/Application/Domain/Infrastructure) на .NET 10 с MediatR и EF Core + PostgreSQL, без бизнес-логики. Добавлены docker-compose, .env.example и README для локального запуска. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4bedbe1 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +POSTGRES_DB=tnsu_approval_route +POSTGRES_USER=tnsu_approval_route +POSTGRES_PASSWORD=change-me +POSTGRES_PORT=5435 +API_PORT=8082 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..648dba9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.env +.env.local +.env.example +*.user + +**/bin/ +**/obj/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d10abd --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Tnsu.ApprovalRoute + +Каркас проекта на .NET 10, повторяющий архитектуру [tnsu-inventory](../tnsu-inventory): 4 слоя (Api / Application / Domain / Infrastructure), MediatR, EF Core + PostgreSQL. Бизнес-логика не реализована — только структура решения и минимальная точка входа. + +## Структура + +``` +backend/ + Directory.Build.props # общие настройки таргета (net10.0, nullable, etc.) + Directory.Packages.props # централизованные версии пакетов + Tnsu.ApprovalRoute.sln + src/ + Tnsu.ApprovalRoute.Domain/ # сущности домена (пусто) + Tnsu.ApprovalRoute.Application/ # MediatR-хендлеры, DI-регистрация слоя + Tnsu.ApprovalRoute.Infrastructure/ # EF Core DbContext, DI-регистрация слоя + Tnsu.ApprovalRoute.Api/ # ASP.NET Core Minimal API, Program.cs +``` + +## Запуск локально + +```bash +cd backend +dotnet run --project src/Tnsu.ApprovalRoute.Api +``` + +Требуется PostgreSQL (строка подключения — `ConnectionStrings:Postgres` в `appsettings.json`). + +## Запуск через Docker Compose + +```bash +cp .env.example .env +docker compose up --build +``` + +Поднимает `postgres` и `api` (health-check на `/health`). diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..6a89ba8 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,4 @@ +**/bin +**/obj +**/.vs +**/.idea diff --git a/backend/Directory.Build.props b/backend/Directory.Build.props new file mode 100644 index 0000000..2748bd9 --- /dev/null +++ b/backend/Directory.Build.props @@ -0,0 +1,9 @@ + + + net10.0 + latest + enable + enable + $(MSBuildProjectName.Replace(" ", "_")) + + diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props new file mode 100644 index 0000000..775e9b8 --- /dev/null +++ b/backend/Directory.Packages.props @@ -0,0 +1,13 @@ + + + true + + + + + + + + + + diff --git a/backend/Tnsu.ApprovalRoute.sln b/backend/Tnsu.ApprovalRoute.sln new file mode 100644 index 0000000..e505b17 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.sln @@ -0,0 +1,84 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Domain", "src\Tnsu.ApprovalRoute.Domain\Tnsu.ApprovalRoute.Domain.csproj", "{67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Application", "src\Tnsu.ApprovalRoute.Application\Tnsu.ApprovalRoute.Application.csproj", "{2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Infrastructure", "src\Tnsu.ApprovalRoute.Infrastructure\Tnsu.ApprovalRoute.Infrastructure.csproj", "{46EF4247-8B98-4CC9-A981-186C8D24163C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Api", "src\Tnsu.ApprovalRoute.Api\Tnsu.ApprovalRoute.Api.csproj", "{4697B342-BE47-435B-B854-75F9277AD769}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Debug|x64.ActiveCfg = Debug|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Debug|x64.Build.0 = Debug|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Debug|x86.ActiveCfg = Debug|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Debug|x86.Build.0 = Debug|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Release|Any CPU.Build.0 = Release|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Release|x64.ActiveCfg = Release|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Release|x64.Build.0 = Release|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Release|x86.ActiveCfg = Release|Any CPU + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}.Release|x86.Build.0 = Release|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Debug|x64.ActiveCfg = Debug|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Debug|x64.Build.0 = Debug|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Debug|x86.Build.0 = Debug|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Release|Any CPU.Build.0 = Release|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Release|x64.ActiveCfg = Release|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Release|x64.Build.0 = Release|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Release|x86.ActiveCfg = Release|Any CPU + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}.Release|x86.Build.0 = Release|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Debug|x64.ActiveCfg = Debug|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Debug|x64.Build.0 = Debug|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Debug|x86.ActiveCfg = Debug|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Debug|x86.Build.0 = Debug|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Release|Any CPU.Build.0 = Release|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Release|x64.ActiveCfg = Release|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Release|x64.Build.0 = Release|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Release|x86.ActiveCfg = Release|Any CPU + {46EF4247-8B98-4CC9-A981-186C8D24163C}.Release|x86.Build.0 = Release|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Debug|x64.ActiveCfg = Debug|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Debug|x64.Build.0 = Debug|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Debug|x86.ActiveCfg = Debug|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Debug|x86.Build.0 = Debug|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Release|Any CPU.Build.0 = Release|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Release|x64.ActiveCfg = Release|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Release|x64.Build.0 = Release|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Release|x86.ActiveCfg = Release|Any CPU + {4697B342-BE47-435B-B854-75F9277AD769}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {46EF4247-8B98-4CC9-A981-186C8D24163C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {4697B342-BE47-435B-B854-75F9277AD769} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection +EndGlobal diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Dockerfile b/backend/src/Tnsu.ApprovalRoute.Api/Dockerfile new file mode 100644 index 0000000..a9f4110 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Api/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props Directory.Packages.props ./ +COPY src/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj src/Tnsu.ApprovalRoute.Domain/ +COPY src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj src/Tnsu.ApprovalRoute.Application/ +COPY src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj src/Tnsu.ApprovalRoute.Infrastructure/ +COPY src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj src/Tnsu.ApprovalRoute.Api/ +RUN dotnet restore src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj +COPY src/ src/ +RUN dotnet publish src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj -c Release -o /app/publish /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* +COPY --from=build /app/publish . +ENV ASPNETCORE_URLS=http://+:8080 +EXPOSE 8080 +ENTRYPOINT ["dotnet", "Tnsu.ApprovalRoute.Api.dll"] diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Program.cs b/backend/src/Tnsu.ApprovalRoute.Api/Program.cs new file mode 100644 index 0000000..6a18908 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Api/Program.cs @@ -0,0 +1,20 @@ +using Tnsu.ApprovalRoute.Application; +using Tnsu.ApprovalRoute.Infrastructure; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddApprovalRouteInfrastructure(builder.Configuration); +builder.Services.AddApprovalRouteApplication(); +builder.Services.AddOpenApi(); +builder.Services.AddHealthChecks(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) + app.MapOpenApi(); + +app.MapHealthChecks("/health"); + +app.Run(); + +public partial class Program; diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj b/backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj new file mode 100644 index 0000000..24f7ac4 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json b/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json new file mode 100644 index 0000000..7cd39c3 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "Postgres": "Host=localhost;Port=5435;Database=tnsu_approval_route;Username=tnsu_approval_route;Password=tnsu_approval_route" + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs b/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs new file mode 100644 index 0000000..51291b7 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Tnsu.ApprovalRoute.Application; + +public static class ApplicationServiceCollectionExtensions +{ + public static IServiceCollection AddApprovalRouteApplication(this IServiceCollection services) + { + services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); + return services; + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj b/backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj new file mode 100644 index 0000000..60bfbd4 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj b/backend/src/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj @@ -0,0 +1,2 @@ + + diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs new file mode 100644 index 0000000..b470fd9 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; + +namespace Tnsu.ApprovalRoute.Infrastructure; + +public static class InfrastructureServiceCollectionExtensions +{ + public static IServiceCollection AddApprovalRouteInfrastructure( + this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(opt => + opt.UseNpgsql(configuration.GetConnectionString("Postgres"))); + + return services; + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs new file mode 100644 index 0000000..6fa2086 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs @@ -0,0 +1,7 @@ +using Microsoft.EntityFrameworkCore; + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence; + +public class ApprovalRouteDbContext(DbContextOptions options) : DbContext(options) +{ +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj new file mode 100644 index 0000000..f268684 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a664dbb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +name: tnsu-approval-route + +services: + postgres: + image: postgres:16-alpine + container_name: tnsu-approval-route-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-tnsu_approval_route} + POSTGRES_USER: ${POSTGRES_USER:-tnsu_approval_route} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tnsu_approval_route} + ports: + - "${POSTGRES_PORT:-5435}:5432" + volumes: + - approval-route-postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-tnsu_approval_route} -d ${POSTGRES_DB:-tnsu_approval_route}"] + interval: 5s + timeout: 3s + retries: 10 + + api: + build: + context: ./backend + dockerfile: src/Tnsu.ApprovalRoute.Api/Dockerfile + container_name: tnsu-approval-route-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Development} + ASPNETCORE_URLS: http://+:8080 + ConnectionStrings__Postgres: "Host=postgres;Port=5432;Database=${POSTGRES_DB:-tnsu_approval_route};Username=${POSTGRES_USER:-tnsu_approval_route};Password=${POSTGRES_PASSWORD:-tnsu_approval_route}" + ports: + - "${API_PORT:-8082}:8080" + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + +volumes: + approval-route-postgres-data: