From 245169370d106fa59ef3154224ae1e2c30b2dd48 Mon Sep 17 00:00:00 2001 From: Seif Mohamed Date: Thu, 28 May 2026 17:52:42 +0300 Subject: [PATCH] Add GetAllContests and enhance submission handling --- .../Controllers/Contests/ContestController.cs | 16 +++++ .../Execution/IExecutionService.cs | 7 ++ .../GetAllContests/GetAllContestsQuery.cs | 4 ++ .../GetAllContestsQueryHandler.cs | 24 +++++++ .../GetAllContests/GetAllContestsResponse.cs | 12 ++++ .../Mapping/ContestMappings.cs | 12 ++++ .../Mapping/SubmitMappings.cs | 38 +++++------ .../GetProblemById/GetProblemByIdResponse.cs | 2 + .../SubmitSolutions/SubmitSolutionCommand.cs | 2 +- .../SubmitSolutionCommandHandler.cs | 66 +++++++++++-------- .../Abstractions/IContestRepository.cs | 3 + .../Abstractions/IProblemRepository.cs | 4 ++ .../Models/Contests/ContestErrors.cs | 8 +++ .../Repositories/ContestRepository.cs | 9 +++ .../Repositories/ProblemRepository.cs | 13 +++- 15 files changed, 171 insertions(+), 49 deletions(-) create mode 100644 src/CodeClash.Application/Contests/GetAllContests/GetAllContestsQuery.cs create mode 100644 src/CodeClash.Application/Contests/GetAllContests/GetAllContestsQueryHandler.cs create mode 100644 src/CodeClash.Application/Contests/GetAllContests/GetAllContestsResponse.cs diff --git a/src/CodeClash.API/Controllers/Contests/ContestController.cs b/src/CodeClash.API/Controllers/Contests/ContestController.cs index 77886bf..89aee9e 100644 --- a/src/CodeClash.API/Controllers/Contests/ContestController.cs +++ b/src/CodeClash.API/Controllers/Contests/ContestController.cs @@ -1,7 +1,9 @@ using CodeClash.Application.Contests.AddContestProblems; using CodeClash.Application.Contests.CreateContest; +using CodeClash.Application.Contests.GetAllContests; using CodeClash.Application.Contests.GetContest; using CodeClash.Application.Contests.RegisterInContest; +using CodeClash.Domain.Premitives.Responses; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,6 +16,8 @@ public class ContestController( ISender sender) : ControllerBase { [HttpGet("{id:guid}/problems")] + [ProducesResponseType(typeof(ContestProblemResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ContestProblemResponse), StatusCodes.Status400BadRequest)] public async Task GetContestProblems( Guid id, CancellationToken cancellationToken) @@ -25,6 +29,18 @@ public async Task GetContestProblems( : BadRequest(result); } + [HttpGet] + public async Task GetAllContests( + CancellationToken cancellationToken) + { + var result = await sender.Send(new GetAllContestsQuery(), cancellationToken); + + return result.IsSuccess + ? Ok(result.Value) + : BadRequest(result.Error); + } + + [Authorize] [HttpPost] public async Task CreateContest( diff --git a/src/CodeClash.Application/Abstractions/Execution/IExecutionService.cs b/src/CodeClash.Application/Abstractions/Execution/IExecutionService.cs index 9e3f819..f9a1c34 100644 --- a/src/CodeClash.Application/Abstractions/Execution/IExecutionService.cs +++ b/src/CodeClash.Application/Abstractions/Execution/IExecutionService.cs @@ -1,4 +1,5 @@ using CodeClash.Application.DTO; +using CodeClash.Domain.Models.TestCases; using CodeClash.Domain.Premitives; using CodeClash.Domain.Premitives.Responses; @@ -9,6 +10,12 @@ namespace CodeClash.Application.Abstractions.Execution; /// public interface IExecutionService { + Task RunCodeAsync( + string code, + Language language, + List testCases, + decimal runTimeLimit); + /// /// Executes user code with given language and test cases. /// diff --git a/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsQuery.cs b/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsQuery.cs new file mode 100644 index 0000000..3fee0c2 --- /dev/null +++ b/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsQuery.cs @@ -0,0 +1,4 @@ +using CodeClash.Application.Abstractions.Messaging; + +namespace CodeClash.Application.Contests.GetAllContests; +public record GetAllContestsQuery : IQuery>; diff --git a/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsQueryHandler.cs b/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsQueryHandler.cs new file mode 100644 index 0000000..f29cf04 --- /dev/null +++ b/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsQueryHandler.cs @@ -0,0 +1,24 @@ +using CodeClash.Application.Abstractions.Messaging; +using CodeClash.Application.Mapping; +using CodeClash.Domain.Abstractions; +using CodeClash.Domain.Premitives; + +namespace CodeClash.Application.Contests.GetAllContests; +internal sealed class GetAllContestsQueryHandler( + IContestRepository contestRepository) + : IQueryHandler> +{ + public async Task>> Handle( + GetAllContestsQuery request, + CancellationToken cancellationToken) + { + var contests = await contestRepository.GetAllAsync(cancellationToken); + + IReadOnlyList response = contests + .Select(c => c.ToGetAllContestsResponse()) + .ToList(); + + + return Result.Success(response); + } +} diff --git a/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsResponse.cs b/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsResponse.cs new file mode 100644 index 0000000..f575656 --- /dev/null +++ b/src/CodeClash.Application/Contests/GetAllContests/GetAllContestsResponse.cs @@ -0,0 +1,12 @@ +using CodeClash.Domain.Premitives; + +namespace CodeClash.Application.Contests.GetAllContests; +public record GetAllContestsResponse( + Guid Id, + string Name, + DateTime StartDate, + DateTime EndDate, + TimeSpan Duration, + ContestStatus ContestStatus, + int ParticipantsCount, + int ProblemsCount); diff --git a/src/CodeClash.Application/Mapping/ContestMappings.cs b/src/CodeClash.Application/Mapping/ContestMappings.cs index b363e54..46ff743 100644 --- a/src/CodeClash.Application/Mapping/ContestMappings.cs +++ b/src/CodeClash.Application/Mapping/ContestMappings.cs @@ -1,4 +1,5 @@ using CodeClash.Application.Contests.CreateContest; +using CodeClash.Application.Contests.GetAllContests; using CodeClash.Domain.Models.Contests; namespace CodeClash.Application.Mapping; @@ -18,4 +19,15 @@ public static CreateContestResponse ToCreateContestResponse(this Contest contest contest.Id, contest.Name); + public static GetAllContestsResponse ToGetAllContestsResponse(this Contest contest) + => new( + contest.Id, + contest.Name, + contest.StartDate, + contest.EndDate, + contest.Duration, + contest.ContestStatus, + contest.Registrations?.Count ?? 0, + contest.Problems?.Count ?? 0 + ); } diff --git a/src/CodeClash.Application/Mapping/SubmitMappings.cs b/src/CodeClash.Application/Mapping/SubmitMappings.cs index 4661842..3bb66c0 100644 --- a/src/CodeClash.Application/Mapping/SubmitMappings.cs +++ b/src/CodeClash.Application/Mapping/SubmitMappings.cs @@ -42,31 +42,31 @@ public static GetSubmissionDataResponse ToSubmit(this Submit submit) }; } - public static async Task ToEntityAsync( + public static Submit ToEntity( this SubmitSolutionCommand command, - string userId) - { - return new Submit - { - UserId = userId, - ProblemId = command.ProblemId, - - ContestId = command.ContestId == Guid.Empty + string userId, + string codeContent) + => + new Submit + { + UserId = userId, + ProblemId = command.ProblemId, + ContestId = command.ContestId == Guid.Empty ? null : command.ContestId, - Code = await ReadFileAsync(command.Code), + Code = codeContent, - Language = command.Language, - SubmissionDate = DateTime.Now, + Language = command.Language, + SubmissionDate = DateTime.UtcNow, + + // default values + Result = SubmissionResult.Pending, + SubmitTime = null, + SubmitMemory = null, + Error = null + }; - // default values - Result = SubmissionResult.Pending, - SubmitTime = null, - SubmitMemory = null, - Error = null - }; - } public static SubmitSolutionCommandResponse ToResponse( this Submit submit, diff --git a/src/CodeClash.Application/Problems/GetProblemById/GetProblemByIdResponse.cs b/src/CodeClash.Application/Problems/GetProblemById/GetProblemByIdResponse.cs index 858c738..25deb67 100644 --- a/src/CodeClash.Application/Problems/GetProblemById/GetProblemByIdResponse.cs +++ b/src/CodeClash.Application/Problems/GetProblemById/GetProblemByIdResponse.cs @@ -10,6 +10,8 @@ public sealed class GetProblemByIdResponse public List TasteCases { get; set; } public List Topics { get; set; } + public Guid ContestId { get; set; } + public decimal Accepted { get; set; } public decimal Submissions { get; set; } public decimal AcceptanceRate => Submissions > 0 ? Accepted / Submissions * 100 : 0; diff --git a/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommand.cs b/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommand.cs index bcf7588..aa9dad7 100644 --- a/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommand.cs +++ b/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommand.cs @@ -6,5 +6,5 @@ namespace CodeClash.Application.SolveProblem.SubmitSolutions; public record SubmitSolutionCommand( Guid ProblemId, IFormFile Code, - Guid ContestId, + Guid? ContestId, Language Language) : ICommand; diff --git a/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommandHandler.cs b/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommandHandler.cs index cdb8f91..2de417c 100644 --- a/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommandHandler.cs +++ b/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommandHandler.cs @@ -1,80 +1,90 @@ using System.Security.Claims; using CodeClash.Application.Abstractions.Execution; +using CodeClash.Application.Abstractions.File; using CodeClash.Application.Abstractions.Messaging; -using CodeClash.Application.DTO; using CodeClash.Application.Mapping; using CodeClash.Domain.Abstractions; using CodeClash.Domain.Models.Contests; using CodeClash.Domain.Models.Problems; using CodeClash.Domain.Premitives; +using CodeClash.Domain.Premitives.Responses; using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; namespace CodeClash.Application.SolveProblem.SubmitSolutions; internal sealed class SubmitSolutionCommandHandler( IProblemRepository problemRepository, - IContestRepository contestRepository, ISubmitRepository submitRepository, IUnitOfWork unitOfWork, IExecutionService executionService, - IHttpContextAccessor contextAccessor) : ICommandHandler + IHttpContextAccessor contextAccessor, + IFileService fileService) + : ICommandHandler { public async Task> Handle( SubmitSolutionCommand request, CancellationToken cancellationToken) { + // Auth var userId = contextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); - if (userId is null) { - return Result.Failure(new Error("Auth.Error", "Unauthorized")); + return Result.Failure(new Error("Auth.Unauthorized", "Unauthorized")); } + // Load problem with testcases var problem = - await problemRepository.GetByIdAsync(request.ProblemId); - + await problemRepository.GetProblemIncludingContestAndTestcases(request.ProblemId, cancellationToken); if (problem is null) { - return Result.Failure(ProblemErrors.NotFound); + return Result.Failure + (ProblemErrors.NotFound); } - var contest = - await contestRepository.GetByIdAsync(request.ContestId); - - if (contest is null) + // Contest validation + if (request.ContestId.HasValue && problem.Contest is null) { return Result.Failure(ContestErrors.NotFound); } - var problemTestcases = await - problemRepository.GetTestCasesByProblemId(request.ProblemId) - .ToListAsync(cancellationToken); - - string codeContent; + // Contest status check + if (problem.Contest?.ContestStatus == ContestStatus.Upcoming) + { + return Result.Failure(ContestErrors.NotStarted); + } - using (var reader = new StreamReader(request.Code.OpenReadStream())) + if (problem.Contest?.ContestStatus == ContestStatus.Ended) { - codeContent = await reader.ReadToEndAsync(cancellationToken); + return Result.Failure(ContestErrors.Ended); } - var testCasesDtos = problemTestcases - .Select(t => new TestCasesDto { Input = t.Input, Output = t.Output }) - .ToList(); + // Read code file + string codeContent = await fileService.ReadFile(request.Code); + // Execute var executionResult = await executionService.RunCodeAsync( codeContent, request.Language, - testCasesDtos, + problem.Testcases.ToList(), problem.RunTimeLimit); - var submission = await request.ToEntityAsync(userId); + // Build submission entity + var submission = request.ToEntity(userId, codeContent); + submission.Result = executionResult.SubmissionResult; + submission.SubmitTime = (executionResult as AcceptedResponse)?.ExecutionTime; + submission.Error = (executionResult as CompilationErrorResponse)?.Message; + submission.SubmitMemory = 0m; + // Persist submitRepository.Add(submission); - await unitOfWork.SaveChangesAsync(cancellationToken); - var submitResponse = submission.ToResponse(executionResult); + // Update standings if accepted in running contest + if (problem.Contest?.ContestStatus == ContestStatus.Running + && executionResult.SubmissionResult == SubmissionResult.Accepted) + { + // cache contest standing + } - return Result.Success(submitResponse); + return Result.Success(submission.ToResponse(executionResult)); } } diff --git a/src/CodeClash.Domain/Abstractions/IContestRepository.cs b/src/CodeClash.Domain/Abstractions/IContestRepository.cs index 11dd713..75763ea 100644 --- a/src/CodeClash.Domain/Abstractions/IContestRepository.cs +++ b/src/CodeClash.Domain/Abstractions/IContestRepository.cs @@ -4,6 +4,9 @@ namespace CodeClash.Domain.Abstractions; public interface IContestRepository : IGenericRepository { + Task> GetAllAsync( + CancellationToken cancellationToken = default); + Task> GetContestProblemsByIdAsync(Guid contestId); Task GetProblemCountAsync(Guid contestId); Task HasProblemAsync(Guid contestId, Guid problemId); diff --git a/src/CodeClash.Domain/Abstractions/IProblemRepository.cs b/src/CodeClash.Domain/Abstractions/IProblemRepository.cs index 43ad35b..0a24ead 100644 --- a/src/CodeClash.Domain/Abstractions/IProblemRepository.cs +++ b/src/CodeClash.Domain/Abstractions/IProblemRepository.cs @@ -18,6 +18,10 @@ Task GetSubmissionsProblemCountAsync( Guid problemId, CancellationToken cancellationToken = default); + Task GetProblemIncludingContestAndTestcases( + Guid problemId, + CancellationToken cancellationToken = default); + Task CheckUserSolvedProblemAsync( Guid problemId, string userId, diff --git a/src/CodeClash.Domain/Models/Contests/ContestErrors.cs b/src/CodeClash.Domain/Models/Contests/ContestErrors.cs index de018f9..536ce94 100644 --- a/src/CodeClash.Domain/Models/Contests/ContestErrors.cs +++ b/src/CodeClash.Domain/Models/Contests/ContestErrors.cs @@ -7,4 +7,12 @@ public static class ContestErrors "Contest.NotFound", "The contest with the specified identifier was not found"); + public static readonly Error NotStarted = new( + "Contest.NotStarted", + "The contest with the specified identifier was not started yet!"); + + public static readonly Error Ended = new( + "Contest.Ended", + "The contest with the specified identifier Ended!"); + } diff --git a/src/CodeClash.Infrastructure/Repositories/ContestRepository.cs b/src/CodeClash.Infrastructure/Repositories/ContestRepository.cs index 1966ee8..1637900 100644 --- a/src/CodeClash.Infrastructure/Repositories/ContestRepository.cs +++ b/src/CodeClash.Infrastructure/Repositories/ContestRepository.cs @@ -36,4 +36,13 @@ public async Task AddProblemAsync(Guid contestId, Guid problemId) problem.ContestId = contestId; } } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _context.Set() + .AsNoTracking() + .Include(x => x.Registrations) + .Include(x => x.Problems) + .ToListAsync(cancellationToken); + } } diff --git a/src/CodeClash.Infrastructure/Repositories/ProblemRepository.cs b/src/CodeClash.Infrastructure/Repositories/ProblemRepository.cs index 223b3f4..9160f85 100644 --- a/src/CodeClash.Infrastructure/Repositories/ProblemRepository.cs +++ b/src/CodeClash.Infrastructure/Repositories/ProblemRepository.cs @@ -48,6 +48,17 @@ public async Task GetAcceptedProblemCountAsync( .FirstOrDefaultAsync(x => x.Id == problemId, cancellationToken); } + public async Task GetProblemIncludingContestAndTestcases( + Guid problemId, + CancellationToken cancellationToken = default) + { + return await _context.Set() + .Include(x => x.Contest) + .Include(y => y.Testcases) + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == problemId, cancellationToken); + } + public async Task GetSubmissionsProblemCountAsync( Guid problemId, CancellationToken cancellationToken = default) @@ -59,6 +70,6 @@ public async Task GetSubmissionsProblemCountAsync( public IQueryable GetTestCasesByProblemId(Guid problemId) { return _context.Set() - .Where(x => x.ProblemId == problemId); + .Where(x => x.ProblemId == problemId).AsNoTracking(); } }