Привет. Мне дали задание для моего проекта - создать два контроллера (апи) целиком для двух сущностей. Без использования Entity Framework, а писать напрямую запросы. Посоветуй с чего можно начать? Вот код моего проекта: "/Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/Dockerfile: [﻿# Stage: Base (Runtime Image) FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base USER $APP_UID WORKDIR /app EXPOSE 8080 EXPOSE 8081 # Stage: Build (SDK Image) FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src # Копируем только проектный файл, чтобы сначала восстановить зависимости COPY EaseMoney.csproj ./ RUN dotnet restore "EaseMoney.csproj" # Копируем весь оставшийся код COPY . . # Выполняем сборку проекта WORKDIR "/src" RUN dotnet build "EaseMoney.csproj" -c $BUILD_CONFIGURATION -o /app/build # Stage: Publish FROM build AS publish ARG BUILD_CONFIGURATION=Release # Применяем миграции # Выполняем публикацию проекта RUN dotnet publish "EaseMoney.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false # Stage: Final (Runtime Image) FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "EaseMoney.dll"]] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/Makefile: [test: ls ] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/appsettings.json: [{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=db;Port=5432;Database=easemoney_db;Username=postgres;Password=yourpassword" }, "JwtSettings": { "SecretKey": "ВашСлучайныйДлинныйСекретныйКлючИзНеобходимоКоличествоСимволов", "Issuer": "EaseMoneyAPI", "Audience": "EaseMoneyClient", "ExpiryMinutes": 60 } } ] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/EaseMoney.http: [@EaseMoney_HostAddress = http://localhost:5185 GET {{EaseMoney_HostAddress}}/weatherforecast/ Accept: application/json ### ] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/docker-compose.yml: [﻿version: '3.3' services: easemoney-app: build: context: . dockerfile: Dockerfile ports: - "8090:8080" - "8091:8091" depends_on: - db environment: - ASPNETCORE_ENVIRONMENT=Development - ConnectionStrings__DefaultConnection=Server=db;Port=5432;Database=easemoney_db;User Id=postgres;Password=yourpassword - ASPNETCORE_URLS=http://+:8080 networks: - easemoney-net db: image: postgres:14 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: yourpassword POSTGRES_DB: easemoney_db ports: - "5432:5432" volumes: - postgres-data:/var/lib/postgresql/data networks: - easemoney-net migrate: image: mcr.microsoft.com/dotnet/sdk:8.0 depends_on: - db working_dir: /app volumes: - ./:/app # command: > # bash -c " # echo 'Current directory:' && # pwd && # echo 'Contents of /app:' && # ls -la /app && # echo 'Contents of /app/EaseMoney:' && # ls -la /app/EaseMoney && # dotnet tool install --global dotnet-ef && # export PATH=\$PATH:/root/.dotnet/tools && # dotnet ef database update # " command: > bash -c " dotnet tool install --global dotnet-ef && export PATH=\$PATH:/root/.dotnet/tools && dotnet ef database update " environment: - ASPNETCORE_ENVIRONMENT=Development - ConnectionStrings__DefaultConnection=Server=db;Port=5432;Database=easemoney_db;User Id=postgres;Password=yourpassword networks: - easemoney-net volumes: postgres-data: networks: easemoney-net:] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/appsettings.Development.json: [{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "DefaultConnection": "Server=db;Port=5432;Database=easemoney_db;Username=postgres;Password=yourpassword" } } ] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/EaseMoney.csproj: [ net8.0 enable enable true Linux runtime; build; native; contentfiles; analyzers; buildtransitive all .dockerignore ] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/Program.cs: [using System.Text; using EaseMoney.Application.Services; using EaseMoney.Core.Ports; using EaseMoney.Infrastructure.Persistence; using EaseMoney.Infrastructure.Persistence.Repositories; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); // Добавление настроек JWT из конфигурации var jwtSettings = builder.Configuration.GetSection("JwtSettings"); var secretKey = jwtSettings.GetValue("SecretKey"); var issuer = jwtSettings.GetValue("Issuer"); var audience = jwtSettings.GetValue("Audience"); // Подключение базы данных builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); // Регистрация портов и адаптеров builder.Services.AddScoped(); builder.Services.AddScoped(); // Регистрация сервисов builder.Services.AddScoped(); // Добавление CORS builder.Services.AddCors(options => { options.AddPolicy("AllowSpecificOrigin", builder => { builder.WithOrigins("http://localhost:4200") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // Если используете куки или авторизацию }); }); // Добавление аутентификации JWT builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = issuer, ValidAudience = audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), ClockSkew = TimeSpan.Zero // TODO: Можно настроить для продакшена }; }); builder.Services.AddAuthorization(); // Подключение контроллеров и Swagger builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Поддержка Swagger if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); c.RoutePrefix = "swagger"; }); } // Важно: вызов CORS перед другими middleware app.UseCors("AllowSpecificOrigin"); // Вызов аутентификации и авторизации app.UseAuthentication(); app.UseAuthorization(); app.UseHttpsRedirection(); app.MapControllers(); // Дополнительные заголовки безопасности app.Use(async (context, next) => { context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload"); await next(); }); app.Run();] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/EaseMoney/a.txt: [] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/Properties/launchSettings.json: [﻿{ "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:25779", "sslPort": 44339 } }, "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", "applicationUrl": "http://localhost:5185", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", "applicationUrl": "https://localhost:7010;http://localhost:5185", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Ports/IUserService.cs: [using EaseMoney.Application.DTOs; namespace EaseMoney.Core.Ports; public interface IUserService { Task RegisterUserAsync(RegisterUserDto model); Task<(bool IsVerified, bool IsError)> VerifyUserAsync(VerifyUserDto model); }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Ports/IUserRepository.cs: [using EaseMoney.Core.Entities; namespace EaseMoney.Core.Ports; public interface IUserRepository { Task GetUserByUsernameAsync(string username); Task AddUserAsync(User user); }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/Transaction.cs: [namespace EaseMoney.Core.Entities; public class Transaction { public int Id { get; set; } public Guid UserId { get; set; } public decimal Amount { get; set; } public DateTime TransactionDate { get; set; } = DateTime.Now; public string Description { get; set; } public bool IsRecurring { get; set; } = false; public DateTime CreatedAt { get; set; } = DateTime.Now; public User User { get; set; } public IncomeTransaction IncomeTransaction { get; set; } public ExpenseTransaction ExpenseTransaction { get; set; } public Reminder Reminder { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/Budget.cs: [namespace EaseMoney.Core.Entities; public class Budget { public int Id { get; set; } public Guid UserId { get; set; } public string Name { get; set; } public decimal TotalAmount { get; set; } public decimal GoalAmount { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public DateTime CreatedAt { get; set; } public User User { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/ExpenseCategory.cs: [namespace EaseMoney.Core.Entities; public class ExpenseCategory { public int Id { get; set; } public Guid UserId { get; set; } public string Name { get; set; } public DateTime CreatedAt { get; set; } = DateTime.Now; public User User { get; set; } public ICollection ExpenseTransaction { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/User.cs: [using System.ComponentModel.DataAnnotations; namespace EaseMoney.Core.Entities; public class User { public int Id { get; set; } public string Username { get; set; } public string PasswordHash { get; set; } public ICollection IncomeCategories { get; set; } public ICollection ExpenseCategories { get; set; } public ICollection Transactions { get; set; } public ICollection Budgets { get; set; } public ICollection Loans { get; set; } public ICollection Reminders { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/IncomeTransaction.cs: [namespace EaseMoney.Core.Entities; public class IncomeTransaction { public int Id { get; set; } public int TransactionId { get; set; } public int CategoryId { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public Transaction Transaction { get; set; } public IncomeCategory IncomeCategory { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/IncomeCategory.cs: [namespace EaseMoney.Core.Entities; public class IncomeCategory { public int Id { get; set; } public Guid UserId { get; set; } public string Name { get; set; } public DateTime CreatedAt { get; set; } = DateTime.Now; public User User { get; set; } public ICollection IncomeTransaction { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/Payment.cs: [namespace EaseMoney.Core.Entities; public class Payment { public int Id { get; set; } public int LoanId { get; set; } public int PaymentNumber { get; set; } public DateTime PaymentDate { get; set; } public decimal Amount { get; set; } public string Status { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public Loan Loan { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/ExpenseTransaction.cs: [namespace EaseMoney.Core.Entities; public class ExpenseTransaction { public int Id { get; set; } public int TransactionId { get; set; } public int CategoryId { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public Transaction Transaction { get; set; } public ExpenseCategory ExpenseCategory { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/Reminder.cs: [namespace EaseMoney.Core.Entities; public class Reminder { public int Id { get; set; } public Guid UserId { get; set; } public int TransactionId { get; set; } public DateTime ReminderDate { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public User User { get; set; } public Transaction Transaction { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Core/Entities/Loan.cs: [namespace EaseMoney.Core.Entities; public class Loan { public int Id { get; set; } public Guid UserdId { get; set; } public string BankName { get; set; } public decimal LoanAmount { get; set; } public decimal TotalAmount { get; set; } public DateTime CreatedAt { get; set; } = DateTime.Now; public User User { get; set; } public ICollection Payments { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/API/Controllers/AuthController.cs: [using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using EaseMoney.Application.DTOs; using EaseMoney.Application.Services; using EaseMoney.Core.Ports; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace EaseMoney.API.Controllers; [Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { private readonly IUserService _userService; private readonly IConfiguration _configuration; public AuthController(IUserService userService, IConfiguration configuration) { _userService = userService; _configuration = configuration; } [HttpPost("register")] public async Task Register([FromBody] RegisterUserDto model) { try { var result = await _userService.RegisterUserAsync(model); if (result) { return Ok(new { Message = "User registered successfully" }); } else { return BadRequest(new { Message = "User registration failed. User may already exist or there was an error." }); } } catch (Exception e) { return StatusCode(500, "An unexpected error occurred. Please try again."); } } [HttpPost("verify")] public async Task Verify([FromBody] VerifyUserDto model) { try { var (isValid, isError) = await _userService.VerifyUserAsync(model); // If there is no error and user is valid if (!isError && isValid) { var token = GenerateJwtToken(model.Username); return Ok(new { Message = "Verification successful", Token = token }); } else if (isError) { // Return generic error message return StatusCode(500, "An unexpected error occurred. Please try qweqwe."); } else { return Unauthorized("Invalid credentials."); } } catch (Exception e) { return StatusCode(500, $"An unexpected error occurred. Please try again. {e}"); } } private string GenerateJwtToken(string username) { var jwtSettings = _configuration.GetSection("JwtSettings"); var secretKey = jwtSettings.GetValue("SecretKey"); var issuer = jwtSettings.GetValue("Issuer"); var audience = jwtSettings.GetValue("Audience"); var expiryMinutes = jwtSettings.GetValue("ExpiryMinutes"); var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); // Добавляем утверждения (claims) var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, username), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // Можно добавить дополнительные claims, например, роли пользователя }; var token = new JwtSecurityToken( issuer: issuer, audience: audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(expiryMinutes), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Application/DTOs/RegisterUserDto.cs: [using System.ComponentModel.DataAnnotations; namespace EaseMoney.Application.DTOs; public class RegisterUserDto { [Required] [MinLength(4, ErrorMessage = "Minimum length is 4")] [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "Only english letters are allowed.")] public required string Username { get; set; } [Required] [MinLength(8, ErrorMessage = "Minimum length is 8")] [RegularExpression(@"^[a-zA-Z0-9!@#$%^&*()]+$", ErrorMessage = "Only English letters, numbers, and special characters are allowed.")] public required string Password { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Application/DTOs/VerifyUserDto.cs: [using System.ComponentModel.DataAnnotations; namespace EaseMoney.Application.DTOs; public class VerifyUserDto { [Required] [MinLength(4, ErrorMessage = "Minimum length is 4")] [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "Only english letters are allowed.")] public required string Username { get; set; } [Required] [MinLength(8, ErrorMessage = "Minimum length is 8")] [RegularExpression(@"^[a-zA-Z0-9!@#$%^&*()]+$", ErrorMessage = "Only English letters, numbers, and special characters are allowed.")] public required string Password { get; set; } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Application/Services/UserService.cs: [using System.Security.Cryptography; using System.Text; using EaseMoney.Application.DTOs; using EaseMoney.Core.Entities; using EaseMoney.Core.Ports; using Konscious.Security.Cryptography; namespace EaseMoney.Application.Services; public class UserService : IUserService { private readonly IUserRepository _userRepository; public UserService(IUserRepository userRepository) { _userRepository = userRepository; } public async Task RegisterUserAsync(RegisterUserDto registerUserDto) { try { var existingUser = await _userRepository.GetUserByUsernameAsync(registerUserDto.Username); if (existingUser != null) { return false; // Пользователь уже существует } // Создаем нового пользователя var newUser = new User { Username = registerUserDto.Username, PasswordHash = HashPasswordWithArgon2(registerUserDto.Password) // Хешируем пароль с Argon2 }; await _userRepository.AddUserAsync(newUser); return true; } catch (Exception e) { return false; } } public async Task<(bool IsVerified, bool IsError)> VerifyUserAsync(VerifyUserDto verifyUserDto) { try { var user = await _userRepository.GetUserByUsernameAsync(verifyUserDto.Username); if (user == null) { return (false, false); } return (VerifyPassword(verifyUserDto.Password, user.PasswordHash), false); } catch (Exception ex) { return (false, true); } } public bool VerifyPassword(string enteredPassword, string storedHash) { return VerifyPasswordWithArgon2(enteredPassword, storedHash); } private string HashPasswordWithArgon2(string password) { // Настройка Argon2 var salt = GenerateSalt(); var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) { Salt = salt, DegreeOfParallelism = 8, // Количество параллельных потоков Iterations = 4, // Количество итераций MemorySize = 65536 // Размер памяти в KB (64 MB) }; var hash = argon2.GetBytes(32); // Получаем 32-байтный хеш return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}"; // Возвращаем хеш с солью } private bool VerifyPasswordWithArgon2(string password, string storedHash) { var parts = storedHash.Split(':'); if (parts.Length != 2) return false; var salt = Convert.FromBase64String(parts[0]); var storedPasswordHash = Convert.FromBase64String(parts[1]); var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) { Salt = salt, DegreeOfParallelism = 8, Iterations = 4, MemorySize = 65536 }; var hash = argon2.GetBytes(32); // Сравниваем хеши return hash.SequenceEqual(storedPasswordHash); } private byte[] GenerateSalt() { var salt = new byte[16]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(salt); } return salt; } } ] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Infrastructure/Persistence/ApplicationDbContext.cs: [using EaseMoney.Core.Entities; using EaseMoney.Infrastructure.Persistence.Configurations; using Microsoft.EntityFrameworkCore; namespace EaseMoney.Infrastructure.Persistence; public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } public DbSet Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new UserConfiguration()); } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs: [using EaseMoney.Core.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace EaseMoney.Infrastructure.Persistence.Configurations; public class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(u => u.Id); builder.Property(u => u.Username).IsRequired().HasMaxLength(100); builder.Property(u => u.PasswordHash).IsRequired(); } }] /Users/sergeilogachev/RiderProjects/EaseMoney/EaseMoney/src/Infrastructure/Persistence/Repositories/UserRepository.cs: [using EaseMoney.Core.Entities; using EaseMoney.Core.Ports; using Microsoft.EntityFrameworkCore; namespace EaseMoney.Infrastructure.Persistence.Repositories; public class UserRepository : IUserRepository { private readonly ApplicationDbContext _context; public UserRepository(ApplicationDbContext context) { _context = context; } public async Task GetUserByUsernameAsync(string username) { if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username), "Username cannot be null or empty."); try { return await _context.Users.SingleOrDefaultAsync(u => u.Username == username); } catch (InvalidOperationException) { throw new InvalidOperationException("More than one user with the same username exists in the database."); } } public async Task AddUserAsync(User user) { if (user == null) throw new ArgumentNullException(nameof(user), "user can not be null"); _context.Users.Add(user); await _context.SaveChangesAsync(); } }] "