Compare commits

...

3 Commits

Author SHA1 Message Date
9b73c84256 Merge branch 'dev'
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m5s
2025-10-24 00:49:09 +03:00
8d95a78cbf Удалён лишний файл 2025-10-24 00:45:45 +03:00
5d5d091dbe Refactors repositories for improved structure
Improves repository structure by introducing a generic DbCrud class and dedicated repositories for each entity.

This change enhances code maintainability and testability by separating concerns.
It also introduces soft delete functionality to the generic repository.
2025-10-24 00:44:53 +03:00
12 changed files with 212 additions and 112 deletions

View File

@@ -3,4 +3,9 @@ namespace LiquidCode.Api.Authentication.Requests;
/// <summary>
/// Модель запроса для входа пользователя
/// </summary>
public record LoginRequest(string Username, string Password);
/// <param name="Username">Имя пользователя</param>
/// <param name="Password">Пароль</param>
public record LoginRequest(
string Username,
string Password
);

View File

@@ -7,15 +7,6 @@ namespace LiquidCode.Domain.Interfaces.Repositories;
/// </summary>
public interface IMissionRepository : IRepository<DbMission>
{
/// <summary>
/// Получает миссии с пагинацией
/// </summary>
/// <param name="pageSize">Количество элементов на странице</param>
/// <param name="pageNumber">Номер страницы (начиная с нуля)</param>
/// <param name="cancellationToken">Токен отмены</param>
/// <returns>Кортеж (миссии, естьСледующаяСтраница)</returns>
Task<(IEnumerable<DbMission> Missions, bool HasNextPage)> GetMissionsPageAsync(
int pageSize, int pageNumber, CancellationToken cancellationToken = default);
/// <summary>
/// Получает миссии по автору
@@ -35,12 +26,12 @@ public interface IMissionRepository : IRepository<DbMission>
/// <summary>
/// Добавляет текстовые данные миссии
/// </summary>
Task AddMissionTextAsync(DbMissionPublicTextData textData, CancellationToken cancellationToken = default);
Task CreateMissionTextAsync(DbMissionPublicTextData textData, CancellationToken cancellationToken = default);
/// <summary>
/// Добавляет несколько записей текстовых данных миссии
/// </summary>
Task AddMissionTextsAsync(IEnumerable<DbMissionPublicTextData> textData, CancellationToken cancellationToken = default);
Task CreateMissionTextsAsync(IEnumerable<DbMissionPublicTextData> textData, CancellationToken cancellationToken = default);
/// <summary>
/// Подсчитывает общее количество миссий

View File

@@ -1,10 +1,12 @@
using LiquidCode.Infrastructure.Database.Entities;
namespace LiquidCode.Domain.Interfaces.Repositories;
/// <summary>
/// Базовый интерфейс репозитория для общих операций CRUD
/// </summary>
/// <typeparam name="TEntity">Тип сущности, управляемой этим репозиторием</typeparam>
public interface IRepository<TEntity> where TEntity : class
public interface IRepository<TEntity> where TEntity : class, ISoftDeletable
{
/// <summary>
/// Находит сущность по ее ID
@@ -12,14 +14,19 @@ public interface IRepository<TEntity> where TEntity : class
Task<TEntity?> FindByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Получает все сущности
/// Получает все сущности с пагинацией
/// </summary>
Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
/// <param name="pageSize">Количество элементов на странице</param>
/// <param name="pageNumber">Номер страницы (начиная с 0)</param>
/// <param name="cancellationToken">Токен отмены</param>
/// <returns>Кортеж (сущности, естьСледующаяСтраница)</returns>
Task<(IEnumerable<TEntity> Items, bool HasNextPage)> GetPageAsync(
int pageSize, int pageNumber, CancellationToken cancellationToken = default);
/// <summary>
/// Добавляет новую сущность
/// </summary>
Task AddAsync(TEntity entity, CancellationToken cancellationToken = default);
Task CreateAsync(TEntity entity, CancellationToken cancellationToken = default);
/// <summary>
/// Обновляет существующую сущность
@@ -29,7 +36,12 @@ public interface IRepository<TEntity> where TEntity : class
/// <summary>
/// Удаляет сущность
/// </summary>
Task RemoveAsync(TEntity entity, CancellationToken cancellationToken = default);
Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);
/// <summary>
/// Мягко удаляет сущность
/// </summary>
Task SoftDeleteAsync(TEntity entity, CancellationToken cancellationToken = default);
/// <summary>
/// Сохраняет все изменения, сделанные в базе данных

View File

@@ -23,12 +23,12 @@ public interface ISubmitRepository : IRepository<DbUserSubmit>
Task<DbUserSubmit?> GetSubmissionWithDetailsAsync(int submissionId, CancellationToken cancellationToken = default);
/// <summary>
/// Получает решение для отправки
/// Получает решение
/// </summary>
Task<DbSolution?> GetSolutionAsync(int submissionId, CancellationToken cancellationToken = default);
/// <summary>
/// Добавляет решение для отправки
/// Добавляет решение
/// </summary>
Task AddSolutionAsync(DbSolution solution, CancellationToken cancellationToken = default);
}

View File

@@ -55,7 +55,7 @@ public class AuthenticationService : IAuthenticationService
Salt = "" // BCrypt управляет солью внутренне
};
await _userRepository.AddAsync(newUser, cancellationToken);
await _userRepository.CreateAsync(newUser, cancellationToken);
_logger.LogInformation("User registered successfully: {Username}", request.Username);
// Автоматически войти пользователю

View File

@@ -83,7 +83,7 @@ public class MissionService : IMissionService
UpdatedAt = DateTime.UtcNow
};
await _missionRepository.AddAsync(dbMission, cancellationToken);
await _missionRepository.CreateAsync(dbMission, cancellationToken);
// Распарсить и сохранить текстовые данные миссии
var missionTexts = ExtractMissionTexts(statementSectionsPath, dbMission.Id);
@@ -104,7 +104,7 @@ public class MissionService : IMissionService
}
// Добавить текстовые данные миссии в базу данных
await _missionRepository.AddMissionTextsAsync(missionTexts, cancellationToken);
await _missionRepository.CreateMissionTextsAsync(missionTexts, cancellationToken);
await _missionRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Mission uploaded successfully: {MissionId}", dbMission.Id);
@@ -159,7 +159,7 @@ public class MissionService : IMissionService
return null;
}
var (missions, hasNextPage) = await _missionRepository.GetMissionsPageAsync(pageSize, pageNumber, cancellationToken);
var (missions, hasNextPage) = await _missionRepository.GetPageAsync(pageSize, pageNumber, cancellationToken);
var apiList = missions.Select(MissionResponse.FromEntity);
return new MissionsPageResponse(hasNextPage, apiList);

View File

@@ -72,7 +72,7 @@ public class SubmitService : ISubmitService
Solution = solution
};
await _submitRepository.AddAsync(submission, cancellationToken);
await _submitRepository.CreateAsync(submission, cancellationToken);
_logger.LogInformation("Solution submitted: UserId={UserId}, MissionId={MissionId}, SolutionId={SolutionId}", userId, missionId, solution.Id);
return solution;

View File

@@ -0,0 +1,70 @@
using LiquidCode.Domain.Interfaces.Repositories;
using LiquidCode.Infrastructure.Database;
using LiquidCode.Infrastructure.Database.Entities;
using Microsoft.EntityFrameworkCore;
namespace LiquidCode.Infrastructure.Database.Repositories;
/// <summary>
/// Базовая реализация репозитория, предоставляющая общие операции CRUD
/// </summary>
public class DbCrud<TEntity> : IRepository<TEntity> where TEntity : class, ISoftDeletable
{
private readonly LiquidDbContext _dbContext;
public readonly DbSet<TEntity> DbSet;
public DbCrud(LiquidDbContext dbContext)
{
_dbContext = dbContext;
DbSet = dbContext.Set<TEntity>();
}
public virtual async Task<TEntity?> FindByIdAsync(int id, CancellationToken cancellationToken = default) =>
await DbSet.FindAsync(new object?[] { id }, cancellationToken);
public virtual async Task<(IEnumerable<TEntity> Items, bool HasNextPage)> GetPageAsync(
int pageSize, int pageNumber, CancellationToken cancellationToken = default)
{
if (pageSize <= 0 || pageNumber < 0)
throw new ArgumentException("Page size must be positive, page number must be non-negative");
var totalCount = await DbSet.CountAsync(cancellationToken);
var hasNextPage = totalCount > pageSize * (pageNumber + 1);
var items = await DbSet
.OrderBy(x => EF.Property<int>(x, "Id"))
.Skip(pageSize * pageNumber)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (items, hasNextPage);
}
public virtual async Task CreateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await DbSet.AddAsync(entity, cancellationToken);
await SaveChangesAsync(cancellationToken);
}
public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
DbSet.Update(entity);
await SaveChangesAsync(cancellationToken);
}
public virtual async Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default)
{
DbSet.Remove(entity);
await SaveChangesAsync(cancellationToken);
}
public virtual async Task SoftDeleteAsync(TEntity entity, CancellationToken cancellationToken = default)
{
entity.IsDeleted = true;
DbSet.Update(entity);
await SaveChangesAsync(cancellationToken);
}
public async Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
await _dbContext.SaveChangesAsync(cancellationToken);
}

View File

@@ -8,52 +8,63 @@ namespace LiquidCode.Infrastructure.Database.Repositories;
/// <summary>
/// Реализация репозитория для операций с базой данных, связанных с миссиями
/// </summary>
public class MissionRepository : Repository<DbMission>, IMissionRepository
public class MissionRepository : IMissionRepository
{
public MissionRepository(LiquidDbContext dbContext) : base(dbContext)
private readonly LiquidDbContext _dbContext;
private readonly DbCrud<DbMission> _missionRepository;
public MissionRepository(LiquidDbContext dbContext)
{
_dbContext = dbContext;
_missionRepository = new DbCrud<DbMission>(dbContext);
}
public async Task<(IEnumerable<DbMission> Missions, bool HasNextPage)> GetMissionsPageAsync(
int pageSize, int pageNumber, CancellationToken cancellationToken = default)
{
if (pageSize <= 0 || pageNumber < 0)
throw new ArgumentException("Page size must be positive, page number must be non-negative");
// IRepository<DbMission> implementation (delegated to _missionRepository)
public Task<DbMission?> FindByIdAsync(int id, CancellationToken cancellationToken = default) =>
_missionRepository.FindByIdAsync(id, cancellationToken);
var totalCount = await DbSet.CountAsync(cancellationToken);
var hasNextPage = totalCount > pageSize * (pageNumber + 1);
public Task<(IEnumerable<DbMission> Items, bool HasNextPage)> GetPageAsync(
int pageSize, int pageNumber, CancellationToken cancellationToken = default) =>
_missionRepository.GetPageAsync(pageSize, pageNumber, cancellationToken);
var missions = await DbSet
.OrderBy(m => m.Id)
.Skip(pageSize * pageNumber)
.Take(pageSize)
.ToListAsync(cancellationToken);
public Task CreateAsync(DbMission entity, CancellationToken cancellationToken = default) =>
_missionRepository.CreateAsync(entity, cancellationToken);
return (missions, hasNextPage);
}
public Task UpdateAsync(DbMission entity, CancellationToken cancellationToken = default) =>
_missionRepository.UpdateAsync(entity, cancellationToken);
public Task DeleteAsync(DbMission entity, CancellationToken cancellationToken = default) =>
_missionRepository.DeleteAsync(entity, cancellationToken);
public Task SoftDeleteAsync(DbMission entity, CancellationToken cancellationToken = default) =>
_missionRepository.SoftDeleteAsync(entity, cancellationToken);
public Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
_missionRepository.SaveChangesAsync(cancellationToken);
// IMissionRepository specific methods
public async Task<IEnumerable<DbMission>> GetMissionsByAuthorAsync(int authorId, CancellationToken cancellationToken = default) =>
await DbSet
await _dbContext.Set<DbMission>()
.Where(m => m.Author.Id == authorId)
.OrderByDescending(m => m.CreatedAt)
.ToListAsync(cancellationToken);
public async Task<DbMissionPublicTextData?> GetMissionTextAsync(int missionId, string language, CancellationToken cancellationToken = default) =>
await DbContext.MissionsTextData
await _dbContext.MissionsTextData
.FirstOrDefaultAsync(m => m.MissionId == missionId && m.Language == language, cancellationToken);
public async Task<IEnumerable<string>> GetMissionLanguagesAsync(int missionId, CancellationToken cancellationToken = default) =>
await DbContext.MissionsTextData
await _dbContext.MissionsTextData
.Where(m => m.MissionId == missionId)
.Select(m => m.Language)
.ToListAsync(cancellationToken);
public async Task AddMissionTextAsync(DbMissionPublicTextData textData, CancellationToken cancellationToken = default) =>
await DbContext.MissionsTextData.AddAsync(textData, cancellationToken);
public async Task CreateMissionTextAsync(DbMissionPublicTextData textData, CancellationToken cancellationToken = default) =>
await _dbContext.MissionsTextData.AddAsync(textData, cancellationToken);
public async Task AddMissionTextsAsync(IEnumerable<DbMissionPublicTextData> textData, CancellationToken cancellationToken = default) =>
await DbContext.MissionsTextData.AddRangeAsync(textData, cancellationToken);
public async Task CreateMissionTextsAsync(IEnumerable<DbMissionPublicTextData> textData, CancellationToken cancellationToken = default) =>
await _dbContext.MissionsTextData.AddRangeAsync(textData, cancellationToken);
public async Task<int> CountMissionsAsync(CancellationToken cancellationToken = default) =>
await DbSet.CountAsync(cancellationToken);
await _dbContext.Set<DbMission>().CountAsync(cancellationToken);
}

View File

@@ -1,47 +0,0 @@
using LiquidCode.Domain.Interfaces.Repositories;
using LiquidCode.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
namespace LiquidCode.Infrastructure.Database.Repositories;
/// <summary>
/// Базовая реализация репозитория, предоставляющая общие операции CRUD
/// </summary>
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
protected readonly LiquidDbContext DbContext;
protected readonly DbSet<TEntity> DbSet;
public Repository(LiquidDbContext dbContext)
{
DbContext = dbContext;
DbSet = dbContext.Set<TEntity>();
}
public virtual async Task<TEntity?> FindByIdAsync(int id, CancellationToken cancellationToken = default) =>
await DbSet.FindAsync(new object?[] { id }, cancellationToken);
public virtual async Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default) =>
await DbSet.ToListAsync(cancellationToken);
public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default)
{
await DbSet.AddAsync(entity, cancellationToken);
await SaveChangesAsync(cancellationToken);
}
public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
DbSet.Update(entity);
await SaveChangesAsync(cancellationToken);
}
public virtual async Task RemoveAsync(TEntity entity, CancellationToken cancellationToken = default)
{
DbSet.Remove(entity);
await SaveChangesAsync(cancellationToken);
}
public async Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
await DbContext.SaveChangesAsync(cancellationToken);
}

View File

@@ -8,34 +8,63 @@ namespace LiquidCode.Infrastructure.Database.Repositories;
/// <summary>
/// Реализация репозитория для операций с базой данных, связанных с отправками пользователей
/// </summary>
public class SubmitRepository : Repository<DbUserSubmit>, ISubmitRepository
public class SubmitRepository : ISubmitRepository
{
public SubmitRepository(LiquidDbContext dbContext) : base(dbContext)
private readonly LiquidDbContext _dbContext;
private readonly DbCrud<DbUserSubmit> _submitRepository;
public SubmitRepository(LiquidDbContext dbContext)
{
_dbContext = dbContext;
_submitRepository = new DbCrud<DbUserSubmit>(dbContext);
}
// IRepository<DbUserSubmit> implementation (delegated to _submitRepository)
public Task<DbUserSubmit?> FindByIdAsync(int id, CancellationToken cancellationToken = default) =>
_submitRepository.FindByIdAsync(id, cancellationToken);
public Task<(IEnumerable<DbUserSubmit> Items, bool HasNextPage)> GetPageAsync(
int pageSize, int pageNumber, CancellationToken cancellationToken = default) =>
_submitRepository.GetPageAsync(pageSize, pageNumber, cancellationToken);
public Task CreateAsync(DbUserSubmit entity, CancellationToken cancellationToken = default) =>
_submitRepository.CreateAsync(entity, cancellationToken);
public Task UpdateAsync(DbUserSubmit entity, CancellationToken cancellationToken = default) =>
_submitRepository.UpdateAsync(entity, cancellationToken);
public Task DeleteAsync(DbUserSubmit entity, CancellationToken cancellationToken = default) =>
_submitRepository.DeleteAsync(entity, cancellationToken);
public Task SoftDeleteAsync(DbUserSubmit entity, CancellationToken cancellationToken = default) =>
_submitRepository.SoftDeleteAsync(entity, cancellationToken);
public Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
_submitRepository.SaveChangesAsync(cancellationToken);
// ISubmitRepository specific methods
public async Task<IEnumerable<DbUserSubmit>> GetSubmissionsByUserAsync(int userId, CancellationToken cancellationToken = default) =>
await DbSet
await _dbContext.Set<DbUserSubmit>()
.Where(s => s.User.Id == userId)
.OrderByDescending(s => s.Solution!.Time)
.ToListAsync(cancellationToken);
public async Task<IEnumerable<DbUserSubmit>> GetSubmissionsByMissionAsync(int missionId, CancellationToken cancellationToken = default) =>
await DbSet
await _dbContext.Set<DbUserSubmit>()
.Where(s => s.Solution!.Mission.Id == missionId)
.OrderByDescending(s => s.Solution!.Time)
.ToListAsync(cancellationToken);
public async Task<DbUserSubmit?> GetSubmissionWithDetailsAsync(int submissionId, CancellationToken cancellationToken = default) =>
await DbSet
await _dbContext.Set<DbUserSubmit>()
.Include(s => s.User)
.Include(s => s.Solution)
.FirstOrDefaultAsync(s => s.Id == submissionId, cancellationToken);
public async Task<DbSolution?> GetSolutionAsync(int solutionId, CancellationToken cancellationToken = default) =>
await DbContext.Solutions
await _dbContext.Solutions
.FirstOrDefaultAsync(s => s.Id == solutionId, cancellationToken);
public async Task AddSolutionAsync(DbSolution solution, CancellationToken cancellationToken = default) =>
await DbContext.Solutions.AddAsync(solution, cancellationToken);
await _dbContext.Solutions.AddAsync(solution, cancellationToken);
}

View File

@@ -8,39 +8,68 @@ namespace LiquidCode.Infrastructure.Database.Repositories;
/// <summary>
/// Реализация репозитория для операций с базой данных, связанных с пользователями
/// </summary>
public class UserRepository : Repository<DbUser>, IUserRepository
public class UserRepository : IUserRepository
{
public UserRepository(LiquidDbContext dbContext) : base(dbContext)
private readonly LiquidDbContext _dbContext;
private readonly DbCrud<DbUser> _userRepository;
public UserRepository(LiquidDbContext dbContext)
{
_dbContext = dbContext;
_userRepository = new DbCrud<DbUser>(dbContext);
}
// IRepository<DbUser> implementation (delegated to _userRepository)
public Task<DbUser?> FindByIdAsync(int id, CancellationToken cancellationToken = default) =>
_userRepository.FindByIdAsync(id, cancellationToken);
public Task<(IEnumerable<DbUser> Items, bool HasNextPage)> GetPageAsync(
int pageSize, int pageNumber, CancellationToken cancellationToken = default) =>
_userRepository.GetPageAsync(pageSize, pageNumber, cancellationToken);
public Task CreateAsync(DbUser entity, CancellationToken cancellationToken = default) =>
_userRepository.CreateAsync(entity, cancellationToken);
public Task UpdateAsync(DbUser entity, CancellationToken cancellationToken = default) =>
_userRepository.UpdateAsync(entity, cancellationToken);
public Task DeleteAsync(DbUser entity, CancellationToken cancellationToken = default) =>
_userRepository.DeleteAsync(entity, cancellationToken);
public Task SoftDeleteAsync(DbUser entity, CancellationToken cancellationToken = default) =>
_userRepository.SoftDeleteAsync(entity, cancellationToken);
public Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
_userRepository.SaveChangesAsync(cancellationToken);
// IUserRepository specific methods
public async Task<DbUser?> FindByUsernameAsync(string username, CancellationToken cancellationToken = default) =>
await DbSet.FirstOrDefaultAsync(u => u.Username == username, cancellationToken);
await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == username, cancellationToken);
public async Task<bool> UserExistsAsync(string username, CancellationToken cancellationToken = default) =>
await DbSet.AnyAsync(u => u.Username == username, cancellationToken);
await _dbContext.Users.AnyAsync(u => u.Username == username, cancellationToken);
public async Task<int> GetRefreshTokenCountAsync(int userId, CancellationToken cancellationToken = default) =>
await DbContext.RefreshTokens.CountAsync(t => t.DbUser.Id == userId, cancellationToken);
await _dbContext.RefreshTokens.CountAsync(t => t.DbUser.Id == userId, cancellationToken);
public async Task<DbRefreshToken?> GetOldestRefreshTokenAsync(int userId, CancellationToken cancellationToken = default) =>
await DbContext.RefreshTokens
await _dbContext.RefreshTokens
.Where(t => t.DbUser.Id == userId)
.OrderBy(t => t.Expires)
.FirstOrDefaultAsync(cancellationToken);
public async Task AddRefreshTokenAsync(DbRefreshToken token, CancellationToken cancellationToken = default) =>
await DbContext.RefreshTokens.AddAsync(token, cancellationToken);
await _dbContext.RefreshTokens.AddAsync(token, cancellationToken);
public async Task RemoveRefreshTokenAsync(string tokenString, CancellationToken cancellationToken = default)
{
var token = await DbContext.RefreshTokens.FirstOrDefaultAsync(t => t.Token == tokenString, cancellationToken);
var token = await _dbContext.RefreshTokens.FirstOrDefaultAsync(t => t.Token == tokenString, cancellationToken);
if (token != null)
DbContext.RefreshTokens.Remove(token);
_dbContext.RefreshTokens.Remove(token);
}
public async Task<DbRefreshToken?> FindRefreshTokenAsync(string tokenString, CancellationToken cancellationToken = default) =>
await DbContext.RefreshTokens
await _dbContext.RefreshTokens
.Include(t => t.DbUser)
.FirstOrDefaultAsync(t => t.Token == tokenString, cancellationToken);
}