Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/CodeClash.API/Controllers/Contests/ContestController.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<IActionResult> GetContestProblems(
Guid id,
CancellationToken cancellationToken)
Expand All @@ -25,6 +29,18 @@ public async Task<IActionResult> GetContestProblems(
: BadRequest(result);
}

[HttpGet]
public async Task<IActionResult> 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<IActionResult> CreateContest(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CodeClash.Application.DTO;
using CodeClash.Domain.Models.TestCases;
using CodeClash.Domain.Premitives;
using CodeClash.Domain.Premitives.Responses;

Expand All @@ -9,6 +10,12 @@ namespace CodeClash.Application.Abstractions.Execution;
/// </summary>
public interface IExecutionService
{
Task<BaseSubmissionResponse> RunCodeAsync(
string code,
Language language,
List<Testcase> testCases,
decimal runTimeLimit);

/// <summary>
/// Executes user code with given language and test cases.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using CodeClash.Application.Abstractions.Messaging;

namespace CodeClash.Application.Contests.GetAllContests;
public record GetAllContestsQuery : IQuery<IReadOnlyList<GetAllContestsResponse>>;
Original file line number Diff line number Diff line change
@@ -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<GetAllContestsQuery, IReadOnlyList<GetAllContestsResponse>>
{
public async Task<Result<IReadOnlyList<GetAllContestsResponse>>> Handle(
GetAllContestsQuery request,
CancellationToken cancellationToken)
{
var contests = await contestRepository.GetAllAsync(cancellationToken);

IReadOnlyList<GetAllContestsResponse> response = contests
.Select(c => c.ToGetAllContestsResponse())
.ToList();


return Result.Success(response);
}
}
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 12 additions & 0 deletions src/CodeClash.Application/Mapping/ContestMappings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CodeClash.Application.Contests.CreateContest;
using CodeClash.Application.Contests.GetAllContests;
using CodeClash.Domain.Models.Contests;

namespace CodeClash.Application.Mapping;
Expand All @@ -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
);
}
38 changes: 19 additions & 19 deletions src/CodeClash.Application/Mapping/SubmitMappings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,31 @@ public static GetSubmissionDataResponse ToSubmit(this Submit submit)
};
}

public static async Task<Submit> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public sealed class GetProblemByIdResponse
public List<TestCasesDto> TasteCases { get; set; }
public List<TopicDto> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ namespace CodeClash.Application.SolveProblem.SubmitSolutions;
public record SubmitSolutionCommand(
Guid ProblemId,
IFormFile Code,
Guid ContestId,
Guid? ContestId,
Language Language) : ICommand<SubmitSolutionCommandResponse>;
Original file line number Diff line number Diff line change
@@ -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<SubmitSolutionCommand, SubmitSolutionCommandResponse>
IHttpContextAccessor contextAccessor,
IFileService fileService)
: ICommandHandler<SubmitSolutionCommand, SubmitSolutionCommandResponse>
{
public async Task<Result<SubmitSolutionCommandResponse>> Handle(
SubmitSolutionCommand request,
CancellationToken cancellationToken)
{
// Auth
var userId = contextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (userId is null)
{
return Result.Failure<SubmitSolutionCommandResponse>(new Error("Auth.Error", "Unauthorized"));
return Result.Failure<SubmitSolutionCommandResponse>(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<SubmitSolutionCommandResponse>(ProblemErrors.NotFound);
return Result.Failure<SubmitSolutionCommandResponse>
(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<SubmitSolutionCommandResponse>(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<SubmitSolutionCommandResponse>(ContestErrors.NotStarted);
}

using (var reader = new StreamReader(request.Code.OpenReadStream()))
if (problem.Contest?.ContestStatus == ContestStatus.Ended)
{
codeContent = await reader.ReadToEndAsync(cancellationToken);
return Result.Failure<SubmitSolutionCommandResponse>(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));
}
}
3 changes: 3 additions & 0 deletions src/CodeClash.Domain/Abstractions/IContestRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
namespace CodeClash.Domain.Abstractions;
public interface IContestRepository : IGenericRepository<Contest>
{
Task<IReadOnlyList<Contest>> GetAllAsync(
CancellationToken cancellationToken = default);

Task<IReadOnlyList<Problem>> GetContestProblemsByIdAsync(Guid contestId);
Task<int> GetProblemCountAsync(Guid contestId);
Task<bool> HasProblemAsync(Guid contestId, Guid problemId);
Expand Down
4 changes: 4 additions & 0 deletions src/CodeClash.Domain/Abstractions/IProblemRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Task<int> GetSubmissionsProblemCountAsync(
Guid problemId,
CancellationToken cancellationToken = default);

Task<Problem?> GetProblemIncludingContestAndTestcases(
Guid problemId,
CancellationToken cancellationToken = default);

Task<bool> CheckUserSolvedProblemAsync(
Guid problemId,
string userId,
Expand Down
8 changes: 8 additions & 0 deletions src/CodeClash.Domain/Models/Contests/ContestErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!");

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,13 @@ public async Task AddProblemAsync(Guid contestId, Guid problemId)
problem.ContestId = contestId;
}
}

public async Task<IReadOnlyList<Contest>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _context.Set<Contest>()
.AsNoTracking()
.Include(x => x.Registrations)
.Include(x => x.Problems)
.ToListAsync(cancellationToken);
}
}
13 changes: 12 additions & 1 deletion src/CodeClash.Infrastructure/Repositories/ProblemRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ public async Task<int> GetAcceptedProblemCountAsync(
.FirstOrDefaultAsync(x => x.Id == problemId, cancellationToken);
}

public async Task<Problem?> GetProblemIncludingContestAndTestcases(
Guid problemId,
CancellationToken cancellationToken = default)
{
return await _context.Set<Problem>()
.Include(x => x.Contest)
.Include(y => y.Testcases)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == problemId, cancellationToken);
}

public async Task<int> GetSubmissionsProblemCountAsync(
Guid problemId,
CancellationToken cancellationToken = default)
Expand All @@ -59,6 +70,6 @@ public async Task<int> GetSubmissionsProblemCountAsync(
public IQueryable<Testcase> GetTestCasesByProblemId(Guid problemId)
{
return _context.Set<Testcase>()
.Where(x => x.ProblemId == problemId);
.Where(x => x.ProblemId == problemId).AsNoTracking();
}
}
Loading