From a93d798778b443f90b81555022b11ccbea20fb8c Mon Sep 17 00:00:00 2001 From: admclamb Date: Wed, 10 Jun 2026 00:51:12 -0400 Subject: [PATCH 01/34] setting up clean slate and domain --- .editorconfig | 4 +- .gitattributes | 67 -- .github/dependabot.yml | 17 - .github/labeler.yml | 19 - .github/workflows/ci.yml | 54 -- .github/workflows/labeler.yml | 17 - .../master_app-awapi-scrum-centralus-03.yml | 65 -- .github/workflows/sonar-scan.yml | 58 -- .github/workflows/stale.yml | 26 - Algowars.Api/Algowars.Api.csproj | 13 + Algowars.Api/Algowars.Api.http | 6 + Algowars.Api/Controllers/AccountController.cs | 17 + Algowars.Api/Controllers/ProblemController.cs | 11 + .../Controllers/SubmissionController.cs | 11 + Algowars.Api/Program.cs | 23 + Algowars.Api/WeatherForecast.cs | 13 + Algowars.Api/appsettings.Development.json | 8 + Algowars.Api/appsettings.json | 9 + .../Algowars.Application.csproj | 9 + Algowars.Application/Class1.cs | 7 + .../Algowars.Domain.Tests.csproj | 27 + .../User/Entities/UserTests.cs | 145 +++++ .../User/ValueObjects/UsernameTests.cs | 121 ++++ Algowars.Domain/Algowars.Domain.csproj | 9 + Algowars.Domain/SeedWork/AggregateRoot.cs | 5 + Algowars.Domain/SeedWork/DomainException.cs | 5 + Algowars.Domain/SeedWork/Entity.cs | 32 + Algowars.Domain/User/Entities/User.cs | 20 + .../Exceptions/InvalidUserSubException.cs | 9 + .../Exceptions/InvalidUsernameException.cs | 9 + Algowars.Domain/User/ValueObjects/Username.cs | 25 + .../Algowars.Infrastructure.csproj | 9 + Algowars.Infrastructure/Class1.cs | 7 + Algowars.UnitTests/Algowars.UnitTests.csproj | 23 + Algowars.slnx | 10 - LICENSE.txt | 201 ------ README.md | 34 - Server.slnx | 7 + docker-compose.yml | 38 -- global.json | 6 - scripts/setup-user-secrets.ps1 | 125 ---- scripts/setup-user-secrets.sh | 151 ----- src/ApplicationCore/ApplicationCore.csproj | 16 - .../Commands/AbstractCommandHandler.cs | 43 -- .../CreateAccount/CreateAccountCommand.cs | 4 - .../CreateAccount/CreateAccountHandler.cs | 64 -- .../CreateAccount/CreateAccountValidator.cs | 44 -- .../UpdateProfileSettingsCommand.cs | 5 - .../UpdateProfileSettingsHandler.cs | 28 - .../UpdateProfileSettingsValidator.cs | 16 - .../UpdateUsername/UpdateUsernameCommand.cs | 5 - .../UpdateUsername/UpdateUsernameHandler.cs | 42 -- .../UpdateUsername/UpdateUsernameValidator.cs | 29 - .../UpsertAccount/UpsertAccountCommand.cs | 5 - .../UpsertAccount/UpsertAccountHandler.cs | 65 -- .../UpsertAccount/UpsertAccountValidator.cs | 13 - src/ApplicationCore/Commands/ICommand.cs | 8 - .../Commands/ICommandHandler.cs | 13 - .../CreateSubmissionCommand.cs | 4 - .../CreateSubmissionHandler.cs | 66 -- .../CreateSubmissionValidator.cs | 15 - .../FinalizeEvaluationCommand.cs | 6 - .../FinalizeEvaluationHandler.cs | 33 - .../FinalizeEvaluationValidator.cs | 11 - .../IncrementSubmissionOutboxesCommand.cs | 8 - .../IncrementSubmissionOutboxesHandler.cs | 33 - .../IncrementSubmissionOutboxesValidator.cs | 14 - .../ProcessEvaluationCommand.cs | 7 - .../ProcessEvaluationHandler.cs | 32 - .../ProcessEvaluationValidator.cs | 11 - ...ocessPollingSubmissionExecutionsCommand.cs | 6 - ...ocessPollingSubmissionExecutionsHandler.cs | 17 - ...essPollingSubmissionExecutionsValidator.cs | 11 - .../ProcessSubmissionExecutionsCommand.cs | 7 - .../ProcessSubmissionExecutionsHandler.cs | 32 - .../ProcessSubmissionExecutionsValidator.cs | 15 - .../SaveExecutionTokensCommand.cs | 7 - .../SaveExecutionTokensHandler.cs | 32 - .../SaveExecutionTokensValidator.cs | 11 - .../Common/Pagination/PaginatedResult.cs | 14 - .../Common/Pagination/PaginationRequest.cs | 13 - .../Common/Pagination/SortDirection.cs | 7 - src/ApplicationCore/DependencyInjection.cs | 32 - .../Domain/Accounts/AccountContext.cs | 13 - .../Domain/Accounts/AccountModel.cs | 40 -- .../Domain/BaseAuditableModel.cs | 19 - src/ApplicationCore/Domain/BaseModel.cs | 7 - .../Domain/CodeExecution/CodeBuildResult.cs | 16 - .../CodeExecution/CodeBuilderContext.cs | 22 - .../CodeExecution/CodeExecutionContext.cs | 16 - .../Judge0/Judge0BatchGetResponse.cs | 9 - .../Judge0/Judge0BatchRequest.cs | 9 - .../CodeExecution/Judge0/Judge0StatusModel.cs | 7 - .../Judge0/Judge0SubmissionRequest.cs | 18 - .../Judge0/Judge0SubmissionResponse.cs | 36 -- .../Judge0SubmissionTokenOnlyResponse.cs | 9 - .../Problems/Languages/LanguageVersion.cs | 13 - .../Problems/Languages/ProgrammingLanguage.cs | 10 - .../Domain/Problems/ProblemModel.cs | 55 -- .../ProblemSetups/ProblemSetupModel.cs | 27 - .../Domain/Problems/ProblemStatus.cs | 9 - .../Domain/Problems/TagModel.cs | 8 - .../Problems/TestSuites/HarnessTemplate.cs | 8 - .../TestSuites/TestCaseExpectedOutputModel.cs | 16 - .../TestSuites/TestCaseInputParamModel.cs | 12 - .../TestSuites/TestCaseInputValueTypeModel.cs | 8 - .../Problems/TestSuites/TestCaseModel.cs | 10 - .../TestSuites/TestCaseOutputTypeModel.cs | 10 - .../Problems/TestSuites/TestSuiteModel.cs | 12 - .../Problems/TestSuites/TestSuiteType.cs | 7 - .../Outboxes/SubmissionOutboxModel.cs | 14 - .../Outboxes/SubmissionOutboxStatus.cs | 9 - .../Outboxes/SubmissionOutboxType.cs | 10 - .../Domain/Submissions/SubmissionModel.cs | 63 -- .../Domain/Submissions/SubmissionResult.cs | 26 - .../Domain/Submissions/SubmissionStatus.cs | 22 - .../Dtos/Accounts/AccountDto.cs | 18 - .../Dtos/Accounts/ProfileAggregateDto.cs | 3 - .../Dtos/Accounts/ProfileSettingsDto.cs | 3 - .../Dtos/Languages/LanguageVersionDto.cs | 12 - .../Dtos/Languages/ProgrammingLanguageDto.cs | 12 - .../Dtos/Problems/CreateProblemDto.cs | 8 - .../Dtos/Problems/ProblemDto.cs | 22 - .../Dtos/Problems/ProblemSetupDto.cs | 16 - .../Dtos/Problems/ProblemSubmissionDto.cs | 14 - .../Dtos/Problems/Tests/TestCaseDto.cs | 7 - .../Dtos/Problems/Tests/TestSuiteDto.cs | 6 - .../GetSubmissionsPaginatedRequest.cs | 11 - .../Dtos/Submissions/SubmissionDto.cs | 14 - .../Dtos/Submissions/SubmissionResultDto.cs | 12 - .../Dtos/Submissions/SubmissionStatusDto.cs | 8 - .../SubmissionTestCaseResultDto.cs | 10 - .../Interfaces/Clients/IJudge0Client.cs | 19 - .../Interfaces/Messaging/IMessagePublisher.cs | 7 - .../Repositories/IAccountRepository.cs | 32 - .../Repositories/IProblemRepository.cs | 33 - .../Repositories/ISubmissionRepository.cs | 50 -- .../Interfaces/Services/IAccountAppService.cs | 48 -- .../Services/ICodeBuilderService.cs | 9 - .../Services/ICodeExecutionService.cs | 18 - .../Services/IExecutionComparisonService.cs | 8 - .../Interfaces/Services/IProblemAppService.cs | 37 -- .../Interfaces/Services/ISlugService.cs | 6 - .../Services/ISubmissionAppService.cs | 70 --- .../Logging/LoggingEventIds.cs | 89 --- .../Mappings/ProblemMappings.cs | 22 - .../Messaging/SubmissionCreatedMessage.cs | 7 - .../SubmissionEvaluationPollMessage.cs | 7 - .../Messaging/SubmissionExecutedMessage.cs | 7 - .../SubmissionExecutionPollMessage.cs | 7 - .../SubmissionReadyToEvaluateMessage.cs | 7 - .../GetAccountBySub/GetAccountBySubHandler.cs | 41 -- .../GetAccountBySub/GetAccountBySubQuery.cs | 5 - .../GetProfileAggregateHandler.cs | 49 -- .../GetProfileAggregateQuery.cs | 5 - .../GetProfileSettingsHandler.cs | 28 - .../GetProfileSettingsQuery.cs | 5 - src/ApplicationCore/Queries/IQuery.cs | 6 - src/ApplicationCore/Queries/IQueryHandler.cs | 8 - .../GetAvailableLanguagesHandler.cs | 38 -- .../GetAvailableLanguagesQuery.cs | 5 - .../GetProblemBySlugHandler.cs | 32 - .../GetProblemBySlug/GetProblemBySlugQuery.cs | 5 - .../GetProblemSetup/GetProblemSetupHandler.cs | 52 -- .../GetProblemSetup/GetProblemSetupQuery.cs | 6 - .../GetProblemSetupsForExecutionHandler.cs | 26 - .../GetProblemSetupsForExecutionQuery.cs | 6 - .../GetProblemsPageableHandler.cs | 53 -- .../GetProblemsPageableQuery.cs | 7 - .../GetSolutionsByProblemIdHandler.cs | 40 -- .../GetSolutionsByProblemIdQuery.cs | 10 - .../GetSubmissionOutboxesHandler.cs | 26 - .../GetSubmissionOutboxesQuery.cs | 5 - .../GetSubmissionStatusHandler.cs | 64 -- .../GetSubmissionStatusQuery.cs | 5 - .../GetSubmissionsPaginatedHandler.cs | 55 -- .../GetSubmissionsPaginatedQuery.cs | 11 - .../GetUserSubmissionsByProblemIdHandler.cs | 39 -- .../GetUserSubmissionsByProblemIdQuery.cs | 12 - .../Services/AccountAppService.cs | 96 --- .../Services/CodeBuilderService.cs | 112 ---- .../Services/CodeExecutionService.cs | 174 ------ .../Services/ExecutionComparisonService.cs | 25 - .../Services/ProblemAppService.cs | 96 --- .../Services/SubmissionAppService.cs | 127 ---- .../Settings/ConnectionStringsSettings.cs | 7 - src/ApplicationCore/Settings/CorsSettings.cs | 7 - src/ApplicationCore/Settings/ISettings.cs | 6 - .../Settings/MediatRSettings.cs | 8 - .../CodeExecution/Judge0/Judge0Client.cs | 185 ------ .../Configuration/Auth0ManagementOptions.cs | 6 - .../Configuration/Auth0Options.cs | 10 - .../Configuration/ConnectionStringOptions.cs | 8 - .../Configuration/CorsOptions.cs | 6 - .../Configuration/ExecutionEnginesOptions.cs | 6 - .../Configuration/Judge0Options.cs | 20 - .../Configuration/LogLevelOptions.cs | 9 - .../Configuration/LoggingOptions.cs | 9 - .../Configuration/MessageBusOptions.cs | 25 - src/Infrastructure/DependencyInjection.cs | 220 ------- src/Infrastructure/Infrastructure.csproj | 38 -- src/Infrastructure/Jobs/JobBase.cs | 50 -- .../JobHandlers/EvaluateSubmissionHandler.cs | 118 ---- .../Jobs/JobHandlers/PollEvaluationHandler.cs | 60 -- .../PollSubmissionExecutionHander.cs | 69 --- .../JobHandlers/SubmissionExecutionHandler.cs | 111 ---- src/Infrastructure/Jobs/JobType.cs | 9 - .../Mappings/AccountMappings.cs | 22 - .../Mappings/ProblemMappings.cs | 40 -- .../Mappings/SubmissionMappings.cs | 26 - .../Consumers/SubmissionCreatedConsumer.cs | 158 ----- .../SubmissionEvaluationPollConsumer.cs | 67 -- .../Consumers/SubmissionExecutedConsumer.cs | 126 ---- .../SubmissionReadyToEvaluateConsumer.cs | 128 ---- .../Messaging/MassTransitMessagePublisher.cs | 11 - .../Persistence/AppDbContext.cs | 155 ----- .../Entities/Account/AccountEntity.cs | 46 -- .../Entities/BaseAuditableEntity.cs | 28 - .../LanguageVersionEngineMappingEntity.cs | 26 - .../Language/LanguageVersionEntity.cs | 31 - .../Language/ProgrammingLanguageEntity.cs | 22 - .../Entities/Problem/HarnessTemplateEntity.cs | 17 - .../Entities/Problem/ProblemEntity.cs | 44 -- .../Entities/Problem/ProblemHistoryEntity.cs | 39 -- .../Entities/Problem/ProblemSetupEntity.cs | 44 -- .../Entities/Problem/ProblemStatus.cs | 18 - .../Persistence/Entities/Problem/TagEntity.cs | 18 - .../Outbox/SubmissionOutboxEntity.cs | 47 -- .../Outbox/SubmissionOutboxStatusEntity.cs | 20 - .../Outbox/SubmissionOutboxTypeEntity.cs | 20 - .../Entities/Submission/SubmissionEntity.cs | 36 -- .../Submission/SubmissionResultEntity.cs | 72 --- .../Submission/SubmissionStatusEntity.cs | 23 - .../Submission/SubmissionStatusTypeEntity.cs | 17 - .../Entities/TestSuite/TestCaseEntity.cs | 27 - .../TestSuite/TestCaseExpectedOutputEntity.cs | 26 - .../Entities/TestSuite/TestCaseInputEntity.cs | 26 - .../TestCasesInputsValueTypeEntity.cs | 14 - .../TestSuite/TestCasesOutputTypeEntity.cs | 17 - .../Entities/TestSuite/TestSuiteEntity.cs | 28 - .../Entities/TestSuite/TestSuiteTypeEntity.cs | 18 - .../Repositories/AccountRepository.cs | 109 ---- .../Repositories/ProblemRepository.cs | 467 -------------- .../Repositories/SubmissionRepository.cs | 583 ------------------ src/Infrastructure/Services/SlugService.cs | 41 -- .../Attributes/GlobalRateLimitAttribute.cs | 14 - .../Attributes/RequireAccountAttribute.cs | 5 - .../Attributes/UserRateLimitAttribute.cs | 14 - src/PublicApi/Authorization/RbacHandler.cs | 30 - .../Authorization/RbacRequirement.cs | 9 - .../Contracts/Account/CreateAccountDto.cs | 3 - .../Account/UpdateProfileSettingsDto.cs | 3 - .../Contracts/Account/UpdateUsernameDto.cs | 3 - .../Contracts/Account/UpsertAccountDto.cs | 3 - .../Submission/CreateSubmissionDto.cs | 3 - .../Controllers/AccountController.cs | 211 ------- .../Controllers/BaseApiController.cs | 50 -- .../Controllers/ProblemController.cs | 173 ------ .../Controllers/SubmissionController.cs | 63 -- .../Extensions/AuthenticationExtensions.cs | 28 - .../Extensions/AuthorizationExtensions.cs | 33 - .../Extensions/MiddlewareExtensions.cs | 11 - .../RateLimitRegistrationExtensions.cs | 123 ---- .../SettingsRegistrationExtensions.cs | 24 - .../Filters/WrapResponseAttribute.cs | 32 - .../Middleware/AccountContextMiddleware.cs | 71 --- .../ApplicationBuilderExtensions.cs | 17 - .../ExceptionMiddlewareExtensions.cs | 87 --- src/PublicApi/Program.cs | 78 --- src/PublicApi/PublicApi.csproj | 23 - src/PublicApi/PublicApi.http | 6 - src/PublicApi/ServiceCollectionExtensions.cs | 30 - src/PublicApi/appsettings.Development.json | 44 -- src/PublicApi/appsettings.json | 26 - .../Accounts/CreateAccountHandlerTests.cs | 78 --- .../Accounts/CreateAccountValidatorTests.cs | 131 ---- .../CreateSubmissionHandlerTests.cs | 73 --- .../CreateSubmissionValidatorTests.cs | 55 -- ...rementSubmissionOutboxesHandlerTests.cs.cs | 92 --- ...crementSubmissionOutboxesValidatorTests.cs | 69 --- ...ProcessSubmissionExecutionsHandlerTests.cs | 37 -- ...ocessSubmissionExecutionsValidatorTests.cs | 9 - .../Common/Pagination/PaginatedResultTests.cs | 113 ---- .../Domain/Accounts/AccountModelTests.cs | 68 -- .../Domain/BaseAuditableModelTests.cs | 77 --- .../ApplicationCore/Domain/BaseModelTests.cs | 31 - .../Languages/LanguageVersionTests.cs | 74 --- .../Languages/ProgrammingLanguageTests.cs | 84 --- .../Domain/Problems/ProblemModelTests.cs | 292 --------- .../Accounts/GetAccountBySubHandlerTests.cs | 99 --- .../GetProfileAggregateHandlerTests.cs | 96 --- .../GetProfileSettingsHandlerTests.cs | 74 --- .../Problems/GetProblemBySlugHandlerTests.cs | 129 ---- .../GetProblemsPageableHandlerTests.cs | 140 ----- .../Services/SubmissionAppServiceTests.cs | 51 -- .../Controllers/ProblemControllerTests.cs | 62 -- tests/UnitTests/UnitTests.csproj | 48 -- 297 files changed, 582 insertions(+), 11080 deletions(-) delete mode 100644 .gitattributes delete mode 100644 .github/dependabot.yml delete mode 100644 .github/labeler.yml delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/labeler.yml delete mode 100644 .github/workflows/master_app-awapi-scrum-centralus-03.yml delete mode 100644 .github/workflows/sonar-scan.yml delete mode 100644 .github/workflows/stale.yml create mode 100644 Algowars.Api/Algowars.Api.csproj create mode 100644 Algowars.Api/Algowars.Api.http create mode 100644 Algowars.Api/Controllers/AccountController.cs create mode 100644 Algowars.Api/Controllers/ProblemController.cs create mode 100644 Algowars.Api/Controllers/SubmissionController.cs create mode 100644 Algowars.Api/Program.cs create mode 100644 Algowars.Api/WeatherForecast.cs create mode 100644 Algowars.Api/appsettings.Development.json create mode 100644 Algowars.Api/appsettings.json create mode 100644 Algowars.Application/Algowars.Application.csproj create mode 100644 Algowars.Application/Class1.cs create mode 100644 Algowars.Domain.Tests/Algowars.Domain.Tests.csproj create mode 100644 Algowars.Domain.Tests/User/Entities/UserTests.cs create mode 100644 Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs create mode 100644 Algowars.Domain/Algowars.Domain.csproj create mode 100644 Algowars.Domain/SeedWork/AggregateRoot.cs create mode 100644 Algowars.Domain/SeedWork/DomainException.cs create mode 100644 Algowars.Domain/SeedWork/Entity.cs create mode 100644 Algowars.Domain/User/Entities/User.cs create mode 100644 Algowars.Domain/User/Exceptions/InvalidUserSubException.cs create mode 100644 Algowars.Domain/User/Exceptions/InvalidUsernameException.cs create mode 100644 Algowars.Domain/User/ValueObjects/Username.cs create mode 100644 Algowars.Infrastructure/Algowars.Infrastructure.csproj create mode 100644 Algowars.Infrastructure/Class1.cs create mode 100644 Algowars.UnitTests/Algowars.UnitTests.csproj delete mode 100644 Algowars.slnx delete mode 100644 LICENSE.txt delete mode 100644 README.md create mode 100644 Server.slnx delete mode 100644 docker-compose.yml delete mode 100644 global.json delete mode 100644 scripts/setup-user-secrets.ps1 delete mode 100644 scripts/setup-user-secrets.sh delete mode 100644 src/ApplicationCore/ApplicationCore.csproj delete mode 100644 src/ApplicationCore/Commands/AbstractCommandHandler.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountCommand.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountHandler.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountValidator.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsCommand.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsHandler.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsValidator.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameCommand.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameHandler.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameValidator.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountCommand.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountHandler.cs delete mode 100644 src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountValidator.cs delete mode 100644 src/ApplicationCore/Commands/ICommand.cs delete mode 100644 src/ApplicationCore/Commands/ICommandHandler.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionCommand.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionHandler.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionValidator.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationCommand.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationHandler.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationValidator.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesCommand.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesHandler.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesValidator.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationCommand.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationHandler.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationValidator.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsCommand.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsHandler.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsValidator.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsCommand.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsHandler.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsValidator.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensCommand.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensHandler.cs delete mode 100644 src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensValidator.cs delete mode 100644 src/ApplicationCore/Common/Pagination/PaginatedResult.cs delete mode 100644 src/ApplicationCore/Common/Pagination/PaginationRequest.cs delete mode 100644 src/ApplicationCore/Common/Pagination/SortDirection.cs delete mode 100644 src/ApplicationCore/DependencyInjection.cs delete mode 100644 src/ApplicationCore/Domain/Accounts/AccountContext.cs delete mode 100644 src/ApplicationCore/Domain/Accounts/AccountModel.cs delete mode 100644 src/ApplicationCore/Domain/BaseAuditableModel.cs delete mode 100644 src/ApplicationCore/Domain/BaseModel.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/CodeBuildResult.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/CodeBuilderContext.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/CodeExecutionContext.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0BatchGetResponse.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0BatchRequest.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0StatusModel.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionRequest.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionResponse.cs delete mode 100644 src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionTokenOnlyResponse.cs delete mode 100644 src/ApplicationCore/Domain/Problems/Languages/LanguageVersion.cs delete mode 100644 src/ApplicationCore/Domain/Problems/Languages/ProgrammingLanguage.cs delete mode 100644 src/ApplicationCore/Domain/Problems/ProblemModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/ProblemSetups/ProblemSetupModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/ProblemStatus.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TagModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TestSuites/HarnessTemplate.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TestSuites/TestCaseExpectedOutputModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TestSuites/TestCaseInputParamModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TestSuites/TestCaseInputValueTypeModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TestSuites/TestCaseModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TestSuites/TestCaseOutputTypeModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TestSuites/TestSuiteModel.cs delete mode 100644 src/ApplicationCore/Domain/Problems/TestSuites/TestSuiteType.cs delete mode 100644 src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxModel.cs delete mode 100644 src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxStatus.cs delete mode 100644 src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxType.cs delete mode 100644 src/ApplicationCore/Domain/Submissions/SubmissionModel.cs delete mode 100644 src/ApplicationCore/Domain/Submissions/SubmissionResult.cs delete mode 100644 src/ApplicationCore/Domain/Submissions/SubmissionStatus.cs delete mode 100644 src/ApplicationCore/Dtos/Accounts/AccountDto.cs delete mode 100644 src/ApplicationCore/Dtos/Accounts/ProfileAggregateDto.cs delete mode 100644 src/ApplicationCore/Dtos/Accounts/ProfileSettingsDto.cs delete mode 100644 src/ApplicationCore/Dtos/Languages/LanguageVersionDto.cs delete mode 100644 src/ApplicationCore/Dtos/Languages/ProgrammingLanguageDto.cs delete mode 100644 src/ApplicationCore/Dtos/Problems/CreateProblemDto.cs delete mode 100644 src/ApplicationCore/Dtos/Problems/ProblemDto.cs delete mode 100644 src/ApplicationCore/Dtos/Problems/ProblemSetupDto.cs delete mode 100644 src/ApplicationCore/Dtos/Problems/ProblemSubmissionDto.cs delete mode 100644 src/ApplicationCore/Dtos/Problems/Tests/TestCaseDto.cs delete mode 100644 src/ApplicationCore/Dtos/Problems/Tests/TestSuiteDto.cs delete mode 100644 src/ApplicationCore/Dtos/Submissions/GetSubmissionsPaginatedRequest.cs delete mode 100644 src/ApplicationCore/Dtos/Submissions/SubmissionDto.cs delete mode 100644 src/ApplicationCore/Dtos/Submissions/SubmissionResultDto.cs delete mode 100644 src/ApplicationCore/Dtos/Submissions/SubmissionStatusDto.cs delete mode 100644 src/ApplicationCore/Dtos/Submissions/SubmissionTestCaseResultDto.cs delete mode 100644 src/ApplicationCore/Interfaces/Clients/IJudge0Client.cs delete mode 100644 src/ApplicationCore/Interfaces/Messaging/IMessagePublisher.cs delete mode 100644 src/ApplicationCore/Interfaces/Repositories/IAccountRepository.cs delete mode 100644 src/ApplicationCore/Interfaces/Repositories/IProblemRepository.cs delete mode 100644 src/ApplicationCore/Interfaces/Repositories/ISubmissionRepository.cs delete mode 100644 src/ApplicationCore/Interfaces/Services/IAccountAppService.cs delete mode 100644 src/ApplicationCore/Interfaces/Services/ICodeBuilderService.cs delete mode 100644 src/ApplicationCore/Interfaces/Services/ICodeExecutionService.cs delete mode 100644 src/ApplicationCore/Interfaces/Services/IExecutionComparisonService.cs delete mode 100644 src/ApplicationCore/Interfaces/Services/IProblemAppService.cs delete mode 100644 src/ApplicationCore/Interfaces/Services/ISlugService.cs delete mode 100644 src/ApplicationCore/Interfaces/Services/ISubmissionAppService.cs delete mode 100644 src/ApplicationCore/Logging/LoggingEventIds.cs delete mode 100644 src/ApplicationCore/Mappings/ProblemMappings.cs delete mode 100644 src/ApplicationCore/Messaging/SubmissionCreatedMessage.cs delete mode 100644 src/ApplicationCore/Messaging/SubmissionEvaluationPollMessage.cs delete mode 100644 src/ApplicationCore/Messaging/SubmissionExecutedMessage.cs delete mode 100644 src/ApplicationCore/Messaging/SubmissionExecutionPollMessage.cs delete mode 100644 src/ApplicationCore/Messaging/SubmissionReadyToEvaluateMessage.cs delete mode 100644 src/ApplicationCore/Queries/Accounts/GetAccountBySub/GetAccountBySubHandler.cs delete mode 100644 src/ApplicationCore/Queries/Accounts/GetAccountBySub/GetAccountBySubQuery.cs delete mode 100644 src/ApplicationCore/Queries/Accounts/GetProfileAggregate/GetProfileAggregateHandler.cs delete mode 100644 src/ApplicationCore/Queries/Accounts/GetProfileAggregate/GetProfileAggregateQuery.cs delete mode 100644 src/ApplicationCore/Queries/Accounts/GetProfileSettings/GetProfileSettingsHandler.cs delete mode 100644 src/ApplicationCore/Queries/Accounts/GetProfileSettings/GetProfileSettingsQuery.cs delete mode 100644 src/ApplicationCore/Queries/IQuery.cs delete mode 100644 src/ApplicationCore/Queries/IQueryHandler.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetAvailableLanguages/GetAvailableLanguagesHandler.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetAvailableLanguages/GetAvailableLanguagesQuery.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetProblemBySlug/GetProblemBySlugHandler.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetProblemBySlug/GetProblemBySlugQuery.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetProblemSetup/GetProblemSetupHandler.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetProblemSetup/GetProblemSetupQuery.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetProblemSetupsForExecution/GetProblemSetupsForExecutionHandler.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetProblemSetupsForExecution/GetProblemSetupsForExecutionQuery.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetProblemsPageable/GetProblemsPageableHandler.cs delete mode 100644 src/ApplicationCore/Queries/Problems/GetProblemsPageable/GetProblemsPageableQuery.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetSolutionsByProblemIdQuery/GetSolutionsByProblemIdHandler.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetSolutionsByProblemIdQuery/GetSolutionsByProblemIdQuery.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetSubmissionOutboxes/GetSubmissionOutboxesHandler.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetSubmissionOutboxes/GetSubmissionOutboxesQuery.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetSubmissionStatus/GetSubmissionStatusHandler.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetSubmissionStatus/GetSubmissionStatusQuery.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetSubmissionsPaginated/GetSubmissionsPaginatedHandler.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetSubmissionsPaginated/GetSubmissionsPaginatedQuery.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetUserSubmissionsByProblemIdQuery/GetUserSubmissionsByProblemIdHandler.cs delete mode 100644 src/ApplicationCore/Queries/Submissions/GetUserSubmissionsByProblemIdQuery/GetUserSubmissionsByProblemIdQuery.cs delete mode 100644 src/ApplicationCore/Services/AccountAppService.cs delete mode 100644 src/ApplicationCore/Services/CodeBuilderService.cs delete mode 100644 src/ApplicationCore/Services/CodeExecutionService.cs delete mode 100644 src/ApplicationCore/Services/ExecutionComparisonService.cs delete mode 100644 src/ApplicationCore/Services/ProblemAppService.cs delete mode 100644 src/ApplicationCore/Services/SubmissionAppService.cs delete mode 100644 src/ApplicationCore/Settings/ConnectionStringsSettings.cs delete mode 100644 src/ApplicationCore/Settings/CorsSettings.cs delete mode 100644 src/ApplicationCore/Settings/ISettings.cs delete mode 100644 src/ApplicationCore/Settings/MediatRSettings.cs delete mode 100644 src/Infrastructure/CodeExecution/Judge0/Judge0Client.cs delete mode 100644 src/Infrastructure/Configuration/Auth0ManagementOptions.cs delete mode 100644 src/Infrastructure/Configuration/Auth0Options.cs delete mode 100644 src/Infrastructure/Configuration/ConnectionStringOptions.cs delete mode 100644 src/Infrastructure/Configuration/CorsOptions.cs delete mode 100644 src/Infrastructure/Configuration/ExecutionEnginesOptions.cs delete mode 100644 src/Infrastructure/Configuration/Judge0Options.cs delete mode 100644 src/Infrastructure/Configuration/LogLevelOptions.cs delete mode 100644 src/Infrastructure/Configuration/LoggingOptions.cs delete mode 100644 src/Infrastructure/Configuration/MessageBusOptions.cs delete mode 100644 src/Infrastructure/DependencyInjection.cs delete mode 100644 src/Infrastructure/Infrastructure.csproj delete mode 100644 src/Infrastructure/Jobs/JobBase.cs delete mode 100644 src/Infrastructure/Jobs/JobHandlers/EvaluateSubmissionHandler.cs delete mode 100644 src/Infrastructure/Jobs/JobHandlers/PollEvaluationHandler.cs delete mode 100644 src/Infrastructure/Jobs/JobHandlers/PollSubmissionExecutionHander.cs delete mode 100644 src/Infrastructure/Jobs/JobHandlers/SubmissionExecutionHandler.cs delete mode 100644 src/Infrastructure/Jobs/JobType.cs delete mode 100644 src/Infrastructure/Mappings/AccountMappings.cs delete mode 100644 src/Infrastructure/Mappings/ProblemMappings.cs delete mode 100644 src/Infrastructure/Mappings/SubmissionMappings.cs delete mode 100644 src/Infrastructure/Messaging/Consumers/SubmissionCreatedConsumer.cs delete mode 100644 src/Infrastructure/Messaging/Consumers/SubmissionEvaluationPollConsumer.cs delete mode 100644 src/Infrastructure/Messaging/Consumers/SubmissionExecutedConsumer.cs delete mode 100644 src/Infrastructure/Messaging/Consumers/SubmissionReadyToEvaluateConsumer.cs delete mode 100644 src/Infrastructure/Messaging/MassTransitMessagePublisher.cs delete mode 100644 src/Infrastructure/Persistence/AppDbContext.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Account/AccountEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/BaseAuditableEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Language/LanguageVersionEngineMappingEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Language/LanguageVersionEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Language/ProgrammingLanguageEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Problem/HarnessTemplateEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Problem/ProblemEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Problem/ProblemHistoryEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Problem/ProblemSetupEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Problem/ProblemStatus.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Problem/TagEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxStatusEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxTypeEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Submission/SubmissionEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Submission/SubmissionResultEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Submission/SubmissionStatusEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/Submission/SubmissionStatusTypeEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/TestSuite/TestCaseEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/TestSuite/TestCaseExpectedOutputEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/TestSuite/TestCaseInputEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/TestSuite/TestCasesInputsValueTypeEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/TestSuite/TestCasesOutputTypeEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/TestSuite/TestSuiteEntity.cs delete mode 100644 src/Infrastructure/Persistence/Entities/TestSuite/TestSuiteTypeEntity.cs delete mode 100644 src/Infrastructure/Repositories/AccountRepository.cs delete mode 100644 src/Infrastructure/Repositories/ProblemRepository.cs delete mode 100644 src/Infrastructure/Repositories/SubmissionRepository.cs delete mode 100644 src/Infrastructure/Services/SlugService.cs delete mode 100644 src/PublicApi/Attributes/GlobalRateLimitAttribute.cs delete mode 100644 src/PublicApi/Attributes/RequireAccountAttribute.cs delete mode 100644 src/PublicApi/Attributes/UserRateLimitAttribute.cs delete mode 100644 src/PublicApi/Authorization/RbacHandler.cs delete mode 100644 src/PublicApi/Authorization/RbacRequirement.cs delete mode 100644 src/PublicApi/Contracts/Account/CreateAccountDto.cs delete mode 100644 src/PublicApi/Contracts/Account/UpdateProfileSettingsDto.cs delete mode 100644 src/PublicApi/Contracts/Account/UpdateUsernameDto.cs delete mode 100644 src/PublicApi/Contracts/Account/UpsertAccountDto.cs delete mode 100644 src/PublicApi/Contracts/Submission/CreateSubmissionDto.cs delete mode 100644 src/PublicApi/Controllers/AccountController.cs delete mode 100644 src/PublicApi/Controllers/BaseApiController.cs delete mode 100644 src/PublicApi/Controllers/ProblemController.cs delete mode 100644 src/PublicApi/Controllers/SubmissionController.cs delete mode 100644 src/PublicApi/Extensions/AuthenticationExtensions.cs delete mode 100644 src/PublicApi/Extensions/AuthorizationExtensions.cs delete mode 100644 src/PublicApi/Extensions/MiddlewareExtensions.cs delete mode 100644 src/PublicApi/Extensions/RateLimitRegistrationExtensions.cs delete mode 100644 src/PublicApi/Extensions/SettingsRegistrationExtensions.cs delete mode 100644 src/PublicApi/Filters/WrapResponseAttribute.cs delete mode 100644 src/PublicApi/Middleware/AccountContextMiddleware.cs delete mode 100644 src/PublicApi/Middleware/ApplicationBuilderExtensions.cs delete mode 100644 src/PublicApi/Middleware/ExceptionMiddlewareExtensions.cs delete mode 100644 src/PublicApi/Program.cs delete mode 100644 src/PublicApi/PublicApi.csproj delete mode 100644 src/PublicApi/PublicApi.http delete mode 100644 src/PublicApi/ServiceCollectionExtensions.cs delete mode 100644 src/PublicApi/appsettings.Development.json delete mode 100644 src/PublicApi/appsettings.json delete mode 100644 tests/UnitTests/ApplicationCore/Commands/Accounts/CreateAccountHandlerTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Commands/Accounts/CreateAccountValidatorTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Commands/Submissions/CreateSubmissionHandlerTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Commands/Submissions/CreateSubmissionValidatorTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxesHandlerTests.cs.cs delete mode 100644 tests/UnitTests/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxesValidatorTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutionsHandlerTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutionsValidatorTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Common/Pagination/PaginatedResultTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Domain/Accounts/AccountModelTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Domain/BaseAuditableModelTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Domain/BaseModelTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Domain/Problems/Languages/LanguageVersionTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Domain/Problems/Languages/ProgrammingLanguageTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Domain/Problems/ProblemModelTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Queries/Accounts/GetAccountBySubHandlerTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Queries/Accounts/GetProfileAggregateHandlerTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Queries/Accounts/GetProfileSettingsHandlerTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Queries/Problems/GetProblemBySlugHandlerTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Queries/Problems/GetProblemsPageableHandlerTests.cs delete mode 100644 tests/UnitTests/ApplicationCore/Services/SubmissionAppServiceTests.cs delete mode 100644 tests/UnitTests/PublicApi/Controllers/ProblemControllerTests.cs delete mode 100644 tests/UnitTests/UnitTests.csproj diff --git a/.editorconfig b/.editorconfig index 849a2dd..4bc7aeb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -121,7 +121,7 @@ csharp_style_prefer_readonly_struct = true csharp_style_prefer_readonly_struct_member = true # Code-block preferences -csharp_prefer_braces = true:error +csharp_prefer_braces = false:none csharp_prefer_simple_using_statement = true:error csharp_prefer_system_threading_lock = true csharp_style_namespace_declarations = file_scoped:error @@ -244,4 +244,4 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case +dotnet_naming_style.begins_with_i.capitalization = pascal_case \ No newline at end of file diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index e91cfb4..0000000 --- a/.gitattributes +++ /dev/null @@ -1,67 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain - -# Use CRLF for C# files -*.cs text eol=crlf -*.csproj text eol=crlf diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index b528813..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "nuget" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - labels: - - "dependencies" \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 9502beb..0000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,19 +0,0 @@ -backend: - - changed-files: - - any-glob-to-any-file: ['src/**', '!src/**/*.test.*'] - -tests: - - changed-files: - - any-glob-to-any-file: ['**/*.Tests/**', '**/*Tests.cs', '**/*Test.cs'] - -infrastructure: - - changed-files: - - any-glob-to-any-file: ['.github/**', 'Dockerfile*', 'docker-compose*', '*.yml', '*.yaml'] - -database: - - changed-files: - - any-glob-to-any-file: ['**/Migrations/**', '**/DbContext*', '**/*.sql'] - -dependencies: - - changed-files: - - any-glob-to-any-file: ['**/*.csproj', '**/Directory.Packages.props', '**/NuGet.Config'] \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8dceb03..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Continuous Integration - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - dotnet-version: ["10.x.x"] - - steps: - - uses: actions/checkout@v6 - - name: Setup dotnet ${{ matrix.dotnet-version }} - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ matrix.dotnet-version }} - - name: Display dotnet version - run: dotnet --version - - - name: Install dependencies - run: dotnet restore - - - name: build - run: dotnet build --no-restore - - - name: Unit Tests - run: dotnet test --no-restore --no-build --collect:"XPlat Code Coverage" --logger trx --results-directory "TestResults-${{ matrix.dotnet-version }}" - - - name: Upload dotnet test results - uses: actions/upload-artifact@v4 - with: - name: dotnet-results-${{ matrix.dotnet-version }} - path: TestResults-${{ matrix.dotnet-version }} - if: ${{ always() }} - - - name: Install dotnet format - run: dotnet tool install -g dotnet-format - - - name: Run dotnet linter - run: dotnet format --verify-no-changes - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - files: "TestResults-${{ matrix.dotnet-version }}/**/coverage.cobertura.xml" - fail_ci_if_error: false - if: ${{ always() }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index 24bb130..0000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Label PRs - -on: - pull_request_target: - types: [opened, synchronize, reopened] - -jobs: - labeler: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/labeler@v5 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - configuration-path: .github/labeler.yml \ No newline at end of file diff --git a/.github/workflows/master_app-awapi-scrum-centralus-03.yml b/.github/workflows/master_app-awapi-scrum-centralus-03.yml deleted file mode 100644 index 07b2c3b..0000000 --- a/.github/workflows/master_app-awapi-scrum-centralus-03.yml +++ /dev/null @@ -1,65 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Deploy - app-awapi-scrum-centralus-03 - -on: - push: - branches: - - master - workflow_dispatch: - -jobs: - build: - runs-on: windows-latest - permissions: - contents: read #This is required for actions/checkout - - steps: - - uses: actions/checkout@v4 - - - name: Set up .NET Core - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.x' - - - name: Build with dotnet - run: dotnet build --configuration Release - - - name: dotnet publish - run: dotnet publish -c Release -o "${{env.DOTNET_ROOT}}/myapp" - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v4 - with: - name: .net-app - path: ${{env.DOTNET_ROOT}}/myapp - - deploy: - runs-on: windows-latest - needs: build - permissions: - id-token: write #This is required for requesting the JWT - contents: read #This is required for actions/checkout - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v4 - with: - name: .net-app - - - name: Login to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_8AB6E917B0CE4368B07BE3E984A7F778 }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_8ADD74451CF544BDAC4CE9049E400B16 }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_61DA8EF64169488485D2CBDB9C3F695C }} - - - name: Deploy to Azure Web App - id: deploy-to-webapp - uses: azure/webapps-deploy@v3 - with: - app-name: 'app-awapi-scrum-centralus-03' - slot-name: 'Production' - package: . - diff --git a/.github/workflows/sonar-scan.yml b/.github/workflows/sonar-scan.yml deleted file mode 100644 index 90ccd03..0000000 --- a/.github/workflows/sonar-scan.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: SonarQube Analysis - -on: - pull_request: - types: [opened, synchronize, reopened] - push: - branches: - - master - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - dotnet-version: ["10.x.x"] - - steps: - - uses: actions/checkout@v6 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ matrix.dotnet-version }} - - - name: Setup JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: zulu - - - name: Install SonarScanner - run: | - dotnet tool update --global dotnet-sonarscanner - echo "$HOME/.dotnet/tools" >> $GITHUB_PATH - - - name: Run SonarScanner and tests - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - dotnet-sonarscanner begin \ - /k:"algowars_server" \ - /o:"algowars" \ - /d:sonar.token="$SONAR_TOKEN" \ - /d:sonar.cs.opencover.reportsPaths="TestResults/coverage.opencover.xml" \ - /d:sonar.cs.vstest.reportsPaths="TestResults/*.trx" - - dotnet restore - dotnet build --no-restore - dotnet test --no-restore --no-build --collect:"XPlat Code Coverage" --logger trx --results-directory TestResults - - dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" - - name: Upload dotnet test results - uses: actions/upload-artifact@v4 - with: - name: dotnet-results - path: TestResults - if: ${{ always() }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index d92b0dd..0000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Mark stale issues and PRs - -on: - schedule: - - cron: '0 0 * * *' - workflow_dispatch: - -jobs: - stale: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/stale@v9 - with: - stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs.' - stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs.' - close-issue-message: 'This issue was closed automatically after 14 days of inactivity.' - close-pr-message: 'This PR was closed automatically after 14 days of inactivity.' - days-before-stale: 60 - days-before-close: 14 - stale-issue-label: 'stale' - stale-pr-label: 'stale' - exempt-issue-labels: 'pinned,security,P1' - exempt-pr-labels: 'pinned,security' \ No newline at end of file diff --git a/Algowars.Api/Algowars.Api.csproj b/Algowars.Api/Algowars.Api.csproj new file mode 100644 index 0000000..2e73281 --- /dev/null +++ b/Algowars.Api/Algowars.Api.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/Algowars.Api/Algowars.Api.http b/Algowars.Api/Algowars.Api.http new file mode 100644 index 0000000..503e3e7 --- /dev/null +++ b/Algowars.Api/Algowars.Api.http @@ -0,0 +1,6 @@ +@Algowars.Api_HostAddress = http://localhost:5041 + +GET {{Algowars.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Algowars.Api/Controllers/AccountController.cs b/Algowars.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..0ed9b15 --- /dev/null +++ b/Algowars.Api/Controllers/AccountController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Algowars.Api.Controllers; + +[ApiController] +[Route("api/v{version:apiVersion}/[controller]")] +public class AccountController : Controller +{ + [HttpPut] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult UpdateAccount() + { + return Ok(); + } +} diff --git a/Algowars.Api/Controllers/ProblemController.cs b/Algowars.Api/Controllers/ProblemController.cs new file mode 100644 index 0000000..d1ddfb9 --- /dev/null +++ b/Algowars.Api/Controllers/ProblemController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Algowars.Api.Controllers; + +public class ProblemController : Controller +{ + public IActionResult Index() + { + return View(); + } +} diff --git a/Algowars.Api/Controllers/SubmissionController.cs b/Algowars.Api/Controllers/SubmissionController.cs new file mode 100644 index 0000000..005a1e7 --- /dev/null +++ b/Algowars.Api/Controllers/SubmissionController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Algowars.Api.Controllers; + +public class SubmissionController : Controller +{ + public IActionResult Index() + { + return View(); + } +} diff --git a/Algowars.Api/Program.cs b/Algowars.Api/Program.cs new file mode 100644 index 0000000..666a9c5 --- /dev/null +++ b/Algowars.Api/Program.cs @@ -0,0 +1,23 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Algowars.Api/WeatherForecast.cs b/Algowars.Api/WeatherForecast.cs new file mode 100644 index 0000000..6f01752 --- /dev/null +++ b/Algowars.Api/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace Algowars.Api +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/Algowars.Api/appsettings.Development.json b/Algowars.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Algowars.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Algowars.Api/appsettings.json b/Algowars.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Algowars.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Algowars.Application/Algowars.Application.csproj b/Algowars.Application/Algowars.Application.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/Algowars.Application/Algowars.Application.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Algowars.Application/Class1.cs b/Algowars.Application/Class1.cs new file mode 100644 index 0000000..279cebc --- /dev/null +++ b/Algowars.Application/Class1.cs @@ -0,0 +1,7 @@ +namespace Algowars.Application +{ + public class Class1 + { + + } +} diff --git a/Algowars.Domain.Tests/Algowars.Domain.Tests.csproj b/Algowars.Domain.Tests/Algowars.Domain.Tests.csproj new file mode 100644 index 0000000..61dad3f --- /dev/null +++ b/Algowars.Domain.Tests/Algowars.Domain.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/Algowars.Domain.Tests/User/Entities/UserTests.cs b/Algowars.Domain.Tests/User/Entities/UserTests.cs new file mode 100644 index 0000000..f01d65e --- /dev/null +++ b/Algowars.Domain.Tests/User/Entities/UserTests.cs @@ -0,0 +1,145 @@ +using Algowars.Domain.User.Exceptions; +using Algowars.Domain.User.ValueObjects; +using UserEntity = Algowars.Domain.User.Entities.User; + +namespace Algowars.Domain.Tests.User.Entities; + +public class UserTests +{ + private static readonly Username ValidUsername = new("alice"); + private const string ValidSub = "auth0|abc123"; + + [Test] + public void Constructor_ValidArguments_CreatesUser() + { + var user = new UserEntity(ValidUsername, ValidSub); + + Assert.Multiple(() => + { + Assert.That(user.Username, Is.EqualTo(ValidUsername)); + Assert.That(user.Sub, Is.EqualTo(ValidSub)); + Assert.That(user.Id, Is.Not.EqualTo(Guid.Empty)); + }); + } + + [Test] + public void Constructor_NullUsername_ThrowsInvalidUsernameException() + { + Assert.Throws(() => new UserEntity(null!, ValidSub)); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + public void Constructor_EmptyOrWhitespaceSub_ThrowsInvalidUserSubException(string sub) + { + Assert.Throws(() => new UserEntity(ValidUsername, sub)); + } + + [Test] + public void Constructor_SetsSubCorrectly() + { + var user = new UserEntity(ValidUsername, ValidSub); + Assert.That(user.Sub, Is.EqualTo(ValidSub)); + } + + [Test] + public void Constructor_SubWithSpecialCharacters_Succeeds() + { + var user = new UserEntity(ValidUsername, "google-oauth2|abc.123-xyz"); + Assert.That(user.Sub, Is.EqualTo("google-oauth2|abc.123-xyz")); + } + + [Test] + public void Constructor_GeneratesNonEmptyId() + { + var user = new UserEntity(ValidUsername, ValidSub); + Assert.That(user.Id, Is.Not.EqualTo(Guid.Empty)); + } + + [Test] + public void Constructor_GeneratesUniqueIds() + { + var user1 = new UserEntity(ValidUsername, ValidSub); + var user2 = new UserEntity(ValidUsername, ValidSub); + + Assert.That(user1.Id, Is.Not.EqualTo(user2.Id)); + } + + [Test] + public void Equals_SameInstance_IsEqual() + { + var user = new UserEntity(ValidUsername, ValidSub); + Assert.That(user, Is.EqualTo(user)); + } + + [Test] + public void Equals_DifferentInstances_AreNotEqual() + { + var user1 = new UserEntity(ValidUsername, ValidSub); + var user2 = new UserEntity(ValidUsername, ValidSub); + + Assert.That(user1, Is.Not.EqualTo(user2)); + } + + [Test] + public void Equals_Null_IsNotEqual() + { + var user = new UserEntity(ValidUsername, ValidSub); + Assert.That(user.Equals(null), Is.False); + } + + [Test] + public void GetHashCode_SameUser_ReturnsSameHash() + { + var user = new UserEntity(ValidUsername, ValidSub); + Assert.That(user.GetHashCode(), Is.EqualTo(user.GetHashCode())); + } + + [Test] + public void GetHashCode_DifferentUsers_ReturnDifferentHashes() + { + var user1 = new UserEntity(ValidUsername, ValidSub); + var user2 = new UserEntity(ValidUsername, ValidSub); + + Assert.That(user1.GetHashCode(), Is.Not.EqualTo(user2.GetHashCode())); + } + + [Test] + public void ChangeUsername_ValidUsername_UpdatesUsername() + { + var user = new UserEntity(ValidUsername, ValidSub); + var newUsername = new Username("bob"); + + user.ChangeUsername(newUsername); + + Assert.That(user.Username, Is.EqualTo(newUsername)); + } + + [Test] + public void ChangeUsername_DoesNotAffectOtherProperties() + { + var user = new UserEntity(ValidUsername, ValidSub); + var originalId = user.Id; + string originalSub = user.Sub; + + user.ChangeUsername(new Username("bob")); + + Assert.Multiple(() => + { + Assert.That(user.Id, Is.EqualTo(originalId)); + Assert.That(user.Sub, Is.EqualTo(originalSub)); + }); + } + + [Test] + public void ChangeUsername_MultipleTimes_UsesLatestValue() + { + var user = new UserEntity(ValidUsername, ValidSub); + + user.ChangeUsername(new Username("bob")); + user.ChangeUsername(new Username("charlie")); + + Assert.That(user.Username.Value, Is.EqualTo("charlie")); + } +} diff --git a/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs b/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs new file mode 100644 index 0000000..d0f1e73 --- /dev/null +++ b/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs @@ -0,0 +1,121 @@ +using Algowars.Domain.User.Exceptions; +using Algowars.Domain.User.ValueObjects; + +namespace Algowars.Domain.Tests.User.ValueObjects; + +public class UsernameTests +{ + [Test] + public void Constructor_ValidValue_SetsValue() + { + var username = new Username("alice"); + Assert.That(username.Value, Is.EqualTo("alice")); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(null)] + public void Constructor_EmptyOrWhitespace_ThrowsInvalidUsernameException(string? value) + { + Assert.Throws(() => new Username(value!)); + } + + [Test] + public void Constructor_AtMinLength_Succeeds() + { + string atMin = new('a', Username.MinLength); + Assert.DoesNotThrow(() => new Username(atMin)); + } + + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string atMax = new('a', Username.MaxLength); + Assert.DoesNotThrow(() => new Username(atMax)); + } + + [Test] + public void Constructor_OneBelowMinLength_ThrowsInvalidUsernameException() + { + string tooShort = new('a', Username.MinLength - 1); + Assert.Throws(() => new Username(tooShort)); + } + + [Test] + public void Constructor_OneAboveMaxLength_ThrowsInvalidUsernameException() + { + string tooLong = new('a', Username.MaxLength + 1); + Assert.Throws(() => new Username(tooLong)); + } + + [Test] + public void Constructor_FarExceedsMaxLength_ThrowsInvalidUsernameException() + { + string veryLong = new('a', 1000); + Assert.Throws(() => new Username(veryLong)); + } + + [TestCase("alice123")] + [TestCase("ALICE")] + [TestCase("Alice")] + [TestCase("123")] + [TestCase("a")] + public void Constructor_ValidCharacterVariations_Succeeds(string value) + { + Assert.DoesNotThrow(() => new Username(value)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new Username("alice"); + var b = new Username("alice"); + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void Equality_DifferentValue_AreNotEqual() + { + var a = new Username("alice"); + var b = new Username("bob"); + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_DifferentCasing_AreNotEqual() + { + var a = new Username("alice"); + var b = new Username("Alice"); + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameReference_AreEqual() + { + var a = new Username("alice"); + Assert.That(a, Is.EqualTo(a)); + } + + [Test] + public void ImplicitOperator_ReturnsStringValue() + { + var username = new Username("alice"); + string value = username; + Assert.That(value, Is.EqualTo("alice")); + } + + [Test] + public void ToString_ReturnsValue() + { + var username = new Username("alice"); + Assert.That(username.ToString(), Is.EqualTo("alice")); + } + + [Test] + public void ToString_MatchesImplicitOperator() + { + var username = new Username("alice"); + Assert.That(username.ToString(), Is.EqualTo((string)username)); + } +} \ No newline at end of file diff --git a/Algowars.Domain/Algowars.Domain.csproj b/Algowars.Domain/Algowars.Domain.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/Algowars.Domain/Algowars.Domain.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Algowars.Domain/SeedWork/AggregateRoot.cs b/Algowars.Domain/SeedWork/AggregateRoot.cs new file mode 100644 index 0000000..9d92b00 --- /dev/null +++ b/Algowars.Domain/SeedWork/AggregateRoot.cs @@ -0,0 +1,5 @@ +namespace Algowars.Domain.SeedWork; + +public abstract class AggregateRoot : Entity +{ +} \ No newline at end of file diff --git a/Algowars.Domain/SeedWork/DomainException.cs b/Algowars.Domain/SeedWork/DomainException.cs new file mode 100644 index 0000000..3700ce4 --- /dev/null +++ b/Algowars.Domain/SeedWork/DomainException.cs @@ -0,0 +1,5 @@ +namespace Algowars.Domain.SeedWork; + +public abstract class DomainException(string message) : Exception(message) +{ +} \ No newline at end of file diff --git a/Algowars.Domain/SeedWork/Entity.cs b/Algowars.Domain/SeedWork/Entity.cs new file mode 100644 index 0000000..ba45d81 --- /dev/null +++ b/Algowars.Domain/SeedWork/Entity.cs @@ -0,0 +1,32 @@ +namespace Algowars.Domain.SeedWork; + +public abstract class Entity +{ + public Guid Id { get; protected set; } + + protected Entity() + { + Id = Guid.NewGuid(); + } + + public override bool Equals(object? obj) + { + if (obj is not Entity other) return false; + if (ReferenceEquals(this, other)) return true; + if (GetType() != other.GetType()) return false; + return Id == other.Id; + } + + public override int GetHashCode() => Id.GetHashCode(); + + public static bool operator ==(Entity? left, Entity? right) + { + if (left is null) return right is null; + return left.Equals(right); + } + + public static bool operator !=(Entity? left, Entity? right) + { + return !(left == right); + } +} diff --git a/Algowars.Domain/User/Entities/User.cs b/Algowars.Domain/User/Entities/User.cs new file mode 100644 index 0000000..0f8c5da --- /dev/null +++ b/Algowars.Domain/User/Entities/User.cs @@ -0,0 +1,20 @@ + +using Algowars.Domain.SeedWork; +using Algowars.Domain.User.Exceptions; +using Algowars.Domain.User.ValueObjects; + +namespace Algowars.Domain.User.Entities; + +public sealed class User(Username username, string sub) : AggregateRoot +{ + public Username Username { get; private set; } = username ?? throw new InvalidUsernameException("Username is required."); + + public string Sub { get; private set; } = string.IsNullOrWhiteSpace(sub) + ? throw new InvalidUserSubException() + : sub; + + public void ChangeUsername(Username username) + { + Username = username; + } +} diff --git a/Algowars.Domain/User/Exceptions/InvalidUserSubException.cs b/Algowars.Domain/User/Exceptions/InvalidUserSubException.cs new file mode 100644 index 0000000..a8110fe --- /dev/null +++ b/Algowars.Domain/User/Exceptions/InvalidUserSubException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.User.Exceptions; + +public sealed class InvalidUserSubException : DomainException +{ + public InvalidUserSubException() + : base("User sub is required.") { } +} \ No newline at end of file diff --git a/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs b/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs new file mode 100644 index 0000000..a1e262d --- /dev/null +++ b/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.User.Exceptions; + +public sealed class InvalidUsernameException : DomainException +{ + public InvalidUsernameException(string reason) + : base($"Username is invalid: {reason}") { } +} \ No newline at end of file diff --git a/Algowars.Domain/User/ValueObjects/Username.cs b/Algowars.Domain/User/ValueObjects/Username.cs new file mode 100644 index 0000000..c0967b9 --- /dev/null +++ b/Algowars.Domain/User/ValueObjects/Username.cs @@ -0,0 +1,25 @@ +using Algowars.Domain.User.Exceptions; + +namespace Algowars.Domain.User.ValueObjects; + +public sealed record Username +{ + public static readonly int MaxLength = 20; + public static readonly int MinLength = 1; + + public string Value { get; } + + public Username(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidUsernameException("Username cannot be null or whitespace."); + + if (value.Length < MinLength || value.Length > MaxLength) + throw new InvalidUsernameException($"Username must be between {MinLength} and {MaxLength} characters."); + + Value = value; + } + + public static implicit operator string(Username username) => username.Value; + public override string ToString() => Value; +} \ No newline at end of file diff --git a/Algowars.Infrastructure/Algowars.Infrastructure.csproj b/Algowars.Infrastructure/Algowars.Infrastructure.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/Algowars.Infrastructure/Algowars.Infrastructure.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Algowars.Infrastructure/Class1.cs b/Algowars.Infrastructure/Class1.cs new file mode 100644 index 0000000..c25b286 --- /dev/null +++ b/Algowars.Infrastructure/Class1.cs @@ -0,0 +1,7 @@ +namespace Algowars.Infrastructure +{ + public class Class1 + { + + } +} diff --git a/Algowars.UnitTests/Algowars.UnitTests.csproj b/Algowars.UnitTests/Algowars.UnitTests.csproj new file mode 100644 index 0000000..1cb0eda --- /dev/null +++ b/Algowars.UnitTests/Algowars.UnitTests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + diff --git a/Algowars.slnx b/Algowars.slnx deleted file mode 100644 index 4d6aaf5..0000000 --- a/Algowars.slnx +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md deleted file mode 100644 index 9f54250..0000000 --- a/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Algowars Server - -[![ci](https://github.com/algowars/server/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/algowars/server/actions/workflows/ci.yml) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=algowars_server&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=algowars_server) -[![codecov](https://codecov.io/gh/algowars/server/branch/master/graph/badge.svg)](https://codecov.io/gh/algowars/server) - -## Local user secrets setup - -The scripts below populate user secrets for `src/PublicApi/PublicApi.csproj`. -CORS is configured in `src/PublicApi/appsettings.Development.json`. - -### PowerShell (Windows) - -From the repository root: - -```powershell -./scripts/setup-user-secrets.ps1 -``` - -### Bash (Linux/macOS/Git Bash) - -From the repository root: - -```bash -./scripts/setup-user-secrets.sh -``` - -### Prompt behavior - -- Press `Enter` to use the shown default value. -- Type `skip` to leave a key unchanged. -- Set `MessageBus:Transport` to `RabbitMQ` or `AzureServiceBus`. - - `RabbitMQ`: prompts only RabbitMQ settings. - - `AzureServiceBus`: prompts only Azure Service Bus connection string. diff --git a/Server.slnx b/Server.slnx new file mode 100644 index 0000000..14f6221 --- /dev/null +++ b/Server.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 918a191..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -services: - rabbitmq: - image: rabbitmq:3-management - container_name: algowars-rabbitmq - ports: - - "5672:5672" # AMQP - - "15672:15672" # Management UI (http://localhost:15672, guest/guest) - environment: - RABBITMQ_DEFAULT_USER: guest - RABBITMQ_DEFAULT_PASS: guest - volumes: - - rabbitmq_data:/var/lib/rabbitmq - healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - postgres: - image: postgres:17 - container_name: algowars-postgres - ports: - - "5432:5432" - environment: - POSTGRES_USER: myuser - POSTGRES_PASSWORD: mypassword - POSTGRES_DB: algowars - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U myuser"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - rabbitmq_data: - postgres_data: diff --git a/global.json b/global.json deleted file mode 100644 index f72210c..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "10.0.100", - "rollForward": "latestFeature" - } -} \ No newline at end of file diff --git a/scripts/setup-user-secrets.ps1 b/scripts/setup-user-secrets.ps1 deleted file mode 100644 index b0a8ed9..0000000 --- a/scripts/setup-user-secrets.ps1 +++ /dev/null @@ -1,125 +0,0 @@ -$ErrorActionPreference = 'Stop' - -$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path -$repoRoot = Split-Path -Parent $scriptRoot -$projectPath = Join-Path $repoRoot 'src/PublicApi/PublicApi.csproj' -$skipToken = '__SKIP__' - -if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { - Write-Error 'dotnet CLI was not found in PATH.' - exit 1 -} - -if (-not (Test-Path $projectPath)) { - Write-Error "Could not find project at $projectPath" - exit 1 -} - -function Read-SettingValue { - param( - [Parameter(Mandatory = $true)] - [string]$Prompt, - [string]$Default = '', - [switch]$Secret - ) - - $suffix = if ($Default -ne '') { " [$Default]" } else { '' } - - if ($Secret) { - $secureInput = Read-Host -Prompt ($Prompt + $suffix) -AsSecureString - $plain = [System.Net.NetworkCredential]::new('', $secureInput).Password - - if ($plain -ieq 'skip') { - return $skipToken - } - - if ([string]::IsNullOrWhiteSpace($plain)) { - return $Default - } - - return $plain - } - - $value = Read-Host -Prompt ($Prompt + $suffix) - - if ($value -ieq 'skip') { - return $skipToken - } - - if ([string]::IsNullOrWhiteSpace($value)) { - return $Default - } - - return $value -} - -function Set-UserSecret { - param( - [Parameter(Mandatory = $true)] - [string]$Key, - [Parameter(Mandatory = $true)] - [string]$Value - ) - - if ($Value -eq $skipToken -or [string]::IsNullOrEmpty($Value)) { - Write-Host "Skipping $Key" -ForegroundColor Yellow - return - } - - dotnet user-secrets set --project "$projectPath" "$Key" "$Value" | Out-Null - Write-Host "Set $Key" -ForegroundColor Green -} - -Write-Host 'Configure PublicApi user secrets from appsettings values.' -ForegroundColor Cyan -Write-Host "Press Enter to accept a default value when shown, or type 'skip' to leave a key unchanged." -ForegroundColor Cyan -Write-Host '' - -$commonSettings = @( - @{ Key = 'Auth0:Audience'; Prompt = 'Auth0 Audience'; Default = '' }, - @{ Key = 'Auth0:Domain'; Prompt = 'Auth0 Domain'; Default = '' }, - @{ Key = 'Auth0:Management:ClientId'; Prompt = 'Auth0 Management ClientId'; Default = '' }, - @{ Key = 'ConnectionStrings:DefaultConnection'; Prompt = 'Connection string'; Default = 'Host=localhost;Port=5432;Database=algowars;Username=myuser;Password=mypassword' }, - @{ Key = 'ExecutionEngines:Judge0:Enabled'; Prompt = 'Judge0 Enabled (true/false)'; Default = 'true' }, - @{ Key = 'ExecutionEngines:Judge0:RunWorker'; Prompt = 'Judge0 RunWorker (true/false)'; Default = 'true' }, - @{ Key = 'ExecutionEngines:Judge0:BaseUrl'; Prompt = 'Judge0 BaseUrl'; Default = '' }, - @{ Key = 'ExecutionEngines:Judge0:ApiKey'; Prompt = 'Judge0 ApiKey'; Default = ''; Secret = $true }, - @{ Key = 'ExecutionEngines:Judge0:Host'; Prompt = 'Judge0 Host'; Default = '' }, - @{ Key = 'ExecutionEngines:Judge0:ShouldWait'; Prompt = 'Judge0 ShouldWait (true/false)'; Default = 'false' }, - @{ Key = 'ExecutionEngines:Judge0:IsEncoded'; Prompt = 'Judge0 IsEncoded (true/false)'; Default = 'true' }, - @{ Key = 'ExecutionEngines:Judge0:DefaultTimeoutInSeconds'; Prompt = 'Judge0 timeout in seconds'; Default = '10' } -) - -$rabbitMqSettings = @( - @{ Key = 'MessageBus:RabbitMQ:Host'; Prompt = 'RabbitMQ Host'; Default = 'localhost' }, - @{ Key = 'MessageBus:RabbitMQ:VirtualHost'; Prompt = 'RabbitMQ VirtualHost'; Default = '/' }, - @{ Key = 'MessageBus:RabbitMQ:Username'; Prompt = 'RabbitMQ Username'; Default = 'guest' }, - @{ Key = 'MessageBus:RabbitMQ:Password'; Prompt = 'RabbitMQ Password'; Default = 'guest'; Secret = $true } -) - -$azureServiceBusSettings = @( - @{ Key = 'MessageBus:AzureServiceBus:ConnectionString'; Prompt = 'Azure Service Bus ConnectionString'; Default = ''; Secret = $true } -) - -foreach ($setting in $commonSettings) { - $value = Read-SettingValue -Prompt $setting.Prompt -Default $setting.Default -Secret:([bool]($setting.Secret)) - Set-UserSecret -Key $setting.Key -Value $value -} - -$transport = Read-SettingValue -Prompt 'MessageBus Transport (RabbitMQ/AzureServiceBus)' -Default 'RabbitMQ' -Set-UserSecret -Key 'MessageBus:Transport' -Value $transport - -if ($transport -ieq 'AzureServiceBus') { - foreach ($setting in $azureServiceBusSettings) { - $value = Read-SettingValue -Prompt $setting.Prompt -Default $setting.Default -Secret:([bool]($setting.Secret)) - Set-UserSecret -Key $setting.Key -Value $value - } -} -else { - foreach ($setting in $rabbitMqSettings) { - $value = Read-SettingValue -Prompt $setting.Prompt -Default $setting.Default -Secret:([bool]($setting.Secret)) - Set-UserSecret -Key $setting.Key -Value $value - } -} - -Write-Host '' -Write-Host 'Done. User secrets have been applied to PublicApi.' -ForegroundColor Cyan diff --git a/scripts/setup-user-secrets.sh b/scripts/setup-user-secrets.sh deleted file mode 100644 index 7472b19..0000000 --- a/scripts/setup-user-secrets.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(dirname "$SCRIPT_DIR")" -PROJECT_PATH="$REPO_ROOT/src/PublicApi/PublicApi.csproj" -SKIP_TOKEN="__SKIP__" - -if ! command -v dotnet >/dev/null 2>&1; then - echo "dotnet CLI was not found in PATH." >&2 - exit 1 -fi - -if [ ! -f "$PROJECT_PATH" ]; then - echo "Could not find project at $PROJECT_PATH" >&2 - exit 1 -fi - -read_value() { - local prompt="$1" - local default_value="${2-}" - local secret="${3-false}" - local value="" - - if [ -n "$default_value" ]; then - prompt="$prompt [$default_value]" - fi - - if [ "$secret" = "true" ]; then - read -r -s -p "$prompt: " value - echo >&2 - else - read -r -p "$prompt: " value - fi - - if [ "$value" = "skip" ] || [ "$value" = "SKIP" ]; then - printf '%s' "$SKIP_TOKEN" - return - fi - - if [ -z "$value" ]; then - value="$default_value" - fi - - printf '%s' "$value" -} - -set_secret() { - local key="$1" - local value="$2" - - if [ "$value" = "$SKIP_TOKEN" ] || [ -z "$value" ]; then - echo "Skipping $key" - return - fi - - dotnet user-secrets set --project "$PROJECT_PATH" "$key" "$value" >/dev/null - echo "Set $key" -} - -echo "Configure PublicApi user secrets from appsettings values." -echo "Press Enter to accept a default value when shown, or type 'skip' to leave a key unchanged." -echo - -KEYS=( - "Auth0:Audience" - "Auth0:Domain" - "Auth0:Management:ClientId" - "ConnectionStrings:DefaultConnection" - "ExecutionEngines:Judge0:Enabled" - "ExecutionEngines:Judge0:RunWorker" - "ExecutionEngines:Judge0:BaseUrl" - "ExecutionEngines:Judge0:ApiKey" - "ExecutionEngines:Judge0:Host" - "ExecutionEngines:Judge0:ShouldWait" - "ExecutionEngines:Judge0:IsEncoded" - "ExecutionEngines:Judge0:DefaultTimeoutInSeconds" -) - -PROMPTS=( - "Auth0 Audience" - "Auth0 Domain" - "Auth0 Management ClientId" - "Connection string" - "Judge0 Enabled (true/false)" - "Judge0 RunWorker (true/false)" - "Judge0 BaseUrl" - "Judge0 ApiKey" - "Judge0 Host" - "Judge0 ShouldWait (true/false)" - "Judge0 IsEncoded (true/false)" - "Judge0 timeout in seconds" -) - -DEFAULTS=( - "" - "" - "" - "Host=localhost;Port=5432;Database=algowars;Username=myuser;Password=mypassword" - "true" - "true" - "" - "" - "" - "false" - "true" - "10" -) - -SECRETS=( - "false" - "false" - "false" - "false" - "false" - "false" - "false" - "true" - "false" - "false" - "false" - "false" -) - -for i in "${!KEYS[@]}"; do - value="$(read_value "${PROMPTS[$i]}" "${DEFAULTS[$i]}" "${SECRETS[$i]}")" - set_secret "${KEYS[$i]}" "$value" -done - -transport="$(read_value "MessageBus Transport (RabbitMQ/AzureServiceBus)" "RabbitMQ")" -set_secret "MessageBus:Transport" "$transport" - -if [ "$transport" = "AzureServiceBus" ]; then - value="$(read_value "Azure Service Bus ConnectionString" "" "true")" - set_secret "MessageBus:AzureServiceBus:ConnectionString" "$value" -else - value="$(read_value "RabbitMQ Host" "localhost")" - set_secret "MessageBus:RabbitMQ:Host" "$value" - - value="$(read_value "RabbitMQ VirtualHost" "/")" - set_secret "MessageBus:RabbitMQ:VirtualHost" "$value" - - value="$(read_value "RabbitMQ Username" "guest")" - set_secret "MessageBus:RabbitMQ:Username" "$value" - - value="$(read_value "RabbitMQ Password" "guest" "true")" - set_secret "MessageBus:RabbitMQ:Password" "$value" -fi - -echo -echo "Done. User secrets have been applied to PublicApi." diff --git a/src/ApplicationCore/ApplicationCore.csproj b/src/ApplicationCore/ApplicationCore.csproj deleted file mode 100644 index 51b41c7..0000000 --- a/src/ApplicationCore/ApplicationCore.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - net10.0 - enable - enable - - - - - - - - - - - diff --git a/src/ApplicationCore/Commands/AbstractCommandHandler.cs b/src/ApplicationCore/Commands/AbstractCommandHandler.cs deleted file mode 100644 index 7df1496..0000000 --- a/src/ApplicationCore/Commands/AbstractCommandHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Ardalis.Result; -using FluentValidation; - -namespace ApplicationCore.Commands; - -public abstract class AbstractCommandHandler( - IValidator? validator = null -) : ICommandHandler - where TCommand : ICommand -{ - private readonly IValidator? _validator = validator; - - public async Task> Handle(TCommand request, CancellationToken cancellationToken) - { - if (_validator is not null) - { - var validationResult = await _validator.ValidateAsync(request, cancellationToken); - if (!validationResult.IsValid) - { - var errors = validationResult - .Errors.Select(e => - { - string identifier = e.FormattedMessagePlaceholderValues - .TryGetValue("PropertyName", out object? displayName) - ? displayName?.ToString() ?? e.PropertyName - : e.PropertyName; - - return new ValidationError(identifier, e.ErrorMessage); - }) - .ToList(); - - return Result.Invalid(errors); - } - } - - return await HandleValidated(request, cancellationToken); - } - - protected abstract Task> HandleValidated( - TCommand request, - CancellationToken cancellationToken - ); -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountCommand.cs b/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountCommand.cs deleted file mode 100644 index 914b48d..0000000 --- a/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountCommand.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ApplicationCore.Commands.Accounts.CreateAccount; - -public sealed record CreateAccountCommand(string Username, string Sub, string ImageUrl) - : ICommand; \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountHandler.cs b/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountHandler.cs deleted file mode 100644 index 3bf3ab4..0000000 --- a/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountHandler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Repositories; -using ApplicationCore.Logging; -using Ardalis.Result; -using FluentValidation; -using Microsoft.Extensions.Logging; - -namespace ApplicationCore.Commands.Accounts.CreateAccount; - -public sealed partial class CreateAccountHandler( - IAccountRepository accounts, - ILogger logger, - IValidator validator -) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated( - CreateAccountCommand request, - CancellationToken cancellationToken - ) - { - var id = Guid.NewGuid(); - - var account = new AccountModel - { - Id = id, - Username = request.Username, - Sub = request.Sub, - ImageUrl = request.ImageUrl, - LastModifiedById = null, - }; - - try - { - await accounts.AddAsync(account, cancellationToken); - } - catch (Exception ex) - { - LogCreateFailed(logger, request.Username, request.Sub, ex.Message); - return Result.Error("Unexpected error creating account."); - } - - LogCreated(logger, id, request.Sub); - return Result.Success(id); - } - - [LoggerMessage( - EventId = LoggingEventIds.Accounts.Created, - Level = LogLevel.Information, - Message = "Created account {accountId} for sub {sub}" - )] - private static partial void LogCreated(ILogger logger, Guid accountId, string sub); - - [LoggerMessage( - EventId = LoggingEventIds.Accounts.CreateFailed, - Level = LogLevel.Error, - Message = "Failed to create account for {username}/{sub}. DB message: {dbMessage}" - )] - private static partial void LogCreateFailed( - ILogger logger, - string username, - string sub, - string dbMessage - ); -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountValidator.cs b/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountValidator.cs deleted file mode 100644 index c9b7900..0000000 --- a/src/ApplicationCore/Commands/Accounts/CreateAccount/CreateAccountValidator.cs +++ /dev/null @@ -1,44 +0,0 @@ -using ApplicationCore.Interfaces.Repositories; -using FluentValidation; - -namespace ApplicationCore.Commands.Accounts.CreateAccount; - -public sealed class CreateAccountValidator : AbstractValidator -{ - public CreateAccountValidator(IAccountRepository accounts) - { - RuleFor(x => x.Username) - .NotEmpty() - .MaximumLength(16) - .Must(IsValidUsername) - .WithMessage("Username contains invalid characters") - .MustAsync( - async (username, ct) => await accounts.GetByUsernameAsync(username, ct) is null - ) - .WithMessage("Username already exists"); - - RuleFor(x => x.Sub) - .NotEmpty() - .MustAsync(async (sub, ct) => await accounts.GetBySubAsync(sub, ct) is null) - .WithMessage("Account already exists"); - - RuleFor(x => x.ImageUrl) - .Must(IsValidUrl) - .When(x => !string.IsNullOrWhiteSpace(x.ImageUrl)) - .WithMessage("ImageUrl must be a valid URL"); - - RuleFor(x => x) - .MustAsync( - async (cmd, ct) => - await accounts.GetByUsernameOrSubAsync(cmd.Username, cmd.Sub, ct) is null - ) - .WithMessage("Username already exists"); - } - - private static bool IsValidUsername(string username) => - username.All(c => char.IsLetterOrDigit(c) || c is '_' or '-'); - - private static bool IsValidUrl(string url) => - Uri.TryCreate(url, UriKind.Absolute, out var uri) - && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsCommand.cs b/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsCommand.cs deleted file mode 100644 index 552b7c8..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace ApplicationCore.Commands.Accounts.UpdateProfileSettings; - -public sealed record UpdateProfileSettingsCommand(Guid AccountId, string? Bio) : ICommand; - -public sealed record UpdateProfileSettingsResult(string? Bio); \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsHandler.cs b/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsHandler.cs deleted file mode 100644 index 371ad46..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; - -namespace ApplicationCore.Commands.Accounts.UpdateProfileSettings; - -public sealed class UpdateProfileSettingsHandler( - IAccountRepository accounts, - IValidator validator -) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated( - UpdateProfileSettingsCommand request, - CancellationToken cancellationToken - ) - { - var account = await accounts.GetByIdAsync(request.AccountId, cancellationToken); - - if (account is null) - { - return Result.NotFound(); - } - - await accounts.UpdateAboutAsync(account.Id, request.Bio, cancellationToken); - - return Result.Success(new UpdateProfileSettingsResult(request.Bio)); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsValidator.cs b/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsValidator.cs deleted file mode 100644 index fa00ef1..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpdateProfileSettings/UpdateProfileSettingsValidator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Accounts.UpdateProfileSettings; - -public sealed class UpdateProfileSettingsValidator : AbstractValidator -{ - public UpdateProfileSettingsValidator() - { - RuleFor(x => x.AccountId) - .NotEmpty(); - - RuleFor(x => x.Bio) - .MaximumLength(255) - .WithName("Bio"); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameCommand.cs b/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameCommand.cs deleted file mode 100644 index 6384a5d..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace ApplicationCore.Commands.Accounts.UpdateUsername; - -public sealed record UpdateUsernameCommand(Guid AccountId, string NewUsername, DateTime? UsernameLastChangedAt) : ICommand; - -public sealed record UpdateUsernameResult(Guid Id, string Username, DateTime UsernameLastChangedAt); \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameHandler.cs b/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameHandler.cs deleted file mode 100644 index 59c1df3..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; - -namespace ApplicationCore.Commands.Accounts.UpdateUsername; - -public sealed class UpdateUsernameHandler( - IAccountRepository accounts, - IValidator validator -) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated( - UpdateUsernameCommand request, - CancellationToken cancellationToken - ) - { - AccountModel? account = await accounts.GetByIdAsync(request.AccountId, cancellationToken); - - if (account is null) - { - return Result.NotFound(); - } - - bool usernameTaken = await accounts.ExistsByUsernameAsync(request.NewUsername, cancellationToken); - - if (usernameTaken) - { - return Result.Invalid(new ValidationError("Username", "Username is already taken.")); - } - - account.ChangeUsername(request.NewUsername); - - await accounts.UpdateUsernameAsync(account.Id, account.Username, account.UsernameLastChangedAt!.Value, cancellationToken); - - return Result.Success(new UpdateUsernameResult( - account.Id, - account.Username, - account.UsernameLastChangedAt!.Value - )); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameValidator.cs b/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameValidator.cs deleted file mode 100644 index d24e58a..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpdateUsername/UpdateUsernameValidator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Accounts.UpdateUsername; - -public sealed class UpdateUsernameValidator : AbstractValidator -{ - private const int CooldownDays = 30; - - public UpdateUsernameValidator() - { - RuleFor(x => x.AccountId) - .NotEmpty(); - - RuleFor(x => x.NewUsername) - .NotEmpty() - .MinimumLength(3) - .MaximumLength(36) - .Matches(@"^[a-zA-Z0-9_-]+$") - .WithName("Username") - .WithMessage("Username may only contain letters, numbers, underscores, and hyphens."); - - RuleFor(x => x.UsernameLastChangedAt) - .Must(lastChanged => - lastChanged is null || - (DateTime.UtcNow - lastChanged.Value).TotalDays >= CooldownDays - ) - .WithMessage($"Username can only be changed once every {CooldownDays} days."); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountCommand.cs b/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountCommand.cs deleted file mode 100644 index 748f14e..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace ApplicationCore.Commands.Accounts.UpsertAccount; - -public sealed record UpsertAccountCommand(string Sub, string? ImageUrl) : ICommand; - -public sealed record AccountUpsertResult(Guid Id, string Username, string? ImageUrl, DateTime CreatedOn); \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountHandler.cs b/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountHandler.cs deleted file mode 100644 index 8522418..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountHandler.cs +++ /dev/null @@ -1,65 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using Bogus; -using FluentValidation; - -namespace ApplicationCore.Commands.Accounts.UpsertAccount; - -public sealed class UpsertAccountHandler( - IAccountRepository accounts, - IValidator validator -) : AbstractCommandHandler(validator) -{ - private static readonly Faker Faker = new(); - - protected override async Task> HandleValidated( - UpsertAccountCommand request, - CancellationToken cancellationToken - ) - { - AccountModel? existing = await accounts.GetBySubAsync(request.Sub, cancellationToken); - - if (existing is not null) - { - await accounts.UpdateImageUrlAsync(existing.Id, request.ImageUrl, cancellationToken); - - return Result.Success(new AccountUpsertResult( - existing.Id, - existing.Username, - request.ImageUrl, - existing.CreatedOn - )); - } - - Guid id = Guid.NewGuid(); - string username = await GenerateUniqueUsernameAsync(accounts, cancellationToken); - - AccountModel account = new() - { - Id = id, - Username = username, - Sub = request.Sub, - ImageUrl = request.ImageUrl, - LastModifiedById = null, - }; - - await accounts.AddAsync(account, cancellationToken); - - return Result.Success(new AccountUpsertResult(id, username, request.ImageUrl, DateTime.UtcNow)); - } - - private static async Task GenerateUniqueUsernameAsync( - IAccountRepository accounts, - CancellationToken cancellationToken - ) - { - string usernameBase = $"{Faker.Hacker.Adjective()}_{Faker.Hacker.Noun()}" - .ToLowerInvariant() - .Replace(" ", "_"); - - int count = await accounts.CountByUsernameBaseAsync(usernameBase, cancellationToken); - - return count == 0 ? usernameBase : $"{usernameBase}_{count}"; - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountValidator.cs b/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountValidator.cs deleted file mode 100644 index 030b029..0000000 --- a/src/ApplicationCore/Commands/Accounts/UpsertAccount/UpsertAccountValidator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Accounts.UpsertAccount; - -public sealed class UpsertAccountValidator : AbstractValidator -{ - public UpsertAccountValidator() - { - RuleFor(x => x.Sub) - .NotEmpty() - .MaximumLength(255); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/ICommand.cs b/src/ApplicationCore/Commands/ICommand.cs deleted file mode 100644 index 2ff105e..0000000 --- a/src/ApplicationCore/Commands/ICommand.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Ardalis.Result; -using MediatR; - -namespace ApplicationCore.Commands; - -public interface ICommand : IRequest> { } - -public interface ICommand : IRequest { } \ No newline at end of file diff --git a/src/ApplicationCore/Commands/ICommandHandler.cs b/src/ApplicationCore/Commands/ICommandHandler.cs deleted file mode 100644 index 6ae6b17..0000000 --- a/src/ApplicationCore/Commands/ICommandHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Ardalis.Result; -using MediatR; - -namespace ApplicationCore.Commands; - -public interface ICommandHandler - : IRequestHandler> - where TCommand : ICommand -{ } - -public interface ICommandHandler : IRequestHandler - where TCommand : ICommand -{ } \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionCommand.cs b/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionCommand.cs deleted file mode 100644 index 8f875dc..0000000 --- a/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionCommand.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ApplicationCore.Commands.Submissions.CreateSubmission; - -public sealed record CreateSubmissionCommand(int ProblemSetupId, string Code, Guid CreatedById) - : ICommand; \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionHandler.cs b/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionHandler.cs deleted file mode 100644 index c5ed93a..0000000 --- a/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionHandler.cs +++ /dev/null @@ -1,66 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Interfaces.Messaging; -using ApplicationCore.Interfaces.Repositories; -using ApplicationCore.Logging; -using ApplicationCore.Messaging; -using Ardalis.Result; -using FluentValidation; -using Microsoft.Extensions.Logging; - -namespace ApplicationCore.Commands.Submissions.CreateSubmission; - -public sealed partial class CreateSubmissionHandler( - ISubmissionRepository submissionRepository, - IMessagePublisher messagePublisher, - IValidator validator, - ILogger logger -) : AbstractCommandHandler(validator) -{ - private readonly ILogger _logger = logger; - protected override async Task> HandleValidated( - CreateSubmissionCommand request, - CancellationToken cancellationToken - ) - { - var submission = new SubmissionModel - { - Id = Guid.NewGuid(), - Code = request.Code, - ProblemSetupId = request.ProblemSetupId, - CreatedOn = DateTime.UtcNow, - CreatedById = request.CreatedById, - }; - - try - { - var outboxId = await submissionRepository.SaveAsync(submission, cancellationToken); - - await messagePublisher.PublishAsync( - new SubmissionCreatedMessage { SubmissionId = submission.Id, OutboxId = outboxId }, - cancellationToken - ); - } - catch (Exception ex) - { - LogCreateFailed(submission.Id, request.ProblemSetupId, request.CreatedById, ex); - return Result.Error("Unexpected error creating submission."); - } - - LogCreated(submission.Id, request.ProblemSetupId, request.CreatedById); - return Result.Success(submission.Id); - } - - [LoggerMessage( - EventId = LoggingEventIds.Submissions.Created, - Level = LogLevel.Information, - Message = "Submission {submissionId} created for setup {problemSetupId} by account {createdById}" - )] - private partial void LogCreated(Guid submissionId, int problemSetupId, Guid createdById); - - [LoggerMessage( - EventId = LoggingEventIds.Submissions.CreateFailed, - Level = LogLevel.Error, - Message = "Failed to create submission for setup {problemSetupId} by account {createdById} (attempted id {submissionId})" - )] - private partial void LogCreateFailed(Guid submissionId, int problemSetupId, Guid createdById, Exception ex); -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionValidator.cs b/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionValidator.cs deleted file mode 100644 index 04977c8..0000000 --- a/src/ApplicationCore/Commands/Submissions/CreateSubmission/CreateSubmissionValidator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Submissions.CreateSubmission; - -public sealed class CreateSubmissionValidator : AbstractValidator -{ - public CreateSubmissionValidator() - { - RuleFor(x => x.Code).NotEmpty().MaximumLength(50_000); - - RuleFor(x => x.ProblemSetupId).GreaterThan(0); - - RuleFor(x => x.CreatedById).NotEmpty(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationCommand.cs b/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationCommand.cs deleted file mode 100644 index a8e9f59..0000000 --- a/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MediatR; - -namespace ApplicationCore.Commands.Submissions.FinalizeEvaluation; - -public sealed record FinalizeEvaluationCommand(IEnumerable OutboxIds, DateTime Now) - : ICommand; \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationHandler.cs b/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationHandler.cs deleted file mode 100644 index 18b5bc2..0000000 --- a/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.FinalizeEvaluation; - -public sealed class FinalizeEvaluationHandler( - ISubmissionRepository submissionRepository, - IValidator validator -) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated( - FinalizeEvaluationCommand request, - CancellationToken cancellationToken - ) - { - try - { - await submissionRepository.FinalizeEvaluationAsync( - request.OutboxIds, - request.Now, - cancellationToken - ); - - return Result.Success(); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationValidator.cs b/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationValidator.cs deleted file mode 100644 index ec3ee27..0000000 --- a/src/ApplicationCore/Commands/Submissions/FinalizeEvaluation/FinalizeEvaluationValidator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Submissions.FinalizeEvaluation; - -public sealed class FinalizeEvaluationValidator : AbstractValidator -{ - public FinalizeEvaluationValidator() - { - RuleFor(x => x.OutboxIds).NotNull(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesCommand.cs b/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesCommand.cs deleted file mode 100644 index a523f6f..0000000 --- a/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesCommand.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediatR; - -namespace ApplicationCore.Commands.Submissions.IncrementSubmissionOutboxes; - -public sealed record IncrementSubmissionOutboxesCommand( - IEnumerable OutboxIds, - DateTime Timestamp -) : ICommand; \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesHandler.cs b/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesHandler.cs deleted file mode 100644 index 8058b18..0000000 --- a/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.IncrementSubmissionOutboxes; - -public sealed class IncrementSubmissionOutboxesHandler( - ISubmissionRepository submissionRepository, - IValidator validator -) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated( - IncrementSubmissionOutboxesCommand request, - CancellationToken cancellationToken - ) - { - try - { - await submissionRepository.IncrementOutboxesCountAsync( - request.OutboxIds, - request.Timestamp, - cancellationToken - ); - - return Result.Success(); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesValidator.cs b/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesValidator.cs deleted file mode 100644 index 82657da..0000000 --- a/src/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxes/IncrementSubmissionOutboxesValidator.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Submissions.IncrementSubmissionOutboxes; - -public sealed class IncrementSubmissionOutboxesValidator - : AbstractValidator -{ - public IncrementSubmissionOutboxesValidator() - { - RuleFor(x => x.OutboxIds).NotEmpty(); - - RuleFor(x => x.Timestamp).LessThanOrEqualTo(DateTime.UtcNow); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationCommand.cs b/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationCommand.cs deleted file mode 100644 index e767bd5..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.ProcessEvaluation; - -public sealed record ProcessEvaluationCommand(IEnumerable Submissions) - : ICommand; \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationHandler.cs b/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationHandler.cs deleted file mode 100644 index 6f19025..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.ProcessEvaluation; - -public sealed class ProcessEvaluationHandler( - ISubmissionRepository submissionRepository, - IValidator validator -) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated( - ProcessEvaluationCommand request, - CancellationToken cancellationToken - ) - { - try - { - await submissionRepository.ProcessEvaluationAsync( - request.Submissions, - cancellationToken - ); - - return Result.Success(); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationValidator.cs b/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationValidator.cs deleted file mode 100644 index 1f3236a..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessEvaluation/ProcessEvaluationValidator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Submissions.ProcessEvaluation; - -public sealed class ProcessEvaluationValidator : AbstractValidator -{ - public ProcessEvaluationValidator() - { - RuleFor(x => x.Submissions).NotNull(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsCommand.cs b/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsCommand.cs deleted file mode 100644 index 65f91f8..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.ProcessPollingSubmissionExecutions; - -public sealed record ProcessPollingSubmissionExecutionsCommand(IEnumerable Submissions) : ICommand; \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsHandler.cs b/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsHandler.cs deleted file mode 100644 index d8d079c..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsHandler.cs +++ /dev/null @@ -1,17 +0,0 @@ -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.ProcessPollingSubmissionExecutions; - -public sealed class ProcessPollingSubmissionExecutionsHandler(ISubmissionRepository submissionRepository, IValidator validator) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated(ProcessPollingSubmissionExecutionsCommand request, CancellationToken cancellationToken) - { - await submissionRepository.ProcessPollingSubmissionExecutionsAsync( - request.Submissions, cancellationToken); - - return Result.Success(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsValidator.cs b/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsValidator.cs deleted file mode 100644 index c04c4a3..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessPollingSubmissionExecutions/ProcessPollingSubmissionExecutionsValidator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Submissions.ProcessPollingSubmissionExecutions; - -public class ProcessPollingSubmissionExecutionsValidator : AbstractValidator -{ - public ProcessPollingSubmissionExecutionsValidator() - { - RuleFor(x => x.Submissions).NotNull(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsCommand.cs b/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsCommand.cs deleted file mode 100644 index 4168009..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.ProcessSubmissionExecutions; - -public sealed record ProcessSubmissionExecutionsCommand(IEnumerable Submissions) - : ICommand; \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsHandler.cs b/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsHandler.cs deleted file mode 100644 index 4b33816..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.ProcessSubmissionExecutions; - -public sealed class ProcessSubmissionExecutionsHandler( - ISubmissionRepository submissionRepository, - IValidator validator -) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated( - ProcessSubmissionExecutionsCommand request, - CancellationToken cancellationToken - ) - { - try - { - await submissionRepository.ProcessSubmissionInitializationAsync( - request.Submissions, - cancellationToken - ); - - return Result.Success(); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsValidator.cs b/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsValidator.cs deleted file mode 100644 index 179de42..0000000 --- a/src/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutions/ProcessSubmissionExecutionsValidator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentValidation; -using System; -using System.Collections.Generic; -using System.Text; - -namespace ApplicationCore.Commands.Submissions.ProcessSubmissionExecutions; - -public sealed class ProcessSubmissionExecutionsValidator - : AbstractValidator -{ - public ProcessSubmissionExecutionsValidator() - { - RuleFor(x => x.Submissions).NotNull(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensCommand.cs b/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensCommand.cs deleted file mode 100644 index 3c57aa3..0000000 --- a/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.SaveExecutionTokens; - -public sealed record SaveExecutionTokensCommand(IEnumerable Submissions) - : ICommand; \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensHandler.cs b/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensHandler.cs deleted file mode 100644 index 1b3a8b3..0000000 --- a/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; -using MediatR; - -namespace ApplicationCore.Commands.Submissions.SaveExecutionTokens; - -public sealed class SaveExecutionTokensHandler( - ISubmissionRepository submissionRepository, - IValidator validator -) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated( - SaveExecutionTokensCommand request, - CancellationToken cancellationToken - ) - { - try - { - await submissionRepository.SaveExecutionTokensAsync( - request.Submissions, - cancellationToken - ); - - return Result.Success(); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensValidator.cs b/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensValidator.cs deleted file mode 100644 index 8307f72..0000000 --- a/src/ApplicationCore/Commands/Submissions/SaveExecutionTokens/SaveExecutionTokensValidator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FluentValidation; - -namespace ApplicationCore.Commands.Submissions.SaveExecutionTokens; - -public sealed class SaveExecutionTokensValidator : AbstractValidator -{ - public SaveExecutionTokensValidator() - { - RuleFor(x => x.Submissions).NotNull(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Common/Pagination/PaginatedResult.cs b/src/ApplicationCore/Common/Pagination/PaginatedResult.cs deleted file mode 100644 index 0e94b53..0000000 --- a/src/ApplicationCore/Common/Pagination/PaginatedResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace ApplicationCore.Common.Pagination; - -public sealed class PaginatedResult -{ - public required IReadOnlyList Results { get; init; } - public required int Total { get; init; } - public required int Page { get; init; } - public required int Size { get; init; } - - private int TotalPages => Size > 0 && Total > 0 ? (int)Math.Ceiling((double)Total / Size) : 0; - public bool HasPrevious => Page > 1; - public bool HasNext => Page < TotalPages; - public int Offset => (Page - 1) * Size; -} \ No newline at end of file diff --git a/src/ApplicationCore/Common/Pagination/PaginationRequest.cs b/src/ApplicationCore/Common/Pagination/PaginationRequest.cs deleted file mode 100644 index ac3203e..0000000 --- a/src/ApplicationCore/Common/Pagination/PaginationRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ApplicationCore.Common.Pagination; - -public sealed class PaginationRequest -{ - public required int Page { get; init; } - - public required int Size { get; init; } - - public string? Query { get; init; } - - public DateTime Timestamp { get; init; } = DateTime.UtcNow; - public SortDirection Direction { get; init; } = SortDirection.Desc; -} \ No newline at end of file diff --git a/src/ApplicationCore/Common/Pagination/SortDirection.cs b/src/ApplicationCore/Common/Pagination/SortDirection.cs deleted file mode 100644 index d1b70ae..0000000 --- a/src/ApplicationCore/Common/Pagination/SortDirection.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Common.Pagination; - -public enum SortDirection -{ - Asc, - Desc, -} \ No newline at end of file diff --git a/src/ApplicationCore/DependencyInjection.cs b/src/ApplicationCore/DependencyInjection.cs deleted file mode 100644 index a01885b..0000000 --- a/src/ApplicationCore/DependencyInjection.cs +++ /dev/null @@ -1,32 +0,0 @@ -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Mappings; -using ApplicationCore.Services; -using FluentValidation; -using Mapster; -using MediatR; -using Microsoft.Extensions.DependencyInjection; -using System.Reflection; - -namespace ApplicationCore; - -public static class DependencyInjection -{ - public static IServiceCollection AddApplicationCore(this IServiceCollection services) - { - var assembly = Assembly.GetExecutingAssembly(); - - services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly((assembly))); - services.AddValidatorsFromAssembly(assembly); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - TypeAdapterConfig.GlobalSettings.Scan(typeof(ProblemMappings).Assembly); - - return services; - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Accounts/AccountContext.cs b/src/ApplicationCore/Domain/Accounts/AccountContext.cs deleted file mode 100644 index 4a67348..0000000 --- a/src/ApplicationCore/Domain/Accounts/AccountContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -using ApplicationCore.Dtos.Accounts; - -namespace ApplicationCore.Domain.Accounts; - -public interface IAccountContext -{ - AccountDto? Account { get; set; } -} - -public sealed class AccountContext : IAccountContext -{ - public AccountDto? Account { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Accounts/AccountModel.cs b/src/ApplicationCore/Domain/Accounts/AccountModel.cs deleted file mode 100644 index a3b8968..0000000 --- a/src/ApplicationCore/Domain/Accounts/AccountModel.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace ApplicationCore.Domain.Accounts; - -public sealed class AccountModel : BaseModel -{ - private string _username = string.Empty; - - public string? Sub { get; init; } - - public string? About { get; set; } - - public required string Username - { - get => _username; - init => _username = value; - } - - public string? PreviousUsername { get; private set; } - - public DateTime? UsernameLastChangedAt { get; private set; } - - public string? ImageUrl { get; init; } - - public DateTime CreatedOn { get; init; } - - public DateTime? LastModifiedOn { get; set; } - - public Guid? LastModifiedById { get; set; } - - public void ChangeUsername(string newUsername) - { - if (Username == newUsername) - { - return; - } - - PreviousUsername = Username; - _username = newUsername; - UsernameLastChangedAt = DateTime.UtcNow; - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/BaseAuditableModel.cs b/src/ApplicationCore/Domain/BaseAuditableModel.cs deleted file mode 100644 index aeb1643..0000000 --- a/src/ApplicationCore/Domain/BaseAuditableModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using ApplicationCore.Domain.Accounts; - -namespace ApplicationCore.Domain; - -public abstract class BaseAuditableModel : BaseModel - where TId : notnull -{ - public DateTime CreatedOn { get; set; } - - public Guid? CreatedById { get; set; } - - public AccountModel? CreatedBy { get; init; } - - public DateTime? LastModifiedOn { get; set; } - - public Guid LastModifiedById { get; set; } - - public DateTime? DeletedOn { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/BaseModel.cs b/src/ApplicationCore/Domain/BaseModel.cs deleted file mode 100644 index f8bc46e..0000000 --- a/src/ApplicationCore/Domain/BaseModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Domain; - -public abstract class BaseModel - where TId : notnull -{ - public TId? Id { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/CodeBuildResult.cs b/src/ApplicationCore/Domain/CodeExecution/CodeBuildResult.cs deleted file mode 100644 index 5825160..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/CodeBuildResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace ApplicationCore.Domain.CodeExecution; - -public sealed class CodeBuildResult -{ - public required string FinalCode { get; init; } - - public required string FunctionName { get; init; } - - public string? Inputs { get; init; } - - public string? ExpectedOutput { get; init; } - - public string? InputTypeName { get; init; } - - public int LanguageId { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/CodeBuilderContext.cs b/src/ApplicationCore/Domain/CodeExecution/CodeBuilderContext.cs deleted file mode 100644 index 7d5294e..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/CodeBuilderContext.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ApplicationCore.Domain.Problems.TestSuites; - -namespace ApplicationCore.Domain.CodeExecution; - -public sealed class CodeBuilderContext -{ - public required string Code { get; init; } - - public required string Template { get; init; } - - public required string FunctionName { get; init; } - - public int? LanguageVersionId { get; init; } - - public int? Judge0LanguageId { get; init; } - - public required IEnumerable Inputs { get; init; } - - public required TestCaseExpectedOutputModel ExpectedOutput { get; init; } - - public string? InputTypeName { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/CodeExecutionContext.cs b/src/ApplicationCore/Domain/CodeExecution/CodeExecutionContext.cs deleted file mode 100644 index 272c690..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/CodeExecutionContext.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ApplicationCore.Domain.Problems.ProblemSetups; - -namespace ApplicationCore.Domain.CodeExecution; - -public sealed class CodeExecutionContext -{ - public Guid? SubmissionId { get; set; } - - public required ProblemSetupModel Setup { get; set; } - - public required string Code { get; set; } - - public required IEnumerable BuiltResults { get; set; } = []; - - public required Guid CreatedById { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0BatchGetResponse.cs b/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0BatchGetResponse.cs deleted file mode 100644 index 50c8a9d..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0BatchGetResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ApplicationCore.Domain.CodeExecution.Judge0; - -public sealed record Judge0BatchGetResponse -{ - [JsonPropertyName("submissions")] - public required List Submissions { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0BatchRequest.cs b/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0BatchRequest.cs deleted file mode 100644 index 893414a..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0BatchRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ApplicationCore.Domain.CodeExecution.Judge0; - -public sealed class Judge0BatchRequest -{ - [JsonPropertyName("submissions")] - public required IEnumerable Submissions { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0StatusModel.cs b/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0StatusModel.cs deleted file mode 100644 index 1fcd488..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0StatusModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Domain.CodeExecution.Judge0; - -public record Judge0StatusModel -{ - public int Id { get; init; } - public string? Description { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionRequest.cs b/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionRequest.cs deleted file mode 100644 index 6e449a0..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ApplicationCore.Domain.CodeExecution.Judge0; - -public class Judge0SubmissionRequest -{ - [JsonPropertyName("language_id")] - public int LanguageId { get; init; } - - [JsonPropertyName("source_code")] - public string? SourceCode { get; init; } - - [JsonPropertyName("stdin")] - public string? StdIn { get; init; } - - [JsonPropertyName("expected_output")] - public string? ExpectedOutput { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionResponse.cs b/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionResponse.cs deleted file mode 100644 index 77dd638..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionResponse.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ApplicationCore.Domain.CodeExecution.Judge0; - -public sealed record Judge0SubmissionResponse -{ - [JsonPropertyName("token")] - public Guid Token { get; init; } - - [JsonPropertyName("source_code")] - public string? SourceCode { get; init; } - - [JsonPropertyName("language_id")] - public int LanguageId { get; init; } - - [JsonPropertyName("stdin")] - public string? Stdin { get; init; } - - [JsonPropertyName("expected_output")] - public string? ExpectedOutput { get; init; } - - [JsonPropertyName("stdout")] - public string? Stdout { get; init; } - - [JsonPropertyName("time")] - public string? Time { get; init; } - - [JsonPropertyName("memory")] - public int? Memory { get; init; } - - [JsonPropertyName("stderr")] - public string? Stderr { get; init; } - - [JsonPropertyName("status")] - public required Judge0StatusModel Status { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionTokenOnlyResponse.cs b/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionTokenOnlyResponse.cs deleted file mode 100644 index 672b605..0000000 --- a/src/ApplicationCore/Domain/CodeExecution/Judge0/Judge0SubmissionTokenOnlyResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ApplicationCore.Domain.CodeExecution.Judge0; - -public class Judge0SubmissionTokenOnlyResponse -{ - [JsonPropertyName("token")] - public Guid Token { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/Languages/LanguageVersion.cs b/src/ApplicationCore/Domain/Problems/Languages/LanguageVersion.cs deleted file mode 100644 index 96e1cd2..0000000 --- a/src/ApplicationCore/Domain/Problems/Languages/LanguageVersion.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ApplicationCore.Domain.Problems.Languages; - -public sealed class LanguageVersion : BaseModel -{ - public required string Version { get; init; } - - public string? InitialCode { get; init; } - - public int ProgrammingLanguageId { get; init; } - public ProgrammingLanguage? ProgrammingLanguage { get; init; } - - public int? Judge0LanguageId { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/Languages/ProgrammingLanguage.cs b/src/ApplicationCore/Domain/Problems/Languages/ProgrammingLanguage.cs deleted file mode 100644 index 0182015..0000000 --- a/src/ApplicationCore/Domain/Problems/Languages/ProgrammingLanguage.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ApplicationCore.Domain.Problems.Languages; - -public class ProgrammingLanguage : BaseAuditableModel -{ - public required string Name { get; init; } - - public bool IsArchived { get; init; } - - public IEnumerable Versions { get; init; } = []; -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/ProblemModel.cs b/src/ApplicationCore/Domain/Problems/ProblemModel.cs deleted file mode 100644 index 3c06ce5..0000000 --- a/src/ApplicationCore/Domain/Problems/ProblemModel.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Domain.Problems.ProblemSetups; - -namespace ApplicationCore.Domain.Problems; - -public sealed class ProblemModel : BaseAuditableModel -{ - public required string Title { get; init; } - - public required string Slug { get; init; } - - public required string Question { get; init; } - - public required IEnumerable Tags { get; init; } - - public int Difficulty { get; init; } - - public ProblemStatus Status { get; init; } - - public ICollection ProblemSetups { get; init; } = []; - - public int Version { get; init; } - - public IEnumerable GetAvailableLanguages() - { - if (ProblemSetups.Count == 0) - { - return []; - } - - return ProblemSetups - .Where(s => s.LanguageVersion?.ProgrammingLanguage != null) - .GroupBy(s => s.LanguageVersion!.ProgrammingLanguage!) - .Select(g => - { - var language = g.Key; - - var versions = g.Select(s => s.LanguageVersion!) - .GroupBy(v => v.Id) - .Select(vg => vg.First()) - .OrderBy(v => v.Version) - .ToList(); - - return new ProgrammingLanguage - { - Id = language.Id, - Name = language.Name, - IsArchived = language.IsArchived, - Versions = versions, - }; - }) - .DistinctBy(s => s.Id) - .ToList(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/ProblemSetups/ProblemSetupModel.cs b/src/ApplicationCore/Domain/Problems/ProblemSetups/ProblemSetupModel.cs deleted file mode 100644 index 9a1277a..0000000 --- a/src/ApplicationCore/Domain/Problems/ProblemSetups/ProblemSetupModel.cs +++ /dev/null @@ -1,27 +0,0 @@ -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Domain.Problems.TestSuites; - -namespace ApplicationCore.Domain.Problems.ProblemSetups; - -public sealed class ProblemSetupModel -{ - public required int Id { get; init; } - - public required Guid ProblemId { get; init; } - - public ProblemModel? Problem { get; init; } - - public required string InitialCode { get; init; } - - public string? FunctionName { get; init; } - - public required int LanguageVersionId { get; init; } - - public LanguageVersion? LanguageVersion { get; init; } - - public int Version { get; init; } - - public HarnessTemplate? HarnessTemplate { get; init; } - - public IEnumerable TestSuites { get; init; } = []; -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/ProblemStatus.cs b/src/ApplicationCore/Domain/Problems/ProblemStatus.cs deleted file mode 100644 index d453f3e..0000000 --- a/src/ApplicationCore/Domain/Problems/ProblemStatus.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ApplicationCore.Domain.Problems; - -public enum ProblemStatus -{ - Draft = 1, - Pending = 2, - Published = 3, - Rejected = 4, -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TagModel.cs b/src/ApplicationCore/Domain/Problems/TagModel.cs deleted file mode 100644 index 0bf3410..0000000 --- a/src/ApplicationCore/Domain/Problems/TagModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ApplicationCore.Domain.Problems; - -public sealed class TagModel -{ - public int Id { get; init; } - - public required string Value { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TestSuites/HarnessTemplate.cs b/src/ApplicationCore/Domain/Problems/TestSuites/HarnessTemplate.cs deleted file mode 100644 index a251a16..0000000 --- a/src/ApplicationCore/Domain/Problems/TestSuites/HarnessTemplate.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ApplicationCore.Domain.Problems.TestSuites; - -public sealed class HarnessTemplate -{ - public required int Id { get; init; } - - public required string Template { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseExpectedOutputModel.cs b/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseExpectedOutputModel.cs deleted file mode 100644 index 1362a03..0000000 --- a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseExpectedOutputModel.cs +++ /dev/null @@ -1,16 +0,0 @@ - - -namespace ApplicationCore.Domain.Problems.TestSuites; - -public sealed class TestCaseExpectedOutputModel -{ - public int Id { get; set; } - - public int TestCaseId { get; set; } - - public string Value { get; set; } - - public int OutputValueTypeId { get; set; } - - public TestCaseOutputTypeModel OutputType { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseInputParamModel.cs b/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseInputParamModel.cs deleted file mode 100644 index 246b0c2..0000000 --- a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseInputParamModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ApplicationCore.Domain.Problems.TestSuites; - -public sealed class TestCaseInputParamModel -{ - public int Id { get; set; } - - public required string Value { get; set; } - - public int TestCaseInputValueTypeId { get; set; } - - public required TestCaseInputValueTypeModel InputType { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseInputValueTypeModel.cs b/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseInputValueTypeModel.cs deleted file mode 100644 index e23c819..0000000 --- a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseInputValueTypeModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ApplicationCore.Domain.Problems.TestSuites; - -public sealed class TestCaseInputValueTypeModel -{ - public int Id { get; set; } - - public required string Name { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseModel.cs b/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseModel.cs deleted file mode 100644 index 34c217e..0000000 --- a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ApplicationCore.Domain.Problems.TestSuites; - -public sealed class TestCaseModel -{ - public required int Id { get; init; } - - public IEnumerable Inputs { get; init; } - - public required TestCaseExpectedOutputModel ExpectedOutput { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseOutputTypeModel.cs b/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseOutputTypeModel.cs deleted file mode 100644 index 635eab9..0000000 --- a/src/ApplicationCore/Domain/Problems/TestSuites/TestCaseOutputTypeModel.cs +++ /dev/null @@ -1,10 +0,0 @@ - - -namespace ApplicationCore.Domain.Problems.TestSuites; - -public sealed class TestCaseOutputTypeModel -{ - public int Id { get; set; } - - public string? Name { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TestSuites/TestSuiteModel.cs b/src/ApplicationCore/Domain/Problems/TestSuites/TestSuiteModel.cs deleted file mode 100644 index c9675fb..0000000 --- a/src/ApplicationCore/Domain/Problems/TestSuites/TestSuiteModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ApplicationCore.Domain.Problems.TestSuites; - -public sealed class TestSuiteModel : BaseAuditableModel -{ - public string? Name { get; init; } - - public string? Description { get; init; } - - public TestSuiteType TestSuiteType { get; init; } - - public required IEnumerable TestCases { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Problems/TestSuites/TestSuiteType.cs b/src/ApplicationCore/Domain/Problems/TestSuites/TestSuiteType.cs deleted file mode 100644 index df16d96..0000000 --- a/src/ApplicationCore/Domain/Problems/TestSuites/TestSuiteType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Domain.Problems.TestSuites; - -public enum TestSuiteType -{ - Public = 1, - Hidden = 2, -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxModel.cs b/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxModel.cs deleted file mode 100644 index 11b01f0..0000000 --- a/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace ApplicationCore.Domain.Submissions.Outboxes; - -public sealed class SubmissionOutboxModel -{ - public Guid Id { get; init; } - - public required Guid SubmissionId { get; init; } - - public required SubmissionModel Submission { get; init; } - - public required SubmissionOutboxType Type { get; init; } - - public required SubmissionOutboxStatus Status { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxStatus.cs b/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxStatus.cs deleted file mode 100644 index 96f6428..0000000 --- a/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxStatus.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ApplicationCore.Domain.Submissions.Outboxes; - -public enum SubmissionOutboxStatus -{ - Pending = 1, - Processing = 2, - Completed = 3, - Failed = 4, -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxType.cs b/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxType.cs deleted file mode 100644 index f79ff6b..0000000 --- a/src/ApplicationCore/Domain/Submissions/Outboxes/SubmissionOutboxType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ApplicationCore.Domain.Submissions.Outboxes; - -public enum SubmissionOutboxType -{ - Initialized = 1, - Execute, - PollExecution, - Evaluate, - EvaluationPoll -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Submissions/SubmissionModel.cs b/src/ApplicationCore/Domain/Submissions/SubmissionModel.cs deleted file mode 100644 index 5001c3a..0000000 --- a/src/ApplicationCore/Domain/Submissions/SubmissionModel.cs +++ /dev/null @@ -1,63 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Domain.Problems.Languages; - -namespace ApplicationCore.Domain.Submissions; - -public sealed class SubmissionModel -{ - public required Guid Id { get; init; } - - public string? Code { get; init; } - - public int ProblemSetupId { get; init; } - - public LanguageVersion LanguageVersion { get; init; } - - public DateTime CreatedOn { get; init; } - - public DateTime? CompletedAt { get; set; } - - public Guid CreatedById { get; init; } - - public AccountModel? CreatedBy { get; init; } - - public IEnumerable Results { get; init; } = []; - - public IEnumerable GetResultTokens() - { - return Results.Select(result => result.Id); - } - - public SubmissionStatus GetOverallStatus() - { - if ( - !Results.Any() - || Results.Any(r => r.Status is SubmissionStatus.InQueue or SubmissionStatus.Processing) - ) - { - return SubmissionStatus.Processing; - } - - return Results.All(r => r.Status == SubmissionStatus.Accepted) - ? SubmissionStatus.Accepted - : SubmissionStatus.WrongAnswer; - } - - public int GetAverageRuntimeMs() - { - if (!Results.Any()) - { - return 0; - } - return (int)(Results.Average(r => r.RuntimeMs) ?? 0); - } - - public int GetAverageMemoryKb() - { - if (!Results.Any()) - { - return 0; - } - return (int)(Results.Average(r => r.MemoryKb) ?? 0); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Submissions/SubmissionResult.cs b/src/ApplicationCore/Domain/Submissions/SubmissionResult.cs deleted file mode 100644 index 0e246d6..0000000 --- a/src/ApplicationCore/Domain/Submissions/SubmissionResult.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace ApplicationCore.Domain.Submissions; - -public sealed class SubmissionResult -{ - public Guid Id { get; set; } - - public Guid ExecutionId { get; set; } - - public Guid? ResultId { get; set; } - - public required SubmissionStatus Status { get; set; } - - public DateTime? StartedAt { get; set; } - - public DateTime? FinishedAt { get; set; } - - public string? Stdout { get; set; } - - public string? ProgramOutput { get; set; } - - public string? Stderr { get; set; } - - public int? RuntimeMs { get; set; } - - public int? MemoryKb { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Domain/Submissions/SubmissionStatus.cs b/src/ApplicationCore/Domain/Submissions/SubmissionStatus.cs deleted file mode 100644 index cba82e1..0000000 --- a/src/ApplicationCore/Domain/Submissions/SubmissionStatus.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace ApplicationCore.Domain.Submissions; - -public enum SubmissionStatus -{ - Accepted = 1, - WrongAnswer = 2, - - InQueue = 3, - Processing = 4, - TimeLimitExceeded = 5, - CompilationError = 6, - - RuntimeErrorSigSegv = 7, - RuntimeErrorSigXfsz = 8, - RuntimeErrorSigFpe = 9, - RuntimeErrorSigAbrt = 10, - RuntimeErrorNzec = 11, - RuntimeErrorOther = 12, - - InternalError = 13, - ExecFormatError = 14, -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Accounts/AccountDto.cs b/src/ApplicationCore/Dtos/Accounts/AccountDto.cs deleted file mode 100644 index 2689949..0000000 --- a/src/ApplicationCore/Dtos/Accounts/AccountDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace ApplicationCore.Dtos.Accounts; - -public sealed record AccountDto -{ - public Guid? Id { get; init; } - - public required string Username { get; init; } - - public string? ImageUrl { get; init; } - - public IEnumerable Permissions { get; init; } = []; - - public required DateTime CreatedOn { get; init; } - - public string? About { get; init; } - - public DateTime? UsernameLastChangedAt { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Accounts/ProfileAggregateDto.cs b/src/ApplicationCore/Dtos/Accounts/ProfileAggregateDto.cs deleted file mode 100644 index 1ec3ba9..0000000 --- a/src/ApplicationCore/Dtos/Accounts/ProfileAggregateDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ApplicationCore.Dtos.Accounts; - -public sealed record ProfileAggregateDto(AccountDto Profile); \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Accounts/ProfileSettingsDto.cs b/src/ApplicationCore/Dtos/Accounts/ProfileSettingsDto.cs deleted file mode 100644 index ec46633..0000000 --- a/src/ApplicationCore/Dtos/Accounts/ProfileSettingsDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ApplicationCore.Dtos.Accounts; - -public sealed record ProfileSettingsDto(string Username, DateTime? UsernameLastChangedAt, string Bio); \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Languages/LanguageVersionDto.cs b/src/ApplicationCore/Dtos/Languages/LanguageVersionDto.cs deleted file mode 100644 index d2e1610..0000000 --- a/src/ApplicationCore/Dtos/Languages/LanguageVersionDto.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ApplicationCore.Dtos.Languages; - -public sealed record LanguageVersionDto -{ - public required int Id { get; init; } - - public required string Version { get; init; } - - public string? InitialCode { get; init; } - - public ProgrammingLanguageDto? ProgrammingLanguage { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Languages/ProgrammingLanguageDto.cs b/src/ApplicationCore/Dtos/Languages/ProgrammingLanguageDto.cs deleted file mode 100644 index 97d785a..0000000 --- a/src/ApplicationCore/Dtos/Languages/ProgrammingLanguageDto.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ApplicationCore.Dtos.Languages; - -public sealed record ProgrammingLanguageDto -{ - public required int Id { get; init; } - - public required string Name { get; init; } - - public bool IsArchived { get; init; } - - public IEnumerable Versions { get; init; } = []; -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Problems/CreateProblemDto.cs b/src/ApplicationCore/Dtos/Problems/CreateProblemDto.cs deleted file mode 100644 index 9462efe..0000000 --- a/src/ApplicationCore/Dtos/Problems/CreateProblemDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ApplicationCore.Dtos.Problems; - -public sealed record CreateProblemDto( - string Title, - int EstimatedDifficulty, - string Question, - IEnumerable Tags -); \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Problems/ProblemDto.cs b/src/ApplicationCore/Dtos/Problems/ProblemDto.cs deleted file mode 100644 index ea18b08..0000000 --- a/src/ApplicationCore/Dtos/Problems/ProblemDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ApplicationCore.Dtos.Languages; - -namespace ApplicationCore.Dtos.Problems; - -public record ProblemDto -{ - public required Guid Id { get; init; } - - public required string Title { get; init; } - - public required string Slug { get; init; } - - public string? Question { get; init; } - - public required List Tags { get; init; } - - public required int Difficulty { get; init; } - - public required int Version { get; init; } - - public required IEnumerable AvailableLanguages { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Problems/ProblemSetupDto.cs b/src/ApplicationCore/Dtos/Problems/ProblemSetupDto.cs deleted file mode 100644 index 0c26a62..0000000 --- a/src/ApplicationCore/Dtos/Problems/ProblemSetupDto.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ApplicationCore.Dtos.Problems.Tests; - -namespace ApplicationCore.Dtos.Problems; - -public record ProblemSetupDto -{ - public int Id { get; init; } - - public int Version { get; init; } - - public required string InitialCode { get; init; } - - public int LanguageVersionId { get; init; } - - public required IEnumerable TestSuites { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Problems/ProblemSubmissionDto.cs b/src/ApplicationCore/Dtos/Problems/ProblemSubmissionDto.cs deleted file mode 100644 index d6420af..0000000 --- a/src/ApplicationCore/Dtos/Problems/ProblemSubmissionDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ApplicationCore.Dtos.Accounts; - -namespace ApplicationCore.Dtos.Problems; - -public sealed record ProblemSubmissionDto( - AccountDto CreatedBy, - string Code, - string Status, - string Language, - string LanguageVersion, - DateTime CreatedOn, - int RuntimeMs, - int MemoryKb -); \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Problems/Tests/TestCaseDto.cs b/src/ApplicationCore/Dtos/Problems/Tests/TestCaseDto.cs deleted file mode 100644 index a713195..0000000 --- a/src/ApplicationCore/Dtos/Problems/Tests/TestCaseDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Dtos.Problems.Tests; - -public record TestCaseDto -{ - public string Input { get; init; } = string.Empty; - public string ExpectedOutput { get; init; } = string.Empty; -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Problems/Tests/TestSuiteDto.cs b/src/ApplicationCore/Dtos/Problems/Tests/TestSuiteDto.cs deleted file mode 100644 index 9cb9b23..0000000 --- a/src/ApplicationCore/Dtos/Problems/Tests/TestSuiteDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ApplicationCore.Dtos.Problems.Tests; - -public record TestSuiteDto -{ - public required IEnumerable TestCases { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Submissions/GetSubmissionsPaginatedRequest.cs b/src/ApplicationCore/Dtos/Submissions/GetSubmissionsPaginatedRequest.cs deleted file mode 100644 index 2a53a6f..0000000 --- a/src/ApplicationCore/Dtos/Submissions/GetSubmissionsPaginatedRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ApplicationCore.Dtos.Submissions; - -public sealed class GetSubmissionsPaginatedRequest -{ - public required Guid ProblemId { get; init; } - public int Page { get; init; } = 1; - public int Size { get; init; } = 25; - public DateTime Timestamp { get; init; } = DateTime.UtcNow; - public Guid? FilterByUserId { get; init; } - public bool AcceptedOnly { get; init; } = true; -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Submissions/SubmissionDto.cs b/src/ApplicationCore/Dtos/Submissions/SubmissionDto.cs deleted file mode 100644 index e0c99f3..0000000 --- a/src/ApplicationCore/Dtos/Submissions/SubmissionDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ApplicationCore.Dtos.Accounts; - -namespace ApplicationCore.Dtos.Submissions; - -public sealed class SubmissionDto -{ - public required Guid Id { get; init; } - public required int ProblemSetupId { get; init; } - public required string Status { get; init; } - public required string Code { get; init; } - public required DateTime CreatedOn { get; init; } - public DateTime? CompletedAt { get; init; } - public AccountDto? CreatedBy { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Submissions/SubmissionResultDto.cs b/src/ApplicationCore/Dtos/Submissions/SubmissionResultDto.cs deleted file mode 100644 index b74bd1e..0000000 --- a/src/ApplicationCore/Dtos/Submissions/SubmissionResultDto.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ApplicationCore.Domain.Submissions; - -namespace ApplicationCore.Dtos.Submissions; - -public sealed class SubmissionResultDto -{ - public required Guid Id { get; init; } - public required string Status { get; init; } - public int? RuntimeMs { get; init; } - public int? MemoryKb { get; init; } - public DateTime? FinishedAt { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Submissions/SubmissionStatusDto.cs b/src/ApplicationCore/Dtos/Submissions/SubmissionStatusDto.cs deleted file mode 100644 index aedf97f..0000000 --- a/src/ApplicationCore/Dtos/Submissions/SubmissionStatusDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ApplicationCore.Dtos.Submissions; - -public sealed class SubmissionStatusDto -{ - public required Guid SubmissionId { get; init; } - public required string Status { get; init; } - public required IEnumerable TestCases { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Dtos/Submissions/SubmissionTestCaseResultDto.cs b/src/ApplicationCore/Dtos/Submissions/SubmissionTestCaseResultDto.cs deleted file mode 100644 index 1fe79e9..0000000 --- a/src/ApplicationCore/Dtos/Submissions/SubmissionTestCaseResultDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ApplicationCore.Dtos.Submissions; - -public sealed class SubmissionTestCaseResultDto -{ - public string Input { get; init; } = string.Empty; - public string ExpectedOutput { get; init; } = string.Empty; - public string ActualOutput { get; init; } = string.Empty; - public string? ErrorOutput { get; init; } - public int Status { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Clients/IJudge0Client.cs b/src/ApplicationCore/Interfaces/Clients/IJudge0Client.cs deleted file mode 100644 index 14ea798..0000000 --- a/src/ApplicationCore/Interfaces/Clients/IJudge0Client.cs +++ /dev/null @@ -1,19 +0,0 @@ -using ApplicationCore.Domain.CodeExecution.Judge0; -using Ardalis.Result; - -namespace ApplicationCore.Interfaces.Clients; - -public interface IJudge0Client -{ - Task>> GetAsync( - IEnumerable tokens, - CancellationToken cancellationToken, - IEnumerable? fields = null - ); - - Task>> SubmitAsync( - IEnumerable reqs, - CancellationToken cancellationToken, - IEnumerable? fields = null - ); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Messaging/IMessagePublisher.cs b/src/ApplicationCore/Interfaces/Messaging/IMessagePublisher.cs deleted file mode 100644 index c864f95..0000000 --- a/src/ApplicationCore/Interfaces/Messaging/IMessagePublisher.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Interfaces.Messaging; - -public interface IMessagePublisher -{ - Task PublishAsync(T message, CancellationToken cancellationToken = default) - where T : class; -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Repositories/IAccountRepository.cs b/src/ApplicationCore/Interfaces/Repositories/IAccountRepository.cs deleted file mode 100644 index 4fdd491..0000000 --- a/src/ApplicationCore/Interfaces/Repositories/IAccountRepository.cs +++ /dev/null @@ -1,32 +0,0 @@ -using ApplicationCore.Domain.Accounts; - -namespace ApplicationCore.Interfaces.Repositories; - -public interface IAccountRepository -{ - Task AddAsync(AccountModel accountModel, CancellationToken cancellationToken); - - Task ExistsAsync(Guid id, CancellationToken cancellationToken); - - Task GetByIdAsync(Guid id, CancellationToken cancellationToken); - - Task GetBySubAsync(string sub, CancellationToken cancellationToken); - - Task GetByUsernameAsync(string username, CancellationToken cancellationToken); - - Task GetByUsernameOrSubAsync( - string username, - string sub, - CancellationToken cancellationToken - ); - - Task UpdateImageUrlAsync(Guid id, string? imageUrl, CancellationToken cancellationToken); - - Task UpdateUsernameAsync(Guid id, string username, DateTime usernameLastChangedAt, CancellationToken cancellationToken); - - Task UpdateAboutAsync(Guid id, string? about, CancellationToken cancellationToken); - - Task ExistsByUsernameAsync(string username, CancellationToken cancellationToken); - - Task CountByUsernameBaseAsync(string usernameBase, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Repositories/IProblemRepository.cs b/src/ApplicationCore/Interfaces/Repositories/IProblemRepository.cs deleted file mode 100644 index a00528c..0000000 --- a/src/ApplicationCore/Interfaces/Repositories/IProblemRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Problems; -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Domain.Problems.ProblemSetups; - -namespace ApplicationCore.Interfaces.Repositories; - -public interface IProblemRepository -{ - Task CreateProblemAsync(ProblemModel problem, CancellationToken cancellationToken); - - Task> GetAvailableLanguagesAsync( - CancellationToken cancellationToken - ); - - Task GetProblemBySlugAsync(string slug, CancellationToken cancellationToken); - - Task GetProblemSetupAsync( - Guid problemId, - int languageVersionId, - CancellationToken cancellationToken - ); - - Task> GetProblemsAsync( - PaginationRequest pagination, - CancellationToken cancellationToken - ); - - Task> GetProblemSetupsAsync( - IEnumerable problemSetupIds, - CancellationToken cancellationToken - ); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Repositories/ISubmissionRepository.cs b/src/ApplicationCore/Interfaces/Repositories/ISubmissionRepository.cs deleted file mode 100644 index 7a88cd4..0000000 --- a/src/ApplicationCore/Interfaces/Repositories/ISubmissionRepository.cs +++ /dev/null @@ -1,50 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Domain.Submissions.Outboxes; - -namespace ApplicationCore.Interfaces.Repositories; - -public interface ISubmissionRepository -{ - Task> GetSubmissionOutboxesAsync( - CancellationToken cancellationToken - ); - - Task SaveAsync(SubmissionModel submission, CancellationToken cancellationToken); - - Task IncrementOutboxesCountAsync( - IEnumerable outboxIds, - DateTime now, - CancellationToken cancellationToken - ); - - Task SaveExecutionTokensAsync( - IEnumerable submissions, - CancellationToken cancellationToken - ); - - Task ProcessPollingSubmissionExecutionsAsync( - IEnumerable submissionModels, - CancellationToken cancellationToken - ); - - Task ProcessEvaluationAsync( - IEnumerable submissions, - CancellationToken cancellationToken - ); - - Task FinalizeEvaluationAsync( - IEnumerable outboxIds, - DateTime now, - CancellationToken cancellationToken - ); - - Task ProcessSubmissionInitializationAsync( - IEnumerable submissions, - CancellationToken cancellationToken - ); - - Task> GetSubmissionsByProblemId(Guid problemId, Guid? accountId, PaginationRequest pagination, SubmissionStatus? statusFilter, CancellationToken cancellationToken); - - Task GetSubmissionByIdAsync(Guid submissionId, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Services/IAccountAppService.cs b/src/ApplicationCore/Interfaces/Services/IAccountAppService.cs deleted file mode 100644 index 279d685..0000000 --- a/src/ApplicationCore/Interfaces/Services/IAccountAppService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using ApplicationCore.Commands.Accounts.UpdateProfileSettings; -using ApplicationCore.Commands.Accounts.UpdateUsername; -using ApplicationCore.Commands.Accounts.UpsertAccount; -using ApplicationCore.Dtos.Accounts; -using Ardalis.Result; - -namespace ApplicationCore.Interfaces.Services; - -public interface IAccountAppService -{ - Task> CreateAsync( - string username, - string sub, - string imageUrl, - CancellationToken cancellationToken - ); - - Task> GetAccountBySubAsync(string sub, CancellationToken cancellationToken); - - Task> GetProfileAggregateAsync( - string username, - CancellationToken cancellationToken - ); - - Task> GetProfileSettingsAsync( - string sub, - CancellationToken cancellationToken - ); - - Task> UpsertAccountAsync( - string sub, - string? imageUrl, - CancellationToken cancellationToken - ); - - Task> UpdateUsernameAsync( - Guid accountId, - string newUsername, - DateTime? usernameLastChangedAt, - CancellationToken cancellationToken - ); - - Task> UpdateProfileSettingsAsync( - Guid accountId, - string? bio, - CancellationToken cancellationToken - ); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Services/ICodeBuilderService.cs b/src/ApplicationCore/Interfaces/Services/ICodeBuilderService.cs deleted file mode 100644 index 9026c03..0000000 --- a/src/ApplicationCore/Interfaces/Services/ICodeBuilderService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using ApplicationCore.Domain.CodeExecution; -using Ardalis.Result; - -namespace ApplicationCore.Interfaces.Services; - -public interface ICodeBuilderService -{ - Result> Build(IEnumerable contexts); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Services/ICodeExecutionService.cs b/src/ApplicationCore/Interfaces/Services/ICodeExecutionService.cs deleted file mode 100644 index 1ccdcec..0000000 --- a/src/ApplicationCore/Interfaces/Services/ICodeExecutionService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ApplicationCore.Domain.CodeExecution; -using ApplicationCore.Domain.Submissions; -using Ardalis.Result; - -namespace ApplicationCore.Interfaces.Services; - -public interface ICodeExecutionService -{ - Task>> ExecuteAsync( - IEnumerable contexts, - CancellationToken cancellationToken - ); - - Task>> GetSubmissionResultsAsync( - IEnumerable submissions, - CancellationToken cancellationToken - ); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Services/IExecutionComparisonService.cs b/src/ApplicationCore/Interfaces/Services/IExecutionComparisonService.cs deleted file mode 100644 index d2ce5c1..0000000 --- a/src/ApplicationCore/Interfaces/Services/IExecutionComparisonService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ApplicationCore.Domain.Submissions; - -namespace ApplicationCore.Interfaces.Services; - -public interface IExecutionComparisonService -{ - SubmissionStatus Compare(string? actualOutput, string expectedOutput); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Services/IProblemAppService.cs b/src/ApplicationCore/Interfaces/Services/IProblemAppService.cs deleted file mode 100644 index 0445d29..0000000 --- a/src/ApplicationCore/Interfaces/Services/IProblemAppService.cs +++ /dev/null @@ -1,37 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Problems.ProblemSetups; -using ApplicationCore.Dtos.Languages; -using ApplicationCore.Dtos.Problems; -using Ardalis.Result; - -namespace ApplicationCore.Interfaces.Services; - -public interface IProblemAppService -{ - Task>> GetAvailableLanguagesAsync( - CancellationToken cancellationToken - ); - - Task> GetProblemBySlugAsync( - string slug, - CancellationToken cancellationToken - ); - - Task>> GetProblemsPaginatedAsync( - int pageNumber, - int pageSize, - DateTime timestamp, - CancellationToken cancellationToken - ); - - Task>> GetProblemSetupsForExecutionAsync( - IEnumerable setupIds, - CancellationToken cancellationToken - ); - - Task> GetProblemSetupAsync( - Guid problemId, - int languageVersionId, - CancellationToken cancellationToken - ); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Services/ISlugService.cs b/src/ApplicationCore/Interfaces/Services/ISlugService.cs deleted file mode 100644 index 02d5274..0000000 --- a/src/ApplicationCore/Interfaces/Services/ISlugService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ApplicationCore.Interfaces.Services; - -public interface ISlugService -{ - string GenerateSlug(string input); -} \ No newline at end of file diff --git a/src/ApplicationCore/Interfaces/Services/ISubmissionAppService.cs b/src/ApplicationCore/Interfaces/Services/ISubmissionAppService.cs deleted file mode 100644 index 018d9f3..0000000 --- a/src/ApplicationCore/Interfaces/Services/ISubmissionAppService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Dtos.Submissions; -using Ardalis.Result; -using MediatR; - -namespace ApplicationCore.Interfaces.Services; - -public interface ISubmissionAppService -{ - Task> CreateAsync( - int problemSetupId, - string code, - Guid createdById, - CancellationToken cancellationToken - ); - - Task>> GetSubmissionOutboxesAsync( - CancellationToken cancellationToken - ); - - Task> IncrementOutboxesCountAsync( - IEnumerable outboxIds, - DateTime timestamp, - CancellationToken cancellationToken - ); - - Task> SaveExecutionTokensAsync( - IEnumerable results, - CancellationToken cancellationToken - ); - - Task> ProcessSubmissionExecutionAsync( - IEnumerable results, - CancellationToken cancellationToken - ); - - Task> ProcessPollingSubmissionExecutionsAsync( - IEnumerable results, - CancellationToken cancellationToken - ); - - Task> ProcessEvaluationAsync( - IEnumerable results, - CancellationToken cancellationToken - ); - - Task> FinalizeEvaluationAsync( - IEnumerable outboxIds, - DateTime now, - CancellationToken cancellationToken - ); - - Task>> GetSolutionsAsync( - Guid problemId, - PaginationRequest paginationRequest, - CancellationToken cancellationToken - ); - - Task>> GetSubmissionsPaginatedAsync( - Guid problemId, - Guid accountId, - PaginationRequest paginationRequest, - CancellationToken cancellationToken = default - ); - - Task> GetSubmissionStatusAsync(Guid submissionId, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/ApplicationCore/Logging/LoggingEventIds.cs b/src/ApplicationCore/Logging/LoggingEventIds.cs deleted file mode 100644 index 5529832..0000000 --- a/src/ApplicationCore/Logging/LoggingEventIds.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace ApplicationCore.Logging; - -public static class LoggingEventIds -{ - public static class Accounts - { - public const int UsernameInvalid = 2000; - public const int DuplicateUsername = 2001; - public const int DuplicateSub = 2002; - public const int DuplicateUsernameOrSub = 2003; - - public const int CreateAttempt = 2100; - public const int Created = 2101; - public const int CreateDuplicateDetectedPreQuery = 2102; - public const int CreateDuplicateRace = 2103; - public const int NotFoundBySub = 2104; - public const int CreateFailed = 2105; - - public const int UpdateUsernameFailed = 2301; - - public const int ContextMissingSub = 2200; - public const int ContextResolveFailed = 2201; - } - - public static class Exceptions - { - public const int UnhandledException = 1000; - public const int UnhandledExceptionWithPath = 1001; - } - - public static class Jobs - { - public const int Started = 3000; - public const int Completed = 3001; - public const int Failed = 3002; - - public const int SubmissionExecutionProcessing = 3100; - - public const int PollSubmissionExecutionPolling = 3200; - - public const int EvaluateSubmissionEvaluating = 3300; - public const int EvaluateSubmissionEvaluated = 3301; - - public const int PollEvaluationFinalizing = 3400; - } - - public static class Submissions - { - public const int Created = 4000; - public const int CreateFailed = 4001; - - // Stage 1: SubmissionCreatedConsumer - public const int Stage1Started = 4100; - public const int Stage1OutboxNotFound = 4101; - public const int Stage1SetupFailed = 4102; - public const int Stage1BuildFailed = 4103; - public const int Stage1ExecutionFailed = 4104; - public const int Stage1Completed = 4105; - - // Stage 2: SubmissionExecutedConsumer (poll Judge0) - public const int Stage2Started = 4200; - public const int Stage2OutboxNotFound = 4201; - public const int Stage2PollFailed = 4202; - public const int Stage2StillProcessing = 4203; - public const int Stage2Completed = 4204; - - // Stage 3: SubmissionReadyToEvaluateConsumer - public const int Stage3Started = 4300; - public const int Stage3OutboxNotFound = 4301; - public const int Stage3SetupNotFound = 4302; - public const int Stage3Completed = 4303; - - // Stage 4: SubmissionEvaluationPollConsumer - public const int Stage4Started = 4400; - public const int Stage4OutboxNotFound = 4401; - public const int Stage4Completed = 4402; - } - - public static class Judge0 - { - public const int SubmitStarted = 5000; - public const int SubmitCompleted = 5001; - public const int SubmitFailed = 5002; - - public const int GetStarted = 5100; - public const int GetCompleted = 5101; - public const int GetFailed = 5102; - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Mappings/ProblemMappings.cs b/src/ApplicationCore/Mappings/ProblemMappings.cs deleted file mode 100644 index d550776..0000000 --- a/src/ApplicationCore/Mappings/ProblemMappings.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ApplicationCore.Domain.Problems; -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Dtos.Languages; -using ApplicationCore.Dtos.Problems; -using Mapster; - -namespace ApplicationCore.Mappings; - -public sealed class ProblemMappings : IRegister -{ - public void Register(TypeAdapterConfig config) - { - config - .NewConfig() - .Map(dest => dest.Tags, src => src.Tags.Select(tag => tag.Value).ToList()) - .Map(d => d.AvailableLanguages, s => s.GetAvailableLanguages()); - - config.NewConfig(); - - config.NewConfig(); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Messaging/SubmissionCreatedMessage.cs b/src/ApplicationCore/Messaging/SubmissionCreatedMessage.cs deleted file mode 100644 index ccd0f15..0000000 --- a/src/ApplicationCore/Messaging/SubmissionCreatedMessage.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Messaging; - -public sealed record SubmissionCreatedMessage -{ - public required Guid SubmissionId { get; init; } - public required Guid OutboxId { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Messaging/SubmissionEvaluationPollMessage.cs b/src/ApplicationCore/Messaging/SubmissionEvaluationPollMessage.cs deleted file mode 100644 index baf3fb6..0000000 --- a/src/ApplicationCore/Messaging/SubmissionEvaluationPollMessage.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Messaging; - -public sealed record SubmissionEvaluationPollMessage -{ - public required Guid SubmissionId { get; init; } - public required Guid OutboxId { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Messaging/SubmissionExecutedMessage.cs b/src/ApplicationCore/Messaging/SubmissionExecutedMessage.cs deleted file mode 100644 index ce56402..0000000 --- a/src/ApplicationCore/Messaging/SubmissionExecutedMessage.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Messaging; - -public sealed record SubmissionExecutedMessage -{ - public required Guid SubmissionId { get; init; } - public required Guid OutboxId { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Messaging/SubmissionExecutionPollMessage.cs b/src/ApplicationCore/Messaging/SubmissionExecutionPollMessage.cs deleted file mode 100644 index bfce494..0000000 --- a/src/ApplicationCore/Messaging/SubmissionExecutionPollMessage.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Messaging; - -public sealed record SubmissionExecutionPollMessage -{ - public required Guid SubmissionId { get; init; } - public required Guid OutboxId { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Messaging/SubmissionReadyToEvaluateMessage.cs b/src/ApplicationCore/Messaging/SubmissionReadyToEvaluateMessage.cs deleted file mode 100644 index fae4765..0000000 --- a/src/ApplicationCore/Messaging/SubmissionReadyToEvaluateMessage.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Messaging; - -public sealed record SubmissionReadyToEvaluateMessage -{ - public required Guid SubmissionId { get; init; } - public required Guid OutboxId { get; init; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Accounts/GetAccountBySub/GetAccountBySubHandler.cs b/src/ApplicationCore/Queries/Accounts/GetAccountBySub/GetAccountBySubHandler.cs deleted file mode 100644 index e556493..0000000 --- a/src/ApplicationCore/Queries/Accounts/GetAccountBySub/GetAccountBySubHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Accounts.GetAccountBySub; - -public sealed class GetAccountBySubHandler(IAccountRepository repository) - : IQueryHandler -{ - public async Task> Handle(GetAccountBySubQuery request, CancellationToken ct) - { - if (string.IsNullOrWhiteSpace(request.Sub)) - { - return Result.Invalid([new ValidationError(nameof(request.Sub), "Sub is required")]); - } - - try - { - var account = await repository.GetBySubAsync(request.Sub, ct); - - if (account is null) - { - return Result.NotFound(); - } - - var dto = new AccountDto - { - Id = account.Id, - Username = account.Username, - ImageUrl = account.ImageUrl, - CreatedOn = account.CreatedOn, - UsernameLastChangedAt = account.UsernameLastChangedAt, - }; - return Result.Success(dto); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Accounts/GetAccountBySub/GetAccountBySubQuery.cs b/src/ApplicationCore/Queries/Accounts/GetAccountBySub/GetAccountBySubQuery.cs deleted file mode 100644 index 9e741dc..0000000 --- a/src/ApplicationCore/Queries/Accounts/GetAccountBySub/GetAccountBySubQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using ApplicationCore.Dtos.Accounts; - -namespace ApplicationCore.Queries.Accounts.GetAccountBySub; - -public sealed record GetAccountBySubQuery(string Sub) : IQuery; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Accounts/GetProfileAggregate/GetProfileAggregateHandler.cs b/src/ApplicationCore/Queries/Accounts/GetProfileAggregate/GetProfileAggregateHandler.cs deleted file mode 100644 index e043475..0000000 --- a/src/ApplicationCore/Queries/Accounts/GetProfileAggregate/GetProfileAggregateHandler.cs +++ /dev/null @@ -1,49 +0,0 @@ -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Accounts.GetProfileAggregate; - -public sealed class GetProfileAggregateHandler(IAccountRepository repository) - : IQueryHandler -{ - public async Task> Handle( - GetProfileAggregateQuery request, - CancellationToken cancellationToken - ) - { - if (string.IsNullOrWhiteSpace(request.Username)) - { - return Result.Invalid([ - new ValidationError(nameof(request.Username), "Username is required"), - ]); - } - - try - { - var profile = await repository.GetByUsernameAsync(request.Username, cancellationToken); - - if (profile is null) - { - return Result.NotFound(); - } - - var dto = new ProfileAggregateDto( - new AccountDto - { - Id = profile.Id, - Username = profile.Username, - About = profile.About, - ImageUrl = profile.ImageUrl, - CreatedOn = profile.CreatedOn, - } - ); - - return Result.Success(dto); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Accounts/GetProfileAggregate/GetProfileAggregateQuery.cs b/src/ApplicationCore/Queries/Accounts/GetProfileAggregate/GetProfileAggregateQuery.cs deleted file mode 100644 index 5bf57ad..0000000 --- a/src/ApplicationCore/Queries/Accounts/GetProfileAggregate/GetProfileAggregateQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using ApplicationCore.Dtos.Accounts; - -namespace ApplicationCore.Queries.Accounts.GetProfileAggregate; - -public record GetProfileAggregateQuery(string Username) : IQuery; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Accounts/GetProfileSettings/GetProfileSettingsHandler.cs b/src/ApplicationCore/Queries/Accounts/GetProfileSettings/GetProfileSettingsHandler.cs deleted file mode 100644 index 68bf07e..0000000 --- a/src/ApplicationCore/Queries/Accounts/GetProfileSettings/GetProfileSettingsHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Accounts.GetProfileSettings; - -public sealed class GetProfileSettingsHandler(IAccountRepository accountRepository) - : IQueryHandler -{ - public async Task> Handle( - GetProfileSettingsQuery request, - CancellationToken cancellationToken - ) - { - try - { - var account = await accountRepository.GetBySubAsync(request.Sub, cancellationToken); - - return account is null - ? Result.NotFound() - : Result.Success(new ProfileSettingsDto(account.Username, account.UsernameLastChangedAt, account.About ?? "")); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Accounts/GetProfileSettings/GetProfileSettingsQuery.cs b/src/ApplicationCore/Queries/Accounts/GetProfileSettings/GetProfileSettingsQuery.cs deleted file mode 100644 index b149390..0000000 --- a/src/ApplicationCore/Queries/Accounts/GetProfileSettings/GetProfileSettingsQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using ApplicationCore.Dtos.Accounts; - -namespace ApplicationCore.Queries.Accounts.GetProfileSettings; - -public sealed record GetProfileSettingsQuery(string Sub) : IQuery; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/IQuery.cs b/src/ApplicationCore/Queries/IQuery.cs deleted file mode 100644 index 420f99a..0000000 --- a/src/ApplicationCore/Queries/IQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Ardalis.Result; -using MediatR; - -namespace ApplicationCore.Queries; - -public interface IQuery : IRequest> { } \ No newline at end of file diff --git a/src/ApplicationCore/Queries/IQueryHandler.cs b/src/ApplicationCore/Queries/IQueryHandler.cs deleted file mode 100644 index c1ced49..0000000 --- a/src/ApplicationCore/Queries/IQueryHandler.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Ardalis.Result; -using MediatR; - -namespace ApplicationCore.Queries; - -public interface IQueryHandler : IRequestHandler> - where TQuery : IQuery -{ } \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetAvailableLanguages/GetAvailableLanguagesHandler.cs b/src/ApplicationCore/Queries/Problems/GetAvailableLanguages/GetAvailableLanguagesHandler.cs deleted file mode 100644 index e437feb..0000000 --- a/src/ApplicationCore/Queries/Problems/GetAvailableLanguages/GetAvailableLanguagesHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using ApplicationCore.Dtos.Languages; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Problems.GetAvailableLanguages; - -public sealed class GetAvailableLanguagesHandler(IProblemRepository problemRepository) - : IQueryHandler> -{ - public async Task>> Handle( - GetAvailableLanguagesQuery request, - CancellationToken cancellationToken - ) - { - try - { - var languages = await problemRepository.GetAvailableLanguagesAsync(cancellationToken); - - return Result.Success( - languages.Select(language => new ProgrammingLanguageDto - { - Id = language.Id, - Name = language.Name, - Versions = language.Versions.Select(version => new LanguageVersionDto - { - Id = version.Id, - Version = version.Version, - InitialCode = version.InitialCode, - }), - }) - ); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetAvailableLanguages/GetAvailableLanguagesQuery.cs b/src/ApplicationCore/Queries/Problems/GetAvailableLanguages/GetAvailableLanguagesQuery.cs deleted file mode 100644 index 3e35ed9..0000000 --- a/src/ApplicationCore/Queries/Problems/GetAvailableLanguages/GetAvailableLanguagesQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using ApplicationCore.Dtos.Languages; - -namespace ApplicationCore.Queries.Problems.GetAvailableLanguages; - -public sealed record GetAvailableLanguagesQuery() : IQuery>; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetProblemBySlug/GetProblemBySlugHandler.cs b/src/ApplicationCore/Queries/Problems/GetProblemBySlug/GetProblemBySlugHandler.cs deleted file mode 100644 index c634c34..0000000 --- a/src/ApplicationCore/Queries/Problems/GetProblemBySlug/GetProblemBySlugHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using Mapster; - -namespace ApplicationCore.Queries.Problems.GetProblemBySlug; - -public sealed class GetProblemBySlugHandler(IProblemRepository problemRepository) - : IQueryHandler -{ - public async Task> Handle( - GetProblemBySlugQuery request, - CancellationToken cancellationToken - ) - { - try - { - var problem = await problemRepository.GetProblemBySlugAsync( - request.Slug, - cancellationToken - ); - - return problem is null - ? Result.NotFound() - : Result.Success(problem.Adapt()); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetProblemBySlug/GetProblemBySlugQuery.cs b/src/ApplicationCore/Queries/Problems/GetProblemBySlug/GetProblemBySlugQuery.cs deleted file mode 100644 index 713ef12..0000000 --- a/src/ApplicationCore/Queries/Problems/GetProblemBySlug/GetProblemBySlugQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using ApplicationCore.Dtos.Problems; - -namespace ApplicationCore.Queries.Problems.GetProblemBySlug; - -public sealed record GetProblemBySlugQuery(string Slug) : IQuery; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetProblemSetup/GetProblemSetupHandler.cs b/src/ApplicationCore/Queries/Problems/GetProblemSetup/GetProblemSetupHandler.cs deleted file mode 100644 index 1f66dba..0000000 --- a/src/ApplicationCore/Queries/Problems/GetProblemSetup/GetProblemSetupHandler.cs +++ /dev/null @@ -1,52 +0,0 @@ -using ApplicationCore.Domain.Problems.TestSuites; -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Dtos.Problems.Tests; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Problems.GetProblemSetup; - - -public sealed class GetProblemSetupHandler(IProblemRepository problemRepository) - : IQueryHandler -{ - private readonly IProblemRepository _problemRepository = - problemRepository ?? throw new ArgumentNullException(nameof(problemRepository)); - - public async Task> Handle( - GetProblemSetupQuery request, - CancellationToken cancellationToken - ) - { - var setup = ( - await _problemRepository.GetProblemSetupAsync( - request.ProblemId, - request.LanguageVersionId, - cancellationToken - ) - ); - - if (setup is null) - { - return Result.NotFound(); - } - - return new ProblemSetupDto() - { - Id = setup.Id, - Version = setup.Version, - InitialCode = setup.InitialCode, - LanguageVersionId = setup.LanguageVersionId, - TestSuites = setup - .TestSuites.Where(ts => ts.TestSuiteType == TestSuiteType.Public) - .Select(ts => new TestSuiteDto() - { - TestCases = ts.TestCases.Select(tc => new TestCaseDto() - { - Input = string.Join(",", tc.Inputs.Select(input => input.Value.Trim())), - ExpectedOutput = tc.ExpectedOutput.Value, - }), - }), - }; - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetProblemSetup/GetProblemSetupQuery.cs b/src/ApplicationCore/Queries/Problems/GetProblemSetup/GetProblemSetupQuery.cs deleted file mode 100644 index 0a2513b..0000000 --- a/src/ApplicationCore/Queries/Problems/GetProblemSetup/GetProblemSetupQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using ApplicationCore.Dtos.Problems; - -namespace ApplicationCore.Queries.Problems.GetProblemSetup; - -public sealed record GetProblemSetupQuery(Guid ProblemId, int LanguageVersionId) - : IQuery; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetProblemSetupsForExecution/GetProblemSetupsForExecutionHandler.cs b/src/ApplicationCore/Queries/Problems/GetProblemSetupsForExecution/GetProblemSetupsForExecutionHandler.cs deleted file mode 100644 index 7f3f200..0000000 --- a/src/ApplicationCore/Queries/Problems/GetProblemSetupsForExecution/GetProblemSetupsForExecutionHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using ApplicationCore.Domain.Problems.ProblemSetups; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Problems.GetProblemSetupsForExecution; - -public sealed class GetProblemSetupsForExecutionHandler(IProblemRepository problemRepository) - : IQueryHandler> -{ - public async Task>> Handle( - GetProblemSetupsForExecutionQuery request, - CancellationToken cancellationToken - ) - { - try - { - return Result.Success( - await problemRepository.GetProblemSetupsAsync(request.SetupIds, cancellationToken) - ); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetProblemSetupsForExecution/GetProblemSetupsForExecutionQuery.cs b/src/ApplicationCore/Queries/Problems/GetProblemSetupsForExecution/GetProblemSetupsForExecutionQuery.cs deleted file mode 100644 index 79e9221..0000000 --- a/src/ApplicationCore/Queries/Problems/GetProblemSetupsForExecution/GetProblemSetupsForExecutionQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using ApplicationCore.Domain.Problems.ProblemSetups; - -namespace ApplicationCore.Queries.Problems.GetProblemSetupsForExecution; - -public sealed record GetProblemSetupsForExecutionQuery(IEnumerable SetupIds) - : IQuery>; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetProblemsPageable/GetProblemsPageableHandler.cs b/src/ApplicationCore/Queries/Problems/GetProblemsPageable/GetProblemsPageableHandler.cs deleted file mode 100644 index 213d558..0000000 --- a/src/ApplicationCore/Queries/Problems/GetProblemsPageable/GetProblemsPageableHandler.cs +++ /dev/null @@ -1,53 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Problems.GetProblemsPageable; - -public sealed class GetProblemsPageableHandler(IProblemRepository repository) - : IQueryHandler> -{ - private readonly IProblemRepository _repository = repository; - - public async Task>> Handle( - GetProblemsPageableQuery request, - CancellationToken cancellationToken - ) - { - try - { - var problemPage = await _repository.GetProblemsAsync( - request.Pagination, - cancellationToken - ); - - var dtoItems = problemPage - .Results.Select(p => new ProblemDto - { - Id = p.Id, - Title = p.Title, - Slug = p.Slug, - Tags = [.. p.Tags.Select(t => t.Value)], - Difficulty = p.Difficulty, - Version = p.Version, - AvailableLanguages = [], - }) - .ToList(); - - var dtoPage = new PaginatedResult - { - Results = dtoItems, - Total = problemPage.Total, - Page = problemPage.Page, - Size = problemPage.Size, - }; - - return Result.Success(dtoPage); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Problems/GetProblemsPageable/GetProblemsPageableQuery.cs b/src/ApplicationCore/Queries/Problems/GetProblemsPageable/GetProblemsPageableQuery.cs deleted file mode 100644 index 50117d4..0000000 --- a/src/ApplicationCore/Queries/Problems/GetProblemsPageable/GetProblemsPageableQuery.cs +++ /dev/null @@ -1,7 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Dtos.Problems; - -namespace ApplicationCore.Queries.Problems.GetProblemsPageable; - -public sealed record GetProblemsPageableQuery(PaginationRequest Pagination) - : IQuery>; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetSolutionsByProblemIdQuery/GetSolutionsByProblemIdHandler.cs b/src/ApplicationCore/Queries/Submissions/GetSolutionsByProblemIdQuery/GetSolutionsByProblemIdHandler.cs deleted file mode 100644 index 082e222..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetSolutionsByProblemIdQuery/GetSolutionsByProblemIdHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Submissions.GetSolutionsByProblemIdQuery; - -public sealed class GetSolutionsByProblemIdHandler(ISubmissionRepository submissionRepository) : IQueryHandler> -{ - public async Task>> Handle(GetSolutionsByProblemIdQuery request, CancellationToken cancellationToken) - { - var pageResult = await submissionRepository.GetSubmissionsByProblemId(problemId: request.ProblemId, accountId: null, pagination: request.Pagination, statusFilter: SubmissionStatus.Accepted, cancellationToken: cancellationToken); - - var dtos = pageResult.Results.Select(submission => new ProblemSubmissionDto( - CreatedBy: submission?.CreatedBy != null ? new AccountDto - { - Username = submission.CreatedBy.Username, - CreatedOn = submission.CreatedBy.CreatedOn, - ImageUrl = submission.CreatedBy.ImageUrl - } : null, - Code: submission.Code, - Status: submission.GetOverallStatus().ToString(), - Language: submission.LanguageVersion.ProgrammingLanguage.Name, - LanguageVersion: submission.LanguageVersion.Version, - CreatedOn: submission.CreatedOn, - RuntimeMs: submission.GetAverageRuntimeMs(), - MemoryKb: submission.GetAverageMemoryKb() - )) ?? []; - - return Result.Success(new PaginatedResult - { - Results = [.. dtos], - Total = pageResult.Total, - Page = pageResult.Page, - Size = pageResult.Size, - }); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetSolutionsByProblemIdQuery/GetSolutionsByProblemIdQuery.cs b/src/ApplicationCore/Queries/Submissions/GetSolutionsByProblemIdQuery/GetSolutionsByProblemIdQuery.cs deleted file mode 100644 index cc198ca..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetSolutionsByProblemIdQuery/GetSolutionsByProblemIdQuery.cs +++ /dev/null @@ -1,10 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Dtos.Problems; - -namespace ApplicationCore.Queries.Submissions.GetSolutionsByProblemIdQuery; - -public sealed record GetSolutionsByProblemIdQuery( - Guid ProblemId, - PaginationRequest Pagination -) : IQuery>; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetSubmissionOutboxes/GetSubmissionOutboxesHandler.cs b/src/ApplicationCore/Queries/Submissions/GetSubmissionOutboxes/GetSubmissionOutboxesHandler.cs deleted file mode 100644 index eaa9349..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetSubmissionOutboxes/GetSubmissionOutboxesHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Submissions.GetSubmissionOutboxes; - -public class GetSubmissionOutboxesHandler(ISubmissionRepository submissionRepository) - : IQueryHandler> -{ - public async Task>> Handle( - GetSubmissionOutboxesQuery request, - CancellationToken cancellationToken - ) - { - try - { - return Result.Success( - await submissionRepository.GetSubmissionOutboxesAsync(cancellationToken) - ); - } - catch (Exception ex) - { - return Result.Error(ex.Message); - } - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetSubmissionOutboxes/GetSubmissionOutboxesQuery.cs b/src/ApplicationCore/Queries/Submissions/GetSubmissionOutboxes/GetSubmissionOutboxesQuery.cs deleted file mode 100644 index d6c5fb4..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetSubmissionOutboxes/GetSubmissionOutboxesQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using ApplicationCore.Domain.Submissions.Outboxes; - -namespace ApplicationCore.Queries.Submissions.GetSubmissionOutboxes; - -public sealed record GetSubmissionOutboxesQuery() : IQuery>; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetSubmissionStatus/GetSubmissionStatusHandler.cs b/src/ApplicationCore/Queries/Submissions/GetSubmissionStatus/GetSubmissionStatusHandler.cs deleted file mode 100644 index 415930a..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetSubmissionStatus/GetSubmissionStatusHandler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using ApplicationCore.Domain.Problems.TestSuites; -using ApplicationCore.Dtos.Submissions; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Submissions.GetSubmissionStatus; - -public sealed class GetSubmissionStatusHandler( - ISubmissionRepository submissionRepository, - IProblemRepository problemRepository -) : IQueryHandler -{ - public async Task> Handle(GetSubmissionStatusQuery request, CancellationToken cancellationToken) - { - var submission = await submissionRepository.GetSubmissionByIdAsync(request.SubmissionId, cancellationToken); - - if (submission is null) - { - return Result.NotFound(); - } - - var results = submission.Results.ToList(); - - if (results.Count == 0) - { - return Result.Success(new SubmissionStatusDto - { - SubmissionId = submission.Id, - Status = submission.GetOverallStatus().ToString(), - TestCases = [], - }); - } - - var setups = await problemRepository.GetProblemSetupsAsync([submission.ProblemSetupId], cancellationToken); - var setup = setups.FirstOrDefault(); - - List testCases = setup is not null - ? setup.TestSuites.SelectMany(ts => ts.TestCases).ToList() - : []; - - var testCaseDtos = results.Select((result, i) => - { - var testCase = i < testCases.Count ? testCases[i] : null; - - return new SubmissionTestCaseResultDto - { - Input = testCase is not null - ? string.Join(",", testCase.Inputs.Select(inp => inp.Value.Trim())) - : string.Empty, - ExpectedOutput = testCase?.ExpectedOutput.Value ?? string.Empty, - ActualOutput = result.ProgramOutput ?? result.Stdout ?? string.Empty, - ErrorOutput = result.Stderr, - Status = (int)result.Status, - }; - }); - - return Result.Success(new SubmissionStatusDto - { - SubmissionId = submission.Id, - Status = submission.GetOverallStatus().ToString(), - TestCases = testCaseDtos.ToList(), - }); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetSubmissionStatus/GetSubmissionStatusQuery.cs b/src/ApplicationCore/Queries/Submissions/GetSubmissionStatus/GetSubmissionStatusQuery.cs deleted file mode 100644 index 9b170b4..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetSubmissionStatus/GetSubmissionStatusQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using ApplicationCore.Dtos.Submissions; - -namespace ApplicationCore.Queries.Submissions.GetSubmissionStatus; - -public sealed record GetSubmissionStatusQuery(Guid SubmissionId) : IQuery; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetSubmissionsPaginated/GetSubmissionsPaginatedHandler.cs b/src/ApplicationCore/Queries/Submissions/GetSubmissionsPaginated/GetSubmissionsPaginatedHandler.cs deleted file mode 100644 index ef15e43..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetSubmissionsPaginated/GetSubmissionsPaginatedHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Dtos.Submissions; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Submissions.GetSubmissionsPaginated; - -public sealed class GetSubmissionsPaginatedHandler(ISubmissionRepository repository) - : IQueryHandler> -{ - public async Task>> Handle( - GetSubmissionsPaginatedQuery request, - CancellationToken cancellationToken - ) - { - SubmissionStatus? statusFilter = request.AcceptedOnly ? SubmissionStatus.Accepted : null; - - var page = await repository.GetSubmissionsByProblemId( - request.ProblemId, - request.FilterByUserId, - request.Pagination, - statusFilter, - cancellationToken - ); - - var dtoItems = page.Results - .Select(s => new SubmissionDto - { - Id = s.Id, - ProblemSetupId = s.ProblemSetupId, - Status = s.GetOverallStatus().ToString(), - Code = s.Code ?? string.Empty, - CreatedOn = s.CreatedOn, - CompletedAt = s.CompletedAt, - CreatedBy = s.CreatedBy is null ? null : new AccountDto - { - Id = s.CreatedBy.Id, - Username = s.CreatedBy.Username, - ImageUrl = s.CreatedBy.ImageUrl, - CreatedOn = s.CreatedBy.CreatedOn, - }, - }) - .ToList(); - - return Result.Success(new PaginatedResult - { - Results = dtoItems, - Total = page.Total, - Page = page.Page, - Size = page.Size, - }); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetSubmissionsPaginated/GetSubmissionsPaginatedQuery.cs b/src/ApplicationCore/Queries/Submissions/GetSubmissionsPaginated/GetSubmissionsPaginatedQuery.cs deleted file mode 100644 index a7c1af9..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetSubmissionsPaginated/GetSubmissionsPaginatedQuery.cs +++ /dev/null @@ -1,11 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Dtos.Submissions; - -namespace ApplicationCore.Queries.Submissions.GetSubmissionsPaginated; - -public sealed record GetSubmissionsPaginatedQuery( - Guid ProblemId, - PaginationRequest Pagination, - Guid? FilterByUserId = null, - bool AcceptedOnly = true -) : IQuery>; \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetUserSubmissionsByProblemIdQuery/GetUserSubmissionsByProblemIdHandler.cs b/src/ApplicationCore/Queries/Submissions/GetUserSubmissionsByProblemIdQuery/GetUserSubmissionsByProblemIdHandler.cs deleted file mode 100644 index 4a68f0e..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetUserSubmissionsByProblemIdQuery/GetUserSubmissionsByProblemIdHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; - -namespace ApplicationCore.Queries.Submissions.GetUserSubmissionsByProblemIdQuery; - -public sealed class GetUserSubmissionsByProblemIdHandler(ISubmissionRepository submissionRepository) : IQueryHandler> -{ - public async Task>> Handle(GetUserSubmissionsByProblemIdQuery request, CancellationToken cancellationToken) - { - var pageResult = await submissionRepository.GetSubmissionsByProblemId(request.ProblemId, request.AccountId, request.Pagination, request.StatusFilter, cancellationToken); - - var dtos = pageResult.Results.Select(submission => new ProblemSubmissionDto( - CreatedBy: submission?.CreatedBy != null ? new AccountDto - { - Username = submission.CreatedBy.Username, - CreatedOn = submission.CreatedBy.CreatedOn, - ImageUrl = submission.CreatedBy.ImageUrl - } : null, - Code: submission.Code, - Status: submission.GetOverallStatus().ToString(), - Language: submission.LanguageVersion.ProgrammingLanguage.Name, - LanguageVersion: submission.LanguageVersion.Version, - CreatedOn: submission.CreatedOn, - RuntimeMs: submission.GetAverageRuntimeMs(), - MemoryKb: submission.GetAverageMemoryKb() - )) ?? []; - - return Result.Success(new PaginatedResult - { - Results = [.. dtos], - Total = pageResult.Total, - Page = pageResult.Page, - Size = pageResult.Size, - }); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Queries/Submissions/GetUserSubmissionsByProblemIdQuery/GetUserSubmissionsByProblemIdQuery.cs b/src/ApplicationCore/Queries/Submissions/GetUserSubmissionsByProblemIdQuery/GetUserSubmissionsByProblemIdQuery.cs deleted file mode 100644 index 0890e5e..0000000 --- a/src/ApplicationCore/Queries/Submissions/GetUserSubmissionsByProblemIdQuery/GetUserSubmissionsByProblemIdQuery.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Dtos.Problems; - -namespace ApplicationCore.Queries.Submissions.GetUserSubmissionsByProblemIdQuery; - -public sealed record GetUserSubmissionsByProblemIdQuery( - Guid ProblemId, - Guid AccountId, - PaginationRequest Pagination, - SubmissionStatus? StatusFilter = null -) : IQuery>; \ No newline at end of file diff --git a/src/ApplicationCore/Services/AccountAppService.cs b/src/ApplicationCore/Services/AccountAppService.cs deleted file mode 100644 index 1f838fa..0000000 --- a/src/ApplicationCore/Services/AccountAppService.cs +++ /dev/null @@ -1,96 +0,0 @@ -using ApplicationCore.Commands.Accounts.CreateAccount; -using ApplicationCore.Commands.Accounts.UpdateProfileSettings; -using ApplicationCore.Commands.Accounts.UpdateUsername; -using ApplicationCore.Commands.Accounts.UpsertAccount; -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Queries.Accounts.GetAccountBySub; -using ApplicationCore.Queries.Accounts.GetProfileAggregate; -using ApplicationCore.Queries.Accounts.GetProfileSettings; -using Ardalis.Result; -using MediatR; -using System; -using System.Collections.Generic; -using System.Text; - -namespace ApplicationCore.Services; - -public sealed class AccountAppService(IMediator mediator) : IAccountAppService -{ - public async Task> CreateAsync( - string username, - string sub, - string imageUrl, - CancellationToken cancellationToken - ) - { - var command = new CreateAccountCommand(username, sub, imageUrl); - - var result = await mediator.Send(command, cancellationToken); - - return result; - } - - public async Task> GetAccountBySubAsync( - string sub, - CancellationToken cancellationToken - ) - { - var query = new GetAccountBySubQuery(sub); - - return await mediator.Send(query, cancellationToken); - } - - public async Task> GetProfileAggregateAsync( - string username, - CancellationToken cancellationToken - ) - { - var query = new GetProfileAggregateQuery(username); - - return await mediator.Send(query, cancellationToken); - } - - public async Task> GetProfileSettingsAsync( - string sub, - CancellationToken cancellationToken - ) - { - var query = new GetProfileSettingsQuery(sub); - - return await mediator.Send(query, cancellationToken); - } - public async Task> UpsertAccountAsync( - string sub, - string? imageUrl, - CancellationToken cancellationToken - ) - { - var command = new UpsertAccountCommand(sub, imageUrl); - - return await mediator.Send(command, cancellationToken); - } - - public async Task> UpdateUsernameAsync( - Guid accountId, - string newUsername, - DateTime? usernameLastChangedAt, - CancellationToken cancellationToken - ) - { - var command = new UpdateUsernameCommand(accountId, newUsername, usernameLastChangedAt); - - return await mediator.Send(command, cancellationToken); - } - - public async Task> UpdateProfileSettingsAsync( - Guid accountId, - string? bio, - CancellationToken cancellationToken - ) - { - var command = new UpdateProfileSettingsCommand(accountId, bio); - - return await mediator.Send(command, cancellationToken); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Services/CodeBuilderService.cs b/src/ApplicationCore/Services/CodeBuilderService.cs deleted file mode 100644 index d1d105f..0000000 --- a/src/ApplicationCore/Services/CodeBuilderService.cs +++ /dev/null @@ -1,112 +0,0 @@ -using ApplicationCore.Domain.CodeExecution; -using ApplicationCore.Interfaces.Services; -using Ardalis.Result; - -namespace ApplicationCore.Services; - -public sealed class CodeBuilderService : ICodeBuilderService -{ - public Result> Build(IEnumerable contexts) - { - var results = new List(); - - foreach (var context in contexts) - { - var validation = ValidateContext(context); - if (validation != null) - { - return validation; - } - - string joinedInputs = string.Join( - ",", - context.Inputs.Select(input => input.Value.Trim()) - ); - - string finalCode = RenderTemplate( - context.Template, - context.Code, - context.FunctionName, - context.InputTypeName ?? "", - joinedInputs - ); - - results.Add( - new CodeBuildResult - { - FinalCode = finalCode, - FunctionName = context.FunctionName, - Inputs = joinedInputs, - ExpectedOutput = context.ExpectedOutput.Value, - InputTypeName = context.InputTypeName, - LanguageId = context.Judge0LanguageId ?? 0, - } - ); - } - - return Result>.Success(results); - } - - private static Result>? ValidateContext( - CodeBuilderContext? context - ) - { - if (context == null) - { - return Result>.Invalid( - new ValidationError("One of the contexts is null.") - ); - } - - if (string.IsNullOrWhiteSpace(context.Code)) - { - return Result>.Invalid( - new ValidationError("Initial code is required.") - ); - } - - if (string.IsNullOrWhiteSpace(context.FunctionName)) - { - return Result>.Invalid( - new ValidationError("Function name is required.") - ); - } - - if (string.IsNullOrWhiteSpace(context.Template)) - { - return Result>.Invalid( - new ValidationError("Harness template is required.") - ); - } - - return null; - } - - private static string RenderTemplate( - string template, - string userCode, - string functionName, - string inputTypeName, - string joinedInputs - ) - { - string inputParser = ResolveInputParser(inputTypeName); - - return template - .Replace("{{USER_CODE}}", userCode) - .Replace("{{FUNCTION_NAME}}", functionName) - .Replace("{{INPUT_PARSER}}", inputParser) - .Replace("{{INPUTS}}", joinedInputs); - } - - private static string ResolveInputParser(string inputTypeName) - { - return inputTypeName switch - { - "number" => "const value = parseInt(data.toString(), 10);", - "array:number" => "const value = data.toString().split(',').map(Number);", - "string" => "const value = data.toString().trim();", - _ => "const value = data.toString();", - }; - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Services/CodeExecutionService.cs b/src/ApplicationCore/Services/CodeExecutionService.cs deleted file mode 100644 index 5eefd1b..0000000 --- a/src/ApplicationCore/Services/CodeExecutionService.cs +++ /dev/null @@ -1,174 +0,0 @@ -using ApplicationCore.Domain.CodeExecution; -using ApplicationCore.Domain.CodeExecution.Judge0; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Interfaces.Clients; -using ApplicationCore.Interfaces.Services; -using Ardalis.Result; - -namespace ApplicationCore.Services; - -public sealed class CodeExecutionService(IJudge0Client judge0Client) : ICodeExecutionService -{ - public async Task>> ExecuteAsync( - IEnumerable contexts, - CancellationToken cancellationToken - ) - { - var contextList = contexts.ToList(); - if (contextList.Count == 0) - { - return Result.Success(Enumerable.Empty()); - } - - var submissions = new List(); - var judge0Requests = new List(); - var indexMap = new List<(List Results, int ResultIndex)>(); - - foreach (var context in contextList) - { - var results = new List(); - - foreach (var buildResult in context.BuiltResults) - { - if (buildResult.LanguageId == 0) - { - return Result.Error( - $"No Judge0 language mapping found for this problem setup. Ensure the language version has an engine mapping configured." - ); - } - - judge0Requests.Add( - new Judge0SubmissionRequest - { - LanguageId = buildResult.LanguageId, - SourceCode = buildResult.FinalCode, - StdIn = buildResult.Inputs, - ExpectedOutput = buildResult.ExpectedOutput, - } - ); - - results.Add( - new SubmissionResult { Id = Guid.NewGuid(), Status = SubmissionStatus.InQueue } - ); - - indexMap.Add((results, results.Count - 1)); - } - - var submission = new SubmissionModel - { - Id = context.SubmissionId ?? Guid.NewGuid(), - CreatedById = context.CreatedById, - Results = results, - }; - - submissions.Add(submission); - } - - if (!judge0Requests.Any()) - { - return Result.Success>(submissions); - } - - var judge0Response = await judge0Client.SubmitAsync(judge0Requests, cancellationToken); - - if (!judge0Response.IsSuccess) - { - return Result.Error("Failed to submit code for execution."); - } - - var responseList = judge0Response.Value.ToList(); - - if (responseList.Count != indexMap.Count) - { - return Result.Error("Mismatch between Judge0 responses and submitted jobs."); - } - - for (int i = 0; i < responseList.Count; i++) - { - var response = responseList[i]; - (var results, int resultIndex) = indexMap[i]; - - var result = results[resultIndex]; - result.ExecutionId = response.Token; - result.Status = SubmissionStatus.InQueue; - } - - return Result.Success>(submissions); - } - - public async Task>> GetSubmissionResultsAsync( - IEnumerable submissions, - CancellationToken cancellationToken - ) - { - var submissionList = submissions.ToList(); - if (!submissionList.Any()) - { - return Result.Success(Enumerable.Empty()); - } - - var tokenMap = submissionList - .SelectMany(s => s.Results.Select(r => (Submission: s, Token: r.ExecutionId))) - .ToDictionary(x => x.Token, x => x.Submission); - - var judge0Results = await judge0Client.GetAsync(tokenMap.Keys, cancellationToken); - - if (!judge0Results.IsSuccess) - { - return Result.Error("Failed to retrieve submission results."); - } - - foreach (var result in judge0Results.Value) - { - if (!tokenMap.TryGetValue(result.Token, out var submission)) - { - continue; - } - - var submissionResult = submission.Results.First(r => r.ExecutionId == result.Token); - - submissionResult.Status = MapJudge0SubmissionStatus(result.Status); - submissionResult.Stderr = result.Stderr; - submissionResult.RuntimeMs = decimal.TryParse(result.Time, out decimal seconds) - ? (int)Math.Ceiling(seconds * 1000) - : null; - submissionResult.MemoryKb = result.Memory; - submissionResult.FinishedAt = DateTime.UtcNow; - - string rawStdout = result.Stdout ?? ""; - submissionResult.ProgramOutput = rawStdout; - - if (!string.IsNullOrEmpty(rawStdout)) - { - string[] lines = rawStdout.ReplaceLineEndings("\n").TrimEnd('\n').Split('\n'); - submissionResult.Stdout = lines.Length > 1 ? string.Join("\n", lines[..^1]) : null; - } - } - - return Result.Success>(submissionList); - } - - private static SubmissionStatus MapJudge0SubmissionStatus(Judge0StatusModel status) => - status.Id switch - { - 1 => SubmissionStatus.InQueue, - 2 => SubmissionStatus.Processing, - 3 => SubmissionStatus.Accepted, - 4 => SubmissionStatus.WrongAnswer, - 5 => SubmissionStatus.TimeLimitExceeded, - 6 => SubmissionStatus.CompilationError, - 7 => SubmissionStatus.RuntimeErrorSigSegv, - 8 => SubmissionStatus.RuntimeErrorSigXfsz, - 9 => SubmissionStatus.RuntimeErrorSigFpe, - 10 => SubmissionStatus.RuntimeErrorSigAbrt, - 11 => SubmissionStatus.RuntimeErrorNzec, - 12 => SubmissionStatus.RuntimeErrorOther, - 13 => SubmissionStatus.InternalError, - 14 => SubmissionStatus.ExecFormatError, - _ => throw new ArgumentOutOfRangeException( - nameof(status.Id), - status.Id, - "Unknown Judge0 submission status" - ), - }; -} \ No newline at end of file diff --git a/src/ApplicationCore/Services/ExecutionComparisonService.cs b/src/ApplicationCore/Services/ExecutionComparisonService.cs deleted file mode 100644 index 80379dc..0000000 --- a/src/ApplicationCore/Services/ExecutionComparisonService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Interfaces.Services; - -namespace ApplicationCore.Services; - -public sealed class ExecutionComparisonService : IExecutionComparisonService -{ - public SubmissionStatus Compare(string? programOutput, string expectedOutput) - { - if (programOutput is null) - { - return SubmissionStatus.WrongAnswer; - } - - string[] lines = programOutput.ReplaceLineEndings("\n").TrimEnd('\n').Split('\n'); - string lastLine = lines[^1]; - - string actual = Normalize(lastLine); - string expected = Normalize(expectedOutput); - - return actual == expected ? SubmissionStatus.Accepted : SubmissionStatus.WrongAnswer; - } - - private static string Normalize(string value) => value.Trim().ReplaceLineEndings("\n"); -} \ No newline at end of file diff --git a/src/ApplicationCore/Services/ProblemAppService.cs b/src/ApplicationCore/Services/ProblemAppService.cs deleted file mode 100644 index 212a70b..0000000 --- a/src/ApplicationCore/Services/ProblemAppService.cs +++ /dev/null @@ -1,96 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Problems.ProblemSetups; -using ApplicationCore.Dtos.Languages; -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Queries.Problems.GetAvailableLanguages; -using ApplicationCore.Queries.Problems.GetProblemBySlug; -using ApplicationCore.Queries.Problems.GetProblemSetup; -using ApplicationCore.Queries.Problems.GetProblemSetupsForExecution; -using ApplicationCore.Queries.Problems.GetProblemsPageable; -using Ardalis.Result; -using MediatR; - -namespace ApplicationCore.Services; - -public sealed class ProblemAppService(IMediator mediator) : IProblemAppService -{ - public Task>> GetAvailableLanguagesAsync( - CancellationToken cancellationToken - ) - { - var query = new GetAvailableLanguagesQuery(); - - return mediator.Send(query, cancellationToken); - } - - public async Task> GetProblemBySlugAsync( - string slug, - CancellationToken cancellationToken - ) - { - var query = new GetProblemBySlugQuery(slug); - - return await mediator.Send(query, cancellationToken); - } - - public async Task> GetProblemSetupAsync( - Guid problemId, - int languageVersionId, - CancellationToken cancellationToken - ) - { - var query = new GetProblemSetupQuery(problemId, languageVersionId); - - return await mediator.Send(query, cancellationToken); - } - - public async Task>> GetProblemsPaginatedAsync( - int pageNumber, - int pageSize, - DateTime timestamp, - CancellationToken cancellationToken - ) - { - var pagination = new PaginationRequest - { - Page = pageNumber, - Size = pageSize, - Timestamp = timestamp, - }; - var query = new GetProblemsPageableQuery(pagination); - - return await mediator.Send(query, cancellationToken); - } - - public async Task>> GetProblemsPaginatedAsync( - int pageNumber, - int pageSize, - DateTime timestamp, - string query, - CancellationToken cancellationToken - ) - { - var pagination = new PaginationRequest - { - Page = pageNumber, - Size = pageSize, - Timestamp = timestamp, - Query = query, - }; - - var pageableQuery = new GetProblemsPageableQuery(pagination); - - return await mediator.Send(pageableQuery, cancellationToken); - } - - public Task>> GetProblemSetupsForExecutionAsync( - IEnumerable setupIds, - CancellationToken cancellationToken - ) - { - var query = new GetProblemSetupsForExecutionQuery(setupIds); - - return mediator.Send(query, cancellationToken); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Services/SubmissionAppService.cs b/src/ApplicationCore/Services/SubmissionAppService.cs deleted file mode 100644 index e6a607d..0000000 --- a/src/ApplicationCore/Services/SubmissionAppService.cs +++ /dev/null @@ -1,127 +0,0 @@ -using ApplicationCore.Commands.Submissions.CreateSubmission; -using ApplicationCore.Commands.Submissions.FinalizeEvaluation; -using ApplicationCore.Commands.Submissions.IncrementSubmissionOutboxes; -using ApplicationCore.Commands.Submissions.ProcessEvaluation; -using ApplicationCore.Commands.Submissions.ProcessPollingSubmissionExecutions; -using ApplicationCore.Commands.Submissions.ProcessSubmissionExecutions; -using ApplicationCore.Commands.Submissions.SaveExecutionTokens; -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Dtos.Submissions; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Queries.Submissions.GetSolutionsByProblemIdQuery; -using ApplicationCore.Queries.Submissions.GetSubmissionOutboxes; -using ApplicationCore.Queries.Submissions.GetSubmissionsPaginated; -using ApplicationCore.Queries.Submissions.GetSubmissionStatus; -using ApplicationCore.Queries.Submissions.GetUserSubmissionsByProblemIdQuery; -using Ardalis.Result; -using MediatR; -using static ApplicationCore.Logging.LoggingEventIds; - -namespace ApplicationCore.Services; - -public sealed class SubmissionAppService(IMediator mediator) : ISubmissionAppService -{ - public async Task> CreateAsync( - int problemSetupId, - string code, - Guid createdById, - CancellationToken cancellationToken - ) - { - var command = new CreateSubmissionCommand(problemSetupId, code, createdById); - - return await mediator.Send(command, cancellationToken); - } - - public async Task>> GetSubmissionOutboxesAsync( - CancellationToken cancellationToken - ) - { - var query = new GetSubmissionOutboxesQuery(); - - return await mediator.Send(query, cancellationToken); - } - - public async Task> IncrementOutboxesCountAsync( - IEnumerable outboxIds, - DateTime timestamp, - CancellationToken cancellationToken - ) - { - var command = new IncrementSubmissionOutboxesCommand(outboxIds, timestamp); - - return await mediator.Send(command, cancellationToken); - } - - public async Task> SaveExecutionTokensAsync( - IEnumerable results, - CancellationToken cancellationToken - ) - { - var command = new SaveExecutionTokensCommand(results); - - return await mediator.Send(command, cancellationToken); - } - - public async Task> ProcessSubmissionExecutionAsync( - IEnumerable results, - CancellationToken cancellationToken - ) - { - var command = new ProcessSubmissionExecutionsCommand(results); - - return await mediator.Send(command, cancellationToken); - } - - public async Task> ProcessPollingSubmissionExecutionsAsync( - IEnumerable results, - CancellationToken cancellationToken - ) - { - var command = new ProcessPollingSubmissionExecutionsCommand(results); - - return await mediator.Send(command, cancellationToken); - } - - public async Task> ProcessEvaluationAsync( - IEnumerable results, - CancellationToken cancellationToken - ) - { - var command = new ProcessEvaluationCommand(results); - - return await mediator.Send(command, cancellationToken); - } - - public async Task> FinalizeEvaluationAsync( - IEnumerable outboxIds, - DateTime now, - CancellationToken cancellationToken - ) - { - var command = new FinalizeEvaluationCommand(outboxIds, now); - - return await mediator.Send(command, cancellationToken); - } - - public Task>> GetSolutionsAsync(Guid problemId, PaginationRequest paginationRequest, CancellationToken cancellationToken) - { - var query = new GetSolutionsByProblemIdQuery(problemId, paginationRequest); - return mediator.Send(query, cancellationToken); - } - - public Task>> GetSubmissionsPaginatedAsync(Guid problemId, Guid accountId, PaginationRequest paginationRequest, CancellationToken cancellationToken = default) - { - var query = new GetUserSubmissionsByProblemIdQuery(problemId, accountId, paginationRequest); - return mediator.Send(query, cancellationToken); - } - - public Task> GetSubmissionStatusAsync(Guid submissionId, CancellationToken cancellationToken) - { - var query = new GetSubmissionStatusQuery(submissionId); - return mediator.Send(query, cancellationToken); - } -} \ No newline at end of file diff --git a/src/ApplicationCore/Settings/ConnectionStringsSettings.cs b/src/ApplicationCore/Settings/ConnectionStringsSettings.cs deleted file mode 100644 index a5b88b3..0000000 --- a/src/ApplicationCore/Settings/ConnectionStringsSettings.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Settings; - -public sealed class ConnectionStringsSettings : ISettings -{ - public static string SectionKey => "ConnectionStrings"; - public required string DefaultConnection { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Settings/CorsSettings.cs b/src/ApplicationCore/Settings/CorsSettings.cs deleted file mode 100644 index df2e5f9..0000000 --- a/src/ApplicationCore/Settings/CorsSettings.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Settings; - -public class CorsSettings : ISettings -{ - public static string SectionKey => "CorsSettings"; - public required IEnumerable AllowedOrigins { get; set; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Settings/ISettings.cs b/src/ApplicationCore/Settings/ISettings.cs deleted file mode 100644 index 6cffbc0..0000000 --- a/src/ApplicationCore/Settings/ISettings.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ApplicationCore.Settings; - -public interface ISettings -{ - static abstract string SectionKey { get; } -} \ No newline at end of file diff --git a/src/ApplicationCore/Settings/MediatRSettings.cs b/src/ApplicationCore/Settings/MediatRSettings.cs deleted file mode 100644 index ff27011..0000000 --- a/src/ApplicationCore/Settings/MediatRSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ApplicationCore.Settings; - -public class MediatRSettings : ISettings -{ - public static string SectionKey => "MediatR"; - - public string? LicenseKey { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/CodeExecution/Judge0/Judge0Client.cs b/src/Infrastructure/CodeExecution/Judge0/Judge0Client.cs deleted file mode 100644 index ce4562e..0000000 --- a/src/Infrastructure/CodeExecution/Judge0/Judge0Client.cs +++ /dev/null @@ -1,185 +0,0 @@ -using ApplicationCore.Domain.CodeExecution.Judge0; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Interfaces.Clients; -using ApplicationCore.Logging; -using Ardalis.Result; -using Infrastructure.Configuration; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Diagnostics; -using System.Net.Http.Json; -using System.Text.Json; - -namespace Infrastructure.CodeExecution.Judge0; - -public sealed partial class Judge0Client( - HttpClient httpClient, - IOptions judge0Options, - JsonSerializerOptions jsonOptions, - ILogger logger -) : IJudge0Client -{ - private readonly ILogger _logger = logger; - private readonly Judge0Options _judge0Options = judge0Options.Value; - private const int BatchSize = 20; - - public async Task>> GetAsync( - IEnumerable tokens, - CancellationToken cancellationToken, - IEnumerable? fields = null - ) - { - var tokenList = tokens.ToList(); - LogGetStarted(tokenList.Count); - var sw = Stopwatch.StartNew(); - - try - { - var allSubmissions = new List(); - - foreach (var batch in tokenList.Chunk(BatchSize)) - { - var query = new Dictionary() - { - ["tokens"] = string.Join(",", batch), - ["fields"] = fields is not null ? string.Join(",", fields) : "*", - }; - - string uri = QueryHelpers.AddQueryString("submissions/batch", query); - - var response = await httpClient.GetAsync(uri, cancellationToken); - - response.EnsureSuccessStatusCode(); - - var batchResult = await response.Content.ReadFromJsonAsync( - jsonOptions, - cancellationToken - ); - - if (batchResult?.Submissions is { Count: > 0 }) - { - allSubmissions.AddRange(batchResult.Submissions); - } - } - - if (allSubmissions.Count == 0) - { - return Result.Error("No submissions found"); - } - - LogGetCompleted(tokenList.Count, sw.ElapsedMilliseconds); - return Result.Success(allSubmissions); - } - catch (HttpRequestException ex) - { - LogGetFailed(tokenList.Count, sw.ElapsedMilliseconds, ex); - return Result.Error($"HTTP request failed: {ex.Message}"); - } - catch (JsonException ex) - { - LogGetFailed(tokenList.Count, sw.ElapsedMilliseconds, ex); - return Result.Error($"JSON deserialization failed: {ex.Message}"); - } - catch (Exception ex) - { - LogGetFailed(tokenList.Count, sw.ElapsedMilliseconds, ex); - return Result.Error(ex.Message); - } - } - - public async Task>> SubmitAsync( - IEnumerable reqs, - CancellationToken cancellationToken, - IEnumerable? fields = null - ) - { - var reqList = reqs.ToList(); - LogSubmitStarted(reqList.Count); - var sw = Stopwatch.StartNew(); - - try - { - var allResponses = new List(); - - foreach (var batch in reqList.Chunk(BatchSize)) - { - var query = new Dictionary() - { - ["base64_encoded"] = _judge0Options.IsEncoded.ToString().ToLowerInvariant(), - ["fields"] = fields is not null ? string.Join(",", fields) : "*", - }; - - string uri = QueryHelpers.AddQueryString("submissions/batch", query); - - var payload = new Judge0BatchRequest { Submissions = batch }; - - var response = await httpClient.PostAsJsonAsync(uri, payload, cancellationToken); - - response.EnsureSuccessStatusCode(); - - var body = await response.Content.ReadFromJsonAsync< - List - >(jsonOptions, cancellationToken); - - if (body is { Count: > 0 }) - { - allResponses.AddRange( - body.Select(result => new Judge0SubmissionResponse - { - Token = result.Token, - Status = new Judge0StatusModel { Id = (int)SubmissionStatus.InQueue }, - }) - ); - } - } - - if (allResponses.Count == 0) - { - return Result.Error("No submissions found"); - } - - LogSubmitCompleted(reqList.Count, sw.ElapsedMilliseconds); - return Result.Success(allResponses); - } - catch (HttpRequestException ex) - { - LogSubmitFailed(reqList.Count, sw.ElapsedMilliseconds, ex); - return Result.Error($"HTTP request failed: {ex.Message}"); - } - catch (JsonException ex) - { - LogSubmitFailed(reqList.Count, sw.ElapsedMilliseconds, ex); - return Result.Error($"JSON deserialization failed: {ex.Message}"); - } - catch (Exception ex) - { - LogSubmitFailed(reqList.Count, sw.ElapsedMilliseconds, ex); - return Result.Error(ex.Message); - } - } - - [LoggerMessage(EventId = LoggingEventIds.Judge0.SubmitStarted, Level = LogLevel.Information, - Message = "Judge0: Submitting {count} submission(s)")] - private partial void LogSubmitStarted(int count); - - [LoggerMessage(EventId = LoggingEventIds.Judge0.SubmitCompleted, Level = LogLevel.Information, - Message = "Judge0: Submitted {count} submission(s) in {elapsedMs}ms")] - private partial void LogSubmitCompleted(int count, long elapsedMs); - - [LoggerMessage(EventId = LoggingEventIds.Judge0.SubmitFailed, Level = LogLevel.Error, - Message = "Judge0: Submit failed for {count} submission(s) after {elapsedMs}ms")] - private partial void LogSubmitFailed(int count, long elapsedMs, Exception ex); - - [LoggerMessage(EventId = LoggingEventIds.Judge0.GetStarted, Level = LogLevel.Information, - Message = "Judge0: Polling {count} token(s)")] - private partial void LogGetStarted(int count); - - [LoggerMessage(EventId = LoggingEventIds.Judge0.GetCompleted, Level = LogLevel.Information, - Message = "Judge0: Polled {count} token(s) in {elapsedMs}ms")] - private partial void LogGetCompleted(int count, long elapsedMs); - - [LoggerMessage(EventId = LoggingEventIds.Judge0.GetFailed, Level = LogLevel.Error, - Message = "Judge0: Poll failed for {count} token(s) after {elapsedMs}ms")] - private partial void LogGetFailed(int count, long elapsedMs, Exception ex); -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/Auth0ManagementOptions.cs b/src/Infrastructure/Configuration/Auth0ManagementOptions.cs deleted file mode 100644 index b9b28c4..0000000 --- a/src/Infrastructure/Configuration/Auth0ManagementOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Infrastructure.Configuration; - -public sealed class Auth0ManagementOptions -{ - public required string ClientId { get; init; } -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/Auth0Options.cs b/src/Infrastructure/Configuration/Auth0Options.cs deleted file mode 100644 index b136d47..0000000 --- a/src/Infrastructure/Configuration/Auth0Options.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Infrastructure.Configuration; - -public sealed class Auth0Options -{ - public required string Audience { get; init; } - - public required string Domain { get; init; } - - public required Auth0ManagementOptions Management { get; init; } -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/ConnectionStringOptions.cs b/src/Infrastructure/Configuration/ConnectionStringOptions.cs deleted file mode 100644 index 02d4b56..0000000 --- a/src/Infrastructure/Configuration/ConnectionStringOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Infrastructure.Configuration; - -public sealed class ConnectionStringOptions -{ - public const string SectionName = "ConnectionStrings"; - - public required string DefaultConnection { get; init; } -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/CorsOptions.cs b/src/Infrastructure/Configuration/CorsOptions.cs deleted file mode 100644 index 8a408a9..0000000 --- a/src/Infrastructure/Configuration/CorsOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Infrastructure.Configuration; - -public sealed class CorsOptions -{ - public string[] AllowedOrigins { get; init; } = []; -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/ExecutionEnginesOptions.cs b/src/Infrastructure/Configuration/ExecutionEnginesOptions.cs deleted file mode 100644 index 6f14898..0000000 --- a/src/Infrastructure/Configuration/ExecutionEnginesOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Infrastructure.Configuration; - -public sealed class ExecutionEnginesOptions -{ - public required Judge0Options Judge0 { get; init; } -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/Judge0Options.cs b/src/Infrastructure/Configuration/Judge0Options.cs deleted file mode 100644 index a823d6b..0000000 --- a/src/Infrastructure/Configuration/Judge0Options.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Infrastructure.Configuration; - -public sealed class Judge0Options -{ - public bool Enabled { get; init; } - - public bool RunWorker { get; init; } - - public required string BaseUrl { get; init; } - - public required string ApiKey { get; init; } - - public required string Host { get; init; } - - public bool ShouldWait { get; init; } - - public bool IsEncoded { get; init; } - - public int DefaultTimeoutInSeconds { get; init; } -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/LogLevelOptions.cs b/src/Infrastructure/Configuration/LogLevelOptions.cs deleted file mode 100644 index 3a67473..0000000 --- a/src/Infrastructure/Configuration/LogLevelOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Infrastructure.Configuration; - -internal class LogLevelOptions -{ -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/LoggingOptions.cs b/src/Infrastructure/Configuration/LoggingOptions.cs deleted file mode 100644 index 0b7aeea..0000000 --- a/src/Infrastructure/Configuration/LoggingOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Infrastructure.Configuration; - -internal class LoggingOptions -{ -} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/MessageBusOptions.cs b/src/Infrastructure/Configuration/MessageBusOptions.cs deleted file mode 100644 index a7df64e..0000000 --- a/src/Infrastructure/Configuration/MessageBusOptions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Infrastructure.Configuration; - -public sealed class MessageBusOptions -{ - public const string SectionName = "MessageBus"; - - public string Transport { get; init; } = "RabbitMQ"; - - public RabbitMqOptions RabbitMQ { get; init; } = new(); - - public AzureServiceBusOptions AzureServiceBus { get; init; } = new(); -} - -public sealed class RabbitMqOptions -{ - public string Host { get; init; } = "localhost"; - public string VirtualHost { get; init; } = "/"; - public string Username { get; init; } = "guest"; - public string Password { get; init; } = "guest"; -} - -public sealed class AzureServiceBusOptions -{ - public string ConnectionString { get; init; } = string.Empty; -} \ No newline at end of file diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs deleted file mode 100644 index cbf9a3d..0000000 --- a/src/Infrastructure/DependencyInjection.cs +++ /dev/null @@ -1,220 +0,0 @@ -using ApplicationCore.Interfaces.Clients; -using ApplicationCore.Interfaces.Messaging; -using ApplicationCore.Interfaces.Repositories; -using ApplicationCore.Interfaces.Services; -using Infrastructure.CodeExecution.Judge0; -using Infrastructure.Configuration; -using Infrastructure.Jobs; -using Infrastructure.Jobs.JobHandlers; -using Infrastructure.Messaging; -using Infrastructure.Messaging.Consumers; -using Infrastructure.Persistence; -using Infrastructure.Repositories; -using Infrastructure.Services; -using MassTransit; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Quartz; -using System.Text.Json; - -namespace Infrastructure; - -public static class DependencyInjection -{ - public static IServiceCollection AddInfrastructure( - this IServiceCollection services, - IConfiguration configuration - ) - { - services - .AddOptions(configuration) - .AddPersistence(configuration) - .AddRepositories() - .AddServices() - .AddMessageBus(configuration) - .AddJudge0Client(configuration) - .AddJobs(); - - return services; - } - - private static IServiceCollection AddOptions( - this IServiceCollection services, - IConfiguration configuration - ) - { - services - .AddOptions() - .Bind(configuration.GetSection(ConnectionStringOptions.SectionName)) - .Validate( - o => !string.IsNullOrWhiteSpace(o.DefaultConnection), - "DefaultConnection connection string is required" - ) - .ValidateOnStart(); - - services - .AddOptions() - .Bind(configuration.GetSection("ExecutionEngines")) - .ValidateOnStart(); - - services.AddSingleton( - new JsonSerializerOptions(JsonSerializerDefaults.Web) - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = System - .Text - .Json - .Serialization - .JsonIgnoreCondition - .WhenWritingNull, - } - ); - - return services; - } - - private static IServiceCollection AddPersistence( - this IServiceCollection services, - IConfiguration configuration - ) - { - services.AddDbContext( - (sp, o) => - { - var cs = sp.GetRequiredService>().Value; - o.UseNpgsql(cs.DefaultConnection); - } - ); - - return services; - } - - private static IServiceCollection AddRepositories(this IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - return services; - } - - private static IServiceCollection AddServices(this IServiceCollection services) - { - services.AddScoped(); - - return services; - } - - private static IServiceCollection AddMessageBus( - this IServiceCollection services, - IConfiguration configuration - ) - { - services - .AddOptions() - .Bind(configuration.GetSection(MessageBusOptions.SectionName)) - .ValidateOnStart(); - - services.AddScoped(); - - services.AddMassTransit(bus => - { - bus.AddConsumer(); - bus.AddConsumer(); - bus.AddConsumer(); - bus.AddConsumer(); - - string transport = configuration - .GetSection(MessageBusOptions.SectionName) - .GetValue("Transport") ?? "RabbitMQ"; - - if (transport.Equals("AzureServiceBus", StringComparison.OrdinalIgnoreCase)) - { - bus.UsingAzureServiceBus((ctx, cfg) => - { - var opts = ctx.GetRequiredService>().Value; - cfg.Host(opts.AzureServiceBus.ConnectionString); - cfg.ConfigureEndpoints(ctx); - }); - } - else - { - bus.UsingRabbitMq((ctx, cfg) => - { - var opts = ctx.GetRequiredService>().Value; - cfg.Host(opts.RabbitMQ.Host, opts.RabbitMQ.VirtualHost, h => - { - h.Username(opts.RabbitMQ.Username); - h.Password(opts.RabbitMQ.Password); - }); - cfg.ConfigureEndpoints(ctx); - }); - } - }); - - return services; - } - - private static IServiceCollection AddJobs(this IServiceCollection services) - { - services.AddQuartz(q => - { - q.AddJobAndTrigger(JobType.SubmissionExecution, intervalInMinutes: 60); - q.AddJobAndTrigger(JobType.PollSubmissionExecution, intervalInMinutes: 60); - q.AddJobAndTrigger(JobType.EvaluateSubmission, intervalInMinutes: 60); - q.AddJobAndTrigger(JobType.PollEvaluation, intervalInMinutes: 60); - }); - - services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); - - return services; - } - - private static void AddJobAndTrigger(this IServiceCollectionQuartzConfigurator q, JobType jobType, int intervalInMinutes) - where T : IJob - { - string jobName = jobType.ToString(); - var jobKey = new JobKey(jobName); - - q.AddJob(opts => opts.WithIdentity(jobKey)); - q.AddTrigger(opts => opts - .ForJob(jobKey) - .WithIdentity($"{jobName}-trigger") - .WithSimpleSchedule(s => s - .WithIntervalInMinutes(intervalInMinutes) - .RepeatForever() - ) - ); - } - - private static IServiceCollection AddJudge0Client( - this IServiceCollection services, - IConfiguration configuration - ) - { - services.AddHttpClient( - (serviceProvider, client) => - { - var judge0 = serviceProvider - .GetRequiredService>() - .Value.Judge0; - - string baseUrl = judge0.BaseUrl.EndsWith('/') - ? judge0.BaseUrl - : judge0.BaseUrl + "/"; - - client.BaseAddress = new Uri(baseUrl); - client.Timeout = TimeSpan.FromSeconds(judge0.DefaultTimeoutInSeconds); - - client.DefaultRequestHeaders.Add("x-rapidapi-host", judge0.Host); - client.DefaultRequestHeaders.Add("x-rapidapi-key", judge0.ApiKey); - client.DefaultRequestHeaders.Add("Accept", "application/json"); - } - ); - - return services; - } -} \ No newline at end of file diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj deleted file mode 100644 index 64db651..0000000 --- a/src/Infrastructure/Infrastructure.csproj +++ /dev/null @@ -1,38 +0,0 @@ - - - net10.0 - enable - enable - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - ..\..\..\..\..\.nuget\packages\microsoft.aspnetcore.app.ref\10.0.0\ref\net10.0\Microsoft.Extensions.Hosting.Abstractions.dll - - - diff --git a/src/Infrastructure/Jobs/JobBase.cs b/src/Infrastructure/Jobs/JobBase.cs deleted file mode 100644 index fe7f416..0000000 --- a/src/Infrastructure/Jobs/JobBase.cs +++ /dev/null @@ -1,50 +0,0 @@ -using ApplicationCore.Logging; -using Microsoft.Extensions.Logging; -using Quartz; - -namespace Infrastructure.Jobs; - -public abstract partial class JobBase : IJob -{ - public abstract JobType JobType { get; } - - protected abstract ILogger Logger { get; } - - public async Task Execute(IJobExecutionContext context) - { - LogJobStarted(Logger, JobType); - try - { - await ExecuteJobAsync(context.CancellationToken); - LogJobCompleted(Logger, JobType); - } - catch (Exception ex) - { - LogJobFailed(Logger, JobType, ex); - throw new JobExecutionException(ex, refireImmediately: false); - } - } - - protected abstract Task ExecuteJobAsync(CancellationToken cancellationToken); - - [LoggerMessage( - EventId = LoggingEventIds.Jobs.Started, - Level = LogLevel.Information, - Message = "Job {jobType} started" - )] - private static partial void LogJobStarted(ILogger logger, JobType jobType); - - [LoggerMessage( - EventId = LoggingEventIds.Jobs.Completed, - Level = LogLevel.Information, - Message = "Job {jobType} completed" - )] - private static partial void LogJobCompleted(ILogger logger, JobType jobType); - - [LoggerMessage( - EventId = LoggingEventIds.Jobs.Failed, - Level = LogLevel.Error, - Message = "Job {jobType} failed" - )] - private static partial void LogJobFailed(ILogger logger, JobType jobType, Exception ex); -} \ No newline at end of file diff --git a/src/Infrastructure/Jobs/JobHandlers/EvaluateSubmissionHandler.cs b/src/Infrastructure/Jobs/JobHandlers/EvaluateSubmissionHandler.cs deleted file mode 100644 index 69f7460..0000000 --- a/src/Infrastructure/Jobs/JobHandlers/EvaluateSubmissionHandler.cs +++ /dev/null @@ -1,118 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Logging; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Quartz; - -namespace Infrastructure.Jobs.JobHandlers; - -/// -/// Recovery sweep for Stage 3: compares each result's stdout against its -/// expected output and transitions outbox Evaluate → EvaluationPoll. -/// Mirrors . -/// -[DisallowConcurrentExecution] -public sealed partial class EvaluateSubmissionHandler( - IServiceScopeFactory serviceScopeFactory, - ILogger logger -) : JobBase -{ - public override JobType JobType => JobType.EvaluateSubmission; - - protected override ILogger Logger => logger; - - protected override async Task ExecuteJobAsync(CancellationToken cancellationToken) - { - using var scope = serviceScopeFactory.CreateScope(); - var submissionAppService = scope.ServiceProvider.GetRequiredService(); - var problemAppService = scope.ServiceProvider.GetRequiredService(); - var comparisonService = scope.ServiceProvider.GetRequiredService(); - - var outboxResults = await submissionAppService.GetSubmissionOutboxesAsync(cancellationToken); - - if (!outboxResults.IsSuccess || !outboxResults.Value.Any()) - { - return; - } - - var outboxes = outboxResults.Value - .Where(o => o.Type == SubmissionOutboxType.Evaluate) - .ToList(); - - if (outboxes.Count == 0) - { - return; - } - - LogEvaluating(logger, outboxes.Count); - - var setupIds = outboxes.Select(o => o.Submission.ProblemSetupId).Distinct(); - var setupsMap = ( - await problemAppService.GetProblemSetupsForExecutionAsync(setupIds, cancellationToken) - ).Value.ToDictionary(s => s.Id); - - var evaluated = new List(); - - foreach (var outbox in outboxes) - { - if (!setupsMap.TryGetValue(outbox.Submission.ProblemSetupId, out var setup)) - { - continue; - } - - var expectedOutputs = setup.TestSuites - .SelectMany(ts => ts.TestCases) - .Select(tc => tc.ExpectedOutput.Value) - .ToList(); - - var results = outbox.Submission.Results.ToList(); - - for (int i = 0; i < results.Count; i++) - { - if (results[i].Status != SubmissionStatus.Processing) - { - continue; - } - - string expected = i < expectedOutputs.Count ? expectedOutputs[i] : string.Empty; - results[i].Status = comparisonService.Compare(results[i].ProgramOutput, expected); - } - - evaluated.Add(new SubmissionModel - { - Id = outbox.SubmissionId, - CreatedById = outbox.Submission.CreatedById, - Results = results, - }); - } - - if (evaluated.Count == 0) - { - return; - } - - LogEvaluated(logger, evaluated.Count); - - var outboxIds = outboxes.Select(o => o.Id).ToList(); - var now = DateTime.UtcNow; - - await submissionAppService.IncrementOutboxesCountAsync(outboxIds, now, cancellationToken); - await submissionAppService.ProcessEvaluationAsync(evaluated, cancellationToken); - } - - [LoggerMessage( - EventId = LoggingEventIds.Jobs.EvaluateSubmissionEvaluating, - Level = LogLevel.Information, - Message = "Evaluating {count} submission outboxes" - )] - private static partial void LogEvaluating(ILogger logger, int count); - - [LoggerMessage( - EventId = LoggingEventIds.Jobs.EvaluateSubmissionEvaluated, - Level = LogLevel.Information, - Message = "Evaluated {count} submissions" - )] - private static partial void LogEvaluated(ILogger logger, int count); -} \ No newline at end of file diff --git a/src/Infrastructure/Jobs/JobHandlers/PollEvaluationHandler.cs b/src/Infrastructure/Jobs/JobHandlers/PollEvaluationHandler.cs deleted file mode 100644 index 246810d..0000000 --- a/src/Infrastructure/Jobs/JobHandlers/PollEvaluationHandler.cs +++ /dev/null @@ -1,60 +0,0 @@ -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Logging; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Quartz; - -namespace Infrastructure.Jobs.JobHandlers; - -/// -/// Recovery sweep for Stage 4: finalizes outboxes stuck in EvaluationPoll. -/// Mirrors . -/// -[DisallowConcurrentExecution] -public sealed partial class PollEvaluationHandler( - IServiceScopeFactory serviceScopeFactory, - ILogger logger -) : JobBase -{ - public override JobType JobType => JobType.PollEvaluation; - - protected override ILogger Logger => logger; - - protected override async Task ExecuteJobAsync(CancellationToken cancellationToken) - { - using var scope = serviceScopeFactory.CreateScope(); - var submissionAppService = scope.ServiceProvider.GetRequiredService(); - - var outboxResults = await submissionAppService.GetSubmissionOutboxesAsync(cancellationToken); - - if (!outboxResults.IsSuccess || !outboxResults.Value.Any()) - { - return; - } - - var outboxIds = outboxResults.Value - .Where(o => o.Type == SubmissionOutboxType.EvaluationPoll) - .Select(o => o.Id) - .ToList(); - - if (outboxIds.Count == 0) - { - return; - } - - LogFinalizing(logger, outboxIds.Count); - - var now = DateTime.UtcNow; - - await submissionAppService.IncrementOutboxesCountAsync(outboxIds, now, cancellationToken); - await submissionAppService.FinalizeEvaluationAsync(outboxIds, now, cancellationToken); - } - - [LoggerMessage( - EventId = LoggingEventIds.Jobs.PollEvaluationFinalizing, - Level = LogLevel.Information, - Message = "Finalizing {count} evaluation outboxes" - )] - private static partial void LogFinalizing(ILogger logger, int count); -} \ No newline at end of file diff --git a/src/Infrastructure/Jobs/JobHandlers/PollSubmissionExecutionHander.cs b/src/Infrastructure/Jobs/JobHandlers/PollSubmissionExecutionHander.cs deleted file mode 100644 index b1d256f..0000000 --- a/src/Infrastructure/Jobs/JobHandlers/PollSubmissionExecutionHander.cs +++ /dev/null @@ -1,69 +0,0 @@ -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Logging; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Quartz; - -namespace Infrastructure.Jobs.JobHandlers; - -[DisallowConcurrentExecution] -internal sealed partial class PollSubmissionExecutionHander( - IServiceScopeFactory serviceScopeFactory, - ILogger logger -) : JobBase -{ - public override JobType JobType => JobType.PollSubmissionExecution; - - protected override ILogger Logger => logger; - - protected override async Task ExecuteJobAsync(CancellationToken cancellationToken) - { - using var scope = serviceScopeFactory.CreateScope(); - var submissionAppService = - scope.ServiceProvider.GetRequiredService(); - var codeExecutionService = - scope.ServiceProvider.GetRequiredService(); - - var outboxResults = await submissionAppService.GetSubmissionOutboxesAsync( - cancellationToken - ); - - if (!outboxResults.IsSuccess || !outboxResults.Value.Any()) - { - return; - } - - var outboxes = outboxResults - .Value.Where(outbox => outbox.Type == SubmissionOutboxType.PollExecution) - .ToList(); - - if (outboxes.Count == 0) - { - return; - } - - LogPolling(logger, outboxes.Count); - - var submissionResults = await codeExecutionService.GetSubmissionResultsAsync( - outboxes.Select(o => o.Submission), - cancellationToken - ); - - var outboxIds = outboxes.Select(outbox => outbox.Id).ToList(); - var now = DateTime.UtcNow; - await submissionAppService.IncrementOutboxesCountAsync(outboxIds, now, cancellationToken); - - await submissionAppService.ProcessPollingSubmissionExecutionsAsync( - submissionResults.Value, - cancellationToken - ); - } - - [LoggerMessage( - EventId = LoggingEventIds.Jobs.PollSubmissionExecutionPolling, - Level = LogLevel.Information, - Message = "Polling {count} submission execution outboxes" - )] - private static partial void LogPolling(ILogger logger, int count); -} \ No newline at end of file diff --git a/src/Infrastructure/Jobs/JobHandlers/SubmissionExecutionHandler.cs b/src/Infrastructure/Jobs/JobHandlers/SubmissionExecutionHandler.cs deleted file mode 100644 index 877955c..0000000 --- a/src/Infrastructure/Jobs/JobHandlers/SubmissionExecutionHandler.cs +++ /dev/null @@ -1,111 +0,0 @@ -using ApplicationCore.Domain.CodeExecution; -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Logging; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Quartz; - -namespace Infrastructure.Jobs.JobHandlers; - -[DisallowConcurrentExecution] -public sealed partial class SubmissionExecutionHandler( - IServiceScopeFactory serviceScopeFactory, - ILogger logger -) : JobBase -{ - public override JobType JobType => JobType.SubmissionExecution; - - protected override ILogger Logger => logger; - - protected override async Task ExecuteJobAsync(CancellationToken cancellationToken) - { - using var scope = serviceScopeFactory.CreateScope(); - var submissionAppService = - scope.ServiceProvider.GetRequiredService(); - var problemAppService = scope.ServiceProvider.GetRequiredService(); - var codeBuilderService = scope.ServiceProvider.GetRequiredService(); - var codeExecutionService = - scope.ServiceProvider.GetRequiredService(); - - var outboxResults = await submissionAppService.GetSubmissionOutboxesAsync( - cancellationToken - ); - - if (!outboxResults.IsSuccess || !outboxResults.Value.Any()) - { - return; - } - - var outboxes = outboxResults - .Value.Where(outbox => outbox.Type == SubmissionOutboxType.Initialized) - .ToList(); - - var setupsMap = ( - await problemAppService.GetProblemSetupsForExecutionAsync( - outboxes.Select(outbox => outbox.Submission.ProblemSetupId), - cancellationToken - ) - ).Value.ToDictionary(setup => setup.Id); - - var executionContexts = outboxes - .Select(outbox => - { - var setup = setupsMap[outbox.Submission.ProblemSetupId]; - - var builderContexts = setup - .TestSuites.SelectMany(ts => ts.TestCases) - .Select(tc => new CodeBuilderContext - { - Code = outbox.Submission.Code ?? "", - Template = setup.HarnessTemplate?.Template ?? "", - FunctionName = setup.FunctionName ?? string.Empty, - LanguageVersionId = setup.LanguageVersionId, - Judge0LanguageId = setup.LanguageVersion?.Judge0LanguageId, - Inputs = tc.Inputs, - ExpectedOutput = tc.ExpectedOutput, - }); - - var buildResults = codeBuilderService.Build(builderContexts); - - return new CodeExecutionContext - { - SubmissionId = outbox.SubmissionId, - Setup = setup, - Code = outbox.Submission.Code ?? "", - CreatedById = outbox.Submission.CreatedById, - BuiltResults = buildResults.Value, - }; - }) - .ToList(); - - if (executionContexts.Count == 0) - { - return; - } - - LogProcessing(logger, executionContexts.Count); - - var outboxIds = outboxes.Select(outbox => outbox.Id).ToList(); - var now = DateTime.UtcNow; - - await submissionAppService.IncrementOutboxesCountAsync(outboxIds, now, cancellationToken); - - var submissionResults = await codeExecutionService.ExecuteAsync( - executionContexts, - cancellationToken - ); - - await submissionAppService.ProcessSubmissionExecutionAsync( - submissionResults.Value, - cancellationToken - ); - } - - [LoggerMessage( - EventId = LoggingEventIds.Jobs.SubmissionExecutionProcessing, - Level = LogLevel.Information, - Message = "Processing {count} submission execution outboxes" - )] - private static partial void LogProcessing(ILogger logger, int count); -} \ No newline at end of file diff --git a/src/Infrastructure/Jobs/JobType.cs b/src/Infrastructure/Jobs/JobType.cs deleted file mode 100644 index dcac772..0000000 --- a/src/Infrastructure/Jobs/JobType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Infrastructure.Jobs; - -public enum JobType -{ - SubmissionExecution = 1, - PollSubmissionExecution, - EvaluateSubmission, - PollEvaluation, -} \ No newline at end of file diff --git a/src/Infrastructure/Mappings/AccountMappings.cs b/src/Infrastructure/Mappings/AccountMappings.cs deleted file mode 100644 index d0a4be9..0000000 --- a/src/Infrastructure/Mappings/AccountMappings.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using Infrastructure.Persistence.Entities.Account; -using Mapster; - -namespace Infrastructure.Mappings; - -public sealed class AccountMappings : IRegister -{ - public void Register(TypeAdapterConfig config) - { - config.NewConfig() - .Map(dest => dest.Id, src => src.Id) - .Map(dest => dest.PreviousUsername, src => src.PreviousUsername) - .Map(dest => dest.UsernameLastChangedAt, src => src.UsernameLastChangedAt) - .Map(dest => dest.About, src => src.About); - - config.NewConfig() - .Map(dest => dest.PreviousUsername, src => src.PreviousUsername) - .Map(dest => dest.UsernameLastChangedAt, src => src.UsernameLastChangedAt) - .Map(dest => dest.About, src => src.About); - } -} \ No newline at end of file diff --git a/src/Infrastructure/Mappings/ProblemMappings.cs b/src/Infrastructure/Mappings/ProblemMappings.cs deleted file mode 100644 index 013802e..0000000 --- a/src/Infrastructure/Mappings/ProblemMappings.cs +++ /dev/null @@ -1,40 +0,0 @@ -using ApplicationCore.Domain.Problems; -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Domain.Problems.ProblemSetups; -using ApplicationCore.Domain.Problems.TestSuites; -using Infrastructure.Persistence.Entities.Language; -using Infrastructure.Persistence.Entities.Problem; -using Infrastructure.Persistence.Entities.TestSuite; -using Mapster; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Infrastructure.Mappings; - -public sealed class ProblemMappings : IRegister -{ - public void Register(TypeAdapterConfig config) - { - config.NewConfig(); - - config - .NewConfig() - .Ignore(s => s.Status) - .Map(d => d.Status, s => (ProblemStatus)s.StatusId); - - config - .NewConfig() - .Map(dest => dest.LanguageVersion, src => src.LanguageVersion) - .Map(dest => dest.TestSuites, src => src.TestSuites); - - config - .NewConfig() - .Map(dest => dest.ProgrammingLanguageId, src => src.ProgrammingLanguageId); - - config - .NewConfig() - .Map(dest => dest.TestSuiteType, src => (TestSuiteType)src.TestSuiteTypeId) - .Map(dest => dest.TestCases, src => src.TestCases); - } -} \ No newline at end of file diff --git a/src/Infrastructure/Mappings/SubmissionMappings.cs b/src/Infrastructure/Mappings/SubmissionMappings.cs deleted file mode 100644 index cca16e4..0000000 --- a/src/Infrastructure/Mappings/SubmissionMappings.cs +++ /dev/null @@ -1,26 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Domain.Submissions; -using Infrastructure.Persistence.Entities.Account; -using Infrastructure.Persistence.Entities.Submission; -using Mapster; - -namespace Infrastructure.Mappings; - -public sealed class SubmissionMappings : IRegister -{ - public void Register(TypeAdapterConfig config) - { - config.NewConfig() - .Ignore(dest => dest.PreviousUsername) - .Ignore(dest => dest.UsernameLastChangedAt); - - config.NewConfig() - .Map(dest => dest.Id, src => src.Id) - .Map(dest => dest.Code, src => src.Code) - .Map(dest => dest.ProblemSetupId, src => src.ProblemSetupId) - .Map(dest => dest.CreatedOn, src => src.CreatedOn) - .Map(dest => dest.CompletedAt, src => src.CompletedAt) - .Map(dest => dest.CreatedById, src => src.CreatedById) - .Map(dest => dest.CreatedBy, src => src.CreatedBy); - } -} \ No newline at end of file diff --git a/src/Infrastructure/Messaging/Consumers/SubmissionCreatedConsumer.cs b/src/Infrastructure/Messaging/Consumers/SubmissionCreatedConsumer.cs deleted file mode 100644 index 618adc0..0000000 --- a/src/Infrastructure/Messaging/Consumers/SubmissionCreatedConsumer.cs +++ /dev/null @@ -1,158 +0,0 @@ -using ApplicationCore.Domain.CodeExecution; -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Logging; -using ApplicationCore.Messaging; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Infrastructure.Messaging.Consumers; - -/// -/// Stage 1: Build submission code and send to Judge0. -/// Stores the Judge0 execution tokens (ExecutionId) on each SubmissionResult, -/// transitions the outbox Initialized to PollExecution, then publishes -/// SubmissionExecutionPollMessage to kick off polling. -/// -public sealed partial class SubmissionCreatedConsumer( - IServiceScopeFactory serviceScopeFactory, - ILogger logger -) : IConsumer -{ - private readonly ILogger _logger = logger; - public async Task Consume(ConsumeContext context) - { - CancellationToken cancellationToken = context.CancellationToken; - using IServiceScope scope = serviceScopeFactory.CreateScope(); - - LogStage1Started(context.Message.SubmissionId, context.Message.OutboxId); - - ISubmissionAppService submissionAppService = scope.ServiceProvider.GetRequiredService(); - IProblemAppService problemAppService = scope.ServiceProvider.GetRequiredService(); - ICodeBuilderService codeBuilderService = scope.ServiceProvider.GetRequiredService(); - ICodeExecutionService codeExecutionService = scope.ServiceProvider.GetRequiredService(); - - Ardalis.Result.Result> outboxResults = await submissionAppService.GetSubmissionOutboxesAsync(cancellationToken); - - if (!outboxResults.IsSuccess || !outboxResults.Value.Any()) - { - LogStage1OutboxNotFound(context.Message.SubmissionId, context.Message.OutboxId); - return; - } - - SubmissionOutboxModel? outbox = outboxResults.Value.FirstOrDefault(o => - o.Id == context.Message.OutboxId - && o.Type == SubmissionOutboxType.Initialized); - - if (outbox is null) - { - LogStage1OutboxNotFound(context.Message.SubmissionId, context.Message.OutboxId); - return; - } - - Ardalis.Result.Result> setupResult = await problemAppService.GetProblemSetupsForExecutionAsync( - [outbox.Submission.ProblemSetupId], - cancellationToken - ); - - if (!setupResult.IsSuccess) - { - LogStage1SetupFailed(context.Message.SubmissionId, outbox.Submission.ProblemSetupId, string.Join(", ", setupResult.Errors)); - return; - } - - ApplicationCore.Domain.Problems.ProblemSetups.ProblemSetupModel? setup = setupResult.Value.FirstOrDefault(s => s.Id == outbox.Submission.ProblemSetupId); - - if (setup is null) - { - LogStage1SetupFailed(context.Message.SubmissionId, outbox.Submission.ProblemSetupId, "Setup not found after query"); - return; - } - - System.Collections.Generic.IEnumerable builderContexts = setup.TestSuites - .SelectMany(ts => ts.TestCases) - .Select(tc => new CodeBuilderContext - { - Code = outbox.Submission.Code ?? "", - Template = setup.HarnessTemplate?.Template ?? "", - FunctionName = setup.FunctionName ?? string.Empty, - LanguageVersionId = setup.LanguageVersionId, - Judge0LanguageId = setup.LanguageVersion?.Judge0LanguageId, - Inputs = tc.Inputs, - ExpectedOutput = tc.ExpectedOutput, - }) - .ToList(); - - if (!builderContexts.Any()) - { - LogStage1BuildFailed(context.Message.SubmissionId, setup.Id, "No test cases found for setup"); - return; - } - - Ardalis.Result.Result> buildResult = codeBuilderService.Build(builderContexts); - - if (!buildResult.IsSuccess) - { - LogStage1BuildFailed(context.Message.SubmissionId, setup.Id, string.Join(", ", buildResult.Errors)); - return; - } - - CodeExecutionContext executionContext = new() - { - SubmissionId = outbox.SubmissionId, - Setup = setup, - Code = outbox.Submission.Code ?? "", - CreatedById = outbox.Submission.CreatedById, - BuiltResults = buildResult.Value, - }; - - DateTime now = DateTime.UtcNow; - await submissionAppService.IncrementOutboxesCountAsync([outbox.Id], now, cancellationToken); - - Ardalis.Result.Result> executeResult = await codeExecutionService.ExecuteAsync( - [executionContext], - cancellationToken - ); - - if (!executeResult.IsSuccess) - { - LogStage1ExecutionFailed(context.Message.SubmissionId, string.Join(", ", executeResult.Errors)); - return; - } - - await submissionAppService.SaveExecutionTokensAsync(executeResult.Value, cancellationToken); - - await context.Publish(new SubmissionExecutionPollMessage - { - SubmissionId = context.Message.SubmissionId, - OutboxId = context.Message.OutboxId, - }, cancellationToken); - - LogStage1Completed(context.Message.SubmissionId, context.Message.OutboxId); - } - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage1Started, Level = LogLevel.Information, - Message = "Stage1: Starting submission execution for {submissionId} (outbox {outboxId})")] - private partial void LogStage1Started(Guid submissionId, Guid outboxId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage1OutboxNotFound, Level = LogLevel.Warning, - Message = "Stage1: Outbox not found or not in Initialized state for submission {submissionId} (outbox {outboxId}) — may have already been processed")] - private partial void LogStage1OutboxNotFound(Guid submissionId, Guid outboxId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage1SetupFailed, Level = LogLevel.Error, - Message = "Stage1: Failed to get problem setup {setupId} for submission {submissionId}: {errors}")] - private partial void LogStage1SetupFailed(Guid submissionId, int setupId, string errors); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage1BuildFailed, Level = LogLevel.Error, - Message = "Stage1: Code build failed for submission {submissionId} (setup {setupId}): {errors}")] - private partial void LogStage1BuildFailed(Guid submissionId, int setupId, string errors); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage1ExecutionFailed, Level = LogLevel.Error, - Message = "Stage1: Execution failed for submission {submissionId}: {errors}")] - private partial void LogStage1ExecutionFailed(Guid submissionId, string errors); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage1Completed, Level = LogLevel.Information, - Message = "Stage1: Completed submission execution for {submissionId} (outbox {outboxId})")] - private partial void LogStage1Completed(Guid submissionId, Guid outboxId); -} \ No newline at end of file diff --git a/src/Infrastructure/Messaging/Consumers/SubmissionEvaluationPollConsumer.cs b/src/Infrastructure/Messaging/Consumers/SubmissionEvaluationPollConsumer.cs deleted file mode 100644 index b3484a3..0000000 --- a/src/Infrastructure/Messaging/Consumers/SubmissionEvaluationPollConsumer.cs +++ /dev/null @@ -1,67 +0,0 @@ -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Logging; -using ApplicationCore.Messaging; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Infrastructure.Messaging.Consumers; - -/// -/// Stage 4: Final stage — increments attempt count and sets FinalizedOn -/// on the outbox, completing the submission pipeline. -/// -public sealed partial class SubmissionEvaluationPollConsumer( - IServiceScopeFactory serviceScopeFactory, - ILogger logger -) : IConsumer -{ - private readonly ILogger _logger = logger; - public async Task Consume(ConsumeContext context) - { - var cancellationToken = context.CancellationToken; - using var scope = serviceScopeFactory.CreateScope(); - - LogStage4Started(context.Message.SubmissionId, context.Message.OutboxId); - - var submissionAppService = scope.ServiceProvider.GetRequiredService(); - - var outboxResults = await submissionAppService.GetSubmissionOutboxesAsync(cancellationToken); - - if (!outboxResults.IsSuccess || !outboxResults.Value.Any()) - { - LogStage4OutboxNotFound(context.Message.SubmissionId, context.Message.OutboxId); - return; - } - - var outbox = outboxResults.Value.FirstOrDefault(o => - o.SubmissionId == context.Message.SubmissionId - && o.Type == SubmissionOutboxType.EvaluationPoll); - - if (outbox is null) - { - LogStage4OutboxNotFound(context.Message.SubmissionId, context.Message.OutboxId); - return; - } - - var now = DateTime.UtcNow; - - await submissionAppService.IncrementOutboxesCountAsync([outbox.Id], now, cancellationToken); - await submissionAppService.FinalizeEvaluationAsync([outbox.Id], now, cancellationToken); - - LogStage4Completed(context.Message.SubmissionId, context.Message.OutboxId); - } - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage4Started, Level = LogLevel.Information, - Message = "Stage4: Finalizing evaluation for submission {submissionId} (outbox {outboxId})")] - private partial void LogStage4Started(Guid submissionId, Guid outboxId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage4OutboxNotFound, Level = LogLevel.Warning, - Message = "Stage4: Outbox not found or not in EvaluationPoll state for submission {submissionId} (outbox {outboxId}) — may have already been processed")] - private partial void LogStage4OutboxNotFound(Guid submissionId, Guid outboxId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage4Completed, Level = LogLevel.Information, - Message = "Stage4: Submission {submissionId} finalized (outbox {outboxId})")] - private partial void LogStage4Completed(Guid submissionId, Guid outboxId); -} \ No newline at end of file diff --git a/src/Infrastructure/Messaging/Consumers/SubmissionExecutedConsumer.cs b/src/Infrastructure/Messaging/Consumers/SubmissionExecutedConsumer.cs deleted file mode 100644 index af6361f..0000000 --- a/src/Infrastructure/Messaging/Consumers/SubmissionExecutedConsumer.cs +++ /dev/null @@ -1,126 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Logging; -using ApplicationCore.Messaging; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Infrastructure.Messaging.Consumers; - -/// -/// Stage 2: Poll Judge0 for results using the stored ExecutionId tokens. -/// Increments the attempt count, persists stdout/status, transitions -/// outbox PollExecution → Evaluate once all results are finished, then -/// publishes . -/// If results are still processing, re-publishes -/// to poll again on the next delivery. -/// -public sealed partial class SubmissionExecutedConsumer( - IServiceScopeFactory serviceScopeFactory, - ILogger logger -) : IConsumer -{ - private readonly ILogger _logger = logger; - public async Task Consume(ConsumeContext context) - { - var cancellationToken = context.CancellationToken; - using var scope = serviceScopeFactory.CreateScope(); - - LogStage2Started(context.Message.SubmissionId, context.Message.OutboxId); - - var submissionAppService = scope.ServiceProvider.GetRequiredService(); - var codeExecutionService = scope.ServiceProvider.GetRequiredService(); - - var outboxResults = await submissionAppService.GetSubmissionOutboxesAsync(cancellationToken); - - if (!outboxResults.IsSuccess || !outboxResults.Value.Any()) - { - LogStage2OutboxNotFound(context.Message.SubmissionId, context.Message.OutboxId); - return; - } - - var outbox = outboxResults.Value.FirstOrDefault(o => - o.SubmissionId == context.Message.SubmissionId - && o.Type == SubmissionOutboxType.PollExecution); - - if (outbox is null) - { - LogStage2OutboxNotFound(context.Message.SubmissionId, context.Message.OutboxId); - return; - } - - var now = DateTime.UtcNow; - await submissionAppService.IncrementOutboxesCountAsync([outbox.Id], now, cancellationToken); - - var pollResult = await codeExecutionService.GetSubmissionResultsAsync( - [outbox.Submission], - cancellationToken - ); - - if (!pollResult.IsSuccess) - { - LogStage2PollFailed(context.Message.SubmissionId, string.Join(", ", pollResult.Errors)); - return; - } - - var submission = pollResult.Value.FirstOrDefault(); - - if (submission is null) - { - LogStage2PollFailed(context.Message.SubmissionId, "No submission returned from poll"); - return; - } - - await submissionAppService.ProcessPollingSubmissionExecutionsAsync( - pollResult.Value, - cancellationToken - ); - - bool allFinished = submission.Results.All(r => - r.Status is not SubmissionStatus.InQueue - and not SubmissionStatus.Processing); - - if (!allFinished) - { - LogStage2StillProcessing(context.Message.SubmissionId); - - await context.Publish(new SubmissionExecutionPollMessage - { - SubmissionId = context.Message.SubmissionId, - OutboxId = context.Message.OutboxId, - }, cancellationToken); - - return; - } - - await context.Publish(new SubmissionReadyToEvaluateMessage - { - SubmissionId = context.Message.SubmissionId, - OutboxId = context.Message.OutboxId, - }, cancellationToken); - - LogStage2Completed(context.Message.SubmissionId, context.Message.OutboxId); - } - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage2Started, Level = LogLevel.Information, - Message = "Stage2: Polling execution results for submission {submissionId} (outbox {outboxId})")] - private partial void LogStage2Started(Guid submissionId, Guid outboxId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage2OutboxNotFound, Level = LogLevel.Warning, - Message = "Stage2: Outbox not found or not in PollExecution state for submission {submissionId} (outbox {outboxId}) — may have already been processed")] - private partial void LogStage2OutboxNotFound(Guid submissionId, Guid outboxId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage2PollFailed, Level = LogLevel.Error, - Message = "Stage2: Poll failed for submission {submissionId}: {errors}")] - private partial void LogStage2PollFailed(Guid submissionId, string errors); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage2StillProcessing, Level = LogLevel.Information, - Message = "Stage2: Submission {submissionId} still processing, re-queuing poll")] - private partial void LogStage2StillProcessing(Guid submissionId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage2Completed, Level = LogLevel.Information, - Message = "Stage2: All results received for submission {submissionId} (outbox {outboxId}), advancing to evaluation")] - private partial void LogStage2Completed(Guid submissionId, Guid outboxId); -} \ No newline at end of file diff --git a/src/Infrastructure/Messaging/Consumers/SubmissionReadyToEvaluateConsumer.cs b/src/Infrastructure/Messaging/Consumers/SubmissionReadyToEvaluateConsumer.cs deleted file mode 100644 index 26e565c..0000000 --- a/src/Infrastructure/Messaging/Consumers/SubmissionReadyToEvaluateConsumer.cs +++ /dev/null @@ -1,128 +0,0 @@ -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Services; -using ApplicationCore.Logging; -using ApplicationCore.Messaging; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Infrastructure.Messaging.Consumers; - -/// -/// Stage 3: Compare each result's stdout against its expected output using -/// . Persists Accepted/WrongAnswer -/// statuses, transitions outbox Evaluate → EvaluationPoll, then publishes -/// . -/// -public sealed partial class SubmissionReadyToEvaluateConsumer( - IServiceScopeFactory serviceScopeFactory, - ILogger logger -) : IConsumer -{ - private readonly ILogger _logger = logger; - public async Task Consume(ConsumeContext context) - { - var cancellationToken = context.CancellationToken; - using var scope = serviceScopeFactory.CreateScope(); - - LogStage3Started(context.Message.SubmissionId, context.Message.OutboxId); - - var submissionAppService = scope.ServiceProvider.GetRequiredService(); - var problemAppService = scope.ServiceProvider.GetRequiredService(); - var comparisonService = scope.ServiceProvider.GetRequiredService(); - - var outboxResults = await submissionAppService.GetSubmissionOutboxesAsync(cancellationToken); - - if (!outboxResults.IsSuccess || !outboxResults.Value.Any()) - { - LogStage3OutboxNotFound(context.Message.SubmissionId, context.Message.OutboxId); - return; - } - - var outbox = outboxResults.Value.FirstOrDefault(o => - o.SubmissionId == context.Message.SubmissionId - && o.Type == SubmissionOutboxType.Evaluate); - - if (outbox is null) - { - LogStage3OutboxNotFound(context.Message.SubmissionId, context.Message.OutboxId); - return; - } - - // Reload the problem setup to get expected outputs per test case - var setupResult = await problemAppService.GetProblemSetupsForExecutionAsync( - [outbox.Submission.ProblemSetupId], - cancellationToken - ); - - if (!setupResult.IsSuccess) - { - LogStage3SetupNotFound(context.Message.SubmissionId, outbox.Submission.ProblemSetupId); - return; - } - - var setup = setupResult.Value.FirstOrDefault(s => s.Id == outbox.Submission.ProblemSetupId); - - if (setup is null) - { - LogStage3SetupNotFound(context.Message.SubmissionId, outbox.Submission.ProblemSetupId); - return; - } - - var expectedOutputs = setup.TestSuites - .SelectMany(ts => ts.TestCases) - .Select(tc => tc.ExpectedOutput.Value) - .ToList(); - - var results = outbox.Submission.Results.ToList(); - - for (int i = 0; i < results.Count; i++) - { - if (results[i].Status != SubmissionStatus.Processing) - { - continue; - } - - string expected = i < expectedOutputs.Count ? expectedOutputs[i] : string.Empty; - results[i].Status = comparisonService.Compare(results[i].ProgramOutput, expected); - } - - var evaluated = new SubmissionModel - { - Id = outbox.SubmissionId, - CreatedById = outbox.Submission.CreatedById, - Results = results, - }; - - var now = DateTime.UtcNow; - await submissionAppService.IncrementOutboxesCountAsync([outbox.Id], now, cancellationToken); - - // Persist comparison results and transition Evaluate → EvaluationPoll - await submissionAppService.ProcessEvaluationAsync([evaluated], cancellationToken); - - await context.Publish(new SubmissionEvaluationPollMessage - { - SubmissionId = context.Message.SubmissionId, - OutboxId = context.Message.OutboxId, - }, cancellationToken); - - LogStage3Completed(context.Message.SubmissionId, context.Message.OutboxId); - } - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage3Started, Level = LogLevel.Information, - Message = "Stage3: Starting evaluation for submission {submissionId} (outbox {outboxId})")] - private partial void LogStage3Started(Guid submissionId, Guid outboxId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage3OutboxNotFound, Level = LogLevel.Warning, - Message = "Stage3: Outbox not found or not in Evaluate state for submission {submissionId} (outbox {outboxId}) — may have already been processed")] - private partial void LogStage3OutboxNotFound(Guid submissionId, Guid outboxId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage3SetupNotFound, Level = LogLevel.Error, - Message = "Stage3: Problem setup {setupId} not found for submission {submissionId}")] - private partial void LogStage3SetupNotFound(Guid submissionId, int setupId); - - [LoggerMessage(EventId = LoggingEventIds.Submissions.Stage3Completed, Level = LogLevel.Information, - Message = "Stage3: Evaluation complete for submission {submissionId} (outbox {outboxId})")] - private partial void LogStage3Completed(Guid submissionId, Guid outboxId); -} \ No newline at end of file diff --git a/src/Infrastructure/Messaging/MassTransitMessagePublisher.cs b/src/Infrastructure/Messaging/MassTransitMessagePublisher.cs deleted file mode 100644 index 5b626b9..0000000 --- a/src/Infrastructure/Messaging/MassTransitMessagePublisher.cs +++ /dev/null @@ -1,11 +0,0 @@ -using ApplicationCore.Interfaces.Messaging; -using MassTransit; - -namespace Infrastructure.Messaging; - -internal sealed class MassTransitMessagePublisher(IPublishEndpoint publishEndpoint) : IMessagePublisher -{ - public Task PublishAsync(T message, CancellationToken cancellationToken = default) - where T : class => - publishEndpoint.Publish(message, cancellationToken); -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/AppDbContext.cs b/src/Infrastructure/Persistence/AppDbContext.cs deleted file mode 100644 index 8ca4f03..0000000 --- a/src/Infrastructure/Persistence/AppDbContext.cs +++ /dev/null @@ -1,155 +0,0 @@ -using Infrastructure.Persistence.Entities.Account; -using Infrastructure.Persistence.Entities.Language; -using Infrastructure.Persistence.Entities.Problem; -using Infrastructure.Persistence.Entities.Submission; -using Infrastructure.Persistence.Entities.Submission.Outbox; -using Infrastructure.Persistence.Entities.TestSuite; -using Microsoft.EntityFrameworkCore; -namespace Infrastructure.Persistence; - -public sealed class AppDbContext(DbContextOptions options) : DbContext(options) -{ - public DbSet Accounts { get; set; } - - public DbSet HarnessTemplates { get; set; } - - public DbSet LanguageVersions { get; set; } - - public DbSet LanguageVersionEngineMappings { get; set; } - - public DbSet Problems { get; set; } - - public DbSet ProblemHistories { get; set; } - - public DbSet ProblemSetups { get; set; } - - public DbSet ProblemStatuses { get; set; } - - public DbSet ProgrammingLanguages { get; set; } - - public DbSet SubmissionOutboxes { get; set; } - - public DbSet SubmissionOutboxStatuses { get; set; } - - public DbSet SubmissionOutboxTypes { get; set; } - - public DbSet Submissions { get; set; } - - public DbSet SubmissionResults { get; set; } - - public DbSet SubmissionStatuses { get; set; } - - public DbSet SubmissionStatusTypes { get; set; } - - public DbSet Tags { get; set; } - - public DbSet TestCases { get; set; } - - public DbSet TestCaseExpectedOutputs { get; set; } - - public DbSet TestCasesInputsValueTypes { get; set; } - - public DbSet TestCasesOutputTypes { get; set; } - - public DbSet TestSuites { get; set; } - - public DbSet TestSuiteTypes { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - ModelProblems(modelBuilder); - ModelProblemSetupsTestSuites(modelBuilder); - ModelTestSuiteTestCases(modelBuilder); - ModelTestCaseInputs(modelBuilder); - ModelTestCaseExpectedOutput(modelBuilder); - ModelLanguageVersionEngineMappings(modelBuilder); - } - - private static void ModelProblems(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasMany(p => p.Tags) - .WithMany(t => t.Problems) - .UsingEntity>( - "problem_tags", - j => j.HasOne().WithMany().HasForeignKey("tag_id"), - j => j.HasOne().WithMany().HasForeignKey("problem_id") - ); - } - - private static void ModelProblemSetupsTestSuites(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasMany(ps => ps.TestSuites) - .WithMany(ts => ts.Setups) - .UsingEntity>( - "problem_setup_test_suites", - j => - j.HasOne() - .WithMany() - .HasForeignKey("test_suite_id") - .HasConstraintName("fk_problem_setup_test_suites_test_suite"), - j => - j.HasOne() - .WithMany() - .HasForeignKey("problem_setup_id") - .HasConstraintName("fk_problem_setup_test_suites_problem_setup"), - j => - { - j.ToTable("problem_setup_test_suites"); - j.HasKey("problem_setup_id", "test_suite_id"); - } - ); - } - - private static void ModelTestSuiteTestCases(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasMany(ts => ts.TestCases) - .WithOne(tc => tc.TestSuite) - .HasForeignKey(tc => tc.TestSuiteId) - .HasConstraintName("fk_test_cases_test_suite_id"); - } - - private static void ModelTestCaseInputs(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasMany(tc => tc.InputParams) - .WithOne(i => i.TestCase) - .HasForeignKey(i => i.TestCaseId) - .HasConstraintName("fk_test_cases_inputs_test_case_id"); - } - - private static void ModelTestCaseExpectedOutput(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasOne(tc => tc.ExpectedOutput) - .WithOne(o => o.TestCase) - .HasForeignKey(o => o.TestCaseId) - .HasConstraintName("fk_test_cases_expected_outputs_test_case_id"); - - modelBuilder - .Entity() - .HasOne(o => o.OutputType) - .WithMany() - .HasForeignKey(o => o.OutputValueTypeId) - .HasConstraintName("fk_test_cases_expected_outputs_output_type"); - } - - private static void ModelLanguageVersionEngineMappings(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasMany(lv => lv.EngineMappings) - .WithOne(m => m.LanguageVersion) - .HasForeignKey(m => m.ProgrammingLanguageVersionId) - .HasConstraintName("fk_lang_version_engine_mappings_language_version"); - } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Account/AccountEntity.cs b/src/Infrastructure/Persistence/Entities/Account/AccountEntity.cs deleted file mode 100644 index ee7c74e..0000000 --- a/src/Infrastructure/Persistence/Entities/Account/AccountEntity.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Account; - -[Table("accounts")] -public sealed class AccountEntity -{ - [Key] - [Column("id")] - public Guid Id { get; set; } - - [Column("username"), Required, MaxLength(36)] - public required string Username { get; set; } - - [Column("sub"), Required, MaxLength(255)] - public required string Sub { get; set; } - - [MaxLength(300)] - [Column("image_url")] - public string? ImageUrl { get; set; } - - [Column("created_on")] - public DateTime CreatedOn { get; set; } - - [Column("last_modified_on")] - public DateTime? LastModifiedOn { get; set; } - - [Column("last_modified_by_id")] - public Guid? LastModifiedById { get; set; } - - [ForeignKey(nameof(LastModifiedById))] - public AccountEntity? LastModifiedByAccount { get; set; } - - [Column("deleted_on")] - public DateTime? DeletedOn { get; set; } - - [Column("previous_username"), MaxLength(36)] - public string? PreviousUsername { get; set; } - - [Column("username_last_changed_at")] - public DateTime? UsernameLastChangedAt { get; set; } - - [Column("about"), MaxLength(255)] - public string? About { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/BaseAuditableEntity.cs b/src/Infrastructure/Persistence/Entities/BaseAuditableEntity.cs deleted file mode 100644 index 2c712b3..0000000 --- a/src/Infrastructure/Persistence/Entities/BaseAuditableEntity.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Infrastructure.Persistence.Entities.Account; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities; - -public abstract class BaseAuditableEntity -{ - [Column("created_on")] - public DateTime CreatedOn { get; set; } - - [Column("created_by_id")] - public Guid? CreatedById { get; set; } - - [ForeignKey(nameof(CreatedById))] - public AccountEntity? CreatedBy { get; set; } - - [Column("last_modified_on")] - public DateTime? LastModifiedOn { get; set; } - - [Column("last_modified_by_id")] - public Guid? LastModifiedById { get; set; } - - [ForeignKey(nameof(LastModifiedById))] - public AccountEntity? LastModifiedByAccount { get; set; } - - [Column("deleted_on")] - public DateTime? DeletedOn { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Language/LanguageVersionEngineMappingEntity.cs b/src/Infrastructure/Persistence/Entities/Language/LanguageVersionEngineMappingEntity.cs deleted file mode 100644 index 18a99fb..0000000 --- a/src/Infrastructure/Persistence/Entities/Language/LanguageVersionEngineMappingEntity.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Language; - -[Table("language_version_engine_mappings")] -public sealed class LanguageVersionEngineMappingEntity -{ - [Key, Column("id")] - public int Id { get; init; } - - [Column("programming_language_version_id")] - public int ProgrammingLanguageVersionId { get; init; } - - [ForeignKey(nameof(ProgrammingLanguageVersionId))] - public LanguageVersionEntity? LanguageVersion { get; init; } - - [Column("engine_id")] - public int EngineId { get; init; } - - [Column("engine_language_id")] - public int EngineLanguageId { get; init; } - - [Column("engine_language_name")] - public string? EngineLanguageName { get; init; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Language/LanguageVersionEntity.cs b/src/Infrastructure/Persistence/Entities/Language/LanguageVersionEntity.cs deleted file mode 100644 index d34cf15..0000000 --- a/src/Infrastructure/Persistence/Entities/Language/LanguageVersionEntity.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Infrastructure.Persistence.Entities.Problem; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Language; - -[Table("programming_language_versions")] -public sealed class LanguageVersionEntity : BaseAuditableEntity -{ - [Key] - [Column("id")] - public int Id { get; init; } - - [Required] - [MaxLength(20)] - [Column("version")] - public required string Version { get; init; } - - [Column("programming_language_id")] - public int ProgrammingLanguageId { get; init; } - - [Column("initial_code")] - public string? InitialCode { get; init; } - - [ForeignKey(nameof(ProgrammingLanguageId))] - public ProgrammingLanguageEntity? ProgrammingLanguage { get; set; } - - public IEnumerable ProblemSetups { get; set; } = []; - - public IEnumerable EngineMappings { get; set; } = []; -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Language/ProgrammingLanguageEntity.cs b/src/Infrastructure/Persistence/Entities/Language/ProgrammingLanguageEntity.cs deleted file mode 100644 index 87683bf..0000000 --- a/src/Infrastructure/Persistence/Entities/Language/ProgrammingLanguageEntity.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Language; - -[Table("programming_languages")] -public sealed class ProgrammingLanguageEntity : BaseAuditableEntity -{ - [Key] - [Column("id")] - public int Id { get; init; } - - [Required] - [MaxLength(50)] - [Column("name")] - public required string Name { get; init; } - - [Column("is_archived")] - public bool IsArchived { get; init; } - - public required ICollection Versions { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Problem/HarnessTemplateEntity.cs b/src/Infrastructure/Persistence/Entities/Problem/HarnessTemplateEntity.cs deleted file mode 100644 index b16a98c..0000000 --- a/src/Infrastructure/Persistence/Entities/Problem/HarnessTemplateEntity.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Problem; - -[Table("harness_templates")] -public sealed class HarnessTemplateEntity -{ - [Key] - [Column("id")] - public int Id { get; set; } - - [Required, Column("template")] - public required string Template { get; set; } - - public IEnumerable ProblemSetups { get; set; } = []; -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Problem/ProblemEntity.cs b/src/Infrastructure/Persistence/Entities/Problem/ProblemEntity.cs deleted file mode 100644 index 576f63f..0000000 --- a/src/Infrastructure/Persistence/Entities/Problem/ProblemEntity.cs +++ /dev/null @@ -1,44 +0,0 @@ -using ApplicationCore.Domain.Problems; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Problem; - -[Table("problems")] -public sealed class ProblemEntity : BaseAuditableEntity -{ - [Key] - [Column("id")] - public Guid Id { get; set; } - - [Required] - [MaxLength(100)] - [Column("title")] - public required string Title { get; set; } - - [Required] - [MaxLength(100)] - [Column("slug")] - public required string Slug { get; set; } - - [Required] - [Column("question", TypeName = "text")] - public required string Question { get; set; } - - [Required] - [Column("difficulty")] - public required int Difficulty { get; set; } - - public required ICollection Tags { get; set; } - - [Column("status_id")] - public required int StatusId { get; set; } = (int)ProblemStatus.Pending; - - [Column("version")] - public int Version { get; set; } = 1; - - [ForeignKey(nameof(StatusId))] - public ProblemStatusEntity? Status { get; set; } - - public ICollection ProblemSetups { get; set; } = []; -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Problem/ProblemHistoryEntity.cs b/src/Infrastructure/Persistence/Entities/Problem/ProblemHistoryEntity.cs deleted file mode 100644 index edab569..0000000 --- a/src/Infrastructure/Persistence/Entities/Problem/ProblemHistoryEntity.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Problem; - -[Table("problem_history")] -public sealed class ProblemHistoryEntity -{ - [Key] - [Column("id")] - public Guid Id { get; set; } - - [Column("problem_id")] - public Guid ProblemId { get; set; } - - [Column("version")] - public int Version { get; set; } - - [Column("title")] - public required string Title { get; set; } - - [Column("slug")] - public required string Slug { get; set; } - - [Column("difficulty")] - public int Difficulty { get; set; } - - [Column("question")] - public required string Question { get; set; } - - [Column("archived_on")] - public DateTime ArchivedOn { get; set; } - - [Column("archived_by_id")] - public Guid? ArchivedById { get; set; } - - [Column("archive_reason")] - public string? ArchiveReason { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Problem/ProblemSetupEntity.cs b/src/Infrastructure/Persistence/Entities/Problem/ProblemSetupEntity.cs deleted file mode 100644 index 5861731..0000000 --- a/src/Infrastructure/Persistence/Entities/Problem/ProblemSetupEntity.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Infrastructure.Persistence.Entities.Language; -using Infrastructure.Persistence.Entities.TestSuite; -using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Problem; - -[Table("problem_setups")] -public sealed class ProblemSetupEntity : BaseAuditableEntity -{ - [Key] - [Column("id")] - public int Id { get; set; } - - [Column("problem_id")] - public Guid ProblemId { get; set; } - - [ForeignKey(nameof(ProblemId))] - public ProblemEntity? Problem { get; set; } - - [Column("programming_language_version_id")] - public int ProgrammingLanguageVersionId { get; set; } - - [ForeignKey(nameof(ProgrammingLanguageVersionId))] - public LanguageVersionEntity? LanguageVersion { get; set; } - - [Column("version")] - public int Version { get; set; } - - [Column("initial_code")] - public string? InitialCode { get; set; } - - [Column("function_name")] - public string? FunctionName { get; set; } - - [Column("harness_template_id")] - public int HarnessTemplateId { get; set; } - - [ForeignKey(nameof(HarnessTemplateId))] - public HarnessTemplateEntity? HarnessTemplate { get; set; } - - public IEnumerable TestSuites { get; set; } = []; -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Problem/ProblemStatus.cs b/src/Infrastructure/Persistence/Entities/Problem/ProblemStatus.cs deleted file mode 100644 index 005a030..0000000 --- a/src/Infrastructure/Persistence/Entities/Problem/ProblemStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Problem; - -[Table("problem_statuses")] -public sealed class ProblemStatusEntity : BaseAuditableEntity -{ - [Key] - [Column("id")] - public int Id { get; init; } - - [Column("description")] - [MaxLength(100)] - public string? Description { get; init; } - - public ICollection? Problems { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Problem/TagEntity.cs b/src/Infrastructure/Persistence/Entities/Problem/TagEntity.cs deleted file mode 100644 index 0d28f28..0000000 --- a/src/Infrastructure/Persistence/Entities/Problem/TagEntity.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Problem; - -[Table("tags")] -public sealed class TagEntity -{ - [Key] - [Column("id")] - public int Id { get; set; } - - [MaxLength(50)] - [Column("value")] - public required string Value { get; set; } - - public ICollection? Problems { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxEntity.cs b/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxEntity.cs deleted file mode 100644 index 21171ad..0000000 --- a/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxEntity.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Submission.Outbox; - -[Table("submission_outbox")] -public sealed class SubmissionOutboxEntity -{ - [Key, Column("id")] - public Guid Id { get; set; } - - [Required, Column("submission_id")] - public required Guid SubmissionId { get; set; } - - [ForeignKey(nameof(SubmissionId))] - public SubmissionEntity? Submission { get; set; } - - [Required, Column("submission_outbox_type_id")] - public required int SubmissionOutboxTypeId { get; set; } - - [ForeignKey(nameof(SubmissionOutboxTypeId))] - public SubmissionOutboxTypeEntity? SubmissionOutboxType { get; set; } - - [Required, Column("submission_outbox_status_id")] - public required int SubmissionOutboxStatusId { get; set; } - - [ForeignKey(nameof(SubmissionOutboxStatusId))] - public SubmissionOutboxStatusEntity? SubmissionOutboxStatus { get; set; } - - [Column("attempt_count")] - public int AttemptCount { get; set; } - - [Column("next_attempt_on")] - public DateTime? NextAttemptOn { get; set; } - - [Column("last_error"), MaxLength(255)] - public string? LastError { get; set; } - - [Column("created_on")] - public DateTime CreatedOn { get; set; } - - [Column("process_on")] - public DateTime? ProcessOn { get; set; } - - [Column("finalized_on")] - public DateTime? FinalizedOn { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxStatusEntity.cs b/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxStatusEntity.cs deleted file mode 100644 index 6639ab5..0000000 --- a/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxStatusEntity.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Submission.Outbox; - -[Table("submission_outbox_statuses")] -public sealed class SubmissionOutboxStatusEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Required, Column("name"), MaxLength(100)] - public required string Name { get; set; } - - [Column("description"), MaxLength(500)] - public string? Description { get; set; } - - [Column("created_on")] - public DateTime CreatedOn { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxTypeEntity.cs b/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxTypeEntity.cs deleted file mode 100644 index 4f67490..0000000 --- a/src/Infrastructure/Persistence/Entities/Submission/Outbox/SubmissionOutboxTypeEntity.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Submission.Outbox; - -[Table("submission_outbox_types")] -public sealed class SubmissionOutboxTypeEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Required, Column("name"), MaxLength(100)] - public required string Name { get; set; } - - [Column("description"), MaxLength(500)] - public string? Description { get; set; } - - [Column("created_on")] - public DateTime CreatedOn { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Submission/SubmissionEntity.cs b/src/Infrastructure/Persistence/Entities/Submission/SubmissionEntity.cs deleted file mode 100644 index c4085d0..0000000 --- a/src/Infrastructure/Persistence/Entities/Submission/SubmissionEntity.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Infrastructure.Persistence.Entities.Account; -using Infrastructure.Persistence.Entities.Problem; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Submission; - -[Table("submissions")] -public sealed class SubmissionEntity -{ - [Key, Column("id")] - public Guid Id { get; set; } - - [Column("code")] - public required string Code { get; set; } - - [Required, Column("problem_setup_id")] - public required int ProblemSetupId { get; set; } - - [ForeignKey(nameof(ProblemSetupId))] - public ProblemSetupEntity? ProblemSetup { get; set; } - - [Column("completed_at")] - public DateTime? CompletedAt { get; set; } - - [Column("created_on")] - public DateTime CreatedOn { get; set; } - - [Required, Column("created_by_id")] - public Guid CreatedById { get; set; } - - [ForeignKey(nameof(CreatedById))] - public AccountEntity? CreatedBy { get; set; } - - public List Results { get; set; } = []; -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Submission/SubmissionResultEntity.cs b/src/Infrastructure/Persistence/Entities/Submission/SubmissionResultEntity.cs deleted file mode 100644 index 200f930..0000000 --- a/src/Infrastructure/Persistence/Entities/Submission/SubmissionResultEntity.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Infrastructure.Persistence.Entities.Account; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Submission; - -[Table("submission_results")] -public sealed class SubmissionResultEntity -{ - [Key, Column("id")] - public Guid Id { get; set; } - - [Required, Column("submission_id")] - public Guid SubmissionId { get; set; } - - [ForeignKey(nameof(SubmissionId))] - public SubmissionEntity? Submission { get; set; } - - [Column("status_id")] - public required int StatusId { get; set; } - - [ForeignKey(nameof(StatusId))] - public SubmissionStatusEntity? Status { get; set; } - - [Column("started_at")] - public DateTime? StartedAt { get; set; } - - [Column("finished_at")] - public DateTime? FinishedAt { get; set; } - - [Column("stdout")] - public string? Stdout { get; set; } - - [Column("program_output")] - public string? ProgramOutput { get; set; } - - [Column("execution_id")] - public Guid ExecutionId { get; set; } - - [Column("result_id")] - public Guid? ResultId { get; set; } - - [Column("stderr")] - public string? Stderr { get; set; } - - [Column("runtime_ms")] - public int? RuntimeMs { get; set; } - - [Column("memory_kb")] - public int? MemoryKb { get; set; } - - [Column("created_on")] - public DateTime CreatedOn { get; set; } - - [Column("created_by_id")] - public Guid? CreatedById { get; set; } - - [ForeignKey(nameof(CreatedById))] - public AccountEntity? CreatedBy { get; set; } - - [Column("last_modified_on")] - public DateTime? LastModifiedOn { get; set; } - - [Column("last_modified_by_id")] - public Guid? LastModifiedById { get; set; } - - [ForeignKey(nameof(LastModifiedById))] - public AccountEntity? LastModifiedByAccount { get; set; } - - [Column("deleted_on")] - public DateTime? DeletedOn { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Submission/SubmissionStatusEntity.cs b/src/Infrastructure/Persistence/Entities/Submission/SubmissionStatusEntity.cs deleted file mode 100644 index e667db0..0000000 --- a/src/Infrastructure/Persistence/Entities/Submission/SubmissionStatusEntity.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Submission; - -[Table("submission_statuses")] -public sealed class SubmissionStatusEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Required, MaxLength(100)] - public required string Name { get; set; } - - [Required] - public string? Description { get; set; } - - [Column("status_type_id")] - public int StatusTypeId { get; set; } - - [ForeignKey(nameof(StatusTypeId))] - public SubmissionStatusTypeEntity? StatusType { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/Submission/SubmissionStatusTypeEntity.cs b/src/Infrastructure/Persistence/Entities/Submission/SubmissionStatusTypeEntity.cs deleted file mode 100644 index b54cece..0000000 --- a/src/Infrastructure/Persistence/Entities/Submission/SubmissionStatusTypeEntity.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.Submission; - -[Table("submission_status_types")] -public sealed class SubmissionStatusTypeEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Required, MaxLength(100)] - public required string Name { get; set; } - - [Required] - public string? Description { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseEntity.cs b/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseEntity.cs deleted file mode 100644 index ac3ce81..0000000 --- a/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseEntity.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.TestSuite; - -[Table("test_cases")] -public sealed class TestCaseEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Column("test_suite_id")] - public int TestSuiteId { get; set; } - - [Column("name"), MaxLength(100)] - public string? Name { get; set; } - - [Column("description"), MaxLength(200)] - public string? Description { get; set; } - - [ForeignKey(nameof(TestSuiteId))] - public TestSuiteEntity? TestSuite { get; set; } - - public IEnumerable InputParams { get; set; } = []; - - public TestCaseExpectedOutputEntity? ExpectedOutput { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseExpectedOutputEntity.cs b/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseExpectedOutputEntity.cs deleted file mode 100644 index ac6dd7a..0000000 --- a/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseExpectedOutputEntity.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.TestSuite; - -[Table("test_cases_expected_outputs")] -public sealed class TestCaseExpectedOutputEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Column("test_case_id")] - public int TestCaseId { get; set; } - - [ForeignKey(nameof(TestCaseId))] - public TestCaseEntity? TestCase { get; set; } - - [Column("value")] - public required string Value { get; set; } - - [Column("output_value_type_id")] - public int OutputValueTypeId { get; set; } - - [ForeignKey(nameof(OutputValueTypeId))] - public TestCasesOutputTypeEntity? OutputType { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseInputEntity.cs b/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseInputEntity.cs deleted file mode 100644 index a1a2b71..0000000 --- a/src/Infrastructure/Persistence/Entities/TestSuite/TestCaseInputEntity.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.TestSuite; - -[Table("test_cases_inputs")] -public sealed class TestCaseInputEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Column("test_case_id")] - public int TestCaseId { get; set; } - - [ForeignKey(nameof(TestCaseId))] - public TestCaseEntity? TestCase { get; set; } - - [Column("value")] - public required string Value { get; set; } - - [Column("test_cases_inputs_value_type_id")] - public int TestCasesInputsValueTypeId { get; set; } - - [ForeignKey(nameof(TestCasesInputsValueTypeId))] - public TestCasesInputsValueTypeEntity? TestCasesInputsValueType { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/TestSuite/TestCasesInputsValueTypeEntity.cs b/src/Infrastructure/Persistence/Entities/TestSuite/TestCasesInputsValueTypeEntity.cs deleted file mode 100644 index 269f0c7..0000000 --- a/src/Infrastructure/Persistence/Entities/TestSuite/TestCasesInputsValueTypeEntity.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.TestSuite; - -[Table("test_cases_inputs_value_types")] -public sealed class TestCasesInputsValueTypeEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Column("name"), MaxLength(50)] - public required string Name { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/TestSuite/TestCasesOutputTypeEntity.cs b/src/Infrastructure/Persistence/Entities/TestSuite/TestCasesOutputTypeEntity.cs deleted file mode 100644 index ec01719..0000000 --- a/src/Infrastructure/Persistence/Entities/TestSuite/TestCasesOutputTypeEntity.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text; - -namespace Infrastructure.Persistence.Entities.TestSuite; - -[Table("test_cases_output_value_types")] -public sealed class TestCasesOutputTypeEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Column("name"), MaxLength(50)] - public required string Name { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/TestSuite/TestSuiteEntity.cs b/src/Infrastructure/Persistence/Entities/TestSuite/TestSuiteEntity.cs deleted file mode 100644 index 8bbca25..0000000 --- a/src/Infrastructure/Persistence/Entities/TestSuite/TestSuiteEntity.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Infrastructure.Persistence.Entities.Problem; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.TestSuite; - -[Table("test_suites")] -public sealed class TestSuiteEntity -{ - [Key, Column("id")] - public int Id { get; set; } - - [Required, Column("name"), MaxLength(100)] - public required string Name { get; set; } - - [Column("description"), MaxLength(100)] - public string? Description { get; set; } - - [Column("test_suite_type_id")] - public int TestSuiteTypeId { get; set; } - - [ForeignKey(nameof(TestSuiteTypeId))] - public TestSuiteTypeEntity? TestSuiteType { get; set; } - - public IEnumerable TestCases { get; set; } = []; - - public IEnumerable Setups { get; set; } = []; -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Entities/TestSuite/TestSuiteTypeEntity.cs b/src/Infrastructure/Persistence/Entities/TestSuite/TestSuiteTypeEntity.cs deleted file mode 100644 index 876cea3..0000000 --- a/src/Infrastructure/Persistence/Entities/TestSuite/TestSuiteTypeEntity.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Infrastructure.Persistence.Entities.TestSuite; - -[Table("test_suite_types")] -public sealed class TestSuiteTypeEntity -{ - [Key] - [Column("id")] - public int Id { get; set; } - - [Column("name"), MaxLength(50)] - public required string Name { get; set; } - - [Column("description"), MaxLength(100)] - public string? Description { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/Repositories/AccountRepository.cs b/src/Infrastructure/Repositories/AccountRepository.cs deleted file mode 100644 index fa2c6c1..0000000 --- a/src/Infrastructure/Repositories/AccountRepository.cs +++ /dev/null @@ -1,109 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Repositories; -using Infrastructure.Persistence; -using Infrastructure.Persistence.Entities.Account; -using Mapster; -using Microsoft.EntityFrameworkCore; - -namespace Infrastructure.Repositories; - -public sealed class AccountRepository(AppDbContext db) : IAccountRepository -{ - public async Task AddAsync(AccountModel account, CancellationToken ct) - { - var entity = account.Adapt(); - - db.Accounts.Add(entity); - await db.SaveChangesAsync(ct); - } - - public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken) - { - return await db - .Accounts.ProjectToType() - .SingleOrDefaultAsync(a => a.Id == id, cancellationToken); - } - - public async Task GetByUsernameOrSubAsync( - string username, - string sub, - CancellationToken cancellationToken - ) - { - return await db - .Accounts.ProjectToType() - .SingleOrDefaultAsync(a => a.Username == username || a.Sub == sub, cancellationToken); - } - - public async Task GetByUsernameAsync( - string username, - CancellationToken cancellationToken - ) - { - return await db - .Accounts.ProjectToType() - .SingleOrDefaultAsync(a => a.Username == username, cancellationToken); - } - - public async Task GetBySubAsync(string sub, CancellationToken cancellationToken) - { - return await db - .Accounts - .Where(a => a.Sub == sub) - .ProjectToType() - .SingleOrDefaultAsync(cancellationToken); - } - - public async Task ExistsAsync(Guid id, CancellationToken cancellationToken) - { - return await db - .Accounts.AsNoTracking() - .SingleOrDefaultAsync(a => a.Id == id, cancellationToken) - is not null; - } - - public async Task UpdateImageUrlAsync(Guid id, string? imageUrl, CancellationToken cancellationToken) - { - await db.Accounts - .Where(a => a.Id == id) - .ExecuteUpdateAsync( - s => s.SetProperty(a => a.ImageUrl, imageUrl), - cancellationToken - ); - } - - public async Task CountByUsernameBaseAsync(string usernameBase, CancellationToken cancellationToken) - { - return await db.Accounts - .Where(a => a.Username == usernameBase || a.Username.StartsWith(usernameBase + "_")) - .CountAsync(cancellationToken); - } - - public async Task UpdateUsernameAsync(Guid id, string username, DateTime usernameLastChangedAt, CancellationToken cancellationToken) - { - await db.Accounts - .Where(a => a.Id == id) - .ExecuteUpdateAsync( - s => s - .SetProperty(a => a.Username, username) - .SetProperty(a => a.UsernameLastChangedAt, usernameLastChangedAt), - cancellationToken - ); - } - - public async Task UpdateAboutAsync(Guid id, string? about, CancellationToken cancellationToken) - { - await db.Accounts - .Where(a => a.Id == id) - .ExecuteUpdateAsync( - s => s.SetProperty(a => a.About, about), - cancellationToken - ); - } - - public async Task ExistsByUsernameAsync(string username, CancellationToken cancellationToken) - { - return await db.Accounts - .AnyAsync(a => a.Username == username, cancellationToken); - } -} \ No newline at end of file diff --git a/src/Infrastructure/Repositories/ProblemRepository.cs b/src/Infrastructure/Repositories/ProblemRepository.cs deleted file mode 100644 index 9d84f4b..0000000 --- a/src/Infrastructure/Repositories/ProblemRepository.cs +++ /dev/null @@ -1,467 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Domain.Problems; -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Domain.Problems.ProblemSetups; -using ApplicationCore.Domain.Problems.TestSuites; -using ApplicationCore.Interfaces.Repositories; -using Infrastructure.Persistence; -using Infrastructure.Persistence.Entities.Problem; -using Mapster; -using Microsoft.EntityFrameworkCore; - -namespace Infrastructure.Repositories; - -public sealed class ProblemRepository(AppDbContext db) : IProblemRepository -{ - private readonly AppDbContext _db = db; - - private const int Judge0EngineId = 1; - - public async Task GetProblemByIdAsync( - Guid problemId, - CancellationToken cancellationToken - ) - { - return await _db - .Problems.Where(problem => problem.Id == problemId) - .Select(problem => new ProblemModel() - { - Id = problem.Id, - Slug = problem.Slug, - Title = problem.Title, - Question = problem.Question, - Difficulty = problem.Difficulty, - CreatedById = problem.CreatedById, - CreatedBy = - problem.CreatedBy != null - ? new AccountModel() - { - Id = problem.CreatedBy.Id, - Username = problem.CreatedBy.Username, - ImageUrl = problem.CreatedBy.ImageUrl, - CreatedOn = problem.CreatedOn, - } - : null, - Tags = problem.Tags.Select(tag => new TagModel() - { - Id = tag.Id, - Value = tag.Value, - }), - ProblemSetups = problem - .ProblemSetups.Select(ps => new ProblemSetupModel - { - Id = ps.Id, - ProblemId = ps.ProblemId, - InitialCode = ps.InitialCode ?? "", - Version = ps.Version, - LanguageVersionId = ps.ProgrammingLanguageVersionId, - LanguageVersion = - ps.LanguageVersion != null - ? new LanguageVersion - { - Id = ps.LanguageVersion.Id, - ProgrammingLanguageId = - ps.LanguageVersion.ProgrammingLanguageId, - ProgrammingLanguage = - ps.LanguageVersion.ProgrammingLanguage != null - ? new ProgrammingLanguage - { - Id = ps.LanguageVersion.ProgrammingLanguage.Id, - Name = ps.LanguageVersion.ProgrammingLanguage.Name, - IsArchived = ps.LanguageVersion - .ProgrammingLanguage - .IsArchived, - Versions = new List(), - } - : null, - Version = ps.LanguageVersion.Version, - } - : null, - }) - .ToList(), - }) - .SingleOrDefaultAsync(cancellationToken); - } - - public async Task CreateProblemAsync( - ProblemModel problem, - CancellationToken cancellationToken - ) - { - var normalizedTags = problem - .Tags.Select(t => t.Value.Trim()) - .Where(t => t.Length > 0) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - - var existingTags = await _db - .Tags.Where(t => normalizedTags.Contains(t.Value)) - .ToListAsync(cancellationToken); - - var existingTagValues = existingTags - .Select(t => t.Value) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - var newTags = normalizedTags - .Where(t => !existingTagValues.Contains(t)) - .Select(t => new TagEntity { Value = t }) - .ToList(); - - if (newTags.Count > 0) - { - _db.Tags.AddRange(newTags); - } - - var problemEntity = new ProblemEntity - { - Id = Guid.NewGuid(), - Title = problem.Title, - Slug = problem.Slug, - Question = problem.Question, - Difficulty = problem.Difficulty, - StatusId = (int)ProblemStatus.Pending, - Tags = existingTags.Concat(newTags).ToList(), - }; - - _db.Problems.Add(problemEntity); - - await _db.SaveChangesAsync(cancellationToken); - - return problem.Id; - } - - public async Task> GetAvailableLanguagesAsync( - CancellationToken cancellationToken - ) - { - return await _db - .ProgrammingLanguages.Where(language => - !language.IsArchived && language.DeletedOn == null - ) - .Select(language => new ProgrammingLanguage - { - Id = language.Id, - Name = language.Name, - Versions = language.Versions.Select(version => new LanguageVersion - { - Id = version.Id, - InitialCode = version.InitialCode, - Version = version.Version, - }), - }) - .ToListAsync(cancellationToken); - } - - public async Task GetProblemBySlugAsync( - string slug, - CancellationToken cancellationToken - ) - { - return !string.IsNullOrWhiteSpace(slug) - ? await _db - .Problems.Where(problem => problem.Slug == slug) - .Select(problem => new ProblemModel() - { - Id = problem.Id, - Slug = problem.Slug, - Title = problem.Title, - Question = problem.Question, - Difficulty = problem.Difficulty, - CreatedById = problem.CreatedById, - CreatedBy = - problem.CreatedBy != null - ? new AccountModel() - { - Id = problem.CreatedBy.Id, - Username = problem.CreatedBy.Username, - ImageUrl = problem.CreatedBy.ImageUrl, - CreatedOn = problem.CreatedOn, - } - : null, - Tags = problem.Tags.Select(tag => new TagModel() - { - Id = tag.Id, - Value = tag.Value, - }), - ProblemSetups = problem - .ProblemSetups.Select(ps => new ProblemSetupModel - { - Id = ps.Id, - ProblemId = ps.ProblemId, - InitialCode = ps.InitialCode ?? "", - Version = ps.Version, - LanguageVersionId = ps.ProgrammingLanguageVersionId, - LanguageVersion = - ps.LanguageVersion != null - ? new LanguageVersion - { - Id = ps.LanguageVersion.Id, - ProgrammingLanguageId = - ps.LanguageVersion.ProgrammingLanguageId, - ProgrammingLanguage = - ps.LanguageVersion.ProgrammingLanguage != null - ? new ProgrammingLanguage - { - Id = ps.LanguageVersion.ProgrammingLanguage.Id, - Name = ps.LanguageVersion - .ProgrammingLanguage - .Name, - IsArchived = ps.LanguageVersion - .ProgrammingLanguage - .IsArchived, - Versions = new List(), - } - : null, - Version = ps.LanguageVersion.Version, - } - : null, - }) - .ToList(), - }) - .SingleOrDefaultAsync(cancellationToken) - : null; - } - - public async Task> GetProblemsAsync( - PaginationRequest pagination, - CancellationToken cancellationToken - ) - { - int page = pagination.Page > 0 ? pagination.Page : 1; - int size = pagination.Size > 0 ? pagination.Size : 10; - - var baseQuery = _db - .Problems.Include(p => p.Tags) - .Include(p => p.Status) - .Where(p => - p.CreatedOn <= pagination.Timestamp - && p.DeletedOn == null - && p.StatusId == (int)ProblemStatus.Published - ); - - var ordered = - pagination.Direction == SortDirection.Asc - ? baseQuery.OrderBy(p => p.CreatedOn).ThenBy(p => p.Id) - : baseQuery.OrderByDescending(p => p.CreatedOn).ThenByDescending(p => p.Id); - - int total = await ordered.CountAsync(cancellationToken); - - var problems = await ordered - .Skip((page - 1) * size) - .Take(size) - .Select(problem => new ProblemModel - { - Id = problem.Id, - Title = problem.Title, - Slug = problem.Slug, - Question = problem.Question, - Difficulty = problem.Difficulty, - Tags = problem.Tags.Select(tag => new TagModel { Id = tag.Id, Value = tag.Value }), - Version = problem.Version, - }) - .ToListAsync(cancellationToken); - - return new PaginatedResult - { - Results = problems, - Total = total, - Page = page, - Size = size, - }; - } - - public async Task> GetProblemSetupsAsync( - IEnumerable problemSetupIds, - CancellationToken cancellationToken - ) - { - return await _db - .ProblemSetups.Where(setup => problemSetupIds.Contains(setup.Id)) - .Select(ps => new ProblemSetupModel - { - Id = ps.Id, - ProblemId = ps.ProblemId, - InitialCode = ps.InitialCode ?? "", - Version = ps.Version, - FunctionName = ps.FunctionName, - LanguageVersionId = ps.ProgrammingLanguageVersionId, - LanguageVersion = - ps.LanguageVersion != null - ? new LanguageVersion - { - Id = ps.LanguageVersion.Id, - Version = ps.LanguageVersion.Version, - ProgrammingLanguageId = ps.LanguageVersion.ProgrammingLanguageId, - Judge0LanguageId = ps.LanguageVersion.EngineMappings - .Where(m => m.EngineId == Judge0EngineId) - .Select(m => (int?)m.EngineLanguageId) - .FirstOrDefault(), - ProgrammingLanguage = - ps.LanguageVersion.ProgrammingLanguage != null - ? new ProgrammingLanguage - { - Id = ps.LanguageVersion.ProgrammingLanguage.Id, - Name = ps.LanguageVersion.ProgrammingLanguage.Name, - IsArchived = ps.LanguageVersion - .ProgrammingLanguage - .IsArchived, - Versions = new List(), - } - : null, - } - : null, - HarnessTemplate = - ps.HarnessTemplate != null - ? new HarnessTemplate - { - Id = ps.HarnessTemplate.Id, - Template = ps.HarnessTemplate.Template, - } - : null, - TestSuites = ps - .TestSuites.Select(ts => new TestSuiteModel - { - Id = ts.Id, - Name = ts.Name, - TestSuiteType = (TestSuiteType)ts.TestSuiteTypeId, - TestCases = ts - .TestCases.Select(tc => new TestCaseModel - { - Id = tc.Id, - Inputs = tc.InputParams.Select(param => new TestCaseInputParamModel - { - Id = param.Id, - Value = param.Value, - TestCaseInputValueTypeId = param.TestCasesInputsValueTypeId, - InputType = new TestCaseInputValueTypeModel - { - Id = param.TestCasesInputsValueType.Id, - Name = param.TestCasesInputsValueType.Name, - }, - }), - ExpectedOutput = new TestCaseExpectedOutputModel - { - Id = tc.ExpectedOutput.Id, - TestCaseId = tc.ExpectedOutput.TestCaseId, - Value = tc.ExpectedOutput.Value, - OutputValueTypeId = tc.ExpectedOutput.OutputValueTypeId, - OutputType = new TestCaseOutputTypeModel - { - Id = tc.ExpectedOutput.OutputType.Id, - Name = tc.ExpectedOutput.OutputType.Name - } - } - }) - .ToList(), - }) - .ToList(), - }) - .ToListAsync(cancellationToken); - } - - public async Task GetProblemSetupAsync( - Guid problemId, - int languageVersionId, - CancellationToken cancellationToken - ) - { - return await _db - .ProblemSetups.Where(setup => setup.Problem.Id == problemId && setup.ProgrammingLanguageVersionId == languageVersionId) - .Include(s => s.HarnessTemplate) - .Select(ps => new ProblemSetupModel - { - Id = ps.Id, - ProblemId = ps.ProblemId, - InitialCode = ps.InitialCode ?? "", - Version = ps.Version, - FunctionName = ps.FunctionName, - LanguageVersionId = ps.ProgrammingLanguageVersionId, - LanguageVersion = - ps.LanguageVersion != null - ? new LanguageVersion - { - Id = ps.LanguageVersion.Id, - Version = ps.LanguageVersion.Version, - ProgrammingLanguageId = ps.LanguageVersion.ProgrammingLanguageId, - Judge0LanguageId = ps.LanguageVersion.EngineMappings - .Where(m => m.EngineId == Judge0EngineId) - .Select(m => (int?)m.EngineLanguageId) - .FirstOrDefault(), - ProgrammingLanguage = - ps.LanguageVersion.ProgrammingLanguage != null - ? new ProgrammingLanguage - { - Id = ps.LanguageVersion.ProgrammingLanguage.Id, - Name = ps.LanguageVersion.ProgrammingLanguage.Name, - IsArchived = ps.LanguageVersion - .ProgrammingLanguage - .IsArchived, - Versions = new List(), - } - : null, - } - : null, - HarnessTemplate = - ps.HarnessTemplate != null - ? new HarnessTemplate - { - Id = ps.HarnessTemplate.Id, - Template = ps.HarnessTemplate.Template, - } - : null, - TestSuites = ps - .TestSuites.Select(ts => new TestSuiteModel - { - Id = ts.Id, - Name = ts.Name, - TestSuiteType = (TestSuiteType)ts.TestSuiteTypeId, - TestCases = ts - .TestCases.Select(tc => new TestCaseModel - { - Id = tc.Id, - Inputs = tc.InputParams.Select(param => new TestCaseInputParamModel - { - Id = param.Id, - Value = param.Value, - TestCaseInputValueTypeId = param.TestCasesInputsValueTypeId, - InputType = new TestCaseInputValueTypeModel - { - Id = param.TestCasesInputsValueType.Id, - Name = param.TestCasesInputsValueType.Name, - }, - }), - ExpectedOutput = new TestCaseExpectedOutputModel - { - Id = tc.ExpectedOutput.Id, - TestCaseId = tc.ExpectedOutput.TestCaseId, - Value = tc.ExpectedOutput.Value, - OutputValueTypeId = tc.ExpectedOutput.OutputValueTypeId, - OutputType = new TestCaseOutputTypeModel - { - Id = tc.ExpectedOutput.OutputType.Id, - Name = tc.ExpectedOutput.OutputType.Name, - }, - }, - }) - .ToList(), - }) - .ToList(), - }) - .SingleOrDefaultAsync(cancellationToken); - } - - public async Task GetProblemSetupAsync( - int setupId, - CancellationToken cancellationToken - ) - { - return await _db - .ProblemSetups.Include(ps => ps.LanguageVersion) - .Include(ps => ps.TestSuites) - .ThenInclude(ts => ts.TestCases) - .Where(ps => ps.Id == setupId) - .ProjectToType() - .SingleOrDefaultAsync(cancellationToken); - } -} \ No newline at end of file diff --git a/src/Infrastructure/Repositories/SubmissionRepository.cs b/src/Infrastructure/Repositories/SubmissionRepository.cs deleted file mode 100644 index 6533b4a..0000000 --- a/src/Infrastructure/Repositories/SubmissionRepository.cs +++ /dev/null @@ -1,583 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Domain.Submissions.Outboxes; -using ApplicationCore.Interfaces.Repositories; -using EFCore.BulkExtensions; -using Infrastructure.Persistence; -using Infrastructure.Persistence.Entities.Submission; -using Infrastructure.Persistence.Entities.Submission.Outbox; -using Mapster; -using Microsoft.EntityFrameworkCore; -using System.Linq.Expressions; - -namespace Infrastructure.Repositories; - -public sealed class SubmissionRepository(AppDbContext db) : ISubmissionRepository -{ - public async Task> GetSubmissionOutboxesAsync( - CancellationToken cancellationToken - ) - { - return await db - .SubmissionOutboxes.Where(outbox => - outbox.FinalizedOn == null && outbox.AttemptCount < MaxRetryCount - ) - .Include(outbox => outbox.Submission) - .ThenInclude(submission => submission.Results) - .Select(MapOutboxExpr) - .ToListAsync(cancellationToken: cancellationToken); - } - - public async Task SaveAsync( - SubmissionModel submission, - CancellationToken cancellationToken - ) - { - DateTime createdOn = DateTime.UtcNow; - db.Submissions.Add( - new SubmissionEntity - { - Id = submission.Id, - ProblemSetupId = submission.ProblemSetupId, - Code = submission.Code ?? "", - CreatedOn = createdOn, - CreatedById = submission.CreatedById, - } - ); - - var outboxId = Guid.NewGuid(); - db.SubmissionOutboxes.Add( - new SubmissionOutboxEntity - { - Id = outboxId, - SubmissionId = submission.Id, - SubmissionOutboxTypeId = (int)SubmissionOutboxType.Initialized, - SubmissionOutboxStatusId = (int)SubmissionOutboxStatus.Pending, - CreatedOn = createdOn, - } - ); - - await db.SaveChangesAsync(cancellationToken); - return outboxId; - } - - public Task IncrementOutboxesCountAsync( - IEnumerable outboxIds, - DateTime now, - CancellationToken cancellationToken - ) - { - return db - .SubmissionOutboxes.Where(o => outboxIds.Contains(o.Id)) - .ExecuteUpdateAsync( - setters => - setters - .SetProperty(o => o.AttemptCount, o => o.AttemptCount + 1) - .SetProperty(o => o.ProcessOn, now) - .SetProperty(o => o.NextAttemptOn, (DateTime?)null), - cancellationToken: cancellationToken - ); - } - - public async Task ProcessPollingSubmissionExecutionsAsync( - IEnumerable submissionModels, - CancellationToken cancellationToken - ) - { - await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken); - - try - { - var resultEntities = submissionModels - .SelectMany( - s => s.Results, - (s, sr) => - new SubmissionResultEntity - { - Id = sr.Id, - SubmissionId = s.Id, - ExecutionId = sr.ExecutionId, - ResultId = sr.ResultId ?? sr.Id, - StatusId = (int)sr.Status, - StartedAt = sr.StartedAt, - FinishedAt = sr.FinishedAt, - Stdout = sr.Stdout, - ProgramOutput = sr.ProgramOutput, - Stderr = sr.Stderr, - RuntimeMs = sr.RuntimeMs, - MemoryKb = sr.MemoryKb, - } - ) - .ToList(); - - if (resultEntities.Count != 0) - { - await db.BulkInsertOrUpdateAsync( - resultEntities, - ResultBulkConfig, - cancellationToken: cancellationToken - ); - - var completedSubmissionIds = submissionModels - .Where(s => - s.Results.Any() - && s.Results.All(r => - r.Status - is not SubmissionStatus.InQueue - and not SubmissionStatus.Processing - ) - ) - .Select(s => s.Id) - .Distinct() - .ToList(); - - if (completedSubmissionIds.Count != 0) - { - await db - .SubmissionOutboxes.Where(outbox => - completedSubmissionIds.Contains(outbox.SubmissionId) - && outbox.SubmissionOutboxTypeId - == (int)SubmissionOutboxType.PollExecution - ) - .ExecuteUpdateAsync( - setters => - setters - .SetProperty( - o => o.SubmissionOutboxTypeId, - (int)SubmissionOutboxType.Evaluate - ) - .SetProperty(o => o.AttemptCount, _ => 0), - cancellationToken: cancellationToken - ); - } - } - - await transaction.CommitAsync(cancellationToken); - } - catch - { - await transaction.RollbackAsync(cancellationToken); - throw; - } - } - - public async Task ProcessSubmissionInitializationAsync( - IEnumerable submissions, - CancellationToken cancellationToken - ) - { - await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken); - - try - { - var resultEntities = submissions - .SelectMany( - s => s.Results, - (s, sr) => - new SubmissionResultEntity - { - Id = sr.Id, - SubmissionId = s.Id, - ExecutionId = sr.ExecutionId, - ResultId = sr.Id, - StatusId = (int)SubmissionStatus.InQueue, - CreatedOn = DateTime.UtcNow, - StartedAt = sr.StartedAt, - FinishedAt = sr.FinishedAt, - Stdout = sr.Stdout, - RuntimeMs = sr.RuntimeMs, - MemoryKb = sr.MemoryKb, - } - ) - .ToList(); - - if (resultEntities.Count != 0) - { - await db.BulkInsertOrUpdateAsync( - resultEntities, - ResultBulkConfig, - cancellationToken: cancellationToken - ); - - var submissionIds = resultEntities - .Select(re => re.SubmissionId) - .Distinct() - .ToList(); - - await db - .SubmissionOutboxes.Where(outbox => - submissionIds.Contains(outbox.SubmissionId) - && outbox.SubmissionOutboxTypeId == (int)SubmissionOutboxType.Initialized - ) - .ExecuteUpdateAsync( - setters => - setters - .SetProperty( - o => o.SubmissionOutboxTypeId, - (int)SubmissionOutboxType.PollExecution - ) - .SetProperty(o => o.AttemptCount, _ => 0), - cancellationToken: cancellationToken - ); - } - - await transaction.CommitAsync(cancellationToken); - } - catch - { - await transaction.RollbackAsync(cancellationToken); - throw; - } - } - - private static readonly Expression< - Func - > MapResultExpr = result => new SubmissionResult - { - Id = result.Id, - Status = (SubmissionStatus)result.StatusId, - ExecutionId = result.ExecutionId, - ResultId = result.ResultId, - FinishedAt = result.FinishedAt, - MemoryKb = result.MemoryKb, - RuntimeMs = result.RuntimeMs, - StartedAt = result.StartedAt, - Stdout = result.Stdout, - ProgramOutput = result.ProgramOutput, - Stderr = result.Stderr, - }; - - private static readonly Expression< - Func - > MapOutboxExpr = outbox => new SubmissionOutboxModel - { - Id = outbox.Id, - Status = (SubmissionOutboxStatus)outbox.SubmissionOutboxStatusId, - Type = (SubmissionOutboxType)outbox.SubmissionOutboxTypeId, - SubmissionId = outbox.SubmissionId, - Submission = - outbox.Submission == null - ? null! - : new SubmissionModel - { - Id = outbox.Submission.Id, - ProblemSetupId = outbox.Submission.ProblemSetupId, - Code = outbox.Submission.Code, - CreatedOn = outbox.Submission.CreatedOn, - CreatedById = outbox.Submission.CreatedById, - Results = outbox.Submission.Results.AsQueryable().Select(MapResultExpr), - }, - }; - - private static readonly Expression> MapSubmissionExpr = - submission => new SubmissionModel - { - Id = submission.Id, - Code = submission.Code, - ProblemSetupId = submission.ProblemSetupId, - CreatedOn = submission.CreatedOn, - CompletedAt = submission.CompletedAt, - CreatedById = submission.CreatedById, - LanguageVersion = new LanguageVersion - { - Id = submission.ProblemSetup!.LanguageVersion!.Id, - Version = submission.ProblemSetup.LanguageVersion.Version, - InitialCode = submission.ProblemSetup.LanguageVersion.InitialCode, - ProgrammingLanguageId = submission - .ProblemSetup - .LanguageVersion - .ProgrammingLanguageId, - Judge0LanguageId = null, - ProgrammingLanguage = - submission.ProblemSetup.LanguageVersion.ProgrammingLanguage == null - ? null - : new ProgrammingLanguage - { - Id = submission.ProblemSetup.LanguageVersion.ProgrammingLanguage.Id, - Name = submission.ProblemSetup.LanguageVersion.ProgrammingLanguage.Name, - IsArchived = submission - .ProblemSetup - .LanguageVersion - .ProgrammingLanguage - .IsArchived, - }, - }, - CreatedBy = - submission.CreatedBy == null - ? null - : new AccountModel - { - Username = submission.CreatedBy.Username, - ImageUrl = submission.CreatedBy.ImageUrl, - CreatedOn = submission.CreatedBy.CreatedOn, - Id = submission.CreatedBy.Id, - }, - Results = submission - .Results.Select(result => new SubmissionResult - { - Id = result.Id, - Status = (SubmissionStatus)result.StatusId, - ExecutionId = result.ExecutionId, - ResultId = result.ResultId, - FinishedAt = result.FinishedAt, - MemoryKb = result.MemoryKb, - RuntimeMs = result.RuntimeMs, - StartedAt = result.StartedAt, - Stdout = result.Stdout, - ProgramOutput = result.ProgramOutput, - Stderr = result.Stderr, - }) - .ToList(), - }; - - private const int MaxRetryCount = 5; - - private static readonly BulkConfig ResultBulkConfig = new() - { - PropertiesToExcludeOnUpdate = [nameof(SubmissionResultEntity.CreatedOn)], - }; - - public async Task SaveExecutionTokensAsync( - IEnumerable submissions, - CancellationToken cancellationToken - ) - { - await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken); - - try - { - var resultEntities = submissions - .SelectMany( - s => s.Results, - (s, sr) => - new SubmissionResultEntity - { - Id = sr.Id, - SubmissionId = s.Id, - ExecutionId = sr.ExecutionId, - ResultId = sr.ResultId, - StatusId = (int)SubmissionStatus.InQueue, - CreatedOn = DateTime.UtcNow, - StartedAt = sr.StartedAt, - FinishedAt = sr.FinishedAt, - Stdout = sr.Stdout, - RuntimeMs = sr.RuntimeMs, - MemoryKb = sr.MemoryKb, - } - ) - .ToList(); - - if (resultEntities.Count != 0) - { - await db.BulkInsertOrUpdateAsync( - resultEntities, - ResultBulkConfig, - cancellationToken: cancellationToken - ); - - var submissionIds = resultEntities.Select(r => r.SubmissionId).Distinct().ToList(); - - await db - .SubmissionOutboxes.Where(o => - submissionIds.Contains(o.SubmissionId) - && o.SubmissionOutboxTypeId == (int)SubmissionOutboxType.Initialized - ) - .ExecuteUpdateAsync( - setters => - setters - .SetProperty( - o => o.SubmissionOutboxTypeId, - (int)SubmissionOutboxType.PollExecution - ) - .SetProperty(o => o.AttemptCount, _ => 0), - cancellationToken: cancellationToken - ); - } - - await transaction.CommitAsync(cancellationToken); - } - catch - { - await transaction.RollbackAsync(cancellationToken); - throw; - } - } - - public async Task ProcessEvaluationAsync( - IEnumerable submissions, - CancellationToken cancellationToken - ) - { - await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken); - - try - { - var resultEntities = submissions - .SelectMany( - s => s.Results, - (s, sr) => - new SubmissionResultEntity - { - Id = sr.Id, - SubmissionId = s.Id, - ExecutionId = sr.ExecutionId, - ResultId = sr.ResultId, - StatusId = (int)sr.Status, - StartedAt = sr.StartedAt, - FinishedAt = sr.FinishedAt, - Stdout = sr.Stdout, - ProgramOutput = sr.ProgramOutput, - Stderr = sr.Stderr, - RuntimeMs = sr.RuntimeMs, - MemoryKb = sr.MemoryKb, - } - ) - .ToList(); - - if (resultEntities.Count != 0) - { - await db.BulkInsertOrUpdateAsync( - resultEntities, - ResultBulkConfig, - cancellationToken: cancellationToken - ); - - var submissionIds = resultEntities.Select(r => r.SubmissionId).Distinct().ToList(); - - await db - .SubmissionOutboxes.Where(o => - submissionIds.Contains(o.SubmissionId) - && o.SubmissionOutboxTypeId == (int)SubmissionOutboxType.Evaluate - ) - .ExecuteUpdateAsync( - setters => - setters - .SetProperty( - o => o.SubmissionOutboxTypeId, - (int)SubmissionOutboxType.EvaluationPoll - ) - .SetProperty(o => o.AttemptCount, _ => 0), - cancellationToken: cancellationToken - ); - } - - await transaction.CommitAsync(cancellationToken); - } - catch - { - await transaction.RollbackAsync(cancellationToken); - throw; - } - } - - public async Task FinalizeEvaluationAsync( - IEnumerable outboxIds, - DateTime now, - CancellationToken cancellationToken - ) - { - var ids = outboxIds.ToList(); - - var submissionIds = await db - .SubmissionOutboxes.Where(o => ids.Contains(o.Id)) - .Select(o => o.SubmissionId) - .Distinct() - .ToListAsync(cancellationToken); - - await db - .Submissions.Where(s => submissionIds.Contains(s.Id)) - .ExecuteUpdateAsync( - setters => setters.SetProperty(s => s.CompletedAt, now), - cancellationToken: cancellationToken - ); - - await db - .SubmissionOutboxes.Where(o => ids.Contains(o.Id)) - .ExecuteUpdateAsync( - setters => - setters.SetProperty(o => o.FinalizedOn, now).SetProperty(o => o.ProcessOn, now), - cancellationToken: cancellationToken - ); - } - - public async Task> GetSubmissionsByProblemId( - Guid problemId, - Guid? accountId, - PaginationRequest pagination, - SubmissionStatus? statusFilter, - CancellationToken cancellationToken - ) - { - IQueryable query = db - .Submissions.Where(s => - s.ProblemSetup!.ProblemId == problemId - && (accountId == null || s.CreatedById == accountId) - && s.CreatedOn <= pagination.Timestamp - ) - .Include(s => s.CreatedBy) - .Include(s => s.Results) - .Include(s => s.ProblemSetup) - .ThenInclude(ps => ps!.LanguageVersion) - .ThenInclude(lv => lv!.ProgrammingLanguage); - - if (statusFilter.HasValue) - { - query = query.Where(s => s.Results.All(r => r.StatusId == (int)statusFilter.Value)); - } - - int total = await query.CountAsync(cancellationToken); - - var submissions = await query - .OrderByDescending(s => s.CreatedOn) - .Skip((pagination.Page - 1) * pagination.Size) - .Take(pagination.Size) - .Select(MapSubmissionExpr) - .ToListAsync(cancellationToken); - - return new PaginatedResult - { - Results = submissions, - Total = total, - Page = pagination.Page, - Size = pagination.Size, - }; - } - - public async Task GetSubmissionByIdAsync(Guid submissionId, CancellationToken cancellationToken) - { - var entity = await db.Submissions - .Include(s => s.Results) - .FirstOrDefaultAsync(s => s.Id == submissionId, cancellationToken); - - if (entity == null) - { - return null; - } - - return new SubmissionModel - { - Id = entity.Id, - Code = entity.Code, - ProblemSetupId = entity.ProblemSetupId, - CreatedOn = entity.CreatedOn, - CompletedAt = entity.CompletedAt, - CreatedById = entity.CreatedById, - Results = entity.Results.Select(r => new SubmissionResult - { - Id = r.Id, - Status = (SubmissionStatus)r.StatusId, - ExecutionId = r.ExecutionId, - ResultId = r.ResultId, - FinishedAt = r.FinishedAt, - MemoryKb = r.MemoryKb, - RuntimeMs = r.RuntimeMs, - StartedAt = r.StartedAt, - Stdout = r.Stdout, - ProgramOutput = r.ProgramOutput, - Stderr = r.Stderr, - }).ToList(), - }; - } -} \ No newline at end of file diff --git a/src/Infrastructure/Services/SlugService.cs b/src/Infrastructure/Services/SlugService.cs deleted file mode 100644 index f9333a1..0000000 --- a/src/Infrastructure/Services/SlugService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using ApplicationCore.Interfaces.Services; -using System.Globalization; -using System.Text; -using System.Text.RegularExpressions; - -namespace Infrastructure.Services; - -public sealed partial class SlugService : ISlugService -{ - public string GenerateSlug(string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - return string.Empty; - } - - string normalized = input - .Normalize(NormalizationForm.FormD) - .Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) - .Aggregate(new StringBuilder(), (sb, c) => sb.Append(c)) - .ToString() - .Normalize(NormalizationForm.FormC); - - normalized = normalized.ToLowerInvariant(); - - normalized = NonAlphaNumericRegex().Replace(normalized, ""); - normalized = WhitespaceRegex().Replace(normalized, "-").Trim('-'); - normalized = MultipleDashRegex().Replace(normalized, "-"); - - return normalized; - } - - [GeneratedRegex(@"[^a-z0-9\s-]", RegexOptions.Compiled)] - private static partial Regex NonAlphaNumericRegex(); - - [GeneratedRegex(@"\s+", RegexOptions.Compiled)] - private static partial Regex WhitespaceRegex(); - - [GeneratedRegex(@"-+", RegexOptions.Compiled)] - private static partial Regex MultipleDashRegex(); -} \ No newline at end of file diff --git a/src/PublicApi/Attributes/GlobalRateLimitAttribute.cs b/src/PublicApi/Attributes/GlobalRateLimitAttribute.cs deleted file mode 100644 index 7e10727..0000000 --- a/src/PublicApi/Attributes/GlobalRateLimitAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PublicApi.Attributes; - -[AttributeUsage( - AttributeTargets.Method | AttributeTargets.Class, - AllowMultiple = false, - Inherited = true -)] -public sealed class GlobalRateLimitAttribute(int count, int seconds) : Attribute -{ - public int Count { get; } = count; - public int Seconds { get; } = seconds; - - public string PolicyName => $"Global_{Count}:{Seconds}"; -} \ No newline at end of file diff --git a/src/PublicApi/Attributes/RequireAccountAttribute.cs b/src/PublicApi/Attributes/RequireAccountAttribute.cs deleted file mode 100644 index 59a4eae..0000000 --- a/src/PublicApi/Attributes/RequireAccountAttribute.cs +++ /dev/null @@ -1,5 +0,0 @@ - -namespace PublicApi.Attributes; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)] -public sealed class RequiresAccountAttribute : Attribute { } \ No newline at end of file diff --git a/src/PublicApi/Attributes/UserRateLimitAttribute.cs b/src/PublicApi/Attributes/UserRateLimitAttribute.cs deleted file mode 100644 index 43e984c..0000000 --- a/src/PublicApi/Attributes/UserRateLimitAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PublicApi.Attributes; - -[AttributeUsage( - AttributeTargets.Method | AttributeTargets.Class, - AllowMultiple = false, - Inherited = true -)] -public sealed class UserRateLimitAttribute(int count, int seconds) : Attribute -{ - public int Count { get; } = count; - public int Seconds { get; } = seconds; - - public string PolicyName => $"User_{Count}:{Seconds}"; -} \ No newline at end of file diff --git a/src/PublicApi/Authorization/RbacHandler.cs b/src/PublicApi/Authorization/RbacHandler.cs deleted file mode 100644 index 9c202a2..0000000 --- a/src/PublicApi/Authorization/RbacHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace PublicApi.Authorization; - -public sealed class RbacHandler : AuthorizationHandler -{ - protected override Task HandleRequirementAsync( - AuthorizationHandlerContext context, - RbacRequirement requirement - ) - { - if (!context.User.HasClaim(c => c.Type == "permissions")) - { - return Task.CompletedTask; - } - - var permission = context.User.FindFirst(c => - c.Type == "permissions" && c.Value == requirement.Permission - ); - - if (permission == null) - { - return Task.CompletedTask; - } - - context.Succeed(requirement); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/PublicApi/Authorization/RbacRequirement.cs b/src/PublicApi/Authorization/RbacRequirement.cs deleted file mode 100644 index c140d48..0000000 --- a/src/PublicApi/Authorization/RbacRequirement.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace PublicApi.Authorization; - -public sealed class RbacRequirement(string permission) : IAuthorizationRequirement -{ - public string Permission { get; } = - permission ?? throw new ArgumentNullException(nameof(permission)); -} \ No newline at end of file diff --git a/src/PublicApi/Contracts/Account/CreateAccountDto.cs b/src/PublicApi/Contracts/Account/CreateAccountDto.cs deleted file mode 100644 index 918b124..0000000 --- a/src/PublicApi/Contracts/Account/CreateAccountDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace PublicApi.Contracts.Account; - -public record CreateAccountDto(string Username, string? ImageUrl); \ No newline at end of file diff --git a/src/PublicApi/Contracts/Account/UpdateProfileSettingsDto.cs b/src/PublicApi/Contracts/Account/UpdateProfileSettingsDto.cs deleted file mode 100644 index c4c2420..0000000 --- a/src/PublicApi/Contracts/Account/UpdateProfileSettingsDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace PublicApi.Contracts.Account; - -public record UpdateProfileSettingsDto(string? Bio); \ No newline at end of file diff --git a/src/PublicApi/Contracts/Account/UpdateUsernameDto.cs b/src/PublicApi/Contracts/Account/UpdateUsernameDto.cs deleted file mode 100644 index 06584b5..0000000 --- a/src/PublicApi/Contracts/Account/UpdateUsernameDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace PublicApi.Contracts.Account; - -public record UpdateUsernameDto(string Username); \ No newline at end of file diff --git a/src/PublicApi/Contracts/Account/UpsertAccountDto.cs b/src/PublicApi/Contracts/Account/UpsertAccountDto.cs deleted file mode 100644 index 17c3cc7..0000000 --- a/src/PublicApi/Contracts/Account/UpsertAccountDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace PublicApi.Contracts.Account; - -public record UpsertAccountDto(string? ImageUrl); \ No newline at end of file diff --git a/src/PublicApi/Contracts/Submission/CreateSubmissionDto.cs b/src/PublicApi/Contracts/Submission/CreateSubmissionDto.cs deleted file mode 100644 index 0a82913..0000000 --- a/src/PublicApi/Contracts/Submission/CreateSubmissionDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace PublicApi.Contracts.Submission; - -public sealed record CreateSubmissionDto(int ProblemSetupId, string Code); \ No newline at end of file diff --git a/src/PublicApi/Controllers/AccountController.cs b/src/PublicApi/Controllers/AccountController.cs deleted file mode 100644 index 323db03..0000000 --- a/src/PublicApi/Controllers/AccountController.cs +++ /dev/null @@ -1,211 +0,0 @@ -using ApplicationCore.Commands.Accounts.UpdateProfileSettings; -using ApplicationCore.Commands.Accounts.UpdateUsername; -using ApplicationCore.Commands.Accounts.UpsertAccount; -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Interfaces.Services; -using Asp.Versioning; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; -using PublicApi.Attributes; -using PublicApi.Contracts.Account; - -namespace PublicApi.Controllers; - -[ApiController] -[Route("api/v{version:apiVersion}/[controller]")] -[ApiVersion("1.0")] -public sealed partial class AccountController( - IAccountAppService accountAppService, - IAccountContext accountContext -) : BaseApiController -{ - [HttpPut] - [Authorize] - [EnableRateLimiting("ExtraShort")] - [ProducesResponseType(typeof(AccountUpsertResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task UpsertAccountAsync( - [FromBody] UpsertAccountDto request, - CancellationToken cancellationToken - ) - { - string? sub = GetSub(); - - if (string.IsNullOrEmpty(sub)) - { - return Unauthorized(); - } - - var result = await accountAppService.UpsertAccountAsync(sub, request.ImageUrl, cancellationToken); - - return ToActionResult(result); - } - - [HttpGet("find/profile/{username}")] - [EnableRateLimiting("Short")] - [ProducesResponseType(typeof(ProfileAggregateDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetProfileAsync( - string username, - CancellationToken cancellationToken - ) - { - if (string.IsNullOrEmpty(username)) - { - return BadRequest("Username is required"); - } - - var accountResult = await accountAppService.GetProfileAggregateAsync( - username, - cancellationToken - ); - - if (accountResult.IsSuccess) - { - return Ok(accountResult.Value); - } - - string errors = string.Join(", ", accountResult.Errors); - - return BadRequest(errors); - } - - [HttpGet("find/profile")] - [Authorize] - [EnableRateLimiting("Medium")] - [ProducesResponseType(typeof(AccountDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetProfileAsync(CancellationToken cancellationToken) - { - string? sub = GetSub(); - - if (string.IsNullOrEmpty(sub)) - { - return Unauthorized(); - } - - var result = await accountAppService.GetAccountBySubAsync(sub, cancellationToken); - - if (!result.IsSuccess) - { - return ToActionResult(result); - } - - IEnumerable permissions = - [ - .. User.Claims.Where(c => c.Type == "permissions").Select(c => c.Value), - ]; - - return Ok(result.Value with { Permissions = permissions }); - } - - [HttpGet("settings")] - [Authorize] - [RequiresAccount] - [EnableRateLimiting("Medium")] - [ProducesResponseType(typeof(ProfileSettingsDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetSettingsAsync(CancellationToken cancellationToken) - { - string? sub = GetSub(); - - if (string.IsNullOrEmpty(sub)) - { - return Unauthorized(); - } - - var result = await accountAppService.GetProfileSettingsAsync(sub, cancellationToken); - - return ToActionResult(result); - } - - [HttpPut("settings")] - [Authorize] - [RequiresAccount] - [EnableRateLimiting("ExtraShort")] - [ProducesResponseType(typeof(UpdateProfileSettingsResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task UpdateProfileSettingsAsync( - [FromBody] UpdateProfileSettingsDto request, - CancellationToken cancellationToken - ) - { - if (accountContext.Account is null || accountContext.Account.Id is null) - { - return Unauthorized(); - } - - var result = await accountAppService.UpdateProfileSettingsAsync( - accountContext.Account.Id.Value, - request.Bio, - cancellationToken - ); - - return ToActionResult(result); - } - - [HttpPut("username")] - [Authorize] - [EnableRateLimiting("ExtraShort")] - [ProducesResponseType(typeof(UpdateUsernameResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task UpdateUsernameAsync( - [FromBody] UpdateUsernameDto request, - CancellationToken cancellationToken - ) - { - string? sub = GetSub(); - - if (string.IsNullOrEmpty(sub)) - { - return Unauthorized(); - } - - var accountResult = await accountAppService.GetAccountBySubAsync(sub, cancellationToken); - - if (!accountResult.IsSuccess) - { - // Account doesn't exist yet — upsert to ensure it's created before updating username. - // This handles the race condition where the client submits before AccountInitializer completes. - var upsertResult = await accountAppService.UpsertAccountAsync(sub, null, cancellationToken); - - if (!upsertResult.IsSuccess) - { - return ToActionResult(upsertResult); - } - - accountResult = await accountAppService.GetAccountBySubAsync(sub, cancellationToken); - - if (!accountResult.IsSuccess) - { - return ToActionResult(accountResult); - } - } - - var account = accountResult.Value; - - if (account.Id is null) - { - return Unauthorized(); - } - - var result = await accountAppService.UpdateUsernameAsync( - account.Id.Value, - request.Username, - account.UsernameLastChangedAt, - cancellationToken - ); - - return ToActionResult(result); - } -} \ No newline at end of file diff --git a/src/PublicApi/Controllers/BaseApiController.cs b/src/PublicApi/Controllers/BaseApiController.cs deleted file mode 100644 index 52274c4..0000000 --- a/src/PublicApi/Controllers/BaseApiController.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Ardalis.Result; -using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; - -namespace PublicApi.Controllers; - -[ApiController] -public abstract class BaseApiController : ControllerBase -{ - protected string? GetSub() => User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - - protected IActionResult ToActionResult(Result result) - { - return result.Status switch - { - ResultStatus.Ok => Ok(result.Value), - ResultStatus.NotFound => NotFound( - new { Message = "Resource not found", result.Errors } - ), - ResultStatus.Unauthorized => Unauthorized( - new { Message = "Unauthorized", result.Errors } - ), - ResultStatus.Forbidden => Forbid(), - ResultStatus.Invalid => BadRequest( - new - { - Message = result.ValidationErrors is not null - ? string.Join( - ", ", - result.ValidationErrors.Select(e => - string.IsNullOrWhiteSpace(e.Identifier) - ? e.ErrorMessage - : $"{e.Identifier}: {e.ErrorMessage}" - ) - ) - : "Invalid request.", - Errors = result.ValidationErrors?.Select(e => new - { - Field = e.Identifier, - Error = e.ErrorMessage, - }), - } - ), - _ => StatusCode( - 500, - new { Message = "An unexpected error occurred.", Errors = result.Errors } - ), - }; - } -} \ No newline at end of file diff --git a/src/PublicApi/Controllers/ProblemController.cs b/src/PublicApi/Controllers/ProblemController.cs deleted file mode 100644 index 3678c2c..0000000 --- a/src/PublicApi/Controllers/ProblemController.cs +++ /dev/null @@ -1,173 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Dtos.Languages; -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Dtos.Submissions; -using ApplicationCore.Interfaces.Services; -using Asp.Versioning; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; -using PublicApi.Attributes; - -namespace PublicApi.Controllers; - -[ApiController] -[Route("api/v{version:apiVersion}/[controller]")] -[ApiVersion("1.0")] -public sealed class ProblemController( - IProblemAppService problemAppService, - ISubmissionAppService submissionAppService, - IAccountContext accountContext -) : BaseApiController -{ - [HttpGet("slug/{slug}")] - [EnableRateLimiting("Short")] - [ProducesResponseType(typeof(ProblemDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetBySlugAsync( - string slug, - CancellationToken cancellationToken - ) - { - if (string.IsNullOrWhiteSpace(slug)) - { - return BadRequest("Slug is required."); - } - - var problemResult = await problemAppService.GetProblemBySlugAsync(slug, cancellationToken); - - if (problemResult.IsSuccess) - { - return Ok(problemResult.Value); - } - - string errors = string.Join(", ", problemResult.Errors); - - return BadRequest(errors); - } - - [HttpGet("languages")] - [EnableRateLimiting("Short")] - [Authorize(Policy = "read:languages")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task GetAvailableLanguagesAsync(CancellationToken cancellationToken) - { - return ToActionResult( - await problemAppService.GetAvailableLanguagesAsync(cancellationToken) - ); - } - - [HttpGet] - [EnableRateLimiting("Medium")] - [ProducesResponseType(typeof(PaginatedResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task GetPageableAsync( - [FromQuery] DateTime timestamp, - [FromQuery] int page = 1, - [FromQuery] int size = 25, - CancellationToken cancellationToken = default - ) - { - if (page < 1 || size < 1) - { - return BadRequest("Page and size must be greater than 0."); - } - - return ToActionResult( - await problemAppService.GetProblemsPaginatedAsync( - page, - size, - timestamp, - cancellationToken - ) - ); - } - - [HttpGet("{problemId:guid}/setup")] - [EnableRateLimiting("ExtraShort")] - [ProducesResponseType(typeof(ProblemSetupDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetProblemSetupAsync( - Guid problemId, - [FromQuery] int languageVersionId, - CancellationToken cancellationToken - ) - { - return ToActionResult( - await problemAppService.GetProblemSetupAsync( - problemId, - languageVersionId, - cancellationToken - ) - ); - } - - [HttpGet("{problemId:guid}/solutions")] - [Authorize] - [EnableRateLimiting("Short")] - [ProducesResponseType(typeof(PaginatedResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task GetSolutionsAsync( - Guid problemId, - [FromQuery] int page = 1, - [FromQuery] int size = 25, - [FromQuery] DateTime? timestamp = null, - CancellationToken cancellationToken = default) - { - if (page < 1 || size < 1) - { - return BadRequest("Page and size must be greater than 0."); - } - - return ToActionResult( - await submissionAppService.GetSolutionsAsync(problemId, new PaginationRequest - { - Page = page, - Size = size, - Timestamp = timestamp ?? DateTime.UtcNow, - }, cancellationToken) - ); - } - - [HttpGet("{problemId:guid}/submissions")] - [EnableRateLimiting("Short")] - [Authorize] - [RequiresAccount] - [ProducesResponseType(typeof(PaginatedResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task GetMySubmissionsAsync( - Guid problemId, - [FromQuery] int page = 1, - [FromQuery] int size = 25, - [FromQuery] DateTime? timestamp = null, - CancellationToken cancellationToken = default) - { - if (page < 1 || size < 1) - { - return BadRequest("Page and size must be greater than 0."); - } - - Guid? accountId = accountContext.Account?.Id; - - if (!accountId.HasValue) - { - return Unauthorized("User must be authenticated to view their submissions."); - } - - return ToActionResult( - await submissionAppService.GetSubmissionsPaginatedAsync(problemId, accountId.Value, new PaginationRequest - { - Page = page, - Size = size, - Timestamp = timestamp ?? DateTime.UtcNow, - }, cancellationToken) - ); - } -} \ No newline at end of file diff --git a/src/PublicApi/Controllers/SubmissionController.cs b/src/PublicApi/Controllers/SubmissionController.cs deleted file mode 100644 index a23919d..0000000 --- a/src/PublicApi/Controllers/SubmissionController.cs +++ /dev/null @@ -1,63 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Dtos.Submissions; -using ApplicationCore.Interfaces.Services; -using Asp.Versioning; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; -using PublicApi.Attributes; -using PublicApi.Contracts.Submission; - -namespace PublicApi.Controllers; - -[ApiController] -[Route("api/v{version:apiVersion}/[controller]")] -[ApiVersion("1.0")] -public sealed class SubmissionController( - IAccountContext accountContext, - ISubmissionAppService submissionAppService -) : BaseApiController -{ - [HttpPost("execute")] - [Authorize] - [RequiresAccount] - [ProducesResponseType(typeof(Guid), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task CreateSubmissionAsync( - [FromBody] CreateSubmissionDto createSubmissionDto, - CancellationToken cancellationToken - ) - { - if (accountContext.Account is null) - { - return Unauthorized(); - } - - return ToActionResult( - await submissionAppService.CreateAsync( - createSubmissionDto.ProblemSetupId, - createSubmissionDto.Code, - (Guid)accountContext.Account.Id, - cancellationToken - ) - ); - } - - [HttpGet("{submissionId:guid}")] - [Authorize] - [RequiresAccount] - [ProducesResponseType(typeof(SubmissionStatusDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetSubmissionStatusAsync( - Guid submissionId, - CancellationToken cancellationToken - ) - { - return ToActionResult( - await submissionAppService.GetSubmissionStatusAsync(submissionId, cancellationToken) - ); - } -} \ No newline at end of file diff --git a/src/PublicApi/Extensions/AuthenticationExtensions.cs b/src/PublicApi/Extensions/AuthenticationExtensions.cs deleted file mode 100644 index cd410a5..0000000 --- a/src/PublicApi/Extensions/AuthenticationExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; - -namespace PublicApi.Extensions; - -public static class AuthenticationExtensions -{ - public static IServiceCollection AddAuthTokenValidation( - this IServiceCollection services, - IConfiguration configuration - ) - { - services - .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.Authority = $"https://{configuration["Auth0:Domain"]}/"; - options.Audience = configuration["Auth0:Audience"]; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateAudience = true, - ValidateIssuerSigningKey = true, - }; - }); - - return services; - } -} \ No newline at end of file diff --git a/src/PublicApi/Extensions/AuthorizationExtensions.cs b/src/PublicApi/Extensions/AuthorizationExtensions.cs deleted file mode 100644 index 97b7bf7..0000000 --- a/src/PublicApi/Extensions/AuthorizationExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using PublicApi.Authorization; - -namespace PublicApi.Extensions; - -public static class AuthorizationExtensions -{ - private static readonly string[] permissions = - [ - "create:problems", - "read:admin-problems", - "read:admin-problem", - "read:languages", - ]; - - public static IServiceCollection AddRbacAuthorization(this IServiceCollection services) - { - services.AddAuthorization(options => - { - foreach (string? permission in permissions) - { - options.AddPolicy( - permission, - policy => policy.Requirements.Add(new RbacRequirement(permission)) - ); - } - }); - - services.AddSingleton(); - - return services; - } -} \ No newline at end of file diff --git a/src/PublicApi/Extensions/MiddlewareExtensions.cs b/src/PublicApi/Extensions/MiddlewareExtensions.cs deleted file mode 100644 index 7c1eae3..0000000 --- a/src/PublicApi/Extensions/MiddlewareExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using PublicApi.Middleware; - -namespace PublicApi.Extensions; - -public static class MiddlewareExtensions -{ - public static IApplicationBuilder UseAccountContext(this IApplicationBuilder app) - { - return app.UseMiddleware(); - } -} \ No newline at end of file diff --git a/src/PublicApi/Extensions/RateLimitRegistrationExtensions.cs b/src/PublicApi/Extensions/RateLimitRegistrationExtensions.cs deleted file mode 100644 index 87634d7..0000000 --- a/src/PublicApi/Extensions/RateLimitRegistrationExtensions.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; -using Microsoft.Extensions.DependencyInjection; -using PublicApi.Attributes; -using System; -using System.Linq; -using System.Reflection; -using System.Threading.RateLimiting; - -namespace PublicApi; - -public static class RateLimitRegistrationExtensions -{ - public static IServiceCollection RegisterAllUserAndGlobalRateLimitPolicies( - this IServiceCollection services, - Assembly controllersAssembly - ) - { - var userLimits = new HashSet<(int, int)>(); - var globalLimits = new HashSet<(int, int)>(); - - foreach ( - var type in controllersAssembly - .GetTypes() - .Where(t => - t.IsClass && !t.IsAbstract && typeof(ControllerBase).IsAssignableFrom(t) - ) - ) - { - foreach (var attr in type.GetCustomAttributes(true)) - { - userLimits.Add((attr.Count, attr.Seconds)); - } - - foreach (var attr in type.GetCustomAttributes(true)) - { - globalLimits.Add((attr.Count, attr.Seconds)); - } - - foreach ( - var method in type.GetMethods( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly - ) - ) - { - foreach (var attr in method.GetCustomAttributes(true)) - { - userLimits.Add((attr.Count, attr.Seconds)); - } - - foreach (var attr in method.GetCustomAttributes(true)) - { - globalLimits.Add((attr.Count, attr.Seconds)); - } - } - } - - services.AddRateLimiter(options => - { - foreach (var (count, seconds) in userLimits) - { - AddUserRateLimitPolicy(options, count, seconds); - } - - foreach (var (count, seconds) in globalLimits) - { - AddGlobalRateLimitPolicy(options, count, seconds); - } - }); - - return services; - } - - public static void AddUserRateLimitPolicy(RateLimiterOptions options, int count, int seconds) - { - string policyName = $"User_{count}:{seconds}"; - - options.AddPolicy( - policyName, - context => - { - string userId = - context.User.FindFirst("sub")?.Value ?? context - .User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier) - ?.Value - ?? context.Connection.RemoteIpAddress?.ToString() - ?? "anonymous"; - - return RateLimitPartition.GetFixedWindowLimiter( - partitionKey: userId, - factory: _ => new FixedWindowRateLimiterOptions - { - PermitLimit = count, - Window = TimeSpan.FromSeconds(seconds), - QueueLimit = 0, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst, - } - ); - } - ); - } - - public static void AddGlobalRateLimitPolicy(RateLimiterOptions options, int count, int seconds) - { - string policyName = $"Global_{count}:{seconds}"; - - options.AddPolicy( - policyName, - context => - RateLimitPartition.GetFixedWindowLimiter( - partitionKey: "global", - factory: _ => new FixedWindowRateLimiterOptions - { - PermitLimit = count, - Window = TimeSpan.FromSeconds(seconds), - QueueLimit = 0, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst, - } - ) - ); - } -} \ No newline at end of file diff --git a/src/PublicApi/Extensions/SettingsRegistrationExtensions.cs b/src/PublicApi/Extensions/SettingsRegistrationExtensions.cs deleted file mode 100644 index db67c03..0000000 --- a/src/PublicApi/Extensions/SettingsRegistrationExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using ApplicationCore.Settings; - -namespace PublicApi.Extensions; - -public static class SettingsRegistrationExtensions -{ - public static void RegisterAppSettings( - this IServiceCollection services, - IConfiguration configuration - ) - { - RegisterSetting(services, configuration); - RegisterSetting(services, configuration); - } - - private static void RegisterSetting( - IServiceCollection services, - IConfiguration configuration - ) - where T : class, ISettings - { - services.Configure(configuration.GetSection(T.SectionKey)); - } -} \ No newline at end of file diff --git a/src/PublicApi/Filters/WrapResponseAttribute.cs b/src/PublicApi/Filters/WrapResponseAttribute.cs deleted file mode 100644 index 86eaf01..0000000 --- a/src/PublicApi/Filters/WrapResponseAttribute.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace PublicApi.Filters; - -public class WrapResponseAttribute : ActionFilterAttribute -{ - public override void OnResultExecuting(ResultExecutingContext context) - { - if (context.Result is ObjectResult objectResult && objectResult.Value is not null) - { - if (objectResult.StatusCode >= 200 && objectResult.StatusCode < 300) - { - if (!IsAlreadyWrapped(objectResult.Value)) - { - context.Result = new JsonResult(new { data = objectResult.Value }) - { - StatusCode = objectResult.StatusCode, - }; - } - } - } - - base.OnResultExecuting(context); - } - - private static bool IsAlreadyWrapped(object value) - { - var type = value.GetType(); - return type.GetProperty("data") != null; - } -} \ No newline at end of file diff --git a/src/PublicApi/Middleware/AccountContextMiddleware.cs b/src/PublicApi/Middleware/AccountContextMiddleware.cs deleted file mode 100644 index 3a3c36f..0000000 --- a/src/PublicApi/Middleware/AccountContextMiddleware.cs +++ /dev/null @@ -1,71 +0,0 @@ -using ApplicationCore.Logging; -using Microsoft.ApplicationInsights.DataContracts; -using PublicApi.Attributes; - -namespace PublicApi.Middleware; - -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Services; -using Microsoft.AspNetCore.Http; -using System.Security.Claims; -using System.Threading.Tasks; - -public partial class AccountContextMiddleware( - IAccountAppService accountAppService, - IAccountContext accountContext, - ILogger logger -) : IMiddleware -{ - private readonly ILogger _logger = logger; - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - var endpoint = context.GetEndpoint(); - if (endpoint?.Metadata.GetMetadata() == null) - { - await next(context); - return; - } - - string? sub = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(sub)) - { - LogMissingSub(context.Request.Path); - await next(context); - return; - } - - var result = await accountAppService.GetAccountBySubAsync(sub, context.RequestAborted); - if (result.IsSuccess) - { - accountContext.Account = result.Value; - - var requestTelemetry = context.Features.Get(); - if (requestTelemetry is not null && accountContext.Account is not null) - { - var account = accountContext.Account; - requestTelemetry.Properties.TryAdd("account.id", account.Id?.ToString() ?? string.Empty); - requestTelemetry.Properties.TryAdd("account.username", account.Username); - requestTelemetry.Properties.TryAdd("account.permissions", string.Join(",", account.Permissions)); - requestTelemetry.Properties.TryAdd("account.createdOn", account.CreatedOn.ToString("O")); - } - } - else - { - LogResolveFailed(sub, context.Request.Path, string.Join(", ", result.Errors)); - } - - await next(context); - } - - [LoggerMessage( - EventId = LoggingEventIds.Accounts.ContextMissingSub, - Level = LogLevel.Warning, - Message = "Account context: missing sub claim on [RequiresAccount] endpoint {path}")] - private partial void LogMissingSub(string path); - - [LoggerMessage( - EventId = LoggingEventIds.Accounts.ContextResolveFailed, - Level = LogLevel.Warning, - Message = "Account context: failed to resolve account for sub {sub} on {path}: {errors}")] - private partial void LogResolveFailed(string sub, string path, string errors); -} \ No newline at end of file diff --git a/src/PublicApi/Middleware/ApplicationBuilderExtensions.cs b/src/PublicApi/Middleware/ApplicationBuilderExtensions.cs deleted file mode 100644 index c986885..0000000 --- a/src/PublicApi/Middleware/ApplicationBuilderExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace PublicApi.Middleware; - -public static class ApplicationBuilderExtensions -{ - public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder app) - { - app.UseXContentTypeOptions(); - app.UseReferrerPolicy(opts => opts.NoReferrer()); - app.UseXXssProtection(options => options.EnabledWithBlockMode()); - app.UseXfo(options => options.Deny()); - app.UseCsp(options => - options.DefaultSources(s => s.Self()).StyleSources(s => s.Self().UnsafeInline()) - ); - - return app; - } -} \ No newline at end of file diff --git a/src/PublicApi/Middleware/ExceptionMiddlewareExtensions.cs b/src/PublicApi/Middleware/ExceptionMiddlewareExtensions.cs deleted file mode 100644 index 0049a9e..0000000 --- a/src/PublicApi/Middleware/ExceptionMiddlewareExtensions.cs +++ /dev/null @@ -1,87 +0,0 @@ -using ApplicationCore.Logging; -using Microsoft.ApplicationInsights; -using Microsoft.AspNetCore.Diagnostics; -using System.Net; -using System.Text.Json; - -namespace PublicApi.Middleware; - -public static class ExceptionMiddlewareExtensions -{ - public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app) - { - app.UseExceptionHandler(builder => - { - builder.Run(async context => - { - var logger = context - .RequestServices.GetRequiredService() - .CreateLogger("GlobalExceptionHandler"); - - var telemetryClient = context.RequestServices.GetService(); - - var feature = context.Features.Get(); - var ex = feature?.Error; - - context.Response.ContentType = "application/json"; - - if (ex is FluentValidation.ValidationException) - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - - AppLog.UnhandledExceptionWithPath( - logger, - context.Request.Method, - context.Request.Path, - ex - ); - telemetryClient?.TrackException(ex); - - await context.Response.WriteAsync( - JsonSerializer.Serialize(new { message = "Validation failed" }) - ); - return; - } - - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - - var exceptionToLog = ex ?? new Exception("Unknown error"); - AppLog.UnhandledExceptionWithPath( - logger, - context.Request.Method, - context.Request.Path, - exceptionToLog - ); - telemetryClient?.TrackException(exceptionToLog); - - await context.Response.WriteAsync( - JsonSerializer.Serialize(new { message = "An unexpected error occurred." }) - ); - }); - }); - - return app; - } -} - -public static partial class AppLog -{ - [LoggerMessage( - EventId = LoggingEventIds.Exceptions.UnhandledException, - Level = LogLevel.Error, - Message = "Unhandled exception." - )] - public static partial void UnhandledException(ILogger logger, Exception exception); - - [LoggerMessage( - EventId = LoggingEventIds.Exceptions.UnhandledExceptionWithPath, - Level = LogLevel.Error, - Message = "Unhandled exception for {Method} {Path}." - )] - public static partial void UnhandledExceptionWithPath( - ILogger logger, - string method, - string path, - Exception exception - ); -} \ No newline at end of file diff --git a/src/PublicApi/Program.cs b/src/PublicApi/Program.cs deleted file mode 100644 index 8952b7d..0000000 --- a/src/PublicApi/Program.cs +++ /dev/null @@ -1,78 +0,0 @@ -using ApplicationCore; -using ApplicationCore.Domain.Accounts; -using Asp.Versioning; -using Infrastructure; -using PublicApi; -using PublicApi.Extensions; -using PublicApi.Middleware; -using Scalar.AspNetCore; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.RegisterAppSettings(builder.Configuration); - -builder.Services.AddApplicationCore(); -builder.Services.AddInfrastructure(builder.Configuration); - -builder.Services.AddControllers(); - -builder.Services.RegisterAllUserAndGlobalRateLimitPolicies(typeof(Program).Assembly); - -builder.Services.AddAuthTokenValidation(builder.Configuration); -builder.Services.AddRbacAuthorization(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddApiVersioning(o => -{ - o.DefaultApiVersion = new ApiVersion(1, 0); - o.AssumeDefaultVersionWhenUnspecified = true; - o.ReportApiVersions = true; -}); - -builder.Services.AddMediatR(cfg => -{ - cfg.LicenseKey = builder.Configuration.GetSection("MediatRSettings:LicenseKey").Get(); - cfg.RegisterServicesFromAssembly(typeof(Program).Assembly); -}); - -builder.Services.AddOpenApi(); - -if (!builder.Environment.IsDevelopment()) -{ - builder.Services.AddApplicationInsightsTelemetry(builder.Configuration); -} - -string[] allowedOrigins = - builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; - -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy(policy => - policy.WithOrigins(allowedOrigins).AllowAnyHeader().AllowAnyMethod().AllowCredentials() - ); -}); - -var app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); - app.MapScalarApiReference(options => - { - options.WithOpenApiRoutePattern("/openapi/{documentName}.json"); - }); -} - -if (!app.Environment.IsDevelopment()) -{ - app.UseHttpsRedirection(); -} -app.UseCors(); -app.UseGlobalExceptionHandler(); -app.UseAuthentication(); -app.UseAuthorization(); -app.UseAccountContext(); -app.MapControllers(); - -app.Run(); \ No newline at end of file diff --git a/src/PublicApi/PublicApi.csproj b/src/PublicApi/PublicApi.csproj deleted file mode 100644 index 1bc4cb0..0000000 --- a/src/PublicApi/PublicApi.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - net10.0 - enable - enable - fc934385-92ad-4500-8c5d-dfae2e8ecc14 - - - - - - - - - - - - - - - - - diff --git a/src/PublicApi/PublicApi.http b/src/PublicApi/PublicApi.http deleted file mode 100644 index e092f04..0000000 --- a/src/PublicApi/PublicApi.http +++ /dev/null @@ -1,6 +0,0 @@ -@PublicApi_HostAddress = http://localhost:5198 - -GET {{PublicApi_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/src/PublicApi/ServiceCollectionExtensions.cs b/src/PublicApi/ServiceCollectionExtensions.cs deleted file mode 100644 index 973c726..0000000 --- a/src/PublicApi/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Asp.Versioning; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.RateLimiting; -using Microsoft.IdentityModel.Tokens; -using PublicApi.Authorization; -using PublicApi.Filters; -using System.Threading.RateLimiting; - -namespace PublicApi; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddApiServices( - this IServiceCollection services, - IConfiguration configuration - ) - { - services.AddOpenApi(); - services.AddControllers(options => options.Filters.Add()); - services.AddApiVersioning(o => - { - o.DefaultApiVersion = new ApiVersion(1, 0); - o.AssumeDefaultVersionWhenUnspecified = true; - o.ReportApiVersions = true; - }); - - return services; - } -} \ No newline at end of file diff --git a/src/PublicApi/appsettings.Development.json b/src/PublicApi/appsettings.Development.json deleted file mode 100644 index 5fa4b26..0000000 --- a/src/PublicApi/appsettings.Development.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "Auth0": { - "Audience": "", - "Domain": "", - "Management": { - "ClientId": "" - } - }, - "Cors": { - "AllowedOrigins": [ - "http://localhost:3000" - ] - }, - "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=algowars;Username=myuser;Password=mypassword" - }, - "ExecutionEngines": { - "Judge0": { - "Enabled": true, - "RunWorker": true, - "BaseUrl": "", - "ApiKey": "", - "Host": "", - "ShouldWait": false, - "IsEncoded": true, - "DefaultTimeoutInSeconds": 10 - } - }, - "MessageBus": { - "Transport": "RabbitMQ", - "RabbitMQ": { - "Host": "localhost", - "VirtualHost": "/", - "Username": "guest", - "Password": "guest" - } - } -} diff --git a/src/PublicApi/appsettings.json b/src/PublicApi/appsettings.json deleted file mode 100644 index 467112f..0000000 --- a/src/PublicApi/appsettings.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - }, - "ApplicationInsights": { - "LogLevel": { - "Default": "Warning" - } - } - }, - "AllowedHosts": "*", - "MessageBus": { - "Transport": "RabbitMQ", - "RabbitMQ": { - "Host": "localhost", - "VirtualHost": "/", - "Username": "guest", - "Password": "guest" - }, - "AzureServiceBus": { - "ConnectionString": "" - } - } -} diff --git a/tests/UnitTests/ApplicationCore/Commands/Accounts/CreateAccountHandlerTests.cs b/tests/UnitTests/ApplicationCore/Commands/Accounts/CreateAccountHandlerTests.cs deleted file mode 100644 index 9adb929..0000000 --- a/tests/UnitTests/ApplicationCore/Commands/Accounts/CreateAccountHandlerTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -using ApplicationCore.Commands.Accounts.CreateAccount; -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Repositories; -using FluentValidation; -using Microsoft.Extensions.Logging; -using Moq; - -namespace UnitTests.ApplicationCore.Commands.Accounts; - -[TestFixture] -public sealed class CreateAccountHandlerTests -{ - private Mock _accounts; - private Mock> _logger; - private Mock> _validator; - - private CreateAccountHandler _handler; - - [SetUp] - public void SetUp() - { - _accounts = new(); - _logger = new(); - _validator = new(); - - _validator - .Setup(v => - v.ValidateAsync(It.IsAny(), It.IsAny()) - ) - .ReturnsAsync(new FluentValidation.Results.ValidationResult()); - - _handler = new CreateAccountHandler(_accounts.Object, _logger.Object, _validator.Object); - } - - [Test] - public async Task Handle_creates_account_successfully() - { - _accounts - .Setup(a => a.AddAsync(It.IsAny(), It.IsAny())) - .Returns(Task.CompletedTask); - - var command = new CreateAccountCommand("user1", "sub1", "http://image.url"); - - var result = await _handler.Handle(command, CancellationToken.None); - - using (Assert.EnterMultipleScope()) - { - Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value, Is.Not.EqualTo(Guid.Empty)); - _accounts.Verify( - a => - a.AddAsync( - It.Is(acc => acc.Username == "user1" && acc.Sub == "sub1"), - It.IsAny() - ), - Times.Once - ); - } - } - - [Test] - public async Task Handle_returns_error_when_exception_occurs() - { - _accounts - .Setup(a => a.AddAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception("DB failure")); - - var command = new CreateAccountCommand("user2", "sub2", ""); - - var result = await _handler.Handle(command, CancellationToken.None); - - using (Assert.EnterMultipleScope()) - { - Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Errors, Has.Some.EqualTo("Unexpected error creating account.")); - } - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Commands/Accounts/CreateAccountValidatorTests.cs b/tests/UnitTests/ApplicationCore/Commands/Accounts/CreateAccountValidatorTests.cs deleted file mode 100644 index 4afc04f..0000000 --- a/tests/UnitTests/ApplicationCore/Commands/Accounts/CreateAccountValidatorTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using ApplicationCore.Commands.Accounts.CreateAccount; -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Repositories; -using Moq; - -namespace UnitTests.ApplicationCore.Commands.Accounts; - -[TestFixture] -public sealed class CreateAccountValidatorTests -{ - private Mock _accounts = null!; - private CreateAccountValidator _validator = null!; - - [SetUp] - public void SetUp() - { - _accounts = new Mock(); - _validator = new CreateAccountValidator(_accounts.Object); - } - - [Test] - public async Task Validator_passes_for_valid_command() - { - _accounts - .Setup(a => a.GetByUsernameAsync("user1", It.IsAny())) - .ReturnsAsync((AccountModel?)null); - _accounts - .Setup(a => a.GetBySubAsync("sub1", It.IsAny())) - .ReturnsAsync((AccountModel?)null); - _accounts - .Setup(a => a.GetByUsernameOrSubAsync("user1", "sub1", It.IsAny())) - .ReturnsAsync((AccountModel?)null); - - var command = new CreateAccountCommand("user1", "sub1", "http://example.com"); - var result = await _validator.ValidateAsync(command); - - Assert.That(result.IsValid, Is.True); - } - - [Test] - public async Task Validator_fails_if_username_is_invalid() - { - var command = new CreateAccountCommand("user$invalid", "sub1", ""); - var result = await _validator.ValidateAsync(command); - - Assert.Multiple(() => - { - Assert.That(result.IsValid, Is.False); - Assert.That( - result.Errors, - Has.Some.Property("ErrorMessage").Contains("Username contains invalid characters") - ); - }); - } - - [Test] - public async Task Validator_fails_if_username_already_exists() - { - _accounts - .Setup(a => a.GetByUsernameAsync("user1", It.IsAny())) - .ReturnsAsync(new AccountModel() { Username = "test" }); - - var command = new CreateAccountCommand("user1", "sub1", ""); - var result = await _validator.ValidateAsync(command); - - Assert.Multiple(() => - { - Assert.That(result.IsValid, Is.False); - Assert.That( - result.Errors, - Has.Some.Property("ErrorMessage").Contains("Username already exists") - ); - }); - } - - [Test] - public async Task Validator_fails_if_sub_already_exists() - { - _accounts - .Setup(a => a.GetBySubAsync("sub1", It.IsAny())) - .ReturnsAsync(new AccountModel() { Username = "test" }); - - var command = new CreateAccountCommand("user1", "sub1", ""); - var result = await _validator.ValidateAsync(command); - - Assert.Multiple(() => - { - Assert.That(result.IsValid, Is.False); - Assert.That( - result.Errors, - Has.Some.Property("ErrorMessage").Contains("Account already exists") - ); - }); - } - - [Test] - public async Task Validator_fails_if_username_or_sub_exists() - { - _accounts - .Setup(a => a.GetByUsernameOrSubAsync("user1", "sub1", It.IsAny())) - .ReturnsAsync(new AccountModel() { Username = "test" }); - - var command = new CreateAccountCommand("user1", "sub1", ""); - var result = await _validator.ValidateAsync(command); - - Assert.Multiple(() => - { - Assert.That(result.IsValid, Is.False); - Assert.That( - result.Errors, - Has.Some.Property("ErrorMessage").Contains("Username already exists") - ); - }); - } - - [Test] - public async Task Validator_fails_if_imageUrl_is_invalid() - { - var command = new CreateAccountCommand("user1", "sub1", "ftp://invalid.url"); - var result = await _validator.ValidateAsync(command); - - Assert.Multiple(() => - { - Assert.That(result.IsValid, Is.False); - Assert.That( - result.Errors, - Has.Some.Property("ErrorMessage").Contains("ImageUrl must be a valid URL") - ); - }); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Commands/Submissions/CreateSubmissionHandlerTests.cs b/tests/UnitTests/ApplicationCore/Commands/Submissions/CreateSubmissionHandlerTests.cs deleted file mode 100644 index 5cc6ff3..0000000 --- a/tests/UnitTests/ApplicationCore/Commands/Submissions/CreateSubmissionHandlerTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using ApplicationCore.Commands.Submissions.CreateSubmission; -using ApplicationCore.Domain.Submissions; -using ApplicationCore.Interfaces.Messaging; -using ApplicationCore.Interfaces.Repositories; -using FluentValidation; -using Microsoft.Extensions.Logging; -using Moq; - -namespace UnitTests.ApplicationCore.Commands.Submissions; - -[TestFixture] -public sealed class CreateSubmissionHandlerTests -{ - private Mock _mockSubmissionRepository; - private Mock _mockMessagePublisher; - private Mock> _mockValidator; - private Mock> _mockLogger; - - private CreateSubmissionHandler _sut; - - [SetUp] - public void SetUp() - { - _mockSubmissionRepository = new(); - _mockMessagePublisher = new(); - _mockValidator = new(); - _mockLogger = new(); - - _mockValidator - .Setup(v => - v.ValidateAsync(It.IsAny(), It.IsAny()) - ) - .ReturnsAsync(new FluentValidation.Results.ValidationResult()); - - _sut = new CreateSubmissionHandler( - _mockSubmissionRepository.Object, - _mockMessagePublisher.Object, - _mockValidator.Object, - _mockLogger.Object - ); - } - - [Test] - public async Task Handle_creates_submission_successfully() - { - _mockSubmissionRepository - .Setup(a => a.SaveAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(Guid.NewGuid()); - - var command = new CreateSubmissionCommand(1, "code", Guid.NewGuid()); - - var result = await _sut.Handle(command, CancellationToken.None); - - using (Assert.EnterMultipleScope()) - { - Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value, Is.Not.EqualTo(Guid.Empty)); - - _mockSubmissionRepository.Verify( - a => - a.SaveAsync( - It.Is(s => - s.ProblemSetupId == command.ProblemSetupId - && s.Code == command.Code - && s.CreatedById == command.CreatedById - ), - It.IsAny() - ), - Times.Once - ); - } - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Commands/Submissions/CreateSubmissionValidatorTests.cs b/tests/UnitTests/ApplicationCore/Commands/Submissions/CreateSubmissionValidatorTests.cs deleted file mode 100644 index 037db33..0000000 --- a/tests/UnitTests/ApplicationCore/Commands/Submissions/CreateSubmissionValidatorTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ApplicationCore.Commands.Submissions.CreateSubmission; -using Moq; - -namespace UnitTests.ApplicationCore.Commands.Submissions; - -[TestFixture] -public sealed class CreateSubmissionValidatorTests -{ - private CreateSubmissionValidator _sut; - - [SetUp] - public void SetUp() - { - _sut = new CreateSubmissionValidator(); - } - - [Test] - public void Should_Pass_For_Valid_Command() - { - var command = new CreateSubmissionCommand(1, "print('Hello, World!')", Guid.NewGuid()); - var result = _sut.Validate(command); - Assert.That(result.IsValid, Is.True); - } - - [Test] - public void Code_Should_Fail_If_Empty() - { - var command = new CreateSubmissionCommand(1, "", Guid.NewGuid()); - var result = _sut.Validate(command); - - Assert.That(result.IsValid, Is.False); - } - - [Test] - public void ProblemSetupId_Should_Fail_If_Non_Positive() - { - var command = new CreateSubmissionCommand(0, "print('Hello, World!')", Guid.NewGuid()); - var result = _sut.Validate(command); - Assert.Multiple(() => - { - Assert.That(result.IsValid, Is.False); - }); - } - - [Test] - public void CreatedById_Should_Fail_If_Empty() - { - var command = new CreateSubmissionCommand(1, "print('Hello, World!')", Guid.Empty); - var result = _sut.Validate(command); - Assert.Multiple(() => - { - Assert.That(result.IsValid, Is.False); - }); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxesHandlerTests.cs.cs b/tests/UnitTests/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxesHandlerTests.cs.cs deleted file mode 100644 index a7fcd57..0000000 --- a/tests/UnitTests/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxesHandlerTests.cs.cs +++ /dev/null @@ -1,92 +0,0 @@ -using ApplicationCore.Commands.Submissions.IncrementSubmissionOutboxes; -using ApplicationCore.Interfaces.Repositories; -using Ardalis.Result; -using FluentValidation; -using FluentValidation.Results; -using Moq; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace UnitTests.ApplicationCore.Commands.Submissions; - -[TestFixture] -internal class IncrementSubmissionOutboxesHandlerTests -{ - private IncrementSubmissionOutboxesHandler _sut; - private Mock _mockSubmissionRepository; - private Mock> _mockValidator; - - [SetUp] - public void SetUp() - { - _mockSubmissionRepository = new(); - - _mockValidator = new(); - _mockValidator - .Setup(v => - v.ValidateAsync( - It.IsAny(), - It.IsAny() - ) - ) - .ReturnsAsync(new ValidationResult()); - - _sut = new IncrementSubmissionOutboxesHandler( - _mockSubmissionRepository.Object, - _mockValidator.Object - ); - } - - [Test] - public async Task Handle_ShouldIncrementOutboxesCountSuccessfully() - { - _mockSubmissionRepository - .Setup(r => - r.IncrementOutboxesCountAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny() - ) - ) - .Returns(Task.CompletedTask); - - var command = new IncrementSubmissionOutboxesCommand( - [Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()], - DateTime.UtcNow - ); - var result = await _sut.Handle(command, CancellationToken.None); - - Assert.That(result.IsSuccess, Is.True); - _mockSubmissionRepository.Verify( - r => - r.IncrementOutboxesCountAsync( - command.OutboxIds, - command.Timestamp, - It.IsAny() - ), - Times.Once - ); - } - - [Test] - public async Task Handle_RepositoryError_ShouldReturnErrorResult() - { - _mockSubmissionRepository - .Setup(r => - r.IncrementOutboxesCountAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny() - ) - ) - .ThrowsAsync(new Exception("Database error")); - var command = new IncrementSubmissionOutboxesCommand( - [Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()], - DateTime.UtcNow - ); - var result = await _sut.Handle(command, CancellationToken.None); - Assert.That(result.IsError, Is.True); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxesValidatorTests.cs b/tests/UnitTests/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxesValidatorTests.cs deleted file mode 100644 index 279e231..0000000 --- a/tests/UnitTests/ApplicationCore/Commands/Submissions/IncrementSubmissionOutboxesValidatorTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using ApplicationCore.Commands.Submissions.IncrementSubmissionOutboxes; -using FluentValidation.Results; -using NUnit.Framework; -using System; -using System.Collections.Generic; - -namespace UnitTests.ApplicationCore.Commands.Submissions; - -[TestFixture] -internal class IncrementSubmissionOutboxesValidatorTests -{ - private IncrementSubmissionOutboxesValidator _validator = null!; - - [SetUp] - public void SetUp() - { - _validator = new IncrementSubmissionOutboxesValidator(); - } - - [Test] - public void Validate_WithValidCommand_ShouldBeValid() - { - var command = new IncrementSubmissionOutboxesCommand( - [Guid.NewGuid()], - DateTime.UtcNow.AddSeconds(-1) - ); - - ValidationResult result = _validator.Validate(command); - - Assert.That(result.IsValid, Is.True); - } - - [Test] - public void Validate_WithEmptyOutboxIds_ShouldBeInvalid() - { - var command = new IncrementSubmissionOutboxesCommand([], DateTime.UtcNow.AddSeconds(-1)); - - ValidationResult result = _validator.Validate(command); - - using (Assert.EnterMultipleScope()) - { - Assert.That(result.IsValid, Is.False); - Assert.That( - result.Errors, - Has.Exactly(1).Matches(e => e.PropertyName == "OutboxIds") - ); - } - } - - [Test] - public void Validate_WithFutureTimestamp_ShouldBeInvalid() - { - var command = new IncrementSubmissionOutboxesCommand( - [Guid.NewGuid()], - DateTime.UtcNow.AddMinutes(5) - ); - - ValidationResult result = _validator.Validate(command); - - using (Assert.EnterMultipleScope()) - { - Assert.That(result.IsValid, Is.False); - Assert.That( - result.Errors, - Has.Exactly(1).Matches(e => e.PropertyName == "Timestamp") - ); - } - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutionsHandlerTests.cs b/tests/UnitTests/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutionsHandlerTests.cs deleted file mode 100644 index 6580566..0000000 --- a/tests/UnitTests/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutionsHandlerTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using ApplicationCore.Commands.Submissions.ProcessSubmissionExecutions; -using ApplicationCore.Interfaces.Repositories; -using FluentValidation; -using FluentValidation.Results; -using Moq; - -namespace UnitTests.ApplicationCore.Commands.Submissions; - -[TestFixture] -internal class ProcessSubmissionExecutionsHandlerTests -{ - private Mock _mockSubmissionRepository = null!; - private Mock> _mockValidator = null!; - - private ProcessSubmissionExecutionsHandler _sut = null!; - - [SetUp] - public void SetUp() - { - _mockSubmissionRepository = new Mock(); - _mockValidator = new Mock>(); - - _mockValidator - .Setup(v => - v.ValidateAsync( - It.IsAny(), - It.IsAny() - ) - ) - .ReturnsAsync(new ValidationResult()); - - _sut = new ProcessSubmissionExecutionsHandler( - _mockSubmissionRepository.Object, - _mockValidator.Object - ); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutionsValidatorTests.cs b/tests/UnitTests/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutionsValidatorTests.cs deleted file mode 100644 index 866483a..0000000 --- a/tests/UnitTests/ApplicationCore/Commands/Submissions/ProcessSubmissionExecutionsValidatorTests.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace UnitTests.ApplicationCore.Commands.Submissions; - -internal class ProcessSubmissionExecutionsValidatorTests -{ -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Common/Pagination/PaginatedResultTests.cs b/tests/UnitTests/ApplicationCore/Common/Pagination/PaginatedResultTests.cs deleted file mode 100644 index 175f75e..0000000 --- a/tests/UnitTests/ApplicationCore/Common/Pagination/PaginatedResultTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -using ApplicationCore.Common.Pagination; - -namespace UnitTests.ApplicationCore.Common.Pagination; - -[TestFixture] -public sealed class PaginatedResultTests -{ - [Test] - public void HasPrevious_is_false_on_first_page() - { - var result = new PaginatedResult - { - Results = [1, 2], - Total = 10, - Page = 1, - Size = 2, - }; - - Assert.That(result.HasPrevious, Is.False); - } - - [Test] - public void HasPrevious_is_true_when_page_greater_than_one() - { - var result = new PaginatedResult - { - Results = [3, 4], - Total = 10, - Page = 2, - Size = 2, - }; - - Assert.That(result.HasPrevious, Is.True); - } - - [Test] - public void HasNext_is_true_when_not_on_last_page() - { - var result = new PaginatedResult - { - Results = [1, 2], - Total = 10, - Page = 1, - Size = 2, - }; - - Assert.That(result.HasNext, Is.True); - } - - [Test] - public void HasNext_is_false_on_last_page() - { - var result = new PaginatedResult - { - Results = [9, 10], - Total = 10, - Page = 5, - Size = 2, - }; - - Assert.That(result.HasNext, Is.False); - } - - [Test] - public void Offset_is_calculated_correctly() - { - var result = new PaginatedResult - { - Results = [5, 6], - Total = 10, - Page = 3, - Size = 2, - }; - - Assert.That(result.Offset, Is.EqualTo(4)); - } - - [Test] - public void HasNext_is_false_when_size_is_zero() - { - var result = new PaginatedResult - { - Results = [], - Total = 10, - Page = 1, - Size = 0, - }; - - Assert.Multiple(() => - { - Assert.That(result.HasNext, Is.False); - Assert.That(result.Offset, Is.EqualTo(0)); - }); - } - - [Test] - public void HasNext_is_false_when_total_is_zero() - { - var result = new PaginatedResult - { - Results = [], - Total = 0, - Page = 1, - Size = 10, - }; - - Assert.Multiple(() => - { - Assert.That(result.HasNext, Is.False); - Assert.That(result.HasPrevious, Is.False); - }); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Domain/Accounts/AccountModelTests.cs b/tests/UnitTests/ApplicationCore/Domain/Accounts/AccountModelTests.cs deleted file mode 100644 index 254bdf7..0000000 --- a/tests/UnitTests/ApplicationCore/Domain/Accounts/AccountModelTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using ApplicationCore.Domain.Accounts; - -namespace UnitTests.ApplicationCore.Domain.Accounts; - -[TestFixture] -public sealed class AccountModelTests -{ - [Test] - public void Creating_account_with_username_sets_value() - { - var account = new AccountModel { Username = "user1" }; - - Assert.That(account.Username, Is.EqualTo("user1")); - } - - [Test] - public void Optional_properties_can_be_set() - { - const string sub = "auth0|123"; - const string imageUrl = "https://example.com/avatar.png"; - var createdOn = DateTime.UtcNow; - - var account = new AccountModel - { - Username = "user1", - Sub = sub, - ImageUrl = imageUrl, - CreatedOn = createdOn, - }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(account.Sub, Is.EqualTo(sub)); - Assert.That(account.ImageUrl, Is.EqualTo(imageUrl)); - Assert.That(account.CreatedOn, Is.EqualTo(createdOn)); - } - } - - [Test] - public void Last_modified_fields_default_to_null() - { - var account = new AccountModel { Username = "user1" }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(account.LastModifiedOn, Is.Null); - Assert.That(account.LastModifiedById, Is.Null); - } - } - - [Test] - public void Last_modified_fields_can_be_updated() - { - var modifiedOn = DateTime.UtcNow; - var modifiedBy = Guid.NewGuid(); - - var account = new AccountModel { Username = "user1" }; - - account.LastModifiedOn = modifiedOn; - account.LastModifiedById = modifiedBy; - - using (Assert.EnterMultipleScope()) - { - Assert.That(account.LastModifiedOn, Is.EqualTo(modifiedOn)); - Assert.That(account.LastModifiedById, Is.EqualTo(modifiedBy)); - } - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Domain/BaseAuditableModelTests.cs b/tests/UnitTests/ApplicationCore/Domain/BaseAuditableModelTests.cs deleted file mode 100644 index 387e20c..0000000 --- a/tests/UnitTests/ApplicationCore/Domain/BaseAuditableModelTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -using ApplicationCore.Domain; -using ApplicationCore.Domain.Accounts; - -namespace UnitTests.ApplicationCore.Domain; - -[TestFixture] -public sealed class BaseAuditableModelTests -{ - private sealed class TestAuditableModel : BaseAuditableModel { } - - [Test] - public void All_properties_are_null_or_default_by_default() - { - var entity = new TestAuditableModel(); - - using (Assert.EnterMultipleScope()) - { - Assert.That(entity.CreatedOn, Is.EqualTo(default(DateTime))); - Assert.That(entity.CreatedById, Is.Null); - Assert.That(entity.CreatedBy, Is.Null); - Assert.That(entity.LastModifiedOn, Is.Null); - Assert.That(entity.LastModifiedById, Is.EqualTo(default(Guid))); - Assert.That(entity.DeletedOn, Is.Null); - } - } - - [Test] - public void Created_properties_can_be_set() - { - var createdOn = DateTime.UtcNow; - var createdById = Guid.NewGuid(); - var account = new AccountModel { Username = "creator" }; - - var entity = new TestAuditableModel - { - CreatedOn = createdOn, - CreatedById = createdById, - CreatedBy = account, - }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(entity.CreatedOn, Is.EqualTo(createdOn)); - Assert.That(entity.CreatedById, Is.EqualTo(createdById)); - Assert.That(entity.CreatedBy, Is.EqualTo(account)); - } - } - - [Test] - public void Last_modified_properties_can_be_set() - { - var modifiedOn = DateTime.UtcNow; - var modifiedById = Guid.NewGuid(); - - var entity = new TestAuditableModel - { - LastModifiedOn = modifiedOn, - LastModifiedById = modifiedById, - }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(entity.LastModifiedOn, Is.EqualTo(modifiedOn)); - Assert.That(entity.LastModifiedById, Is.EqualTo(modifiedById)); - } - } - - [Test] - public void Deleted_on_can_be_set() - { - var deletedOn = DateTime.UtcNow; - - var entity = new TestAuditableModel { DeletedOn = deletedOn }; - - Assert.That(entity.DeletedOn, Is.EqualTo(deletedOn)); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Domain/BaseModelTests.cs b/tests/UnitTests/ApplicationCore/Domain/BaseModelTests.cs deleted file mode 100644 index 5721c25..0000000 --- a/tests/UnitTests/ApplicationCore/Domain/BaseModelTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using ApplicationCore.Domain; - -namespace UnitTests.ApplicationCore.Domain; - -[TestFixture] -public sealed class BaseModelTests -{ - private sealed class TestModel : BaseModel { } - - [Test] - public void Id_can_be_set() - { - var id = Guid.NewGuid(); - var entity = new TestModel { Id = id }; - - Assert.That(entity.Id, Is.EqualTo(id)); - } - - [Test] - public void Id_can_be_updated() - { - var firstId = Guid.NewGuid(); - var secondId = Guid.NewGuid(); - - var entity = new TestModel { Id = firstId }; - - entity.Id = secondId; - - Assert.That(entity.Id, Is.EqualTo(secondId)); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Domain/Problems/Languages/LanguageVersionTests.cs b/tests/UnitTests/ApplicationCore/Domain/Problems/Languages/LanguageVersionTests.cs deleted file mode 100644 index d7f6d7e..0000000 --- a/tests/UnitTests/ApplicationCore/Domain/Problems/Languages/LanguageVersionTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using ApplicationCore.Domain.Problems.Languages; - -namespace UnitTests.ApplicationCore.Domain.Problems.Languages; - -[TestFixture] -public sealed class LanguageVersionTests -{ - [Test] - public void Creating_language_version_with_required_version_sets_value() - { - var language = new ProgrammingLanguage - { - Id = 1, - Name = "C#", - IsArchived = false - }; - - var languageVersion = new LanguageVersion - { - Version = "12", - ProgrammingLanguageId = language.Id, - ProgrammingLanguage = language - }; - - Assert.That(languageVersion.Version, Is.EqualTo("12")); - } - - [Test] - public void Optional_initial_code_can_be_set() - { - var language = new ProgrammingLanguage - { - Id = 1, - Name = "Python", - IsArchived = false - }; - - const string code = "print('hello world')"; - - var languageVersion = new LanguageVersion - { - Version = "3.12", - InitialCode = code, - ProgrammingLanguageId = language.Id, - ProgrammingLanguage = language - }; - - Assert.That(languageVersion.InitialCode, Is.EqualTo(code)); - } - - [Test] - public void Programming_language_relationship_is_set_correctly() - { - var language = new ProgrammingLanguage - { - Id = 5, - Name = "Java", - IsArchived = false - }; - - var languageVersion = new LanguageVersion - { - Version = "21", - ProgrammingLanguageId = 5, - ProgrammingLanguage = language - }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(languageVersion.ProgrammingLanguageId, Is.EqualTo(5)); - Assert.That(languageVersion.ProgrammingLanguage, Is.EqualTo(language)); - } - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Domain/Problems/Languages/ProgrammingLanguageTests.cs b/tests/UnitTests/ApplicationCore/Domain/Problems/Languages/ProgrammingLanguageTests.cs deleted file mode 100644 index 00e2a23..0000000 --- a/tests/UnitTests/ApplicationCore/Domain/Problems/Languages/ProgrammingLanguageTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using ApplicationCore.Domain.Problems.Languages; - -namespace UnitTests.ApplicationCore.Domain.Problems.Languages; - -[TestFixture] -public sealed class ProgrammingLanguageTests -{ - [Test] - public void Creating_programming_language_with_required_properties_sets_values() - { - var language = new ProgrammingLanguage { Name = "C#", IsArchived = false }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(language.Name, Is.EqualTo("C#")); - Assert.That(language.IsArchived, Is.False); - } - } - - [Test] - public void Versions_is_initialized_empty_by_default() - { - var language = new ProgrammingLanguage { Name = "Python", IsArchived = false }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(language.Versions, Is.Not.Null); - Assert.That(language.Versions, Is.Empty); - } - } - - [Test] - public void Versions_can_be_assigned() - { - var version = new LanguageVersion - { - Id = 1, - Version = "3.12", - ProgrammingLanguageId = 1, - }; - - var language = new ProgrammingLanguage - { - Name = "Python", - IsArchived = false, - Versions = [version], - }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(language.Versions, Has.Exactly(1).Items); - Assert.That(language.Versions.Single(), Is.EqualTo(version)); - } - } - - [Test] - public void Auditable_fields_can_be_set() - { - var createdOn = DateTime.UtcNow; - var modifiedOn = DateTime.UtcNow.AddMinutes(1); - var deletedOn = DateTime.UtcNow.AddMinutes(2); - var userId = Guid.NewGuid(); - - var language = new ProgrammingLanguage - { - Name = "Java", - IsArchived = false, - CreatedOn = createdOn, - CreatedById = userId, - LastModifiedOn = modifiedOn, - LastModifiedById = userId, - DeletedOn = deletedOn, - }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(language.CreatedOn, Is.EqualTo(createdOn)); - Assert.That(language.CreatedById, Is.EqualTo(userId)); - Assert.That(language.LastModifiedOn, Is.EqualTo(modifiedOn)); - Assert.That(language.LastModifiedById, Is.EqualTo(userId)); - Assert.That(language.DeletedOn, Is.EqualTo(deletedOn)); - } - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Domain/Problems/ProblemModelTests.cs b/tests/UnitTests/ApplicationCore/Domain/Problems/ProblemModelTests.cs deleted file mode 100644 index 98c945d..0000000 --- a/tests/UnitTests/ApplicationCore/Domain/Problems/ProblemModelTests.cs +++ /dev/null @@ -1,292 +0,0 @@ -using ApplicationCore.Domain.Problems; -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Domain.Problems.ProblemSetups; - -namespace UnitTests.ApplicationCore.Domain.Problems; - -[TestFixture] -public sealed class ProblemModelTests -{ - private static ProblemModel CreateProblem(IEnumerable? setups = null) - { - return new ProblemModel - { - Title = "Sample Problem", - Slug = "sample-problem", - Question = "Solve the problem", - Tags = [], - Difficulty = 2, - Status = ProblemStatus.Draft, - Version = 1, - ProblemSetups = setups?.ToList() ?? [], - }; - } - - [Test] - public void GetAvailableLanguages_returns_empty_when_no_problem_setups_exist() - { - var problem = CreateProblem(); - - var result = problem.GetAvailableLanguages(); - - Assert.That(result, Is.Empty); - } - - [Test] - public void GetAvailableLanguages_returns_single_language_with_multiple_versions() - { - var language = new ProgrammingLanguage - { - Id = 1, - Name = "C#", - IsArchived = false, - }; - - var v10 = new LanguageVersion - { - Id = 10, - Version = "10", - ProgrammingLanguage = language, - }; - - var v11 = new LanguageVersion - { - Id = 11, - Version = "11", - ProgrammingLanguage = language, - }; - - var setups = new[] - { - new ProblemSetupModel - { - Id = 1, - ProblemId = Guid.NewGuid(), - LanguageVersionId = v10.Id, - InitialCode = "", - LanguageVersion = v10, - }, - new ProblemSetupModel - { - Id = 2, - ProblemId = Guid.NewGuid(), - LanguageVersionId = v10.Id, - InitialCode = "", - LanguageVersion = v11, - }, - }; - - var problem = CreateProblem(setups); - - var languages = problem.GetAvailableLanguages().ToList(); - - IEnumerable expected = ["10", "11"]; - using (Assert.EnterMultipleScope()) - { - Assert.That(languages, Has.Count.EqualTo(1)); - Assert.That(languages[0].Id, Is.EqualTo(1)); - Assert.That(languages[0].Name, Is.EqualTo("C#")); - Assert.That(languages[0].IsArchived, Is.False); - Assert.That(languages[0].Versions.Select(v => v.Version), Is.EqualTo(expected)); - } - } - - [Test] - public void GetAvailableLanguages_deduplicates_language_versions_by_id() - { - var language = new ProgrammingLanguage - { - Id = 2, - Name = "Python", - IsArchived = false, - }; - - var version = new LanguageVersion - { - Id = 1, - Version = "3.11", - ProgrammingLanguage = language, - }; - - var setups = new[] - { - new ProblemSetupModel - { - Id = 1, - ProblemId = Guid.NewGuid(), - LanguageVersionId = version.Id, - InitialCode = "", - LanguageVersion = version, - }, - new ProblemSetupModel - { - Id = 2, - ProblemId = Guid.NewGuid(), - LanguageVersionId = version.Id, - InitialCode = "", - LanguageVersion = version, - }, - }; - - var problem = CreateProblem(setups); - - var languages = problem.GetAvailableLanguages().ToList(); - - Assert.That(languages.Single().Versions, Has.Exactly(1).Items); - } - - [Test] - public void GetAvailableLanguages_ignores_setups_without_language_version_or_language() - { - var version = new LanguageVersion - { - Id = 1, - Version = "1.0", - ProgrammingLanguage = null!, - }; - - var setups = new[] - { - new ProblemSetupModel - { - Id = 1, - ProblemId = Guid.NewGuid(), - LanguageVersionId = version.Id, - InitialCode = "", - LanguageVersion = null, - }, - new ProblemSetupModel - { - Id = 2, - ProblemId = Guid.NewGuid(), - InitialCode = "", - LanguageVersion = version, - LanguageVersionId = version.Id, - }, - }; - - var problem = CreateProblem(setups); - - var result = problem.GetAvailableLanguages(); - - Assert.That(result, Is.Empty); - } - - [Test] - public void GetAvailableLanguages_orders_versions_by_version_value() - { - var language = new ProgrammingLanguage - { - Id = 3, - Name = "Java", - IsArchived = false, - }; - - var v21 = new LanguageVersion - { - Id = 2, - Version = "21", - ProgrammingLanguage = language, - }; - - var v17 = new LanguageVersion - { - Id = 1, - Version = "17", - ProgrammingLanguage = language, - }; - - var setups = new[] - { - new ProblemSetupModel - { - Id = 1, - ProblemId = Guid.NewGuid(), - InitialCode = "", - LanguageVersion = v21, - LanguageVersionId = v21.Id, - }, - new ProblemSetupModel - { - Id = 2, - ProblemId = Guid.NewGuid(), - InitialCode = "", - LanguageVersion = v17, - LanguageVersionId = v21.Id, - }, - }; - - var problem = CreateProblem(setups); - - var versions = problem - .GetAvailableLanguages() - .Single() - .Versions.Select(v => v.Version) - .ToList(); - - IEnumerable expected = ["17", "21"]; - - Assert.That(versions, Is.EqualTo(expected)); - } - - [Test] - public void GetAvailableLanguages_deduplicates_languages_by_id() - { - var lang1 = new ProgrammingLanguage - { - Id = 1, - Name = "C#", - IsArchived = false, - }; - var lang2 = new ProgrammingLanguage - { - Id = 1, - Name = "C#", - IsArchived = true, - }; - - var v1 = new LanguageVersion - { - Id = 1, - Version = "10", - ProgrammingLanguage = lang1, - }; - var v2 = new LanguageVersion - { - Id = 2, - Version = "11", - ProgrammingLanguage = lang2, - }; - - var setups = new[] - { - new ProblemSetupModel - { - Id = 1, - ProblemId = Guid.NewGuid(), - InitialCode = "", - LanguageVersion = v1, - LanguageVersionId = v1.Id, - }, - new ProblemSetupModel - { - Id = 2, - ProblemId = Guid.NewGuid(), - InitialCode = "", - LanguageVersion = v2, - LanguageVersionId = v2.Id, - }, - }; - - var problem = CreateProblem(setups); - - var result = problem.GetAvailableLanguages().Single(); - - using (Assert.EnterMultipleScope()) - { - Assert.That(result.Id, Is.EqualTo(1)); - Assert.That(result.IsArchived, Is.False); - Assert.That(result.Versions, Has.Exactly(1).Items); - } - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Queries/Accounts/GetAccountBySubHandlerTests.cs b/tests/UnitTests/ApplicationCore/Queries/Accounts/GetAccountBySubHandlerTests.cs deleted file mode 100644 index 6f4e856..0000000 --- a/tests/UnitTests/ApplicationCore/Queries/Accounts/GetAccountBySubHandlerTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Dtos.Accounts; -using ApplicationCore.Interfaces.Repositories; -using ApplicationCore.Queries.Accounts.GetAccountBySub; -using Ardalis.Result; -using Moq; - -namespace UnitTests.ApplicationCore.Queries.Accounts; - -[TestFixture] -public sealed class GetAccountBySubHandlerTests -{ - private Mock _repository = null!; - private GetAccountBySubHandler _handler = null!; - - [SetUp] - public void SetUp() - { - _repository = new Mock(); - _handler = new GetAccountBySubHandler(_repository.Object); - } - - [Test] - public async Task Handle_returns_invalid_when_sub_is_empty() - { - var query = new GetAccountBySubQuery(string.Empty); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.Status, Is.EqualTo(ResultStatus.Invalid)); - Assert.That(result.ValidationErrors.Count(), Is.EqualTo(1)); - }); - } - - [Test] - public async Task Handle_returns_not_found_when_account_does_not_exist() - { - _repository - .Setup(r => r.GetBySubAsync("sub1", It.IsAny())) - .ReturnsAsync((AccountModel?)null); - - var query = new GetAccountBySubQuery("sub1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.That(result.Status, Is.EqualTo(ResultStatus.NotFound)); - } - - [Test] - public async Task Handle_returns_account_dto_when_account_exists() - { - var account = new AccountModel() - { - Id = Guid.NewGuid(), - Username = "user1", - Sub = "sub1", - ImageUrl = "http://image.url", - CreatedOn = DateTime.UtcNow, - }; - - _repository - .Setup(r => r.GetBySubAsync("sub1", It.IsAny())) - .ReturnsAsync(account); - - var query = new GetAccountBySubQuery("sub1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value, Is.Not.Null); - Assert.That(result.Value.Id, Is.EqualTo(account.Id)); - Assert.That(result.Value.Username, Is.EqualTo(account.Username)); - Assert.That(result.Value.ImageUrl, Is.EqualTo(account.ImageUrl)); - Assert.That(result.Value.CreatedOn, Is.EqualTo(account.CreatedOn)); - }); - } - - [Test] - public async Task Handle_returns_error_when_exception_is_thrown() - { - _repository - .Setup(r => r.GetBySubAsync("sub1", It.IsAny())) - .ThrowsAsync(new Exception("db error")); - - var query = new GetAccountBySubQuery("sub1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.Status, Is.EqualTo(ResultStatus.Error)); - Assert.That(result.Errors, Has.Some.EqualTo("db error")); - }); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Queries/Accounts/GetProfileAggregateHandlerTests.cs b/tests/UnitTests/ApplicationCore/Queries/Accounts/GetProfileAggregateHandlerTests.cs deleted file mode 100644 index f95585b..0000000 --- a/tests/UnitTests/ApplicationCore/Queries/Accounts/GetProfileAggregateHandlerTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Repositories; -using ApplicationCore.Queries.Accounts.GetProfileAggregate; -using Ardalis.Result; -using Moq; - -namespace UnitTests.ApplicationCore.Queries.Accounts; - -[TestFixture] -public sealed class GetProfileAggregateHandlerTests -{ - private Mock _repository = null!; - private GetProfileAggregateHandler _handler = null!; - - [SetUp] - public void SetUp() - { - _repository = new Mock(); - _handler = new GetProfileAggregateHandler(_repository.Object); - } - - [Test] - public async Task Handle_returns_invalid_when_username_is_empty() - { - var query = new GetProfileAggregateQuery(""); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.Status, Is.EqualTo(ResultStatus.Invalid)); - Assert.That(result.ValidationErrors, Has.Exactly(1).Items); - Assert.That(result.ValidationErrors.First().Identifier, Is.EqualTo("Username")); - }); - } - - [Test] - public async Task Handle_returns_not_found_when_account_does_not_exist() - { - _repository - .Setup(r => r.GetByUsernameAsync("user1", It.IsAny())) - .ReturnsAsync((AccountModel?)null); - - var query = new GetProfileAggregateQuery("user1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.That(result.Status, Is.EqualTo(ResultStatus.NotFound)); - } - - [Test] - public async Task Handle_returns_profile_aggregate_when_account_exists() - { - var account = new AccountModel - { - Id = Guid.NewGuid(), - Username = "user1", - ImageUrl = "http://image.url", - CreatedOn = DateTime.UtcNow, - }; - - _repository - .Setup(r => r.GetByUsernameAsync("user1", It.IsAny())) - .ReturnsAsync(account); - - var query = new GetProfileAggregateQuery("user1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value.Profile.Username, Is.EqualTo("user1")); - Assert.That(result.Value.Profile.ImageUrl, Is.EqualTo("http://image.url")); - Assert.That(result.Value.Profile.Id, Is.EqualTo(account.Id)); - }); - } - - [Test] - public async Task Handle_returns_error_when_exception_is_thrown() - { - _repository - .Setup(r => r.GetByUsernameAsync("user1", It.IsAny())) - .ThrowsAsync(new Exception("db failure")); - - var query = new GetProfileAggregateQuery("user1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.Status, Is.EqualTo(ResultStatus.Error)); - Assert.That(result.Errors, Has.Some.EqualTo("db failure")); - }); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Queries/Accounts/GetProfileSettingsHandlerTests.cs b/tests/UnitTests/ApplicationCore/Queries/Accounts/GetProfileSettingsHandlerTests.cs deleted file mode 100644 index 6f54ec2..0000000 --- a/tests/UnitTests/ApplicationCore/Queries/Accounts/GetProfileSettingsHandlerTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using ApplicationCore.Domain.Accounts; -using ApplicationCore.Interfaces.Repositories; -using ApplicationCore.Queries.Accounts.GetProfileSettings; -using Ardalis.Result; -using Moq; - -namespace UnitTests.ApplicationCore.Queries.Accounts; - -[TestFixture] -public sealed class GetProfileSettingsHandlerTests -{ - private Mock _repository = null!; - private GetProfileSettingsHandler _handler = null!; - - [SetUp] - public void SetUp() - { - _repository = new Mock(); - _handler = new GetProfileSettingsHandler(_repository.Object); - } - - [Test] - public async Task Handle_returns_not_found_when_account_does_not_exist() - { - _repository - .Setup(r => r.GetBySubAsync("sub1", It.IsAny())) - .ReturnsAsync((AccountModel?)null); - - var query = new GetProfileSettingsQuery("sub1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.That(result.Status, Is.EqualTo(ResultStatus.NotFound)); - } - - [Test] - public async Task Handle_returns_profile_settings_when_account_exists() - { - var account = new AccountModel { Username = "user1", About = "my bio" }; - - _repository - .Setup(r => r.GetBySubAsync("sub1", It.IsAny())) - .ReturnsAsync(account); - - var query = new GetProfileSettingsQuery("sub1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value.Username, Is.EqualTo("user1")); - Assert.That(result.Value.Bio, Is.EqualTo("my bio")); - }); - } - - [Test] - public async Task Handle_returns_error_when_exception_is_thrown() - { - _repository - .Setup(r => r.GetBySubAsync("sub1", It.IsAny())) - .ThrowsAsync(new Exception("db failure")); - - var query = new GetProfileSettingsQuery("sub1"); - - var result = await _handler.Handle(query, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.Status, Is.EqualTo(ResultStatus.Error)); - Assert.That(result.Errors, Has.Some.EqualTo("db failure")); - }); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Queries/Problems/GetProblemBySlugHandlerTests.cs b/tests/UnitTests/ApplicationCore/Queries/Problems/GetProblemBySlugHandlerTests.cs deleted file mode 100644 index ebd139d..0000000 --- a/tests/UnitTests/ApplicationCore/Queries/Problems/GetProblemBySlugHandlerTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -using ApplicationCore.Domain.Problems; -using ApplicationCore.Domain.Problems.Languages; -using ApplicationCore.Domain.Problems.ProblemSetups; -using ApplicationCore.Interfaces.Repositories; -using ApplicationCore.Queries.Problems.GetProblemBySlug; -using Ardalis.Result; -using Mapster; -using Moq; - -namespace UnitTests.ApplicationCore.Queries.Problems; - -[TestFixture] -public sealed class GetProblemBySlugHandlerTests -{ - private Mock _problemRepository = null!; - private GetProblemBySlugHandler _handler = null!; - - [OneTimeSetUp] - public void OneTimeSetUp() - { - TypeAdapterConfig.GlobalSettings.Scan(typeof(ProblemModel).Assembly); - } - - [SetUp] - public void SetUp() - { - _problemRepository = new Mock(); - _handler = new GetProblemBySlugHandler(_problemRepository.Object); - } - - [Test] - public async Task Handle_returns_problem_dto_when_problem_exists() - { - var language = new ProgrammingLanguage - { - Id = 1, - Name = "C#", - IsArchived = false, - }; - - var version = new LanguageVersion - { - Id = 1, - Version = "10", - ProgrammingLanguage = language, - }; - - var problem = new ProblemModel() - { - Id = Guid.NewGuid(), - Title = "Two Sum", - Slug = "two-sum", - Question = "Find two numbers", - Tags = [], - Difficulty = 1, - Version = 1, - ProblemSetups = - [ - new ProblemSetupModel - { - Id = 1, - ProblemId = Guid.NewGuid(), - InitialCode = "", - LanguageVersion = version, - LanguageVersionId = version.Id, - }, - ], - }; - - _problemRepository - .Setup(r => r.GetProblemBySlugAsync("two-sum", It.IsAny())) - .ReturnsAsync(problem); - - var result = await _handler.Handle( - new GetProblemBySlugQuery("two-sum"), - CancellationToken.None - ); - - Assert.That(result.IsSuccess, Is.True); - - var dto = result.Value; - - Assert.Multiple(() => - { - Assert.That(dto.Title, Is.EqualTo("Two Sum")); - Assert.That(dto.Slug, Is.EqualTo("two-sum")); - Assert.That(dto.AvailableLanguages.Count(), Is.EqualTo(1)); - Assert.That(dto.AvailableLanguages.Single().Name, Is.EqualTo("C#")); - Assert.That( - dto.AvailableLanguages.Single().Versions.Single().Version, - Is.EqualTo("10") - ); - }); - } - - [Test] - public async Task Handle_returns_not_found_when_problem_missing() - { - _problemRepository - .Setup(r => r.GetProblemBySlugAsync("missing", It.IsAny())) - .ReturnsAsync((ProblemModel?)null); - - var result = await _handler.Handle( - new GetProblemBySlugQuery("missing"), - CancellationToken.None - ); - - Assert.That(result.Status, Is.EqualTo(ResultStatus.NotFound)); - } - - [Test] - public async Task Handle_returns_error_when_exception_thrown() - { - _problemRepository - .Setup(r => r.GetProblemBySlugAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception("db error")); - - var result = await _handler.Handle( - new GetProblemBySlugQuery("two-sum"), - CancellationToken.None - ); - - Assert.Multiple(() => - { - Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Errors, Has.Some.EqualTo("db error")); - }); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Queries/Problems/GetProblemsPageableHandlerTests.cs b/tests/UnitTests/ApplicationCore/Queries/Problems/GetProblemsPageableHandlerTests.cs deleted file mode 100644 index 0d1b94b..0000000 --- a/tests/UnitTests/ApplicationCore/Queries/Problems/GetProblemsPageableHandlerTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -using ApplicationCore.Common.Pagination; -using ApplicationCore.Domain.Problems; -using ApplicationCore.Interfaces.Repositories; -using ApplicationCore.Queries.Problems.GetProblemsPageable; -using Ardalis.Result; -using Moq; - -namespace UnitTests.ApplicationCore.Queries.Problems; - -[TestFixture] -public sealed class GetProblemsPageableHandlerTests -{ - private Mock _repo = null!; - private GetProblemsPageableHandler _handler = null!; - - [SetUp] - public void SetUp() - { - _repo = new Mock(); - _handler = new GetProblemsPageableHandler(_repo.Object); - } - - [Test] - public async Task Handle_returns_success_with_mapped_paginated_result() - { - var pagination = new PaginationRequest() { Page = 1, Size = 10 }; - - var problems = new PaginatedResult - { - Results = - [ - new ProblemModel - { - Id = Guid.NewGuid(), - Title = "Two Sum", - Slug = "two-sum", - Difficulty = 1, - Version = 1, - Tags = - [ - new TagModel() { Id = 1, Value = "arrays" }, - new TagModel() { Id = 1, Value = "hashmap" }, - ], - Question = "", - }, - new ProblemModel() - { - Id = Guid.NewGuid(), - Title = "Reverse String", - Slug = "reverse-string", - Difficulty = 1, - Version = 1, - Tags = [], - Question = "", - }, - ], - Total = 2, - Page = 1, - Size = 10, - }; - - _repo - .Setup(r => r.GetProblemsAsync(pagination, It.IsAny())) - .ReturnsAsync(problems); - - var result = await _handler.Handle( - new GetProblemsPageableQuery(pagination), - CancellationToken.None - ); - - Assert.That(result.Status, Is.EqualTo(ResultStatus.Ok)); - - Assert.Multiple(() => - { - Assert.That(result.Value.Total, Is.EqualTo(2)); - Assert.That(result.Value.Page, Is.EqualTo(1)); - Assert.That(result.Value.Size, Is.EqualTo(10)); - Assert.That(result.Value.Results.Count, Is.EqualTo(2)); - - var first = result.Value.Results[0]; - Assert.That(first.Title, Is.EqualTo("Two Sum")); - Assert.That(first.Slug, Is.EqualTo("two-sum")); - Assert.That(first.Tags, Is.EquivalentTo(["arrays", "hashmap"])); - - var second = result.Value.Results[1]; - Assert.That(second.Tags, Is.Empty); - }); - } - - [Test] - public async Task Handle_returns_success_with_empty_results_when_repository_returns_none() - { - var pagination = new PaginationRequest() { Page = 1, Size = 10 }; - - var problems = new PaginatedResult - { - Results = [], - Total = 0, - Page = 1, - Size = 10, - }; - - _repo - .Setup(r => r.GetProblemsAsync(pagination, It.IsAny())) - .ReturnsAsync(problems); - - var result = await _handler.Handle( - new GetProblemsPageableQuery(pagination), - CancellationToken.None - ); - - Assert.Multiple(() => - { - Assert.That(result.Status, Is.EqualTo(ResultStatus.Ok)); - Assert.That(result.Value.Results, Is.Empty); - Assert.That(result.Value.Total, Is.EqualTo(0)); - }); - } - - [Test] - public async Task Handle_returns_error_when_repository_throws() - { - var pagination = new PaginationRequest() { Page = 1, Size = 10 }; - - _repo - .Setup(r => r.GetProblemsAsync(pagination, It.IsAny())) - .ThrowsAsync(new Exception("db failure")); - - var result = await _handler.Handle( - new GetProblemsPageableQuery(pagination), - CancellationToken.None - ); - - Assert.Multiple(() => - { - Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Errors, Has.Some.EqualTo("db failure")); - }); - } -} \ No newline at end of file diff --git a/tests/UnitTests/ApplicationCore/Services/SubmissionAppServiceTests.cs b/tests/UnitTests/ApplicationCore/Services/SubmissionAppServiceTests.cs deleted file mode 100644 index ae6a21e..0000000 --- a/tests/UnitTests/ApplicationCore/Services/SubmissionAppServiceTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -using ApplicationCore.Commands.Submissions.CreateSubmission; -using ApplicationCore.Services; -using MediatR; -using Moq; - -namespace UnitTests.ApplicationCore.Services; - -[TestFixture] -public sealed class SubmissionAppServiceTests -{ - private Mock _mockMediator; - private SubmissionAppService _sut; - - [SetUp] - public void SetUp() - { - _mockMediator = new(); - _sut = new SubmissionAppService(_mockMediator.Object); - } - - [Test] - public void CreateAsync_sends_CreateSubmissionCommand_via_mediator() - { - int problemSetupId = 1; - string code = "sample code"; - var createdById = Guid.NewGuid(); - var cancellationToken = CancellationToken.None; - var expectedResult = Guid.NewGuid(); - - _mockMediator - .Setup(m => m.Send(It.IsAny(), cancellationToken)) - .ReturnsAsync(expectedResult); - - var result = _sut.CreateAsync(problemSetupId, code, createdById, cancellationToken).Result; - - Assert.That(result.Value, Is.EqualTo(expectedResult)); - - _mockMediator.Verify( - m => - m.Send( - It.Is(cmd => - cmd.ProblemSetupId == problemSetupId - && cmd.Code == code - && cmd.CreatedById == createdById - ), - cancellationToken - ), - Times.Once - ); - } -} \ No newline at end of file diff --git a/tests/UnitTests/PublicApi/Controllers/ProblemControllerTests.cs b/tests/UnitTests/PublicApi/Controllers/ProblemControllerTests.cs deleted file mode 100644 index d0d4f31..0000000 --- a/tests/UnitTests/PublicApi/Controllers/ProblemControllerTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using ApplicationCore.Dtos.Problems; -using ApplicationCore.Interfaces.Services; -using Ardalis.Result; -using Microsoft.AspNetCore.Mvc; -using Moq; -using PublicApi.Controllers; - -namespace UnitTests.PublicApi.Controllers; - -[TestFixture] -public sealed class ProblemControllerTests -{ - private Mock _problemAppService = null!; - private Mock _accountAppService = null!; - private ProblemController _sut = null!; - - [SetUp] - public void SetUp() - { - _problemAppService = new Mock(); - _accountAppService = new Mock(); - _sut = new ProblemController(_problemAppService.Object, _accountAppService.Object); - } - - [Test] - public async Task GetBySlugAsync_returns_ok_when_problem_exists() - { - var problem = new ProblemDto - { - Id = Guid.NewGuid(), - Title = "Two Sum", - Slug = "two-sum", - Question = "Find two numbers", - Difficulty = 1, - Version = 1, - Tags = [], - AvailableLanguages = [], - }; - - _problemAppService - .Setup(service => service.GetProblemBySlugAsync("two-sum", It.IsAny())) - .ReturnsAsync(Result.Success(problem)); - - var result = await _sut.GetBySlugAsync("two-sum", CancellationToken.None); - - Assert.That(result, Is.InstanceOf()); - Assert.That(((OkObjectResult)result).Value, Is.EqualTo(problem)); - } - - [Test] - public async Task GetBySlugAsync_returns_bad_request_when_slug_is_missing() - { - var result = await _sut.GetBySlugAsync("", CancellationToken.None); - - Assert.That(result, Is.InstanceOf()); - Assert.That(((BadRequestObjectResult)result).Value, Is.EqualTo("Slug is required.")); - _problemAppService.Verify( - service => service.GetProblemBySlugAsync(It.IsAny(), It.IsAny()), - Times.Never - ); - } -} diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj deleted file mode 100644 index 5ce73ef..0000000 --- a/tests/UnitTests/UnitTests.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - net10.0 - latest - enable - enable - false - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - From 89b28a68279d7c30724d9905764f46a3eec60190 Mon Sep 17 00:00:00 2001 From: admclamb Date: Wed, 10 Jun 2026 00:53:46 -0400 Subject: [PATCH 02/34] Move to central package manager --- Algowars.Api/Algowars.Api.csproj | 2 +- .../Algowars.Domain.Tests.csproj | 10 +++++----- .../User/Exceptions/InvalidUsernameException.cs | 4 +--- Directory.Packages.props | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 Directory.Packages.props diff --git a/Algowars.Api/Algowars.Api.csproj b/Algowars.Api/Algowars.Api.csproj index 2e73281..0057157 100644 --- a/Algowars.Api/Algowars.Api.csproj +++ b/Algowars.Api/Algowars.Api.csproj @@ -7,7 +7,7 @@ - + diff --git a/Algowars.Domain.Tests/Algowars.Domain.Tests.csproj b/Algowars.Domain.Tests/Algowars.Domain.Tests.csproj index 61dad3f..9290214 100644 --- a/Algowars.Domain.Tests/Algowars.Domain.Tests.csproj +++ b/Algowars.Domain.Tests/Algowars.Domain.Tests.csproj @@ -9,11 +9,11 @@ - - - - - + + + + + diff --git a/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs b/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs index a1e262d..3a92afd 100644 --- a/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs +++ b/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs @@ -2,8 +2,6 @@ namespace Algowars.Domain.User.Exceptions; -public sealed class InvalidUsernameException : DomainException +public sealed class InvalidUsernameException(string reason) : DomainException($"Username is invalid: {reason}") { - public InvalidUsernameException(string reason) - : base($"Username is invalid: {reason}") { } } \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..6800179 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,17 @@ + + + true + + + + + + + + + + + + + + From 9991290c5495a1e74e1369c7c3140431bc53a1d0 Mon Sep 17 00:00:00 2001 From: admclamb Date: Wed, 10 Jun 2026 23:15:28 -0400 Subject: [PATCH 03/34] Update domain entities --- .../User/Entities/UserTests.cs | 168 ++++++++++++++---- .../User/ValueObjects/BioTests.cs | 79 ++++++++ .../User/ValueObjects/ImageUrlTests.cs | 84 +++++++++ .../User/ValueObjects/UsernameTests.cs | 75 ++++---- Algowars.Domain/User/Entities/User.cs | 36 +++- .../User/Exceptions/InvalidBioException.cs | 9 + .../Exceptions/InvalidImageUrlException.cs | 7 + .../Exceptions/UsernameCooldownException.cs | 9 + Algowars.Domain/User/ValueObjects/Bio.cs | 26 +++ Algowars.Domain/User/ValueObjects/ImageUrl.cs | 30 ++++ Algowars.Domain/User/ValueObjects/Username.cs | 16 +- 11 files changed, 448 insertions(+), 91 deletions(-) create mode 100644 Algowars.Domain.Tests/User/ValueObjects/BioTests.cs create mode 100644 Algowars.Domain.Tests/User/ValueObjects/ImageUrlTests.cs create mode 100644 Algowars.Domain/User/Exceptions/InvalidBioException.cs create mode 100644 Algowars.Domain/User/Exceptions/InvalidImageUrlException.cs create mode 100644 Algowars.Domain/User/Exceptions/UsernameCooldownException.cs create mode 100644 Algowars.Domain/User/ValueObjects/Bio.cs create mode 100644 Algowars.Domain/User/ValueObjects/ImageUrl.cs diff --git a/Algowars.Domain.Tests/User/Entities/UserTests.cs b/Algowars.Domain.Tests/User/Entities/UserTests.cs index f01d65e..117a18a 100644 --- a/Algowars.Domain.Tests/User/Entities/UserTests.cs +++ b/Algowars.Domain.Tests/User/Entities/UserTests.cs @@ -10,44 +10,78 @@ public class UserTests private const string ValidSub = "auth0|abc123"; [Test] - public void Constructor_ValidArguments_CreatesUser() + public void ChangeUsername_DoesNotAffectOtherProperties() { var user = new UserEntity(ValidUsername, ValidSub); + var originalId = user.Id; + string originalSub = user.Sub; - Assert.Multiple(() => + user.ChangeUsername(new Username("bob")); + + using (Assert.EnterMultipleScope()) { - Assert.That(user.Username, Is.EqualTo(ValidUsername)); - Assert.That(user.Sub, Is.EqualTo(ValidSub)); - Assert.That(user.Id, Is.Not.EqualTo(Guid.Empty)); - }); + Assert.That(user.Id, Is.EqualTo(originalId)); + Assert.That(user.Sub, Is.EqualTo(originalSub)); + } } [Test] - public void Constructor_NullUsername_ThrowsInvalidUsernameException() + public void ChangeUsername_FirstChange_Succeeds() { - Assert.Throws(() => new UserEntity(null!, ValidSub)); + var user = new UserEntity(ValidUsername, ValidSub); + var newUsername = new Username("bob"); + + user.ChangeUsername(newUsername); + + Assert.That(user.Username, Is.EqualTo(newUsername)); } - [TestCase("")] - [TestCase(" ")] - [TestCase(" ")] - public void Constructor_EmptyOrWhitespaceSub_ThrowsInvalidUserSubException(string sub) + [Test] + public void ChangeUsername_SetsUsernameLastChangedAt() { - Assert.Throws(() => new UserEntity(ValidUsername, sub)); + var user = new UserEntity(ValidUsername, ValidSub); + var before = DateTime.UtcNow; + + user.ChangeUsername(new Username("bob")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(user.UsernameLastChangedAt, Is.Not.Null); + Assert.That(user.UsernameLastChangedAt, Is.GreaterThanOrEqualTo(before)); + } } [Test] - public void Constructor_SetsSubCorrectly() + public void ChangeUsername_ValidUsername_UpdatesUsername() { var user = new UserEntity(ValidUsername, ValidSub); - Assert.That(user.Sub, Is.EqualTo(ValidSub)); + var newUsername = new Username("bob"); + + user.ChangeUsername(newUsername); + + Assert.That(user.Username, Is.EqualTo(newUsername)); } [Test] - public void Constructor_SubWithSpecialCharacters_Succeeds() + public void ChangeUsername_WithinCooldown_ThrowsUsernameCooldownException() { - var user = new UserEntity(ValidUsername, "google-oauth2|abc.123-xyz"); - Assert.That(user.Sub, Is.EqualTo("google-oauth2|abc.123-xyz")); + var user = new UserEntity(ValidUsername, ValidSub); + user.ChangeUsername(new Username("bob")); + + Assert.Throws(() => user.ChangeUsername(new Username("charlie"))); + } + + [Test] + public void Constructor_BioIsNullByDefault() + { + var user = new UserEntity(ValidUsername, ValidSub); + Assert.That(user.Bio, Is.Null); + } + + [Test] + public void Constructor_EmptyOrWhitespaceSub_ThrowsInvalidUserSubException([Values("", " ", " ")] string sub) + { + Assert.Throws(() => new UserEntity(ValidUsername, sub)); } [Test] @@ -67,10 +101,43 @@ public void Constructor_GeneratesUniqueIds() } [Test] - public void Equals_SameInstance_IsEqual() + public void Constructor_ImageUrlIsNullByDefault() { var user = new UserEntity(ValidUsername, ValidSub); - Assert.That(user, Is.EqualTo(user)); + Assert.That(user.ImageUrl, Is.Null); + } + + [Test] + public void Constructor_NullUsername_ThrowsInvalidUsernameException() + { + Assert.Throws(() => new UserEntity(null!, ValidSub)); + } + + [Test] + public void Constructor_SetsSubCorrectly() + { + var user = new UserEntity(ValidUsername, ValidSub); + Assert.That(user.Sub, Is.EqualTo(ValidSub)); + } + + [Test] + public void Constructor_SubWithSpecialCharacters_Succeeds() + { + var user = new UserEntity(ValidUsername, "google-oauth2|abc.123-xyz"); + Assert.That(user.Sub, Is.EqualTo("google-oauth2|abc.123-xyz")); + } + + [Test] + public void Constructor_ValidArguments_CreatesUser() + { + var user = new UserEntity(ValidUsername, ValidSub); + + using (Assert.EnterMultipleScope()) + { + Assert.That(user.Id, Is.Not.EqualTo(Guid.Empty)); + Assert.That(user.Sub, Is.EqualTo(ValidSub)); + Assert.That(user.Username, Is.EqualTo(ValidUsername)); + } } [Test] @@ -90,10 +157,10 @@ public void Equals_Null_IsNotEqual() } [Test] - public void GetHashCode_SameUser_ReturnsSameHash() + public void Equals_SameInstance_IsEqual() { var user = new UserEntity(ValidUsername, ValidSub); - Assert.That(user.GetHashCode(), Is.EqualTo(user.GetHashCode())); + Assert.That(user, Is.EqualTo(user)); } [Test] @@ -106,40 +173,69 @@ public void GetHashCode_DifferentUsers_ReturnDifferentHashes() } [Test] - public void ChangeUsername_ValidUsername_UpdatesUsername() + public void GetHashCode_SameUser_ReturnsSameHash() { var user = new UserEntity(ValidUsername, ValidSub); - var newUsername = new Username("bob"); - - user.ChangeUsername(newUsername); - - Assert.That(user.Username, Is.EqualTo(newUsername)); + Assert.That(user.GetHashCode(), Is.EqualTo(user.GetHashCode())); } [Test] - public void ChangeUsername_DoesNotAffectOtherProperties() + public void UpdateBio_DoesNotAffectOtherProperties() { var user = new UserEntity(ValidUsername, ValidSub); var originalId = user.Id; string originalSub = user.Sub; - user.ChangeUsername(new Username("bob")); + user.UpdateBio(new Bio("Some bio.")); - Assert.Multiple(() => + using (Assert.EnterMultipleScope()) { Assert.That(user.Id, Is.EqualTo(originalId)); Assert.That(user.Sub, Is.EqualTo(originalSub)); - }); + } } [Test] - public void ChangeUsername_MultipleTimes_UsesLatestValue() + public void UpdateBio_Null_ClearsBio() { var user = new UserEntity(ValidUsername, ValidSub); + user.UpdateBio(new Bio("Some bio.")); - user.ChangeUsername(new Username("bob")); - user.ChangeUsername(new Username("charlie")); + user.UpdateBio(null); + + Assert.That(user.Bio, Is.Null); + } + + [Test] + public void UpdateBio_ValidBio_SetsBio() + { + var user = new UserEntity(ValidUsername, ValidSub); + var bio = new Bio("I love competitive programming."); + + user.UpdateBio(bio); + + Assert.That(user.Bio, Is.EqualTo(bio)); + } + + [Test] + public void UpdateImageUrl_Null_ClearsImageUrl() + { + var user = new UserEntity(ValidUsername, ValidSub); + user.UpdateImageUrl(new ImageUrl("https://example.com/avatar.png")); + + user.UpdateImageUrl(null); + + Assert.That(user.ImageUrl, Is.Null); + } + + [Test] + public void UpdateImageUrl_ValidUrl_SetsImageUrl() + { + var user = new UserEntity(ValidUsername, ValidSub); + var imageUrl = new ImageUrl("https://example.com/avatar.png"); + + user.UpdateImageUrl(imageUrl); - Assert.That(user.Username.Value, Is.EqualTo("charlie")); + Assert.That(user.ImageUrl, Is.EqualTo(imageUrl)); } } diff --git a/Algowars.Domain.Tests/User/ValueObjects/BioTests.cs b/Algowars.Domain.Tests/User/ValueObjects/BioTests.cs new file mode 100644 index 0000000..835b10b --- /dev/null +++ b/Algowars.Domain.Tests/User/ValueObjects/BioTests.cs @@ -0,0 +1,79 @@ +using Algowars.Domain.User.Exceptions; +using Algowars.Domain.User.ValueObjects; + +namespace Algowars.Domain.Tests.User.ValueObjects; + +public class BioTests +{ + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string atMax = new('a', Bio.MaxLength); + Assert.DoesNotThrow(() => new Bio(atMax)); + } + + [Test] + public void Constructor_EmptyOrWhitespace_ThrowsInvalidBioException([Values("", " ", " ", null)] string? value) + { + Assert.Throws(() => new Bio(value!)); + } + + [Test] + public void Constructor_FarExceedsMaxLength_ThrowsInvalidBioException() + { + string veryLong = new('a', 10000); + Assert.Throws(() => new Bio(veryLong)); + } + + [Test] + public void Constructor_OneAboveMaxLength_ThrowsInvalidBioException() + { + string tooLong = new('a', Bio.MaxLength + 1); + Assert.Throws(() => new Bio(tooLong)); + } + + [Test] + public void Constructor_ValidValue_SetsValue() + { + var bio = new Bio("I love competitive programming."); + Assert.That(bio.Value, Is.EqualTo("I love competitive programming.")); + } + + [Test] + public void Equality_DifferentValue_AreNotEqual() + { + var a = new Bio("Hello world."); + var b = new Bio("Goodbye world."); + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new Bio("Hello world."); + var b = new Bio("Hello world."); + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ImplicitOperator_ReturnsStringValue() + { + var bio = new Bio("Hello world."); + string value = bio; + Assert.That(value, Is.EqualTo("Hello world.")); + } + + [Test] + public void ToString_MatchesImplicitOperator() + { + var bio = new Bio("Hello world."); + Assert.That(bio.ToString(), Is.EqualTo((string)bio)); + } + + [Test] + public void ToString_ReturnsValue() + { + var bio = new Bio("Hello world."); + Assert.That(bio.ToString(), Is.EqualTo("Hello world.")); + } +} diff --git a/Algowars.Domain.Tests/User/ValueObjects/ImageUrlTests.cs b/Algowars.Domain.Tests/User/ValueObjects/ImageUrlTests.cs new file mode 100644 index 0000000..0d94559 --- /dev/null +++ b/Algowars.Domain.Tests/User/ValueObjects/ImageUrlTests.cs @@ -0,0 +1,84 @@ +using Algowars.Domain.User.Exceptions; +using Algowars.Domain.User.ValueObjects; + +namespace Algowars.Domain.Tests.User.ValueObjects; + +public class ImageUrlTests +{ + private const string ValidHttpUrl = "https://example.com/avatar.png"; + + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string path = new('a', ImageUrl.MaxLength - "https://x.co/".Length); + string atMax = $"https://x.co/{path}"; + Assert.DoesNotThrow(() => new ImageUrl(atMax)); + } + + [Test] + public void Constructor_EmptyOrWhitespace_ThrowsInvalidImageUrlException([Values("", " ", " ", null)] string? value) + { + Assert.Throws(() => new ImageUrl(value!)); + } + + [Test] + public void Constructor_ExceedsMaxLength_ThrowsInvalidImageUrlException() + { + string path = new('a', ImageUrl.MaxLength); + string tooLong = $"https://x.co/{path}"; + Assert.Throws(() => new ImageUrl(tooLong)); + } + + [Test] + public void Constructor_InvalidUrl_ThrowsInvalidImageUrlException( + [Values("not-a-url", "ftp://example.com/file.png", "example.com/avatar.png", "//example.com/avatar.png")] string value) + { + Assert.Throws(() => new ImageUrl(value)); + } + + [Test] + public void Constructor_ValidUrl_SetsValue( + [Values("https://example.com/avatar.png", "http://example.com/avatar.jpg", "https://avatars.githubusercontent.com/u/12345")] string value) + { + var imageUrl = new ImageUrl(value); + Assert.That(imageUrl.Value, Is.EqualTo(value)); + } + + [Test] + public void Equality_DifferentValue_AreNotEqual() + { + var a = new ImageUrl(ValidHttpUrl); + var b = new ImageUrl("https://example.com/other.png"); + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new ImageUrl(ValidHttpUrl); + var b = new ImageUrl(ValidHttpUrl); + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ImplicitOperator_ReturnsStringValue() + { + var imageUrl = new ImageUrl(ValidHttpUrl); + string value = imageUrl; + Assert.That(value, Is.EqualTo(ValidHttpUrl)); + } + + [Test] + public void ToString_MatchesImplicitOperator() + { + var imageUrl = new ImageUrl(ValidHttpUrl); + Assert.That(imageUrl.ToString(), Is.EqualTo((string)imageUrl)); + } + + [Test] + public void ToString_ReturnsValue() + { + var imageUrl = new ImageUrl(ValidHttpUrl); + Assert.That(imageUrl.ToString(), Is.EqualTo(ValidHttpUrl)); + } +} diff --git a/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs b/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs index d0f1e73..762781b 100644 --- a/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs +++ b/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs @@ -1,4 +1,4 @@ -using Algowars.Domain.User.Exceptions; +using Algowars.Domain.User.Exceptions; using Algowars.Domain.User.ValueObjects; namespace Algowars.Domain.Tests.User.ValueObjects; @@ -6,19 +6,10 @@ namespace Algowars.Domain.Tests.User.ValueObjects; public class UsernameTests { [Test] - public void Constructor_ValidValue_SetsValue() - { - var username = new Username("alice"); - Assert.That(username.Value, Is.EqualTo("alice")); - } - - [TestCase("")] - [TestCase(" ")] - [TestCase(" ")] - [TestCase(null)] - public void Constructor_EmptyOrWhitespace_ThrowsInvalidUsernameException(string? value) + public void Constructor_AtMaxLength_Succeeds() { - Assert.Throws(() => new Username(value!)); + string atMax = new('a', Username.MaxLength); + Assert.DoesNotThrow(() => new Username(atMax)); } [Test] @@ -29,17 +20,16 @@ public void Constructor_AtMinLength_Succeeds() } [Test] - public void Constructor_AtMaxLength_Succeeds() + public void Constructor_EmptyOrWhitespace_ThrowsInvalidUsernameException([Values("", " ", " ", null)] string? value) { - string atMax = new('a', Username.MaxLength); - Assert.DoesNotThrow(() => new Username(atMax)); + Assert.Throws(() => new Username(value!)); } [Test] - public void Constructor_OneBelowMinLength_ThrowsInvalidUsernameException() + public void Constructor_FarExceedsMaxLength_ThrowsInvalidUsernameException() { - string tooShort = new('a', Username.MinLength - 1); - Assert.Throws(() => new Username(tooShort)); + string veryLong = new('a', 1000); + Assert.Throws(() => new Username(veryLong)); } [Test] @@ -50,43 +40,38 @@ public void Constructor_OneAboveMaxLength_ThrowsInvalidUsernameException() } [Test] - public void Constructor_FarExceedsMaxLength_ThrowsInvalidUsernameException() + public void Constructor_OneBelowMinLength_ThrowsInvalidUsernameException() { - string veryLong = new('a', 1000); - Assert.Throws(() => new Username(veryLong)); + string tooShort = new('a', Username.MinLength - 1); + Assert.Throws(() => new Username(tooShort)); } - [TestCase("alice123")] - [TestCase("ALICE")] - [TestCase("Alice")] - [TestCase("123")] - [TestCase("a")] - public void Constructor_ValidCharacterVariations_Succeeds(string value) + [Test] + public void Constructor_ValidCharacterVariations_Succeeds([Values("alice123", "ALICE", "Alice", "123", "a")] string value) { Assert.DoesNotThrow(() => new Username(value)); } [Test] - public void Equality_SameValue_AreEqual() + public void Constructor_ValidValue_SetsValue() { - var a = new Username("alice"); - var b = new Username("alice"); - Assert.That(a, Is.EqualTo(b)); + var username = new Username("alice"); + Assert.That(username.Value, Is.EqualTo("alice")); } [Test] - public void Equality_DifferentValue_AreNotEqual() + public void Equality_DifferentCasing_AreNotEqual() { var a = new Username("alice"); - var b = new Username("bob"); + var b = new Username("Alice"); Assert.That(a, Is.Not.EqualTo(b)); } [Test] - public void Equality_DifferentCasing_AreNotEqual() + public void Equality_DifferentValue_AreNotEqual() { var a = new Username("alice"); - var b = new Username("Alice"); + var b = new Username("bob"); Assert.That(a, Is.Not.EqualTo(b)); } @@ -97,6 +82,14 @@ public void Equality_SameReference_AreEqual() Assert.That(a, Is.EqualTo(a)); } + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new Username("alice"); + var b = new Username("alice"); + Assert.That(a, Is.EqualTo(b)); + } + [Test] public void ImplicitOperator_ReturnsStringValue() { @@ -106,16 +99,16 @@ public void ImplicitOperator_ReturnsStringValue() } [Test] - public void ToString_ReturnsValue() + public void ToString_MatchesImplicitOperator() { var username = new Username("alice"); - Assert.That(username.ToString(), Is.EqualTo("alice")); + Assert.That(username.ToString(), Is.EqualTo((string)username)); } [Test] - public void ToString_MatchesImplicitOperator() + public void ToString_ReturnsValue() { var username = new Username("alice"); - Assert.That(username.ToString(), Is.EqualTo((string)username)); + Assert.That(username.ToString(), Is.EqualTo("alice")); } -} \ No newline at end of file +} diff --git a/Algowars.Domain/User/Entities/User.cs b/Algowars.Domain/User/Entities/User.cs index 0f8c5da..73781e3 100644 --- a/Algowars.Domain/User/Entities/User.cs +++ b/Algowars.Domain/User/Entities/User.cs @@ -1,4 +1,3 @@ - using Algowars.Domain.SeedWork; using Algowars.Domain.User.Exceptions; using Algowars.Domain.User.ValueObjects; @@ -7,14 +6,37 @@ namespace Algowars.Domain.User.Entities; public sealed class User(Username username, string sub) : AggregateRoot { - public Username Username { get; private set; } = username ?? throw new InvalidUsernameException("Username is required."); - - public string Sub { get; private set; } = string.IsNullOrWhiteSpace(sub) - ? throw new InvalidUserSubException() - : sub; - public void ChangeUsername(Username username) { + if (UsernameLastChangedAt.HasValue && + DateTime.UtcNow - UsernameLastChangedAt.Value < TimeSpan.FromDays(MaxDaysUntilUsernameChange)) + throw new UsernameCooldownException(UsernameLastChangedAt.Value); + Username = username; + UsernameLastChangedAt = DateTime.UtcNow; } + + public void UpdateBio(Bio? bio) + { + Bio = bio; + } + + public void UpdateImageUrl(ImageUrl? imageUrl) + { + ImageUrl = imageUrl; + } + + public Bio? Bio { get; private set; } + + public ImageUrl? ImageUrl { get; private set; } + + public string Sub { get; private set; } = string.IsNullOrWhiteSpace(sub) + ? throw new InvalidUserSubException() + : sub; + + public Username Username { get; private set; } = username ?? throw new InvalidUsernameException("Username is required."); + + public DateTime? UsernameLastChangedAt { get; private set; } + + private static readonly int MaxDaysUntilUsernameChange = 30; } diff --git a/Algowars.Domain/User/Exceptions/InvalidBioException.cs b/Algowars.Domain/User/Exceptions/InvalidBioException.cs new file mode 100644 index 0000000..2332557 --- /dev/null +++ b/Algowars.Domain/User/Exceptions/InvalidBioException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.User.Exceptions; + +public sealed class InvalidBioException : DomainException +{ + public InvalidBioException(string reason) + : base($"Bio is invalid: {reason}") { } +} \ No newline at end of file diff --git a/Algowars.Domain/User/Exceptions/InvalidImageUrlException.cs b/Algowars.Domain/User/Exceptions/InvalidImageUrlException.cs new file mode 100644 index 0000000..f7cb4db --- /dev/null +++ b/Algowars.Domain/User/Exceptions/InvalidImageUrlException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.User.Exceptions; + +public sealed class InvalidImageUrlException(string reason) : DomainException($"Image URL is invalid: {reason}") +{ +} \ No newline at end of file diff --git a/Algowars.Domain/User/Exceptions/UsernameCooldownException.cs b/Algowars.Domain/User/Exceptions/UsernameCooldownException.cs new file mode 100644 index 0000000..557aa30 --- /dev/null +++ b/Algowars.Domain/User/Exceptions/UsernameCooldownException.cs @@ -0,0 +1,9 @@ + + +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.User.Exceptions; + +public sealed class UsernameCooldownException(DateTime lastChangedAt) : DomainException($"Username can only be changed once every 30 days. Last changed at: {lastChangedAt}.") +{ +} diff --git a/Algowars.Domain/User/ValueObjects/Bio.cs b/Algowars.Domain/User/ValueObjects/Bio.cs new file mode 100644 index 0000000..dbdf4b5 --- /dev/null +++ b/Algowars.Domain/User/ValueObjects/Bio.cs @@ -0,0 +1,26 @@ +using Algowars.Domain.User.Exceptions; + +namespace Algowars.Domain.User.ValueObjects; + +public sealed record Bio +{ + public Bio(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidBioException("Bio cannot be empty."); + + if (value.Length > MaxLength) + throw new InvalidBioException($"Bio cannot exceed {MaxLength} characters."); + + Value = value; + } + + public static implicit operator string(Bio bio) => bio.Value; + + public override string ToString() => Value; + + + public static readonly int MaxLength = 500; + + public string Value { get; } +} diff --git a/Algowars.Domain/User/ValueObjects/ImageUrl.cs b/Algowars.Domain/User/ValueObjects/ImageUrl.cs new file mode 100644 index 0000000..639fd89 --- /dev/null +++ b/Algowars.Domain/User/ValueObjects/ImageUrl.cs @@ -0,0 +1,30 @@ +using Algowars.Domain.User.Exceptions; + +namespace Algowars.Domain.User.ValueObjects; + +public sealed record ImageUrl +{ + public ImageUrl(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidImageUrlException("Image URL cannot be empty."); + + if (value.Length > MaxLength) + throw new InvalidImageUrlException($"Image URL cannot exceed {MaxLength} characters."); + + if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) || + (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) + throw new InvalidImageUrlException("Image URL must be a valid HTTP or HTTPS URL."); + + Value = value; + } + + public static implicit operator string(ImageUrl url) => url.Value; + + public override string ToString() => Value; + + + public static readonly int MaxLength = 2048; + + public string Value { get; } +} diff --git a/Algowars.Domain/User/ValueObjects/Username.cs b/Algowars.Domain/User/ValueObjects/Username.cs index c0967b9..fb27c95 100644 --- a/Algowars.Domain/User/ValueObjects/Username.cs +++ b/Algowars.Domain/User/ValueObjects/Username.cs @@ -1,14 +1,9 @@ -using Algowars.Domain.User.Exceptions; +using Algowars.Domain.User.Exceptions; namespace Algowars.Domain.User.ValueObjects; public sealed record Username { - public static readonly int MaxLength = 20; - public static readonly int MinLength = 1; - - public string Value { get; } - public Username(string value) { if (string.IsNullOrWhiteSpace(value)) @@ -21,5 +16,12 @@ public Username(string value) } public static implicit operator string(Username username) => username.Value; + public override string ToString() => Value; -} \ No newline at end of file + + public static readonly int MaxLength = 20; + + public static readonly int MinLength = 1; + + public string Value { get; } +} From 8af9e02d39df4b9ac4f1514b7ad711ee6e3b13b7 Mon Sep 17 00:00:00 2001 From: admclamb Date: Thu, 11 Jun 2026 23:12:47 -0400 Subject: [PATCH 04/34] Adding problem domain --- Algowars.Domain/Algowars.Domain.csproj | 5 + .../Problem/Entities/CodeTemplate.cs | 19 ++++ Algowars.Domain/Problem/Entities/Example.cs | 19 ++++ Algowars.Domain/Problem/Entities/Problem.cs | 61 ++++++++++++ .../Problem/Entities/ProblemVersion.cs | 93 +++++++++++++++++++ Algowars.Domain/Problem/Entities/TestCase.cs | 19 ++++ .../Problem/Enums/DifficultyTier.cs | 8 ++ .../Problem/Enums/ProblemStatus.cs | 8 ++ .../Exceptions/InvalidDifficultyException.cs | 7 ++ .../Exceptions/InvalidMemoryLimitException.cs | 7 ++ .../Exceptions/InvalidQuestionException.cs | 7 ++ .../Exceptions/InvalidSlugException.cs | 9 ++ .../Exceptions/InvalidTimeLimitException.cs | 7 ++ .../Exceptions/InvalidTitleException.cs | 7 ++ .../ProblemVersionImmutableException.cs | 9 ++ .../ProblemVersionNotFoundException.cs | 7 ++ Algowars.Domain/Problem/IProblemRepository.cs | 12 +++ .../Problem/ValueObjects/Difficulty.cs | 33 +++++++ .../Problem/ValueObjects/MemoryLimit.cs | 20 ++++ .../Problem/ValueObjects/Question.cs | 24 +++++ Algowars.Domain/Problem/ValueObjects/Slug.cs | 40 ++++++++ .../Problem/ValueObjects/TimeLimit.cs | 20 ++++ Algowars.Domain/Problem/ValueObjects/Title.cs | 24 +++++ 23 files changed, 465 insertions(+) create mode 100644 Algowars.Domain/Problem/Entities/CodeTemplate.cs create mode 100644 Algowars.Domain/Problem/Entities/Example.cs create mode 100644 Algowars.Domain/Problem/Entities/Problem.cs create mode 100644 Algowars.Domain/Problem/Entities/ProblemVersion.cs create mode 100644 Algowars.Domain/Problem/Entities/TestCase.cs create mode 100644 Algowars.Domain/Problem/Enums/DifficultyTier.cs create mode 100644 Algowars.Domain/Problem/Enums/ProblemStatus.cs create mode 100644 Algowars.Domain/Problem/Exceptions/InvalidDifficultyException.cs create mode 100644 Algowars.Domain/Problem/Exceptions/InvalidMemoryLimitException.cs create mode 100644 Algowars.Domain/Problem/Exceptions/InvalidQuestionException.cs create mode 100644 Algowars.Domain/Problem/Exceptions/InvalidSlugException.cs create mode 100644 Algowars.Domain/Problem/Exceptions/InvalidTimeLimitException.cs create mode 100644 Algowars.Domain/Problem/Exceptions/InvalidTitleException.cs create mode 100644 Algowars.Domain/Problem/Exceptions/ProblemVersionImmutableException.cs create mode 100644 Algowars.Domain/Problem/Exceptions/ProblemVersionNotFoundException.cs create mode 100644 Algowars.Domain/Problem/IProblemRepository.cs create mode 100644 Algowars.Domain/Problem/ValueObjects/Difficulty.cs create mode 100644 Algowars.Domain/Problem/ValueObjects/MemoryLimit.cs create mode 100644 Algowars.Domain/Problem/ValueObjects/Question.cs create mode 100644 Algowars.Domain/Problem/ValueObjects/Slug.cs create mode 100644 Algowars.Domain/Problem/ValueObjects/TimeLimit.cs create mode 100644 Algowars.Domain/Problem/ValueObjects/Title.cs diff --git a/Algowars.Domain/Algowars.Domain.csproj b/Algowars.Domain/Algowars.Domain.csproj index b760144..e1d2a50 100644 --- a/Algowars.Domain/Algowars.Domain.csproj +++ b/Algowars.Domain/Algowars.Domain.csproj @@ -6,4 +6,9 @@ enable + + + + + diff --git a/Algowars.Domain/Problem/Entities/CodeTemplate.cs b/Algowars.Domain/Problem/Entities/CodeTemplate.cs new file mode 100644 index 0000000..4adeebf --- /dev/null +++ b/Algowars.Domain/Problem/Entities/CodeTemplate.cs @@ -0,0 +1,19 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Entities; + +public sealed class CodeTemplate : Entity +{ + private CodeTemplate() { } + + internal CodeTemplate(Guid languageId, string starterCode, string wrapperCode) + { + LanguageId = languageId; + StarterCode = starterCode ?? throw new ArgumentNullException(nameof(starterCode)); + WrapperCode = wrapperCode ?? throw new ArgumentNullException(nameof(wrapperCode)); + } + + public Guid LanguageId { get; private set; } + public string StarterCode { get; private set; } = string.Empty; + public string WrapperCode { get; private set; } = string.Empty; +} diff --git a/Algowars.Domain/Problem/Entities/Example.cs b/Algowars.Domain/Problem/Entities/Example.cs new file mode 100644 index 0000000..23166e5 --- /dev/null +++ b/Algowars.Domain/Problem/Entities/Example.cs @@ -0,0 +1,19 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Entities; + +public sealed class Example : Entity +{ + private Example() { } + + internal Example(string input, string output, string? explanation = null) + { + Input = input ?? throw new ArgumentNullException(nameof(input)); + Output = output ?? throw new ArgumentNullException(nameof(output)); + Explanation = explanation; + } + + public string? Explanation { get; private set; } + public string Input { get; private set; } = string.Empty; + public string Output { get; private set; } = string.Empty; +} diff --git a/Algowars.Domain/Problem/Entities/Problem.cs b/Algowars.Domain/Problem/Entities/Problem.cs new file mode 100644 index 0000000..a57e49e --- /dev/null +++ b/Algowars.Domain/Problem/Entities/Problem.cs @@ -0,0 +1,61 @@ +using Algowars.Domain.Problem.Enums; +using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problem.ValueObjects; +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Entities; + +public sealed class Problem : AggregateRoot +{ + public Problem(Slug slug, Title title, Question question, Difficulty difficulty, TimeLimit timeLimit, MemoryLimit memoryLimit) + { + Slug = slug ?? throw new ArgumentNullException(nameof(slug)); + Status = ProblemStatus.Draft; + _versions.Add(new ProblemVersion(1, title, question, difficulty, timeLimit, memoryLimit)); + } + + public void Archive() + { + Status = ProblemStatus.Archived; + } + + public ProblemVersion CreateNewVersion() + { + var latest = CurrentVersion ?? _versions.Last(); + int nextNumber = _versions.Max(v => v.VersionNumber) + 1; + var newVersion = new ProblemVersion(nextNumber, latest.Title, latest.Question, latest.Difficulty, latest.TimeLimit, latest.MemoryLimit); + _versions.Add(newVersion); + return newVersion; + } + + public void Publish(Guid versionId) + { + var version = _versions.FirstOrDefault(v => v.Id == versionId) + ?? throw new ProblemVersionNotFoundException(versionId); + + version.Publish(); + Status = ProblemStatus.Published; + } + + public void UpdateSlug(Slug slug) + { + Slug = slug ?? throw new ArgumentNullException(nameof(slug)); + } + + private Problem() { } + + public ProblemVersion? CurrentVersion => _versions + .Where(v => v.IsPublished) + .OrderByDescending(v => v.VersionNumber) + .FirstOrDefault(); + + public ProblemVersion DraftVersion => _versions + .OrderByDescending(v => v.VersionNumber) + .First(v => !v.IsPublished); + + public Slug Slug { get; private set; } = null!; + public ProblemStatus Status { get; private set; } + public IReadOnlyCollection Versions => _versions.AsReadOnly(); + + private readonly List _versions = []; +} diff --git a/Algowars.Domain/Problem/Entities/ProblemVersion.cs b/Algowars.Domain/Problem/Entities/ProblemVersion.cs new file mode 100644 index 0000000..483536a --- /dev/null +++ b/Algowars.Domain/Problem/Entities/ProblemVersion.cs @@ -0,0 +1,93 @@ +using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problem.ValueObjects; +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Entities; + +public sealed class ProblemVersion : Entity +{ + internal ProblemVersion(int versionNumber, Title title, Question question, Difficulty difficulty, TimeLimit timeLimit, MemoryLimit memoryLimit) + { + VersionNumber = versionNumber; + Title = title ?? throw new ArgumentNullException(nameof(title)); + Question = question ?? throw new ArgumentNullException(nameof(question)); + Difficulty = difficulty ?? throw new ArgumentNullException(nameof(difficulty)); + TimeLimit = timeLimit ?? throw new ArgumentNullException(nameof(timeLimit)); + MemoryLimit = memoryLimit ?? throw new ArgumentNullException(nameof(memoryLimit)); + } + + public void AddCodeTemplate(Guid languageId, string starterCode, string wrapperCode) + { + if (IsPublished) + throw new ProblemVersionImmutableException(); + + _codeTemplates.Add(new CodeTemplate(languageId, starterCode, wrapperCode)); + } + + public void AddExample(string input, string output, string? explanation = null) + { + _examples.Add(new Example(input, output, explanation)); + } + + public void AddTestCase(string input, string expectedOutput, bool isHidden = true) + { + if (IsPublished) + throw new ProblemVersionImmutableException(); + + _testCases.Add(new TestCase(input, expectedOutput, isHidden)); + } + + public void UpdateDifficulty(Difficulty difficulty) + { + Difficulty = difficulty ?? throw new ArgumentNullException(nameof(difficulty)); + } + + public void UpdateMemoryLimit(MemoryLimit memoryLimit) + { + if (IsPublished) + throw new ProblemVersionImmutableException(); + + MemoryLimit = memoryLimit ?? throw new ArgumentNullException(nameof(memoryLimit)); + } + + public void UpdateQuestion(Question question) + { + Question = question ?? throw new ArgumentNullException(nameof(question)); + } + + public void UpdateTimeLimit(TimeLimit timeLimit) + { + if (IsPublished) + throw new ProblemVersionImmutableException(); + + TimeLimit = timeLimit ?? throw new ArgumentNullException(nameof(timeLimit)); + } + + public void UpdateTitle(Title title) + { + Title = title ?? throw new ArgumentNullException(nameof(title)); + } + + internal void Publish() + { + PublishedAt = DateTime.UtcNow; + } + + private ProblemVersion() { } + + public IReadOnlyCollection CodeTemplates => _codeTemplates.AsReadOnly(); + public Difficulty Difficulty { get; private set; } = null!; + public IReadOnlyCollection Examples => _examples.AsReadOnly(); + public bool IsPublished => PublishedAt.HasValue; + public MemoryLimit MemoryLimit { get; private set; } = null!; + public DateTime? PublishedAt { get; private set; } + public Question Question { get; private set; } = null!; + public IReadOnlyCollection TestCases => _testCases.AsReadOnly(); + public TimeLimit TimeLimit { get; private set; } = null!; + public Title Title { get; private set; } = null!; + public int VersionNumber { get; private set; } + + private readonly List _codeTemplates = []; + private readonly List _examples = []; + private readonly List _testCases = []; +} diff --git a/Algowars.Domain/Problem/Entities/TestCase.cs b/Algowars.Domain/Problem/Entities/TestCase.cs new file mode 100644 index 0000000..83b17f0 --- /dev/null +++ b/Algowars.Domain/Problem/Entities/TestCase.cs @@ -0,0 +1,19 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Entities; + +public sealed class TestCase : Entity +{ + private TestCase() { } + + internal TestCase(string input, string expectedOutput, bool isHidden = true) + { + Input = input ?? throw new ArgumentNullException(nameof(input)); + ExpectedOutput = expectedOutput ?? throw new ArgumentNullException(nameof(expectedOutput)); + IsHidden = isHidden; + } + + public string ExpectedOutput { get; private set; } = string.Empty; + public string Input { get; private set; } = string.Empty; + public bool IsHidden { get; private set; } +} diff --git a/Algowars.Domain/Problem/Enums/DifficultyTier.cs b/Algowars.Domain/Problem/Enums/DifficultyTier.cs new file mode 100644 index 0000000..8f52e15 --- /dev/null +++ b/Algowars.Domain/Problem/Enums/DifficultyTier.cs @@ -0,0 +1,8 @@ +namespace Algowars.Domain.Problem.Enums; + +public enum DifficultyTier +{ + Easy = 1, + Medium = 2, + Hard = 3 +} diff --git a/Algowars.Domain/Problem/Enums/ProblemStatus.cs b/Algowars.Domain/Problem/Enums/ProblemStatus.cs new file mode 100644 index 0000000..632a152 --- /dev/null +++ b/Algowars.Domain/Problem/Enums/ProblemStatus.cs @@ -0,0 +1,8 @@ +namespace Algowars.Domain.Problem.Enums; + +public enum ProblemStatus +{ + Draft = 1, + Published = 2, + Archived = 3 +} diff --git a/Algowars.Domain/Problem/Exceptions/InvalidDifficultyException.cs b/Algowars.Domain/Problem/Exceptions/InvalidDifficultyException.cs new file mode 100644 index 0000000..e491e9f --- /dev/null +++ b/Algowars.Domain/Problem/Exceptions/InvalidDifficultyException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Exceptions; + +public sealed class InvalidDifficultyException(string reason) : DomainException($"Difficulty is invalid: {reason}") +{ +} diff --git a/Algowars.Domain/Problem/Exceptions/InvalidMemoryLimitException.cs b/Algowars.Domain/Problem/Exceptions/InvalidMemoryLimitException.cs new file mode 100644 index 0000000..a6e0bf2 --- /dev/null +++ b/Algowars.Domain/Problem/Exceptions/InvalidMemoryLimitException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Exceptions; + +public sealed class InvalidMemoryLimitException(string reason) : DomainException($"Memory limit is invalid: {reason}") +{ +} diff --git a/Algowars.Domain/Problem/Exceptions/InvalidQuestionException.cs b/Algowars.Domain/Problem/Exceptions/InvalidQuestionException.cs new file mode 100644 index 0000000..d0c32a5 --- /dev/null +++ b/Algowars.Domain/Problem/Exceptions/InvalidQuestionException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Exceptions; + +public sealed class InvalidQuestionException(string reason) : DomainException($"Question is invalid: {reason}") +{ +} diff --git a/Algowars.Domain/Problem/Exceptions/InvalidSlugException.cs b/Algowars.Domain/Problem/Exceptions/InvalidSlugException.cs new file mode 100644 index 0000000..e9271de --- /dev/null +++ b/Algowars.Domain/Problem/Exceptions/InvalidSlugException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Exceptions; + +public sealed class InvalidSlugException : DomainException +{ + public InvalidSlugException(string reason) + : base($"Slug is invalid: {reason}") { } +} diff --git a/Algowars.Domain/Problem/Exceptions/InvalidTimeLimitException.cs b/Algowars.Domain/Problem/Exceptions/InvalidTimeLimitException.cs new file mode 100644 index 0000000..a5a24ea --- /dev/null +++ b/Algowars.Domain/Problem/Exceptions/InvalidTimeLimitException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Exceptions; + +public sealed class InvalidTimeLimitException(string reason) : DomainException($"Time limit is invalid: {reason}") +{ +} diff --git a/Algowars.Domain/Problem/Exceptions/InvalidTitleException.cs b/Algowars.Domain/Problem/Exceptions/InvalidTitleException.cs new file mode 100644 index 0000000..03f97a1 --- /dev/null +++ b/Algowars.Domain/Problem/Exceptions/InvalidTitleException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Exceptions; + +public sealed class InvalidTitleException(string reason) : DomainException($"Title is invalid: {reason}") +{ +} diff --git a/Algowars.Domain/Problem/Exceptions/ProblemVersionImmutableException.cs b/Algowars.Domain/Problem/Exceptions/ProblemVersionImmutableException.cs new file mode 100644 index 0000000..359efb1 --- /dev/null +++ b/Algowars.Domain/Problem/Exceptions/ProblemVersionImmutableException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Exceptions; + +public sealed class ProblemVersionImmutableException : DomainException +{ + public ProblemVersionImmutableException() + : base("Cannot modify judging-critical fields on a published version.") { } +} diff --git a/Algowars.Domain/Problem/Exceptions/ProblemVersionNotFoundException.cs b/Algowars.Domain/Problem/Exceptions/ProblemVersionNotFoundException.cs new file mode 100644 index 0000000..88c835d --- /dev/null +++ b/Algowars.Domain/Problem/Exceptions/ProblemVersionNotFoundException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Problem.Exceptions; + +public sealed class ProblemVersionNotFoundException(Guid versionId) : DomainException($"Problem version '{versionId}' was not found.") +{ +} diff --git a/Algowars.Domain/Problem/IProblemRepository.cs b/Algowars.Domain/Problem/IProblemRepository.cs new file mode 100644 index 0000000..d0698ae --- /dev/null +++ b/Algowars.Domain/Problem/IProblemRepository.cs @@ -0,0 +1,12 @@ +using Algowars.Domain.Problem.Entities; +using Algowars.Domain.Problem.ValueObjects; + +namespace Algowars.Domain.Problem; + +public interface IProblemRepository +{ + Task AddAsync(Entities.Problem problem); + Task FindByIdAsync(Guid id); + Task FindBySlugAsync(Slug slug); + Task UpdateAsync(Entities.Problem problem); +} diff --git a/Algowars.Domain/Problem/ValueObjects/Difficulty.cs b/Algowars.Domain/Problem/ValueObjects/Difficulty.cs new file mode 100644 index 0000000..330c6af --- /dev/null +++ b/Algowars.Domain/Problem/ValueObjects/Difficulty.cs @@ -0,0 +1,33 @@ +using Algowars.Domain.Problem.Enums; +using Algowars.Domain.Problem.Exceptions; + +namespace Algowars.Domain.Problem.ValueObjects; + +public sealed record Difficulty +{ + public Difficulty(int value) + { + if (value < MinValue) + throw new InvalidDifficultyException($"Difficulty cannot be less than {MinValue}."); + + Value = value; + } + + public override string ToString() => $"{Value} ({Tier})"; + + public static readonly int EasyMax = 1000; + public static readonly int EasyMin = 0; + public static readonly int HardMin = 2001; + public static readonly int MediumMax = 2000; + public static readonly int MediumMin = 1001; + public static readonly int MinValue = 0; + + public DifficultyTier Tier => Value switch + { + <= 1000 => DifficultyTier.Easy, + <= 2000 => DifficultyTier.Medium, + _ => DifficultyTier.Hard + }; + + public int Value { get; } +} diff --git a/Algowars.Domain/Problem/ValueObjects/MemoryLimit.cs b/Algowars.Domain/Problem/ValueObjects/MemoryLimit.cs new file mode 100644 index 0000000..f11592f --- /dev/null +++ b/Algowars.Domain/Problem/ValueObjects/MemoryLimit.cs @@ -0,0 +1,20 @@ +using Algowars.Domain.Problem.Exceptions; + +namespace Algowars.Domain.Problem.ValueObjects; + +public sealed record MemoryLimit +{ + public MemoryLimit(int megabytes) + { + if (megabytes < MinMegabytes || megabytes > MaxMegabytes) + throw new InvalidMemoryLimitException($"Memory limit must be between {MinMegabytes}MB and {MaxMegabytes}MB."); + + Megabytes = megabytes; + } + + public override string ToString() => $"{Megabytes}MB"; + + public static readonly int MaxMegabytes = 512; + public static readonly int MinMegabytes = 16; + public int Megabytes { get; } +} diff --git a/Algowars.Domain/Problem/ValueObjects/Question.cs b/Algowars.Domain/Problem/ValueObjects/Question.cs new file mode 100644 index 0000000..014e6c5 --- /dev/null +++ b/Algowars.Domain/Problem/ValueObjects/Question.cs @@ -0,0 +1,24 @@ +using Algowars.Domain.Problem.Exceptions; + +namespace Algowars.Domain.Problem.ValueObjects; + +public sealed record Question +{ + public Question(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidQuestionException("Question cannot be empty."); + + if (value.Length < MinLength || value.Length > MaxLength) + throw new InvalidQuestionException($"Question must be between {MinLength} and {MaxLength} characters."); + + Value = value; + } + + public static implicit operator string(Question question) => question.Value; + public override string ToString() => Value; + + public static readonly int MaxLength = 10000; + public static readonly int MinLength = 50; + public string Value { get; } +} diff --git a/Algowars.Domain/Problem/ValueObjects/Slug.cs b/Algowars.Domain/Problem/ValueObjects/Slug.cs new file mode 100644 index 0000000..bba3806 --- /dev/null +++ b/Algowars.Domain/Problem/ValueObjects/Slug.cs @@ -0,0 +1,40 @@ +using System.Text.RegularExpressions; +using Algowars.Domain.Problem.Exceptions; + +namespace Algowars.Domain.Problem.ValueObjects; + +public sealed record Slug +{ + private static readonly Regex ValidSlugPattern = new(@"^[a-z0-9]+(?:-[a-z0-9]+)*$", RegexOptions.Compiled); + + public Slug(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidSlugException("Slug cannot be empty."); + + if (value.Length < MinLength || value.Length > MaxLength) + throw new InvalidSlugException($"Slug must be between {MinLength} and {MaxLength} characters."); + + if (!ValidSlugPattern.IsMatch(value)) + throw new InvalidSlugException("Slug must be lowercase, contain only letters, numbers, and hyphens, and cannot start or end with a hyphen."); + + Value = value; + } + + public static Slug FromTitle(Title title) + { + var slug = title.Value.ToLowerInvariant(); + slug = Regex.Replace(slug, @"[^a-z0-9\s-]", string.Empty); + slug = Regex.Replace(slug, @"\s+", "-"); + slug = Regex.Replace(slug, @"-+", "-"); + slug = slug.Trim('-'); + return new Slug(slug); + } + + public static implicit operator string(Slug slug) => slug.Value; + public override string ToString() => Value; + + public static readonly int MaxLength = 200; + public static readonly int MinLength = 3; + public string Value { get; } +} diff --git a/Algowars.Domain/Problem/ValueObjects/TimeLimit.cs b/Algowars.Domain/Problem/ValueObjects/TimeLimit.cs new file mode 100644 index 0000000..43d4d42 --- /dev/null +++ b/Algowars.Domain/Problem/ValueObjects/TimeLimit.cs @@ -0,0 +1,20 @@ +using Algowars.Domain.Problem.Exceptions; + +namespace Algowars.Domain.Problem.ValueObjects; + +public sealed record TimeLimit +{ + public TimeLimit(int milliseconds) + { + if (milliseconds < MinMilliseconds || milliseconds > MaxMilliseconds) + throw new InvalidTimeLimitException($"Time limit must be between {MinMilliseconds}ms and {MaxMilliseconds}ms."); + + Milliseconds = milliseconds; + } + + public override string ToString() => $"{Milliseconds}ms"; + + public static readonly int MaxMilliseconds = 10000; + public static readonly int MinMilliseconds = 100; + public int Milliseconds { get; } +} diff --git a/Algowars.Domain/Problem/ValueObjects/Title.cs b/Algowars.Domain/Problem/ValueObjects/Title.cs new file mode 100644 index 0000000..31c4662 --- /dev/null +++ b/Algowars.Domain/Problem/ValueObjects/Title.cs @@ -0,0 +1,24 @@ +using Algowars.Domain.Problem.Exceptions; + +namespace Algowars.Domain.Problem.ValueObjects; + +public sealed record Title +{ + public Title(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidTitleException("Title cannot be empty."); + + if (value.Length < MinLength || value.Length > MaxLength) + throw new InvalidTitleException($"Title must be between {MinLength} and {MaxLength} characters."); + + Value = value; + } + + public static implicit operator string(Title title) => title.Value; + public override string ToString() => Value; + + public static readonly int MaxLength = 200; + public static readonly int MinLength = 3; + public string Value { get; } +} From 1e0d95c65ab9e2c30a6022b2ddb4b9ba84eab473 Mon Sep 17 00:00:00 2001 From: admclamb Date: Thu, 11 Jun 2026 23:22:52 -0400 Subject: [PATCH 05/34] add difficulty test --- .../Problem/ValueObjects/DifficultyTests.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Algowars.Domain.Tests/Problem/ValueObjects/DifficultyTests.cs diff --git a/Algowars.Domain.Tests/Problem/ValueObjects/DifficultyTests.cs b/Algowars.Domain.Tests/Problem/ValueObjects/DifficultyTests.cs new file mode 100644 index 0000000..3b0847d --- /dev/null +++ b/Algowars.Domain.Tests/Problem/ValueObjects/DifficultyTests.cs @@ -0,0 +1,63 @@ +using Algowars.Domain.Problem.Enums; +using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problem.ValueObjects; + +namespace Algowars.Domain.Tests.Problem.ValueObjects; + +public class DifficultyTests +{ + [Test] + public void Constructor_AtMinValue_Succeeds() + { + var difficulty = new Difficulty(Difficulty.MinValue); + + Assert.That(difficulty.Value, Is.EqualTo(Difficulty.MinValue)); + } + + [Test] + public void Constructor_BelowMinValue_ThrowsInvalidDifficultyException() + { + Assert.Throws(() => new Difficulty(Difficulty.MinValue - 1)); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new Difficulty(100); + var b = new Difficulty(200); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new Difficulty(500); + var b = new Difficulty(500); + + Assert.That(a, Is.EqualTo(b)); + } + + [TestCase(0, DifficultyTier.Easy)] + [TestCase(500, DifficultyTier.Easy)] + [TestCase(1000, DifficultyTier.Easy)] + [TestCase(1001, DifficultyTier.Medium)] + [TestCase(1500, DifficultyTier.Medium)] + [TestCase(2000, DifficultyTier.Medium)] + [TestCase(2001, DifficultyTier.Hard)] + [TestCase(3000, DifficultyTier.Hard)] + public void Tier_ReturnsCorrectTierForValue(int value, DifficultyTier expectedTier) + { + var difficulty = new Difficulty(value); + + Assert.That(difficulty.Tier, Is.EqualTo(expectedTier)); + } + + [Test] + public void ToString_IncludesValueAndTier() + { + var difficulty = new Difficulty(500); + + Assert.That(difficulty.ToString(), Does.Contain("500").And.Contain("Easy")); + } +} From 6104469180f8efdd8eb12f8c13f5460b2d118da9 Mon Sep 17 00:00:00 2001 From: admclamb Date: Fri, 12 Jun 2026 00:41:59 -0400 Subject: [PATCH 06/34] Finish domains --- .../Language/Entities/LanguageTests.cs | 146 +++++++++ .../Entities/LanguageVersionEntryTests.cs | 63 ++++ .../ValueObjects/LanguageNameTests.cs | 77 +++++ .../ValueObjects/LanguageSlugTests.cs | 127 ++++++++ .../ValueObjects/LanguageVersionTests.cs | 71 +++++ .../Problem/Entities/ProblemTests.cs | 179 +++++++++++ .../Problem/Entities/ProblemVersionTests.cs | 280 ++++++++++++++++++ .../Problem/ValueObjects/DifficultyTests.cs | 6 +- .../Problem/ValueObjects/MemoryLimitTests.cs | 57 ++++ .../Problem/ValueObjects/QuestionTests.cs | 87 ++++++ .../Problem/ValueObjects/SlugTests.cs | 139 +++++++++ .../Problem/ValueObjects/TimeLimitTests.cs | 57 ++++ .../Problem/ValueObjects/TitleTests.cs | 87 ++++++ .../Entities/SubmissionResultTests.cs | 131 ++++++++ .../Submissions/Entities/SubmissionTests.cs | 187 ++++++++++++ .../ValueObjects/SourceCodeTests.cs | 71 +++++ .../User/Entities/UserTests.cs | 6 +- .../User/ValueObjects/BioTests.cs | 4 +- .../User/ValueObjects/ImageUrlTests.cs | 4 +- .../User/ValueObjects/UsernameTests.cs | 4 +- Algowars.Domain/Algowars.Domain.csproj | 5 - .../Languages/Entities/Language.cs | 51 ++++ .../Entities/LanguageVersionEntry.cs | 25 ++ .../Languages/Enums/LanguageStatus.cs | 7 + .../Languages/Enums/LanguageVersionStatus.cs | 7 + .../InvalidLanguageNameException.cs | 9 + .../InvalidLanguageSlugException.cs | 7 + .../InvalidLanguageVersionException.cs | 7 + .../LanguageVersionNotFoundException.cs | 7 + .../Languages/ILanguageRepository.cs | 11 + .../Languages/ValueObjects/LanguageName.cs | 24 ++ .../Languages/ValueObjects/LanguageSlug.cs | 43 +++ .../Languages/ValueObjects/LanguageVersion.cs | 23 ++ .../Entities/CodeTemplate.cs | 2 +- .../{Problem => Problems}/Entities/Example.cs | 2 +- .../{Problem => Problems}/Entities/Problem.cs | 8 +- .../Entities/ProblemVersion.cs | 6 +- .../Entities/TestCase.cs | 2 +- .../Enums/DifficultyTier.cs | 2 +- .../Enums/ProblemStatus.cs | 2 +- .../Exceptions/InvalidDifficultyException.cs | 2 +- .../Exceptions/InvalidMemoryLimitException.cs | 2 +- .../Exceptions/InvalidQuestionException.cs | 2 +- .../Exceptions/InvalidSlugException.cs | 2 +- .../Exceptions/InvalidTimeLimitException.cs | 2 +- .../Exceptions/InvalidTitleException.cs | 2 +- .../ProblemVersionImmutableException.cs | 2 +- .../ProblemVersionNotFoundException.cs | 2 +- .../IProblemRepository.cs | 6 +- .../ValueObjects/Difficulty.cs | 6 +- .../ValueObjects/MemoryLimit.cs | 4 +- .../ValueObjects/Question.cs | 4 +- .../ValueObjects/Slug.cs | 15 +- .../ValueObjects/TimeLimit.cs | 4 +- .../ValueObjects/Title.cs | 4 +- .../Submissions/Entities/Submission.cs | 75 +++++ .../Submissions/Entities/SubmissionResult.cs | 45 +++ .../Enums/SubmissionResultStatus.cs | 13 + .../Submissions/Enums/SubmissionStatus.cs | 9 + .../Submissions/Enums/SubmissionType.cs | 7 + .../Exceptions/InvalidSourceCodeException.cs | 9 + .../InvalidSubmissionStateException.cs | 9 + .../SubmissionNotCompleteException.cs | 9 + .../SubmissionResultNotFoundException.cs | 9 + .../Submissions/ISubmissionRepository.cs | 10 + .../Submissions/ValueObjects/SourceCode.cs | 23 ++ .../{User => Users}/Entities/User.cs | 6 +- .../Exceptions/InvalidBioException.cs | 4 +- .../Exceptions/InvalidImageUrlException.cs | 4 +- .../Exceptions/InvalidUserSubException.cs | 4 +- .../Exceptions/InvalidUsernameException.cs | 4 +- .../Exceptions/UsernameCooldownException.cs | 4 +- .../{User => Users}/ValueObjects/Bio.cs | 4 +- .../{User => Users}/ValueObjects/ImageUrl.cs | 4 +- .../{User => Users}/ValueObjects/Username.cs | 4 +- 75 files changed, 2272 insertions(+), 76 deletions(-) create mode 100644 Algowars.Domain.Tests/Language/Entities/LanguageTests.cs create mode 100644 Algowars.Domain.Tests/Language/Entities/LanguageVersionEntryTests.cs create mode 100644 Algowars.Domain.Tests/Language/ValueObjects/LanguageNameTests.cs create mode 100644 Algowars.Domain.Tests/Language/ValueObjects/LanguageSlugTests.cs create mode 100644 Algowars.Domain.Tests/Language/ValueObjects/LanguageVersionTests.cs create mode 100644 Algowars.Domain.Tests/Problem/Entities/ProblemTests.cs create mode 100644 Algowars.Domain.Tests/Problem/Entities/ProblemVersionTests.cs create mode 100644 Algowars.Domain.Tests/Problem/ValueObjects/MemoryLimitTests.cs create mode 100644 Algowars.Domain.Tests/Problem/ValueObjects/QuestionTests.cs create mode 100644 Algowars.Domain.Tests/Problem/ValueObjects/SlugTests.cs create mode 100644 Algowars.Domain.Tests/Problem/ValueObjects/TimeLimitTests.cs create mode 100644 Algowars.Domain.Tests/Problem/ValueObjects/TitleTests.cs create mode 100644 Algowars.Domain.Tests/Submissions/Entities/SubmissionResultTests.cs create mode 100644 Algowars.Domain.Tests/Submissions/Entities/SubmissionTests.cs create mode 100644 Algowars.Domain.Tests/Submissions/ValueObjects/SourceCodeTests.cs create mode 100644 Algowars.Domain/Languages/Entities/Language.cs create mode 100644 Algowars.Domain/Languages/Entities/LanguageVersionEntry.cs create mode 100644 Algowars.Domain/Languages/Enums/LanguageStatus.cs create mode 100644 Algowars.Domain/Languages/Enums/LanguageVersionStatus.cs create mode 100644 Algowars.Domain/Languages/Exceptions/InvalidLanguageNameException.cs create mode 100644 Algowars.Domain/Languages/Exceptions/InvalidLanguageSlugException.cs create mode 100644 Algowars.Domain/Languages/Exceptions/InvalidLanguageVersionException.cs create mode 100644 Algowars.Domain/Languages/Exceptions/LanguageVersionNotFoundException.cs create mode 100644 Algowars.Domain/Languages/ILanguageRepository.cs create mode 100644 Algowars.Domain/Languages/ValueObjects/LanguageName.cs create mode 100644 Algowars.Domain/Languages/ValueObjects/LanguageSlug.cs create mode 100644 Algowars.Domain/Languages/ValueObjects/LanguageVersion.cs rename Algowars.Domain/{Problem => Problems}/Entities/CodeTemplate.cs (93%) rename Algowars.Domain/{Problem => Problems}/Entities/Example.cs (92%) rename Algowars.Domain/{Problem => Problems}/Entities/Problem.cs (91%) rename Algowars.Domain/{Problem => Problems}/Entities/ProblemVersion.cs (96%) rename Algowars.Domain/{Problem => Problems}/Entities/TestCase.cs (92%) rename Algowars.Domain/{Problem => Problems}/Enums/DifficultyTier.cs (64%) rename Algowars.Domain/{Problem => Problems}/Enums/ProblemStatus.cs (66%) rename Algowars.Domain/{Problem => Problems}/Exceptions/InvalidDifficultyException.cs (76%) rename Algowars.Domain/{Problem => Problems}/Exceptions/InvalidMemoryLimitException.cs (76%) rename Algowars.Domain/{Problem => Problems}/Exceptions/InvalidQuestionException.cs (76%) rename Algowars.Domain/{Problem => Problems}/Exceptions/InvalidSlugException.cs (80%) rename Algowars.Domain/{Problem => Problems}/Exceptions/InvalidTimeLimitException.cs (76%) rename Algowars.Domain/{Problem => Problems}/Exceptions/InvalidTitleException.cs (75%) rename Algowars.Domain/{Problem => Problems}/Exceptions/ProblemVersionImmutableException.cs (83%) rename Algowars.Domain/{Problem => Problems}/Exceptions/ProblemVersionNotFoundException.cs (78%) rename Algowars.Domain/{Problem => Problems}/IProblemRepository.cs (66%) rename Algowars.Domain/{Problem => Problems}/ValueObjects/Difficulty.cs (85%) rename Algowars.Domain/{Problem => Problems}/ValueObjects/MemoryLimit.cs (84%) rename Algowars.Domain/{Problem => Problems}/ValueObjects/Question.cs (87%) rename Algowars.Domain/{Problem => Problems}/ValueObjects/Slug.cs (63%) rename Algowars.Domain/{Problem => Problems}/ValueObjects/TimeLimit.cs (85%) rename Algowars.Domain/{Problem => Problems}/ValueObjects/Title.cs (87%) create mode 100644 Algowars.Domain/Submissions/Entities/Submission.cs create mode 100644 Algowars.Domain/Submissions/Entities/SubmissionResult.cs create mode 100644 Algowars.Domain/Submissions/Enums/SubmissionResultStatus.cs create mode 100644 Algowars.Domain/Submissions/Enums/SubmissionStatus.cs create mode 100644 Algowars.Domain/Submissions/Enums/SubmissionType.cs create mode 100644 Algowars.Domain/Submissions/Exceptions/InvalidSourceCodeException.cs create mode 100644 Algowars.Domain/Submissions/Exceptions/InvalidSubmissionStateException.cs create mode 100644 Algowars.Domain/Submissions/Exceptions/SubmissionNotCompleteException.cs create mode 100644 Algowars.Domain/Submissions/Exceptions/SubmissionResultNotFoundException.cs create mode 100644 Algowars.Domain/Submissions/ISubmissionRepository.cs create mode 100644 Algowars.Domain/Submissions/ValueObjects/SourceCode.cs rename Algowars.Domain/{User => Users}/Entities/User.cs (90%) rename Algowars.Domain/{User => Users}/Exceptions/InvalidBioException.cs (66%) rename Algowars.Domain/{User => Users}/Exceptions/InvalidImageUrlException.cs (60%) rename Algowars.Domain/{User => Users}/Exceptions/InvalidUserSubException.cs (65%) rename Algowars.Domain/{User => Users}/Exceptions/InvalidUsernameException.cs (60%) rename Algowars.Domain/{User => Users}/Exceptions/UsernameCooldownException.cs (81%) rename Algowars.Domain/{User => Users}/ValueObjects/Bio.cs (86%) rename Algowars.Domain/{User => Users}/ValueObjects/ImageUrl.cs (90%) rename Algowars.Domain/{User => Users}/ValueObjects/Username.cs (88%) diff --git a/Algowars.Domain.Tests/Language/Entities/LanguageTests.cs b/Algowars.Domain.Tests/Language/Entities/LanguageTests.cs new file mode 100644 index 0000000..5285dcc --- /dev/null +++ b/Algowars.Domain.Tests/Language/Entities/LanguageTests.cs @@ -0,0 +1,146 @@ +using Algowars.Domain.Languages.Enums; +using Algowars.Domain.Languages.Exceptions; +using Algowars.Domain.Languages.ValueObjects; +using LanguageEntity = Algowars.Domain.Languages.Entities.Language; + +namespace Algowars.Domain.Tests.Language.Entities; + +public class LanguageTests +{ + private static readonly LanguageName ValidName = new("Python"); + private static readonly LanguageSlug ValidSlug = new("python"); + private static readonly LanguageVersion ValidVersion = new("3.11"); + + private static LanguageEntity CreateLanguage() => new(ValidName, ValidSlug); + + [Test] + public void Activate_WhenInactive_SetsStatusToActive() + { + var language = CreateLanguage(); + language.Deactivate(); + + language.Activate(); + + Assert.That(language.Status, Is.EqualTo(LanguageStatus.Active)); + } + + [Test] + public void AddVersion_AddsToVersionsCollection() + { + var language = CreateLanguage(); + + language.AddVersion(ValidVersion); + + Assert.That(language.Versions, Has.Count.EqualTo(1)); + } + + [Test] + public void AddVersion_MultipleVersions_AllAdded() + { + var language = CreateLanguage(); + + language.AddVersion(new LanguageVersion("3.10")); + language.AddVersion(new LanguageVersion("3.11")); + + Assert.That(language.Versions, Has.Count.EqualTo(2)); + } + + [Test] + public void AddVersion_ReturnsEntryWithActiveStatus() + { + var language = CreateLanguage(); + + var entry = language.AddVersion(ValidVersion); + + using (Assert.EnterMultipleScope()) + { + Assert.That(entry.IsActive, Is.True); + Assert.That(entry.Version, Is.EqualTo(ValidVersion)); + } + } + + [Test] + public void Constructor_SetsNameAndSlug() + { + var language = CreateLanguage(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(language.Name, Is.EqualTo(ValidName)); + Assert.That(language.Slug, Is.EqualTo(ValidSlug)); + } + } + + [Test] + public void Constructor_SetsStatusToActive() + { + var language = CreateLanguage(); + + Assert.That(language.Status, Is.EqualTo(LanguageStatus.Active)); + } + + [Test] + public void Constructor_VersionsIsEmpty() + { + var language = CreateLanguage(); + + Assert.That(language.Versions, Is.Empty); + } + + [Test] + public void Deactivate_SetsStatusToInactive() + { + var language = CreateLanguage(); + + language.Deactivate(); + + Assert.That(language.Status, Is.EqualTo(LanguageStatus.Inactive)); + } + + [Test] + public void DeprecateVersion_DoesNotRemoveVersion() + { + var language = CreateLanguage(); + var entry = language.AddVersion(ValidVersion); + + language.DeprecateVersion(entry.Id); + + Assert.That(language.Versions, Has.Count.EqualTo(1)); + } + + [Test] + public void DeprecateVersion_SetsVersionToDeprecated() + { + var language = CreateLanguage(); + var entry = language.AddVersion(ValidVersion); + + language.DeprecateVersion(entry.Id); + + Assert.That(entry.Status, Is.EqualTo(LanguageVersionStatus.Deprecated)); + } + + [Test] + public void DeprecateVersion_UnknownVersionId_ThrowsLanguageVersionNotFoundException() + { + var language = CreateLanguage(); + + Assert.Throws(() => language.DeprecateVersion(Guid.NewGuid())); + } + + [Test] + public void IsActive_WhenActive_IsTrue() + { + var language = CreateLanguage(); + + Assert.That(language.IsActive, Is.True); + } + + [Test] + public void IsActive_WhenInactive_IsFalse() + { + var language = CreateLanguage(); + language.Deactivate(); + + Assert.That(language.IsActive, Is.False); + } +} diff --git a/Algowars.Domain.Tests/Language/Entities/LanguageVersionEntryTests.cs b/Algowars.Domain.Tests/Language/Entities/LanguageVersionEntryTests.cs new file mode 100644 index 0000000..19bc631 --- /dev/null +++ b/Algowars.Domain.Tests/Language/Entities/LanguageVersionEntryTests.cs @@ -0,0 +1,63 @@ +using Algowars.Domain.Languages.Enums; +using Algowars.Domain.Languages.ValueObjects; +using LanguageEntity = Algowars.Domain.Languages.Entities.Language; + +namespace Algowars.Domain.Tests.Language.Entities; + +public class LanguageVersionEntryTests +{ + private static readonly LanguageName ValidName = new("Python"); + private static readonly LanguageSlug ValidSlug = new("python"); + private static readonly LanguageVersion ValidVersion = new("3.11"); + + private static LanguageEntity CreateLanguage() => new(ValidName, ValidSlug); + + [Test] + public void Deprecate_SetsIsActiveToFalse() + { + var language = CreateLanguage(); + var entry = language.AddVersion(ValidVersion); + + entry.Deprecate(); + + Assert.That(entry.IsActive, Is.False); + } + + [Test] + public void Deprecate_SetsStatusToDeprecated() + { + var language = CreateLanguage(); + var entry = language.AddVersion(ValidVersion); + + entry.Deprecate(); + + Assert.That(entry.Status, Is.EqualTo(LanguageVersionStatus.Deprecated)); + } + + [Test] + public void InitialStatus_IsActive() + { + var language = CreateLanguage(); + var entry = language.AddVersion(ValidVersion); + + Assert.That(entry.Status, Is.EqualTo(LanguageVersionStatus.Active)); + } + + [Test] + public void IsActive_WhenActive_IsTrue() + { + var language = CreateLanguage(); + var entry = language.AddVersion(ValidVersion); + + Assert.That(entry.IsActive, Is.True); + } + + [Test] + public void Version_SetCorrectly() + { + var language = CreateLanguage(); + var entry = language.AddVersion(ValidVersion); + + Assert.That(entry.Version, Is.EqualTo(ValidVersion)); + } +} diff --git a/Algowars.Domain.Tests/Language/ValueObjects/LanguageNameTests.cs b/Algowars.Domain.Tests/Language/ValueObjects/LanguageNameTests.cs new file mode 100644 index 0000000..5c9c9fe --- /dev/null +++ b/Algowars.Domain.Tests/Language/ValueObjects/LanguageNameTests.cs @@ -0,0 +1,77 @@ +using Algowars.Domain.Languages.Exceptions; +using Algowars.Domain.Languages.ValueObjects; + +namespace Algowars.Domain.Tests.Language.ValueObjects; + +public class LanguageNameTests +{ + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string value = new('a', LanguageName.MaxLength); + + Assert.That(() => new LanguageName(value), Throws.Nothing); + } + + [Test] + public void Constructor_AtMinLength_Succeeds() + { + Assert.That(() => new LanguageName("C"), Throws.Nothing); + } + + [Test] + public void Constructor_EmptyString_ThrowsInvalidLanguageNameException() + { + Assert.Throws(() => new LanguageName(string.Empty)); + } + + [Test] + public void Constructor_ExceedsMaxLength_ThrowsInvalidLanguageNameException() + { + string value = new('a', LanguageName.MaxLength + 1); + + Assert.Throws(() => new LanguageName(value)); + } + + [Test] + public void Constructor_WhitespaceOnly_ThrowsInvalidLanguageNameException() + { + Assert.Throws(() => new LanguageName(" ")); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new LanguageName("Python"); + var b = new LanguageName("JavaScript"); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new LanguageName("Python"); + var b = new LanguageName("Python"); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ImplicitConversion_ReturnsValue() + { + var name = new LanguageName("Python"); + + string result = name; + + Assert.That(result, Is.EqualTo("Python")); + } + + [Test] + public void ToString_ReturnsValue() + { + var name = new LanguageName("Python"); + + Assert.That(name.ToString(), Is.EqualTo("Python")); + } +} diff --git a/Algowars.Domain.Tests/Language/ValueObjects/LanguageSlugTests.cs b/Algowars.Domain.Tests/Language/ValueObjects/LanguageSlugTests.cs new file mode 100644 index 0000000..f9104e2 --- /dev/null +++ b/Algowars.Domain.Tests/Language/ValueObjects/LanguageSlugTests.cs @@ -0,0 +1,127 @@ +using Algowars.Domain.Languages.Exceptions; +using Algowars.Domain.Languages.ValueObjects; + +namespace Algowars.Domain.Tests.Language.ValueObjects; + +public class LanguageSlugTests +{ + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string value = new('a', LanguageSlug.MaxLength); + + Assert.That(() => new LanguageSlug(value), Throws.Nothing); + } + + [Test] + public void Constructor_AtMinLength_Succeeds() + { + Assert.That(() => new LanguageSlug("c"), Throws.Nothing); + } + + [Test] + public void Constructor_EmptyString_ThrowsInvalidLanguageSlugException() + { + Assert.Throws(() => new LanguageSlug(string.Empty)); + } + + [Test] + public void Constructor_ExceedsMaxLength_ThrowsInvalidLanguageSlugException() + { + string value = new('a', LanguageSlug.MaxLength + 1); + + Assert.Throws(() => new LanguageSlug(value)); + } + + [Test] + public void Constructor_WhitespaceOnly_ThrowsInvalidLanguageSlugException() + { + Assert.Throws(() => new LanguageSlug(" ")); + } + + [TestCase("Python")] + [TestCase("PYTHON")] + [TestCase("-python")] + [TestCase("python-")] + [TestCase("python--311")] + [TestCase("python 311")] + public void Constructor_InvalidFormat_ThrowsInvalidLanguageSlugException(string value) + { + Assert.Throws(() => new LanguageSlug(value)); + } + + [TestCase("python")] + [TestCase("cpp")] + [TestCase("python-311")] + [TestCase("c")] + public void Constructor_ValidFormat_Succeeds(string value) + { + Assert.That(() => new LanguageSlug(value), Throws.Nothing); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new LanguageSlug("python"); + var b = new LanguageSlug("cpp"); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new LanguageSlug("python"); + var b = new LanguageSlug("python"); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void FromName_CollapseMultipleSpaces() + { + var name = new LanguageName("C Sharp"); + + var slug = LanguageSlug.FromName(name); + + Assert.That(slug.Value, Is.EqualTo("c-sharp")); + } + + [Test] + public void FromName_GeneratesValidSlug() + { + var name = new LanguageName("Python"); + + var slug = LanguageSlug.FromName(name); + + Assert.That(slug.Value, Is.EqualTo("python")); + } + + [Test] + public void FromName_StripsSpecialCharacters() + { + var name = new LanguageName("C++"); + + var slug = LanguageSlug.FromName(name); + + Assert.That(slug.Value, Is.EqualTo("c")); + } + + [Test] + public void ImplicitConversion_ReturnsValue() + { + var slug = new LanguageSlug("python"); + + string result = slug; + + Assert.That(result, Is.EqualTo("python")); + } + + [Test] + public void ToString_ReturnsValue() + { + var slug = new LanguageSlug("python"); + + Assert.That(slug.ToString(), Is.EqualTo("python")); + } +} diff --git a/Algowars.Domain.Tests/Language/ValueObjects/LanguageVersionTests.cs b/Algowars.Domain.Tests/Language/ValueObjects/LanguageVersionTests.cs new file mode 100644 index 0000000..5b83bb5 --- /dev/null +++ b/Algowars.Domain.Tests/Language/ValueObjects/LanguageVersionTests.cs @@ -0,0 +1,71 @@ +using Algowars.Domain.Languages.Exceptions; +using Algowars.Domain.Languages.ValueObjects; + +namespace Algowars.Domain.Tests.Language.ValueObjects; + +public class LanguageVersionTests +{ + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string value = new('a', LanguageVersion.MaxLength); + + Assert.That(() => new LanguageVersion(value), Throws.Nothing); + } + + [Test] + public void Constructor_EmptyString_ThrowsInvalidLanguageVersionException() + { + Assert.Throws(() => new LanguageVersion(string.Empty)); + } + + [Test] + public void Constructor_ExceedsMaxLength_ThrowsInvalidLanguageVersionException() + { + string value = new('a', LanguageVersion.MaxLength + 1); + + Assert.Throws(() => new LanguageVersion(value)); + } + + [Test] + public void Constructor_WhitespaceOnly_ThrowsInvalidLanguageVersionException() + { + Assert.Throws(() => new LanguageVersion(" ")); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new LanguageVersion("3.11"); + var b = new LanguageVersion("3.12"); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new LanguageVersion("3.11"); + var b = new LanguageVersion("3.11"); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ImplicitConversion_ReturnsValue() + { + var version = new LanguageVersion("3.11"); + + string result = version; + + Assert.That(result, Is.EqualTo("3.11")); + } + + [Test] + public void ToString_ReturnsValue() + { + var version = new LanguageVersion("3.11"); + + Assert.That(version.ToString(), Is.EqualTo("3.11")); + } +} diff --git a/Algowars.Domain.Tests/Problem/Entities/ProblemTests.cs b/Algowars.Domain.Tests/Problem/Entities/ProblemTests.cs new file mode 100644 index 0000000..9b08180 --- /dev/null +++ b/Algowars.Domain.Tests/Problem/Entities/ProblemTests.cs @@ -0,0 +1,179 @@ +using Algowars.Domain.Problems.Enums; +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; +using ProblemEntity = Algowars.Domain.Problems.Entities.Problem; + +namespace Algowars.Domain.Tests.Problem.Entities; + +public class ProblemTests +{ + private static readonly Slug ValidSlug = new("two-sum"); + private static readonly Title ValidTitle = new("Two Sum"); + private static readonly Question ValidQuestion = new(new string('a', Question.MinLength)); + private static readonly Difficulty ValidDifficulty = new(500); + private static readonly TimeLimit ValidTimeLimit = new(1000); + private static readonly MemoryLimit ValidMemoryLimit = new(64); + + private static ProblemEntity CreateProblem() => + new(ValidSlug, ValidTitle, ValidQuestion, ValidDifficulty, ValidTimeLimit, ValidMemoryLimit); + + [Test] + public void Archive_SetsStatusToArchived() + { + var problem = CreateProblem(); + + problem.Archive(); + + Assert.That(problem.Status, Is.EqualTo(ProblemStatus.Archived)); + } + + [Test] + public void Constructor_CreatesInitialDraftVersion() + { + var problem = CreateProblem(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(problem.Versions, Has.Count.EqualTo(1)); + Assert.That(problem.Versions.First().VersionNumber, Is.EqualTo(1)); + Assert.That(problem.Versions.First().IsPublished, Is.False); + } + } + + [Test] + public void Constructor_SetsSlug() + { + var problem = CreateProblem(); + + Assert.That(problem.Slug, Is.EqualTo(ValidSlug)); + } + + [Test] + public void Constructor_SetsStatusToDraft() + { + var problem = CreateProblem(); + + Assert.That(problem.Status, Is.EqualTo(ProblemStatus.Draft)); + } + + [Test] + public void CreateNewVersion_CopiesValuesFromLatestVersion() + { + var problem = CreateProblem(); + + var newVersion = problem.CreateNewVersion(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(newVersion.Title, Is.EqualTo(ValidTitle)); + Assert.That(newVersion.Question, Is.EqualTo(ValidQuestion)); + Assert.That(newVersion.Difficulty, Is.EqualTo(ValidDifficulty)); + Assert.That(newVersion.TimeLimit, Is.EqualTo(ValidTimeLimit)); + Assert.That(newVersion.MemoryLimit, Is.EqualTo(ValidMemoryLimit)); + } + } + + [Test] + public void CreateNewVersion_IncrementsVersionNumber() + { + var problem = CreateProblem(); + + var newVersion = problem.CreateNewVersion(); + + Assert.That(newVersion.VersionNumber, Is.EqualTo(2)); + } + + [Test] + public void CreateNewVersion_AddsVersionToCollection() + { + var problem = CreateProblem(); + + problem.CreateNewVersion(); + + Assert.That(problem.Versions, Has.Count.EqualTo(2)); + } + + [Test] + public void CurrentVersion_BeforePublish_IsNull() + { + var problem = CreateProblem(); + + Assert.That(problem.CurrentVersion, Is.Null); + } + + [Test] + public void CurrentVersion_AfterPublish_ReturnsPublishedVersion() + { + var problem = CreateProblem(); + var versionId = problem.Versions.First().Id; + + problem.Publish(versionId); + + Assert.That(problem.CurrentVersion, Is.Not.Null); + Assert.That(problem.CurrentVersion!.Id, Is.EqualTo(versionId)); + } + + [Test] + public void DraftVersion_ReturnsLatestUnpublishedVersion() + { + var problem = CreateProblem(); + var newVersion = problem.CreateNewVersion(); + + problem.Publish(problem.Versions.First().Id); + + Assert.That(problem.DraftVersion.Id, Is.EqualTo(newVersion.Id)); + } + + [Test] + public void Publish_SetsStatusToPublished() + { + var problem = CreateProblem(); + var versionId = problem.Versions.First().Id; + + problem.Publish(versionId); + + Assert.That(problem.Status, Is.EqualTo(ProblemStatus.Published)); + } + + [Test] + public void Publish_SetsVersionPublishedAt() + { + var problem = CreateProblem(); + var versionId = problem.Versions.First().Id; + var before = DateTime.UtcNow; + + problem.Publish(versionId); + + Assert.That(problem.Versions.First().PublishedAt, Is.GreaterThanOrEqualTo(before)); + } + + [Test] + public void Publish_UnknownVersionId_ThrowsProblemVersionNotFoundException() + { + var problem = CreateProblem(); + + Assert.Throws(() => problem.Publish(Guid.NewGuid())); + } + + [Test] + public void UpdateSlug_ChangesSlug() + { + var problem = CreateProblem(); + var newSlug = new Slug("three-sum"); + + problem.UpdateSlug(newSlug); + + Assert.That(problem.Slug, Is.EqualTo(newSlug)); + } + + [Test] + public void UpdateSlug_DoesNotAffectVersions() + { + var problem = CreateProblem(); + int versionCount = problem.Versions.Count; + + problem.UpdateSlug(new Slug("three-sum")); + + Assert.That(problem.Versions, Has.Count.EqualTo(versionCount)); + } +} diff --git a/Algowars.Domain.Tests/Problem/Entities/ProblemVersionTests.cs b/Algowars.Domain.Tests/Problem/Entities/ProblemVersionTests.cs new file mode 100644 index 0000000..b769779 --- /dev/null +++ b/Algowars.Domain.Tests/Problem/Entities/ProblemVersionTests.cs @@ -0,0 +1,280 @@ +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; +using ProblemEntity = Algowars.Domain.Problems.Entities.Problem; + +namespace Algowars.Domain.Tests.Problem.Entities; + +public class ProblemVersionTests +{ + private static readonly Slug ValidSlug = new("two-sum"); + private static readonly Title ValidTitle = new("Two Sum"); + private static readonly Question ValidQuestion = new(new string('a', Question.MinLength)); + private static readonly Difficulty ValidDifficulty = new(500); + private static readonly TimeLimit ValidTimeLimit = new(1000); + private static readonly MemoryLimit ValidMemoryLimit = new(64); + + private static ProblemEntity CreateProblem() => + new(ValidSlug, ValidTitle, ValidQuestion, ValidDifficulty, ValidTimeLimit, ValidMemoryLimit); + + [Test] + public void AddCodeTemplate_AfterPublish_ThrowsProblemVersionImmutableException() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + problem.Publish(version.Id); + + Assert.Throws(() => + version.AddCodeTemplate(Guid.NewGuid(), "starter", "wrapper")); + } + + [Test] + public void AddCodeTemplate_BeforePublish_AddsToCollection() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + version.AddCodeTemplate(Guid.NewGuid(), "starter", "wrapper"); + + Assert.That(version.CodeTemplates, Has.Count.EqualTo(1)); + } + + [Test] + public void AddCodeTemplate_SetsProperties() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + var languageId = Guid.NewGuid(); + + version.AddCodeTemplate(languageId, "starter", "wrapper"); + var template = version.CodeTemplates.First(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(template.LanguageId, Is.EqualTo(languageId)); + Assert.That(template.StarterCode, Is.EqualTo("starter")); + Assert.That(template.WrapperCode, Is.EqualTo("wrapper")); + } + } + + [Test] + public void AddExample_AfterPublish_Succeeds() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + problem.Publish(version.Id); + + Assert.That(() => version.AddExample("1", "2"), Throws.Nothing); + } + + [Test] + public void AddExample_SetsProperties() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + version.AddExample("[1,2]", "3", "add them"); + var example = version.Examples.First(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(example.Input, Is.EqualTo("[1,2]")); + Assert.That(example.Output, Is.EqualTo("3")); + Assert.That(example.Explanation, Is.EqualTo("add them")); + } + } + + [Test] + public void AddExample_WithoutExplanation_ExplanationIsNull() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + version.AddExample("[1,2]", "3"); + + Assert.That(version.Examples.First().Explanation, Is.Null); + } + + [Test] + public void AddTestCase_AfterPublish_ThrowsProblemVersionImmutableException() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + problem.Publish(version.Id); + + Assert.Throws(() => + version.AddTestCase("input", "expected")); + } + + [Test] + public void AddTestCase_BeforePublish_AddsToCollection() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + version.AddTestCase("input", "expected"); + + Assert.That(version.TestCases, Has.Count.EqualTo(1)); + } + + [Test] + public void AddTestCase_DefaultsToHidden() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + version.AddTestCase("input", "expected"); + + Assert.That(version.TestCases.First().IsHidden, Is.True); + } + + [Test] + public void AddTestCase_SetsProperties() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + version.AddTestCase("input", "expected", isHidden: false); + var testCase = version.TestCases.First(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(testCase.Input, Is.EqualTo("input")); + Assert.That(testCase.ExpectedOutput, Is.EqualTo("expected")); + Assert.That(testCase.IsHidden, Is.False); + } + } + + [Test] + public void Constructor_SetsInitialProperties() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(version.VersionNumber, Is.EqualTo(1)); + Assert.That(version.Title, Is.EqualTo(ValidTitle)); + Assert.That(version.Question, Is.EqualTo(ValidQuestion)); + Assert.That(version.Difficulty, Is.EqualTo(ValidDifficulty)); + Assert.That(version.TimeLimit, Is.EqualTo(ValidTimeLimit)); + Assert.That(version.MemoryLimit, Is.EqualTo(ValidMemoryLimit)); + } + } + + [Test] + public void IsPublished_BeforePublish_IsFalse() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + Assert.That(version.IsPublished, Is.False); + } + + [Test] + public void IsPublished_AfterPublish_IsTrue() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + + problem.Publish(version.Id); + + Assert.That(version.IsPublished, Is.True); + } + + [Test] + public void UpdateDifficulty_AfterPublish_Succeeds() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + problem.Publish(version.Id); + var newDifficulty = new Difficulty(1500); + + version.UpdateDifficulty(newDifficulty); + + Assert.That(version.Difficulty, Is.EqualTo(newDifficulty)); + } + + [Test] + public void UpdateDifficulty_BeforePublish_UpdatesDifficulty() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + var newDifficulty = new Difficulty(1500); + + version.UpdateDifficulty(newDifficulty); + + Assert.That(version.Difficulty, Is.EqualTo(newDifficulty)); + } + + [Test] + public void UpdateMemoryLimit_AfterPublish_ThrowsProblemVersionImmutableException() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + problem.Publish(version.Id); + + Assert.Throws(() => + version.UpdateMemoryLimit(new MemoryLimit(128))); + } + + [Test] + public void UpdateMemoryLimit_BeforePublish_UpdatesMemoryLimit() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + var newLimit = new MemoryLimit(128); + + version.UpdateMemoryLimit(newLimit); + + Assert.That(version.MemoryLimit, Is.EqualTo(newLimit)); + } + + [Test] + public void UpdateQuestion_AfterPublish_UpdatesQuestion() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + problem.Publish(version.Id); + var newQuestion = new Question(new string('b', Question.MinLength)); + + version.UpdateQuestion(newQuestion); + + Assert.That(version.Question, Is.EqualTo(newQuestion)); + } + + [Test] + public void UpdateTimeLimit_AfterPublish_ThrowsProblemVersionImmutableException() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + problem.Publish(version.Id); + + Assert.Throws(() => + version.UpdateTimeLimit(new TimeLimit(2000))); + } + + [Test] + public void UpdateTimeLimit_BeforePublish_UpdatesTimeLimit() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + var newLimit = new TimeLimit(2000); + + version.UpdateTimeLimit(newLimit); + + Assert.That(version.TimeLimit, Is.EqualTo(newLimit)); + } + + [Test] + public void UpdateTitle_AfterPublish_UpdatesTitle() + { + var problem = CreateProblem(); + var version = problem.Versions.First(); + problem.Publish(version.Id); + var newTitle = new Title("Two Sum Fixed"); + + version.UpdateTitle(newTitle); + + Assert.That(version.Title, Is.EqualTo(newTitle)); + } +} diff --git a/Algowars.Domain.Tests/Problem/ValueObjects/DifficultyTests.cs b/Algowars.Domain.Tests/Problem/ValueObjects/DifficultyTests.cs index 3b0847d..02037b2 100644 --- a/Algowars.Domain.Tests/Problem/ValueObjects/DifficultyTests.cs +++ b/Algowars.Domain.Tests/Problem/ValueObjects/DifficultyTests.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.Problem.Enums; -using Algowars.Domain.Problem.Exceptions; -using Algowars.Domain.Problem.ValueObjects; +using Algowars.Domain.Problems.Enums; +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; namespace Algowars.Domain.Tests.Problem.ValueObjects; diff --git a/Algowars.Domain.Tests/Problem/ValueObjects/MemoryLimitTests.cs b/Algowars.Domain.Tests/Problem/ValueObjects/MemoryLimitTests.cs new file mode 100644 index 0000000..19ef437 --- /dev/null +++ b/Algowars.Domain.Tests/Problem/ValueObjects/MemoryLimitTests.cs @@ -0,0 +1,57 @@ +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; + +namespace Algowars.Domain.Tests.Problem.ValueObjects; + +public class MemoryLimitTests +{ + [Test] + public void Constructor_AtMaxMegabytes_Succeeds() + { + Assert.That(() => new MemoryLimit(MemoryLimit.MaxMegabytes), Throws.Nothing); + } + + [Test] + public void Constructor_AtMinMegabytes_Succeeds() + { + Assert.That(() => new MemoryLimit(MemoryLimit.MinMegabytes), Throws.Nothing); + } + + [Test] + public void Constructor_AboveMaxMegabytes_ThrowsInvalidMemoryLimitException() + { + Assert.Throws(() => new MemoryLimit(MemoryLimit.MaxMegabytes + 1)); + } + + [Test] + public void Constructor_BelowMinMegabytes_ThrowsInvalidMemoryLimitException() + { + Assert.Throws(() => new MemoryLimit(MemoryLimit.MinMegabytes - 1)); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new MemoryLimit(64); + var b = new MemoryLimit(128); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new MemoryLimit(64); + var b = new MemoryLimit(64); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ToString_IncludesMegabytes() + { + var memoryLimit = new MemoryLimit(64); + + Assert.That(memoryLimit.ToString(), Is.EqualTo("64MB")); + } +} diff --git a/Algowars.Domain.Tests/Problem/ValueObjects/QuestionTests.cs b/Algowars.Domain.Tests/Problem/ValueObjects/QuestionTests.cs new file mode 100644 index 0000000..933ec1a --- /dev/null +++ b/Algowars.Domain.Tests/Problem/ValueObjects/QuestionTests.cs @@ -0,0 +1,87 @@ +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; + +namespace Algowars.Domain.Tests.Problem.ValueObjects; + +public class QuestionTests +{ + private static string ValidQuestion => new('a', Question.MinLength); + + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string value = new('a', Question.MaxLength); + + Assert.That(() => new Question(value), Throws.Nothing); + } + + [Test] + public void Constructor_AtMinLength_Succeeds() + { + Assert.That(() => new Question(ValidQuestion), Throws.Nothing); + } + + [Test] + public void Constructor_BelowMinLength_ThrowsInvalidQuestionException() + { + string value = new('a', Question.MinLength - 1); + + Assert.Throws(() => new Question(value)); + } + + [Test] + public void Constructor_EmptyString_ThrowsInvalidQuestionException() + { + Assert.Throws(() => new Question(string.Empty)); + } + + [Test] + public void Constructor_ExceedsMaxLength_ThrowsInvalidQuestionException() + { + string value = new('a', Question.MaxLength + 1); + + Assert.Throws(() => new Question(value)); + } + + [Test] + public void Constructor_WhitespaceOnly_ThrowsInvalidQuestionException() + { + Assert.Throws(() => new Question(" ")); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new Question(new string('a', Question.MinLength)); + var b = new Question(new string('b', Question.MinLength)); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new Question(ValidQuestion); + var b = new Question(ValidQuestion); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ImplicitConversion_ReturnsValue() + { + var question = new Question(ValidQuestion); + + string result = question; + + Assert.That(result, Is.EqualTo(ValidQuestion)); + } + + [Test] + public void ToString_ReturnsValue() + { + var question = new Question(ValidQuestion); + + Assert.That(question.ToString(), Is.EqualTo(ValidQuestion)); + } +} diff --git a/Algowars.Domain.Tests/Problem/ValueObjects/SlugTests.cs b/Algowars.Domain.Tests/Problem/ValueObjects/SlugTests.cs new file mode 100644 index 0000000..ed4d2f2 --- /dev/null +++ b/Algowars.Domain.Tests/Problem/ValueObjects/SlugTests.cs @@ -0,0 +1,139 @@ +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; + +namespace Algowars.Domain.Tests.Problem.ValueObjects; + +public class SlugTests +{ + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string repeated = string.Concat(Enumerable.Repeat("a", Slug.MaxLength)); + + Assert.That(() => new Slug(repeated), Throws.Nothing); + } + + [Test] + public void Constructor_AtMinLength_Succeeds() + { + string value = new('a', Slug.MinLength); + + Assert.That(() => new Slug(value), Throws.Nothing); + } + + [Test] + public void Constructor_BelowMinLength_ThrowsInvalidSlugException() + { + string value = new('a', Slug.MinLength - 1); + + Assert.Throws(() => new Slug(value)); + } + + [Test] + public void Constructor_EmptyString_ThrowsInvalidSlugException() + { + Assert.Throws(() => new Slug(string.Empty)); + } + + [Test] + public void Constructor_ExceedsMaxLength_ThrowsInvalidSlugException() + { + string value = new('a', Slug.MaxLength + 1); + + Assert.Throws(() => new Slug(value)); + } + + [Test] + [TestCase("Two-Sum")] + [TestCase("TWOSUM")] + [TestCase("-two-sum")] + [TestCase("two-sum-")] + [TestCase("two--sum")] + [TestCase("two sum")] + public void Constructor_InvalidFormat_ThrowsInvalidSlugException(string value) + { + Assert.Throws(() => new Slug(value)); + } + + [Test] + [TestCase("two-sum")] + [TestCase("twosum")] + [TestCase("two-sum-123")] + [TestCase("abc")] + public void Constructor_ValidFormat_Succeeds(string value) + { + Assert.That(() => new Slug(value), Throws.Nothing); + } + + [Test] + public void Constructor_WhitespaceOnly_ThrowsInvalidSlugException() + { + Assert.Throws(() => new Slug(" ")); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new Slug("two-sum"); + var b = new Slug("three-sum"); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new Slug("two-sum"); + var b = new Slug("two-sum"); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void FromTitle_GeneratesValidSlug() + { + var title = new Title("Two Sum"); + + var slug = Slug.FromTitle(title); + + Assert.That(slug.Value, Is.EqualTo("two-sum")); + } + + [Test] + public void FromTitle_StripsSpecialCharacters() + { + var title = new Title("Two Sum!"); + + var slug = Slug.FromTitle(title); + + Assert.That(slug.Value, Is.EqualTo("two-sum")); + } + + [Test] + public void FromTitle_CollapseMultipleSpaces() + { + var title = new Title("Two Sum"); + + var slug = Slug.FromTitle(title); + + Assert.That(slug.Value, Is.EqualTo("two-sum")); + } + + [Test] + public void ImplicitConversion_ReturnsValue() + { + var slug = new Slug("two-sum"); + + string result = slug; + + Assert.That(result, Is.EqualTo("two-sum")); + } + + [Test] + public void ToString_ReturnsValue() + { + var slug = new Slug("two-sum"); + + Assert.That(slug.ToString(), Is.EqualTo("two-sum")); + } +} diff --git a/Algowars.Domain.Tests/Problem/ValueObjects/TimeLimitTests.cs b/Algowars.Domain.Tests/Problem/ValueObjects/TimeLimitTests.cs new file mode 100644 index 0000000..790edef --- /dev/null +++ b/Algowars.Domain.Tests/Problem/ValueObjects/TimeLimitTests.cs @@ -0,0 +1,57 @@ +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; + +namespace Algowars.Domain.Tests.Problem.ValueObjects; + +public class TimeLimitTests +{ + [Test] + public void Constructor_AtMaxMilliseconds_Succeeds() + { + Assert.That(() => new TimeLimit(TimeLimit.MaxMilliseconds), Throws.Nothing); + } + + [Test] + public void Constructor_AtMinMilliseconds_Succeeds() + { + Assert.That(() => new TimeLimit(TimeLimit.MinMilliseconds), Throws.Nothing); + } + + [Test] + public void Constructor_AboveMaxMilliseconds_ThrowsInvalidTimeLimitException() + { + Assert.Throws(() => new TimeLimit(TimeLimit.MaxMilliseconds + 1)); + } + + [Test] + public void Constructor_BelowMinMilliseconds_ThrowsInvalidTimeLimitException() + { + Assert.Throws(() => new TimeLimit(TimeLimit.MinMilliseconds - 1)); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new TimeLimit(1000); + var b = new TimeLimit(2000); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new TimeLimit(1000); + var b = new TimeLimit(1000); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ToString_IncludesMilliseconds() + { + var timeLimit = new TimeLimit(1000); + + Assert.That(timeLimit.ToString(), Is.EqualTo("1000ms")); + } +} diff --git a/Algowars.Domain.Tests/Problem/ValueObjects/TitleTests.cs b/Algowars.Domain.Tests/Problem/ValueObjects/TitleTests.cs new file mode 100644 index 0000000..294b238 --- /dev/null +++ b/Algowars.Domain.Tests/Problem/ValueObjects/TitleTests.cs @@ -0,0 +1,87 @@ +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; + +namespace Algowars.Domain.Tests.Problem.ValueObjects; + +public class TitleTests +{ + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string value = new('a', Title.MaxLength); + + Assert.That(() => new Title(value), Throws.Nothing); + } + + [Test] + public void Constructor_AtMinLength_Succeeds() + { + string value = new('a', Title.MinLength); + + Assert.That(() => new Title(value), Throws.Nothing); + } + + [Test] + public void Constructor_EmptyString_ThrowsInvalidTitleException() + { + Assert.Throws(() => new Title(string.Empty)); + } + + [Test] + public void Constructor_ExceedsMaxLength_ThrowsInvalidTitleException() + { + string value = new('a', Title.MaxLength + 1); + + Assert.Throws(() => new Title(value)); + } + + [Test] + public void Constructor_BelowMinLength_ThrowsInvalidTitleException() + { + string value = new('a', Title.MinLength - 1); + + Assert.Throws(() => new Title(value)); + } + + [Test] + public void Constructor_WhitespaceOnly_ThrowsInvalidTitleException() + { + Assert.Throws(() => new Title(" ")); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new Title("Two Sum"); + var b = new Title("Three Sum"); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new Title("Two Sum"); + var b = new Title("Two Sum"); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ImplicitConversion_ReturnsValue() + { + var title = new Title("Two Sum"); + + string result = title; + + Assert.That(result, Is.EqualTo("Two Sum")); + } + + [Test] + public void ToString_ReturnsValue() + { + var title = new Title("Two Sum"); + + Assert.That(title.ToString(), Is.EqualTo("Two Sum")); + } +} diff --git a/Algowars.Domain.Tests/Submissions/Entities/SubmissionResultTests.cs b/Algowars.Domain.Tests/Submissions/Entities/SubmissionResultTests.cs new file mode 100644 index 0000000..5270e49 --- /dev/null +++ b/Algowars.Domain.Tests/Submissions/Entities/SubmissionResultTests.cs @@ -0,0 +1,131 @@ +using Algowars.Domain.Submissions.Entities; +using Algowars.Domain.Submissions.Enums; +using Algowars.Domain.Submissions.ValueObjects; + +namespace Algowars.Domain.Tests.Submissions.Entities; + +public class SubmissionResultTests +{ + private static readonly SourceCode ValidSourceCode = new("int main() {}"); + + private static Submission CreateSubmission(params Guid[] testCaseIds) => + new(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), SubmissionType.Submit, ValidSourceCode, testCaseIds); + + [Test] + public void InitialStatus_IsPending() + { + var testCaseId = Guid.NewGuid(); + var submission = CreateSubmission(testCaseId); + + var result = submission.Results.First(); + + Assert.That(result.Status, Is.EqualTo(SubmissionResultStatus.Pending)); + } + + [Test] + public void IsTerminal_WhenPending_IsFalse() + { + var testCaseId = Guid.NewGuid(); + var submission = CreateSubmission(testCaseId); + + Assert.That(submission.Results.First().IsTerminal, Is.False); + } + + [Test] + public void IsTerminal_WhenProcessing_IsFalse() + { + var testCaseId = Guid.NewGuid(); + var submission = CreateSubmission(testCaseId); + submission.UpdateResult(testCaseId, SubmissionResultStatus.Processing); + + Assert.That(submission.Results.First().IsTerminal, Is.False); + } + + [TestCase(SubmissionResultStatus.Accepted)] + [TestCase(SubmissionResultStatus.WrongAnswer)] + [TestCase(SubmissionResultStatus.TimeLimitExceeded)] + [TestCase(SubmissionResultStatus.MemoryLimitExceeded)] + [TestCase(SubmissionResultStatus.RuntimeError)] + [TestCase(SubmissionResultStatus.CompileError)] + public void IsTerminal_WhenTerminalStatus_IsTrue(SubmissionResultStatus status) + { + var testCaseId = Guid.NewGuid(); + var submission = CreateSubmission(testCaseId); + submission.UpdateResult(testCaseId, status); + + Assert.That(submission.Results.First().IsTerminal, Is.True); + } + + [Test] + public void Update_SetsAllOutputFields() + { + var testCaseId = Guid.NewGuid(); + var submission = CreateSubmission(testCaseId); + + submission.UpdateResult(testCaseId, SubmissionResultStatus.WrongAnswer, + runtime: 150, + memoryUsed: 64, + actualOutput: "wrong", + standardOutput: "stdout", + standardError: "stderr", + compileOutput: "compile"); + + var result = submission.Results.First(); + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Status, Is.EqualTo(SubmissionResultStatus.WrongAnswer)); + Assert.That(result.Runtime, Is.EqualTo(150)); + Assert.That(result.MemoryUsed, Is.EqualTo(64)); + Assert.That(result.ActualOutput, Is.EqualTo("wrong")); + Assert.That(result.StandardOutput, Is.EqualTo("stdout")); + Assert.That(result.StandardError, Is.EqualTo("stderr")); + Assert.That(result.CompileOutput, Is.EqualTo("compile")); + } + } + + [Test] + public void Update_NullableFields_DefaultToNull() + { + var testCaseId = Guid.NewGuid(); + var submission = CreateSubmission(testCaseId); + + submission.UpdateResult(testCaseId, SubmissionResultStatus.Accepted); + + var result = submission.Results.First(); + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Runtime, Is.Null); + Assert.That(result.MemoryUsed, Is.Null); + Assert.That(result.ActualOutput, Is.Null); + Assert.That(result.StandardOutput, Is.Null); + Assert.That(result.StandardError, Is.Null); + Assert.That(result.CompileOutput, Is.Null); + } + } + + [Test] + public void Update_CalledTwice_OverwritesPreviousValues() + { + var testCaseId = Guid.NewGuid(); + var submission = CreateSubmission(testCaseId); + submission.UpdateResult(testCaseId, SubmissionResultStatus.Processing, runtime: 100); + + submission.UpdateResult(testCaseId, SubmissionResultStatus.Accepted, runtime: 200); + + var result = submission.Results.First(); + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Status, Is.EqualTo(SubmissionResultStatus.Accepted)); + Assert.That(result.Runtime, Is.EqualTo(200)); + } + } + + [Test] + public void TestCaseId_SetCorrectly() + { + var testCaseId = Guid.NewGuid(); + var submission = CreateSubmission(testCaseId); + + Assert.That(submission.Results.First().TestCaseId, Is.EqualTo(testCaseId)); + } +} diff --git a/Algowars.Domain.Tests/Submissions/Entities/SubmissionTests.cs b/Algowars.Domain.Tests/Submissions/Entities/SubmissionTests.cs new file mode 100644 index 0000000..f476104 --- /dev/null +++ b/Algowars.Domain.Tests/Submissions/Entities/SubmissionTests.cs @@ -0,0 +1,187 @@ +using Algowars.Domain.Submissions.Entities; +using Algowars.Domain.Submissions.Enums; +using Algowars.Domain.Submissions.Exceptions; +using Algowars.Domain.Submissions.ValueObjects; + +namespace Algowars.Domain.Tests.Submissions.Entities; + +public class SubmissionTests +{ + private static readonly Guid UserId = Guid.NewGuid(); + private static readonly Guid ProblemVersionId = Guid.NewGuid(); + private static readonly Guid LanguageVersionId = Guid.NewGuid(); + private static readonly SourceCode ValidSourceCode = new("int main() {}"); + private static readonly Guid TestCaseId1 = Guid.NewGuid(); + private static readonly Guid TestCaseId2 = Guid.NewGuid(); + + private static Submission CreateSubmission( + SubmissionType type = SubmissionType.Submit, + IEnumerable? testCaseIds = null) => + new(UserId, ProblemVersionId, LanguageVersionId, type, ValidSourceCode, + testCaseIds ?? [TestCaseId1, TestCaseId2]); + + [Test] + public void Complete_AllAccepted_SetsStatusToAccepted() + { + var submission = CreateSubmission(); + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Accepted); + submission.UpdateResult(TestCaseId2, SubmissionResultStatus.Accepted); + + submission.Complete(); + + Assert.That(submission.Status, Is.EqualTo(SubmissionStatus.Accepted)); + } + + [Test] + public void Complete_AnyNonAccepted_SetsStatusToWrongAnswer() + { + var submission = CreateSubmission(); + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Accepted); + submission.UpdateResult(TestCaseId2, SubmissionResultStatus.WrongAnswer); + + submission.Complete(); + + Assert.That(submission.Status, Is.EqualTo(SubmissionStatus.WrongAnswer)); + } + + [TestCase(SubmissionResultStatus.TimeLimitExceeded)] + [TestCase(SubmissionResultStatus.MemoryLimitExceeded)] + [TestCase(SubmissionResultStatus.RuntimeError)] + [TestCase(SubmissionResultStatus.CompileError)] + public void Complete_AnyFailureStatus_SetsStatusToWrongAnswer(SubmissionResultStatus failureStatus) + { + var submission = CreateSubmission(); + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Accepted); + submission.UpdateResult(TestCaseId2, failureStatus); + + submission.Complete(); + + Assert.That(submission.Status, Is.EqualTo(SubmissionStatus.WrongAnswer)); + } + + [Test] + public void Complete_WithPendingResult_ThrowsSubmissionNotCompleteException() + { + var submission = CreateSubmission(); + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Accepted); + + Assert.Throws(() => submission.Complete()); + } + + [Test] + public void Complete_WithProcessingResult_ThrowsSubmissionNotCompleteException() + { + var submission = CreateSubmission(); + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Accepted); + submission.UpdateResult(TestCaseId2, SubmissionResultStatus.Processing); + + Assert.Throws(() => submission.Complete()); + } + + [Test] + public void Constructor_SetsInitialProperties() + { + var submission = CreateSubmission(SubmissionType.Run); + + using (Assert.EnterMultipleScope()) + { + Assert.That(submission.UserId, Is.EqualTo(UserId)); + Assert.That(submission.ProblemVersionId, Is.EqualTo(ProblemVersionId)); + Assert.That(submission.LanguageVersionId, Is.EqualTo(LanguageVersionId)); + Assert.That(submission.Type, Is.EqualTo(SubmissionType.Run)); + Assert.That(submission.SourceCode, Is.EqualTo(ValidSourceCode)); + Assert.That(submission.Status, Is.EqualTo(SubmissionStatus.Queued)); + } + } + + [Test] + public void Constructor_CreatesResultPerTestCaseId() + { + var submission = CreateSubmission(); + + Assert.That(submission.Results, Has.Count.EqualTo(2)); + } + + [Test] + public void Constructor_AllResultsInitiallyPending() + { + var submission = CreateSubmission(); + + Assert.That(submission.Results.All(r => r.Status == SubmissionResultStatus.Pending), Is.True); + } + + [Test] + public void Constructor_WithNoTestCases_ResultsIsEmpty() + { + var submission = CreateSubmission(testCaseIds: []); + + Assert.That(submission.Results, Is.Empty); + } + + [Test] + public void StartRunning_FromQueued_SetsStatusToRunning() + { + var submission = CreateSubmission(); + + submission.StartRunning(); + + Assert.That(submission.Status, Is.EqualTo(SubmissionStatus.Running)); + } + + [Test] + public void StartRunning_WhenAlreadyRunning_ThrowsInvalidSubmissionStateException() + { + var submission = CreateSubmission(); + submission.StartRunning(); + + Assert.Throws(() => submission.StartRunning()); + } + + [Test] + public void UpdateResult_UnknownTestCaseId_ThrowsSubmissionResultNotFoundException() + { + var submission = CreateSubmission(); + + Assert.Throws(() => + submission.UpdateResult(Guid.NewGuid(), SubmissionResultStatus.Accepted)); + } + + [Test] + public void UpdateResult_UpdatesMatchingResult() + { + var submission = CreateSubmission(); + + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Accepted, runtime: 100, memoryUsed: 32); + + var result = submission.Results.First(r => r.TestCaseId == TestCaseId1); + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Status, Is.EqualTo(SubmissionResultStatus.Accepted)); + Assert.That(result.Runtime, Is.EqualTo(100)); + Assert.That(result.MemoryUsed, Is.EqualTo(32)); + } + } + + [Test] + public void UpdateResult_Overwrite_UpdatesExistingResult() + { + var submission = CreateSubmission(); + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Processing); + + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Accepted, runtime: 50); + + var result = submission.Results.First(r => r.TestCaseId == TestCaseId1); + Assert.That(result.Status, Is.EqualTo(SubmissionResultStatus.Accepted)); + } + + [Test] + public void UpdateResult_DoesNotAffectOtherResults() + { + var submission = CreateSubmission(); + + submission.UpdateResult(TestCaseId1, SubmissionResultStatus.Accepted); + + var other = submission.Results.First(r => r.TestCaseId == TestCaseId2); + Assert.That(other.Status, Is.EqualTo(SubmissionResultStatus.Pending)); + } +} diff --git a/Algowars.Domain.Tests/Submissions/ValueObjects/SourceCodeTests.cs b/Algowars.Domain.Tests/Submissions/ValueObjects/SourceCodeTests.cs new file mode 100644 index 0000000..295f339 --- /dev/null +++ b/Algowars.Domain.Tests/Submissions/ValueObjects/SourceCodeTests.cs @@ -0,0 +1,71 @@ +using Algowars.Domain.Submissions.Exceptions; +using Algowars.Domain.Submissions.ValueObjects; + +namespace Algowars.Domain.Tests.Submissions.ValueObjects; + +public class SourceCodeTests +{ + [Test] + public void Constructor_AtMaxLength_Succeeds() + { + string value = new('a', SourceCode.MaxLength); + + Assert.That(() => new SourceCode(value), Throws.Nothing); + } + + [Test] + public void Constructor_EmptyString_ThrowsInvalidSourceCodeException() + { + Assert.Throws(() => new SourceCode(string.Empty)); + } + + [Test] + public void Constructor_ExceedsMaxLength_ThrowsInvalidSourceCodeException() + { + string value = new('a', SourceCode.MaxLength + 1); + + Assert.Throws(() => new SourceCode(value)); + } + + [Test] + public void Constructor_WhitespaceOnly_ThrowsInvalidSourceCodeException() + { + Assert.Throws(() => new SourceCode(" ")); + } + + [Test] + public void Equality_DifferentValues_AreNotEqual() + { + var a = new SourceCode("int main() {}"); + var b = new SourceCode("def solve(): pass"); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void Equality_SameValue_AreEqual() + { + var a = new SourceCode("int main() {}"); + var b = new SourceCode("int main() {}"); + + Assert.That(a, Is.EqualTo(b)); + } + + [Test] + public void ImplicitConversion_ReturnsValue() + { + var code = new SourceCode("int main() {}"); + + string result = code; + + Assert.That(result, Is.EqualTo("int main() {}")); + } + + [Test] + public void ToString_ReturnsValue() + { + var code = new SourceCode("int main() {}"); + + Assert.That(code.ToString(), Is.EqualTo("int main() {}")); + } +} diff --git a/Algowars.Domain.Tests/User/Entities/UserTests.cs b/Algowars.Domain.Tests/User/Entities/UserTests.cs index 117a18a..4fe4dbb 100644 --- a/Algowars.Domain.Tests/User/Entities/UserTests.cs +++ b/Algowars.Domain.Tests/User/Entities/UserTests.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.User.Exceptions; -using Algowars.Domain.User.ValueObjects; -using UserEntity = Algowars.Domain.User.Entities.User; +using Algowars.Domain.Users.Exceptions; +using Algowars.Domain.Users.ValueObjects; +using UserEntity = Algowars.Domain.Users.Entities.User; namespace Algowars.Domain.Tests.User.Entities; diff --git a/Algowars.Domain.Tests/User/ValueObjects/BioTests.cs b/Algowars.Domain.Tests/User/ValueObjects/BioTests.cs index 835b10b..b611b6f 100644 --- a/Algowars.Domain.Tests/User/ValueObjects/BioTests.cs +++ b/Algowars.Domain.Tests/User/ValueObjects/BioTests.cs @@ -1,5 +1,5 @@ -using Algowars.Domain.User.Exceptions; -using Algowars.Domain.User.ValueObjects; +using Algowars.Domain.Users.Exceptions; +using Algowars.Domain.Users.ValueObjects; namespace Algowars.Domain.Tests.User.ValueObjects; diff --git a/Algowars.Domain.Tests/User/ValueObjects/ImageUrlTests.cs b/Algowars.Domain.Tests/User/ValueObjects/ImageUrlTests.cs index 0d94559..be6bc49 100644 --- a/Algowars.Domain.Tests/User/ValueObjects/ImageUrlTests.cs +++ b/Algowars.Domain.Tests/User/ValueObjects/ImageUrlTests.cs @@ -1,5 +1,5 @@ -using Algowars.Domain.User.Exceptions; -using Algowars.Domain.User.ValueObjects; +using Algowars.Domain.Users.Exceptions; +using Algowars.Domain.Users.ValueObjects; namespace Algowars.Domain.Tests.User.ValueObjects; diff --git a/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs b/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs index 762781b..125572f 100644 --- a/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs +++ b/Algowars.Domain.Tests/User/ValueObjects/UsernameTests.cs @@ -1,5 +1,5 @@ -using Algowars.Domain.User.Exceptions; -using Algowars.Domain.User.ValueObjects; +using Algowars.Domain.Users.Exceptions; +using Algowars.Domain.Users.ValueObjects; namespace Algowars.Domain.Tests.User.ValueObjects; diff --git a/Algowars.Domain/Algowars.Domain.csproj b/Algowars.Domain/Algowars.Domain.csproj index e1d2a50..b760144 100644 --- a/Algowars.Domain/Algowars.Domain.csproj +++ b/Algowars.Domain/Algowars.Domain.csproj @@ -6,9 +6,4 @@ enable - - - - - diff --git a/Algowars.Domain/Languages/Entities/Language.cs b/Algowars.Domain/Languages/Entities/Language.cs new file mode 100644 index 0000000..91c74ed --- /dev/null +++ b/Algowars.Domain/Languages/Entities/Language.cs @@ -0,0 +1,51 @@ +using Algowars.Domain.Languages.Enums; +using Algowars.Domain.Languages.Exceptions; +using Algowars.Domain.Languages.ValueObjects; +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Languages.Entities; + +public sealed class Language : AggregateRoot +{ + public Language(LanguageName name, LanguageSlug slug) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Slug = slug ?? throw new ArgumentNullException(nameof(slug)); + Status = LanguageStatus.Active; + } + + public void Activate() + { + Status = LanguageStatus.Active; + } + + public LanguageVersionEntry AddVersion(LanguageVersion version) + { + var entry = new LanguageVersionEntry(version); + _versions.Add(entry); + return entry; + } + + public void Deactivate() + { + Status = LanguageStatus.Inactive; + } + + public void DeprecateVersion(Guid versionId) + { + var version = _versions.FirstOrDefault(v => v.Id == versionId) + ?? throw new LanguageVersionNotFoundException(versionId); + + version.Deprecate(); + } + + private Language() { } + + public bool IsActive => Status == LanguageStatus.Active; + public LanguageName Name { get; private set; } = null!; + public LanguageSlug Slug { get; private set; } = null!; + public LanguageStatus Status { get; private set; } + public IReadOnlyCollection Versions => _versions.AsReadOnly(); + + private readonly List _versions = []; +} diff --git a/Algowars.Domain/Languages/Entities/LanguageVersionEntry.cs b/Algowars.Domain/Languages/Entities/LanguageVersionEntry.cs new file mode 100644 index 0000000..0179aac --- /dev/null +++ b/Algowars.Domain/Languages/Entities/LanguageVersionEntry.cs @@ -0,0 +1,25 @@ +using Algowars.Domain.Languages.Enums; +using Algowars.Domain.Languages.ValueObjects; +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Languages.Entities; + +public sealed class LanguageVersionEntry : Entity +{ + internal LanguageVersionEntry(LanguageVersion version) + { + Version = version ?? throw new ArgumentNullException(nameof(version)); + Status = LanguageVersionStatus.Active; + } + + public void Deprecate() + { + Status = LanguageVersionStatus.Deprecated; + } + + private LanguageVersionEntry() { } + + public bool IsActive => Status == LanguageVersionStatus.Active; + public LanguageVersionStatus Status { get; private set; } + public LanguageVersion Version { get; private set; } = null!; +} diff --git a/Algowars.Domain/Languages/Enums/LanguageStatus.cs b/Algowars.Domain/Languages/Enums/LanguageStatus.cs new file mode 100644 index 0000000..340e1ff --- /dev/null +++ b/Algowars.Domain/Languages/Enums/LanguageStatus.cs @@ -0,0 +1,7 @@ +namespace Algowars.Domain.Languages.Enums; + +public enum LanguageStatus +{ + Active, + Inactive +} diff --git a/Algowars.Domain/Languages/Enums/LanguageVersionStatus.cs b/Algowars.Domain/Languages/Enums/LanguageVersionStatus.cs new file mode 100644 index 0000000..a26869f --- /dev/null +++ b/Algowars.Domain/Languages/Enums/LanguageVersionStatus.cs @@ -0,0 +1,7 @@ +namespace Algowars.Domain.Languages.Enums; + +public enum LanguageVersionStatus +{ + Active, + Deprecated +} diff --git a/Algowars.Domain/Languages/Exceptions/InvalidLanguageNameException.cs b/Algowars.Domain/Languages/Exceptions/InvalidLanguageNameException.cs new file mode 100644 index 0000000..82e370f --- /dev/null +++ b/Algowars.Domain/Languages/Exceptions/InvalidLanguageNameException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Languages.Exceptions; + +public sealed class InvalidLanguageNameException : DomainException +{ + public InvalidLanguageNameException(string reason) + : base($"Language name is invalid: {reason}") { } +} diff --git a/Algowars.Domain/Languages/Exceptions/InvalidLanguageSlugException.cs b/Algowars.Domain/Languages/Exceptions/InvalidLanguageSlugException.cs new file mode 100644 index 0000000..0d45114 --- /dev/null +++ b/Algowars.Domain/Languages/Exceptions/InvalidLanguageSlugException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Languages.Exceptions; + +public sealed class InvalidLanguageSlugException(string reason) : DomainException($"Language slug is invalid: {reason}") +{ +} diff --git a/Algowars.Domain/Languages/Exceptions/InvalidLanguageVersionException.cs b/Algowars.Domain/Languages/Exceptions/InvalidLanguageVersionException.cs new file mode 100644 index 0000000..373bd02 --- /dev/null +++ b/Algowars.Domain/Languages/Exceptions/InvalidLanguageVersionException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Languages.Exceptions; + +public sealed class InvalidLanguageVersionException(string reason) : DomainException($"Language version is invalid: {reason}") +{ +} diff --git a/Algowars.Domain/Languages/Exceptions/LanguageVersionNotFoundException.cs b/Algowars.Domain/Languages/Exceptions/LanguageVersionNotFoundException.cs new file mode 100644 index 0000000..4176e89 --- /dev/null +++ b/Algowars.Domain/Languages/Exceptions/LanguageVersionNotFoundException.cs @@ -0,0 +1,7 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Languages.Exceptions; + +public sealed class LanguageVersionNotFoundException(Guid versionId) : DomainException($"Language version with ID '{versionId}' was not found.") +{ +} diff --git a/Algowars.Domain/Languages/ILanguageRepository.cs b/Algowars.Domain/Languages/ILanguageRepository.cs new file mode 100644 index 0000000..00b85e2 --- /dev/null +++ b/Algowars.Domain/Languages/ILanguageRepository.cs @@ -0,0 +1,11 @@ +using Algowars.Domain.Languages.ValueObjects; + +namespace Algowars.Domain.Languages; + +public interface ILanguageRepository +{ + Task AddAsync(Entities.Language language, CancellationToken cancellationToken = default); + Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task FindBySlugAsync(LanguageSlug slug, CancellationToken cancellationToken = default); + Task UpdateAsync(Entities.Language language, CancellationToken cancellationToken = default); +} diff --git a/Algowars.Domain/Languages/ValueObjects/LanguageName.cs b/Algowars.Domain/Languages/ValueObjects/LanguageName.cs new file mode 100644 index 0000000..18ca8a0 --- /dev/null +++ b/Algowars.Domain/Languages/ValueObjects/LanguageName.cs @@ -0,0 +1,24 @@ +using Algowars.Domain.Languages.Exceptions; + +namespace Algowars.Domain.Languages.ValueObjects; + +public sealed record LanguageName +{ + public LanguageName(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidLanguageNameException("Name cannot be empty."); + + if (value.Length < MinLength || value.Length > MaxLength) + throw new InvalidLanguageNameException($"Name must be between {MinLength} and {MaxLength} characters."); + + Value = value; + } + + public static implicit operator string(LanguageName name) => name.Value; + public override string ToString() => Value; + + public static readonly int MaxLength = 100; + public static readonly int MinLength = 1; + public string Value { get; } +} diff --git a/Algowars.Domain/Languages/ValueObjects/LanguageSlug.cs b/Algowars.Domain/Languages/ValueObjects/LanguageSlug.cs new file mode 100644 index 0000000..41304af --- /dev/null +++ b/Algowars.Domain/Languages/ValueObjects/LanguageSlug.cs @@ -0,0 +1,43 @@ +using System.Text.RegularExpressions; +using Algowars.Domain.Languages.Exceptions; + +namespace Algowars.Domain.Languages.ValueObjects; + +public sealed record LanguageSlug +{ + private static readonly Regex ValidSlugPattern = new(@"^[a-z0-9]+(?:-[a-z0-9]+)*$", RegexOptions.Compiled); + private static readonly Regex InvalidSlugCharactersPattern = new(@"[^a-z0-9\s-]", RegexOptions.Compiled); + private static readonly Regex MultipleWhitespacePattern = new(@"\s+", RegexOptions.Compiled); + private static readonly Regex MultipleHyphensPattern = new(@"-+", RegexOptions.Compiled); + + public LanguageSlug(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidLanguageSlugException("Slug cannot be empty."); + + if (value.Length < MinLength || value.Length > MaxLength) + throw new InvalidLanguageSlugException($"Slug must be between {MinLength} and {MaxLength} characters."); + + if (!ValidSlugPattern.IsMatch(value)) + throw new InvalidLanguageSlugException("Slug must be lowercase, contain only letters, numbers, and hyphens, and cannot start or end with a hyphen."); + + Value = value; + } + + public static LanguageSlug FromName(LanguageName name) + { + string slug = name.Value.ToLowerInvariant(); + slug = InvalidSlugCharactersPattern.Replace(slug, string.Empty); + slug = MultipleWhitespacePattern.Replace(slug, "-"); + slug = MultipleHyphensPattern.Replace(slug, "-"); + slug = slug.Trim('-'); + return new LanguageSlug(slug); + } + + public static implicit operator string(LanguageSlug slug) => slug.Value; + public override string ToString() => Value; + + public static readonly int MaxLength = 100; + public static readonly int MinLength = 1; + public string Value { get; } +} diff --git a/Algowars.Domain/Languages/ValueObjects/LanguageVersion.cs b/Algowars.Domain/Languages/ValueObjects/LanguageVersion.cs new file mode 100644 index 0000000..bc672df --- /dev/null +++ b/Algowars.Domain/Languages/ValueObjects/LanguageVersion.cs @@ -0,0 +1,23 @@ +using Algowars.Domain.Languages.Exceptions; + +namespace Algowars.Domain.Languages.ValueObjects; + +public sealed record LanguageVersion +{ + public LanguageVersion(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidLanguageVersionException("Version cannot be empty."); + + if (value.Length > MaxLength) + throw new InvalidLanguageVersionException($"Version cannot exceed {MaxLength} characters."); + + Value = value; + } + + public static implicit operator string(LanguageVersion version) => version.Value; + public override string ToString() => Value; + + public static readonly int MaxLength = 50; + public string Value { get; } +} diff --git a/Algowars.Domain/Problem/Entities/CodeTemplate.cs b/Algowars.Domain/Problems/Entities/CodeTemplate.cs similarity index 93% rename from Algowars.Domain/Problem/Entities/CodeTemplate.cs rename to Algowars.Domain/Problems/Entities/CodeTemplate.cs index 4adeebf..0c0db60 100644 --- a/Algowars.Domain/Problem/Entities/CodeTemplate.cs +++ b/Algowars.Domain/Problems/Entities/CodeTemplate.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Entities; +namespace Algowars.Domain.Problems.Entities; public sealed class CodeTemplate : Entity { diff --git a/Algowars.Domain/Problem/Entities/Example.cs b/Algowars.Domain/Problems/Entities/Example.cs similarity index 92% rename from Algowars.Domain/Problem/Entities/Example.cs rename to Algowars.Domain/Problems/Entities/Example.cs index 23166e5..f3f97c7 100644 --- a/Algowars.Domain/Problem/Entities/Example.cs +++ b/Algowars.Domain/Problems/Entities/Example.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Entities; +namespace Algowars.Domain.Problems.Entities; public sealed class Example : Entity { diff --git a/Algowars.Domain/Problem/Entities/Problem.cs b/Algowars.Domain/Problems/Entities/Problem.cs similarity index 91% rename from Algowars.Domain/Problem/Entities/Problem.cs rename to Algowars.Domain/Problems/Entities/Problem.cs index a57e49e..8b1502c 100644 --- a/Algowars.Domain/Problem/Entities/Problem.cs +++ b/Algowars.Domain/Problems/Entities/Problem.cs @@ -1,9 +1,9 @@ -using Algowars.Domain.Problem.Enums; -using Algowars.Domain.Problem.Exceptions; -using Algowars.Domain.Problem.ValueObjects; +using Algowars.Domain.Problems.Enums; +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Entities; +namespace Algowars.Domain.Problems.Entities; public sealed class Problem : AggregateRoot { diff --git a/Algowars.Domain/Problem/Entities/ProblemVersion.cs b/Algowars.Domain/Problems/Entities/ProblemVersion.cs similarity index 96% rename from Algowars.Domain/Problem/Entities/ProblemVersion.cs rename to Algowars.Domain/Problems/Entities/ProblemVersion.cs index 483536a..a9a3503 100644 --- a/Algowars.Domain/Problem/Entities/ProblemVersion.cs +++ b/Algowars.Domain/Problems/Entities/ProblemVersion.cs @@ -1,8 +1,8 @@ -using Algowars.Domain.Problem.Exceptions; -using Algowars.Domain.Problem.ValueObjects; +using Algowars.Domain.Problems.Exceptions; +using Algowars.Domain.Problems.ValueObjects; using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Entities; +namespace Algowars.Domain.Problems.Entities; public sealed class ProblemVersion : Entity { diff --git a/Algowars.Domain/Problem/Entities/TestCase.cs b/Algowars.Domain/Problems/Entities/TestCase.cs similarity index 92% rename from Algowars.Domain/Problem/Entities/TestCase.cs rename to Algowars.Domain/Problems/Entities/TestCase.cs index 83b17f0..333578b 100644 --- a/Algowars.Domain/Problem/Entities/TestCase.cs +++ b/Algowars.Domain/Problems/Entities/TestCase.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Entities; +namespace Algowars.Domain.Problems.Entities; public sealed class TestCase : Entity { diff --git a/Algowars.Domain/Problem/Enums/DifficultyTier.cs b/Algowars.Domain/Problems/Enums/DifficultyTier.cs similarity index 64% rename from Algowars.Domain/Problem/Enums/DifficultyTier.cs rename to Algowars.Domain/Problems/Enums/DifficultyTier.cs index 8f52e15..0d429d1 100644 --- a/Algowars.Domain/Problem/Enums/DifficultyTier.cs +++ b/Algowars.Domain/Problems/Enums/DifficultyTier.cs @@ -1,4 +1,4 @@ -namespace Algowars.Domain.Problem.Enums; +namespace Algowars.Domain.Problems.Enums; public enum DifficultyTier { diff --git a/Algowars.Domain/Problem/Enums/ProblemStatus.cs b/Algowars.Domain/Problems/Enums/ProblemStatus.cs similarity index 66% rename from Algowars.Domain/Problem/Enums/ProblemStatus.cs rename to Algowars.Domain/Problems/Enums/ProblemStatus.cs index 632a152..fe43c0e 100644 --- a/Algowars.Domain/Problem/Enums/ProblemStatus.cs +++ b/Algowars.Domain/Problems/Enums/ProblemStatus.cs @@ -1,4 +1,4 @@ -namespace Algowars.Domain.Problem.Enums; +namespace Algowars.Domain.Problems.Enums; public enum ProblemStatus { diff --git a/Algowars.Domain/Problem/Exceptions/InvalidDifficultyException.cs b/Algowars.Domain/Problems/Exceptions/InvalidDifficultyException.cs similarity index 76% rename from Algowars.Domain/Problem/Exceptions/InvalidDifficultyException.cs rename to Algowars.Domain/Problems/Exceptions/InvalidDifficultyException.cs index e491e9f..5ea2a3b 100644 --- a/Algowars.Domain/Problem/Exceptions/InvalidDifficultyException.cs +++ b/Algowars.Domain/Problems/Exceptions/InvalidDifficultyException.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Exceptions; +namespace Algowars.Domain.Problems.Exceptions; public sealed class InvalidDifficultyException(string reason) : DomainException($"Difficulty is invalid: {reason}") { diff --git a/Algowars.Domain/Problem/Exceptions/InvalidMemoryLimitException.cs b/Algowars.Domain/Problems/Exceptions/InvalidMemoryLimitException.cs similarity index 76% rename from Algowars.Domain/Problem/Exceptions/InvalidMemoryLimitException.cs rename to Algowars.Domain/Problems/Exceptions/InvalidMemoryLimitException.cs index a6e0bf2..911af7e 100644 --- a/Algowars.Domain/Problem/Exceptions/InvalidMemoryLimitException.cs +++ b/Algowars.Domain/Problems/Exceptions/InvalidMemoryLimitException.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Exceptions; +namespace Algowars.Domain.Problems.Exceptions; public sealed class InvalidMemoryLimitException(string reason) : DomainException($"Memory limit is invalid: {reason}") { diff --git a/Algowars.Domain/Problem/Exceptions/InvalidQuestionException.cs b/Algowars.Domain/Problems/Exceptions/InvalidQuestionException.cs similarity index 76% rename from Algowars.Domain/Problem/Exceptions/InvalidQuestionException.cs rename to Algowars.Domain/Problems/Exceptions/InvalidQuestionException.cs index d0c32a5..33f7094 100644 --- a/Algowars.Domain/Problem/Exceptions/InvalidQuestionException.cs +++ b/Algowars.Domain/Problems/Exceptions/InvalidQuestionException.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Exceptions; +namespace Algowars.Domain.Problems.Exceptions; public sealed class InvalidQuestionException(string reason) : DomainException($"Question is invalid: {reason}") { diff --git a/Algowars.Domain/Problem/Exceptions/InvalidSlugException.cs b/Algowars.Domain/Problems/Exceptions/InvalidSlugException.cs similarity index 80% rename from Algowars.Domain/Problem/Exceptions/InvalidSlugException.cs rename to Algowars.Domain/Problems/Exceptions/InvalidSlugException.cs index e9271de..25ff126 100644 --- a/Algowars.Domain/Problem/Exceptions/InvalidSlugException.cs +++ b/Algowars.Domain/Problems/Exceptions/InvalidSlugException.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Exceptions; +namespace Algowars.Domain.Problems.Exceptions; public sealed class InvalidSlugException : DomainException { diff --git a/Algowars.Domain/Problem/Exceptions/InvalidTimeLimitException.cs b/Algowars.Domain/Problems/Exceptions/InvalidTimeLimitException.cs similarity index 76% rename from Algowars.Domain/Problem/Exceptions/InvalidTimeLimitException.cs rename to Algowars.Domain/Problems/Exceptions/InvalidTimeLimitException.cs index a5a24ea..e4a8ebc 100644 --- a/Algowars.Domain/Problem/Exceptions/InvalidTimeLimitException.cs +++ b/Algowars.Domain/Problems/Exceptions/InvalidTimeLimitException.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Exceptions; +namespace Algowars.Domain.Problems.Exceptions; public sealed class InvalidTimeLimitException(string reason) : DomainException($"Time limit is invalid: {reason}") { diff --git a/Algowars.Domain/Problem/Exceptions/InvalidTitleException.cs b/Algowars.Domain/Problems/Exceptions/InvalidTitleException.cs similarity index 75% rename from Algowars.Domain/Problem/Exceptions/InvalidTitleException.cs rename to Algowars.Domain/Problems/Exceptions/InvalidTitleException.cs index 03f97a1..f63a8f7 100644 --- a/Algowars.Domain/Problem/Exceptions/InvalidTitleException.cs +++ b/Algowars.Domain/Problems/Exceptions/InvalidTitleException.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Exceptions; +namespace Algowars.Domain.Problems.Exceptions; public sealed class InvalidTitleException(string reason) : DomainException($"Title is invalid: {reason}") { diff --git a/Algowars.Domain/Problem/Exceptions/ProblemVersionImmutableException.cs b/Algowars.Domain/Problems/Exceptions/ProblemVersionImmutableException.cs similarity index 83% rename from Algowars.Domain/Problem/Exceptions/ProblemVersionImmutableException.cs rename to Algowars.Domain/Problems/Exceptions/ProblemVersionImmutableException.cs index 359efb1..0a52392 100644 --- a/Algowars.Domain/Problem/Exceptions/ProblemVersionImmutableException.cs +++ b/Algowars.Domain/Problems/Exceptions/ProblemVersionImmutableException.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Exceptions; +namespace Algowars.Domain.Problems.Exceptions; public sealed class ProblemVersionImmutableException : DomainException { diff --git a/Algowars.Domain/Problem/Exceptions/ProblemVersionNotFoundException.cs b/Algowars.Domain/Problems/Exceptions/ProblemVersionNotFoundException.cs similarity index 78% rename from Algowars.Domain/Problem/Exceptions/ProblemVersionNotFoundException.cs rename to Algowars.Domain/Problems/Exceptions/ProblemVersionNotFoundException.cs index 88c835d..0d6e0ad 100644 --- a/Algowars.Domain/Problem/Exceptions/ProblemVersionNotFoundException.cs +++ b/Algowars.Domain/Problems/Exceptions/ProblemVersionNotFoundException.cs @@ -1,6 +1,6 @@ using Algowars.Domain.SeedWork; -namespace Algowars.Domain.Problem.Exceptions; +namespace Algowars.Domain.Problems.Exceptions; public sealed class ProblemVersionNotFoundException(Guid versionId) : DomainException($"Problem version '{versionId}' was not found.") { diff --git a/Algowars.Domain/Problem/IProblemRepository.cs b/Algowars.Domain/Problems/IProblemRepository.cs similarity index 66% rename from Algowars.Domain/Problem/IProblemRepository.cs rename to Algowars.Domain/Problems/IProblemRepository.cs index d0698ae..c9758eb 100644 --- a/Algowars.Domain/Problem/IProblemRepository.cs +++ b/Algowars.Domain/Problems/IProblemRepository.cs @@ -1,7 +1,7 @@ -using Algowars.Domain.Problem.Entities; -using Algowars.Domain.Problem.ValueObjects; +using Algowars.Domain.Problems.Entities; +using Algowars.Domain.Problems.ValueObjects; -namespace Algowars.Domain.Problem; +namespace Algowars.Domain.Problems; public interface IProblemRepository { diff --git a/Algowars.Domain/Problem/ValueObjects/Difficulty.cs b/Algowars.Domain/Problems/ValueObjects/Difficulty.cs similarity index 85% rename from Algowars.Domain/Problem/ValueObjects/Difficulty.cs rename to Algowars.Domain/Problems/ValueObjects/Difficulty.cs index 330c6af..9424a34 100644 --- a/Algowars.Domain/Problem/ValueObjects/Difficulty.cs +++ b/Algowars.Domain/Problems/ValueObjects/Difficulty.cs @@ -1,7 +1,7 @@ -using Algowars.Domain.Problem.Enums; -using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problems.Enums; +using Algowars.Domain.Problems.Exceptions; -namespace Algowars.Domain.Problem.ValueObjects; +namespace Algowars.Domain.Problems.ValueObjects; public sealed record Difficulty { diff --git a/Algowars.Domain/Problem/ValueObjects/MemoryLimit.cs b/Algowars.Domain/Problems/ValueObjects/MemoryLimit.cs similarity index 84% rename from Algowars.Domain/Problem/ValueObjects/MemoryLimit.cs rename to Algowars.Domain/Problems/ValueObjects/MemoryLimit.cs index f11592f..8ad7297 100644 --- a/Algowars.Domain/Problem/ValueObjects/MemoryLimit.cs +++ b/Algowars.Domain/Problems/ValueObjects/MemoryLimit.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problems.Exceptions; -namespace Algowars.Domain.Problem.ValueObjects; +namespace Algowars.Domain.Problems.ValueObjects; public sealed record MemoryLimit { diff --git a/Algowars.Domain/Problem/ValueObjects/Question.cs b/Algowars.Domain/Problems/ValueObjects/Question.cs similarity index 87% rename from Algowars.Domain/Problem/ValueObjects/Question.cs rename to Algowars.Domain/Problems/ValueObjects/Question.cs index 014e6c5..1fcab96 100644 --- a/Algowars.Domain/Problem/ValueObjects/Question.cs +++ b/Algowars.Domain/Problems/ValueObjects/Question.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problems.Exceptions; -namespace Algowars.Domain.Problem.ValueObjects; +namespace Algowars.Domain.Problems.ValueObjects; public sealed record Question { diff --git a/Algowars.Domain/Problem/ValueObjects/Slug.cs b/Algowars.Domain/Problems/ValueObjects/Slug.cs similarity index 63% rename from Algowars.Domain/Problem/ValueObjects/Slug.cs rename to Algowars.Domain/Problems/ValueObjects/Slug.cs index bba3806..3e1c41f 100644 --- a/Algowars.Domain/Problem/ValueObjects/Slug.cs +++ b/Algowars.Domain/Problems/ValueObjects/Slug.cs @@ -1,11 +1,14 @@ using System.Text.RegularExpressions; -using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problems.Exceptions; -namespace Algowars.Domain.Problem.ValueObjects; +namespace Algowars.Domain.Problems.ValueObjects; public sealed record Slug { private static readonly Regex ValidSlugPattern = new(@"^[a-z0-9]+(?:-[a-z0-9]+)*$", RegexOptions.Compiled); + private static readonly Regex InvalidSlugCharactersPattern = new(@"[^a-z0-9\s-]", RegexOptions.Compiled); + private static readonly Regex MultipleWhitespacePattern = new(@"\s+", RegexOptions.Compiled); + private static readonly Regex MultipleHyphensPattern = new(@"-+", RegexOptions.Compiled); public Slug(string value) { @@ -23,10 +26,10 @@ public Slug(string value) public static Slug FromTitle(Title title) { - var slug = title.Value.ToLowerInvariant(); - slug = Regex.Replace(slug, @"[^a-z0-9\s-]", string.Empty); - slug = Regex.Replace(slug, @"\s+", "-"); - slug = Regex.Replace(slug, @"-+", "-"); + string slug = title.Value.ToLowerInvariant(); + slug = InvalidSlugCharactersPattern.Replace(slug, string.Empty); + slug = MultipleWhitespacePattern.Replace(slug, "-"); + slug = MultipleHyphensPattern.Replace(slug, "-"); slug = slug.Trim('-'); return new Slug(slug); } diff --git a/Algowars.Domain/Problem/ValueObjects/TimeLimit.cs b/Algowars.Domain/Problems/ValueObjects/TimeLimit.cs similarity index 85% rename from Algowars.Domain/Problem/ValueObjects/TimeLimit.cs rename to Algowars.Domain/Problems/ValueObjects/TimeLimit.cs index 43d4d42..04ecd8b 100644 --- a/Algowars.Domain/Problem/ValueObjects/TimeLimit.cs +++ b/Algowars.Domain/Problems/ValueObjects/TimeLimit.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problems.Exceptions; -namespace Algowars.Domain.Problem.ValueObjects; +namespace Algowars.Domain.Problems.ValueObjects; public sealed record TimeLimit { diff --git a/Algowars.Domain/Problem/ValueObjects/Title.cs b/Algowars.Domain/Problems/ValueObjects/Title.cs similarity index 87% rename from Algowars.Domain/Problem/ValueObjects/Title.cs rename to Algowars.Domain/Problems/ValueObjects/Title.cs index 31c4662..3cbdec3 100644 --- a/Algowars.Domain/Problem/ValueObjects/Title.cs +++ b/Algowars.Domain/Problems/ValueObjects/Title.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.Problem.Exceptions; +using Algowars.Domain.Problems.Exceptions; -namespace Algowars.Domain.Problem.ValueObjects; +namespace Algowars.Domain.Problems.ValueObjects; public sealed record Title { diff --git a/Algowars.Domain/Submissions/Entities/Submission.cs b/Algowars.Domain/Submissions/Entities/Submission.cs new file mode 100644 index 0000000..d679d92 --- /dev/null +++ b/Algowars.Domain/Submissions/Entities/Submission.cs @@ -0,0 +1,75 @@ +using Algowars.Domain.SeedWork; +using Algowars.Domain.Submissions.Entities; +using Algowars.Domain.Submissions.Enums; +using Algowars.Domain.Submissions.Exceptions; +using Algowars.Domain.Submissions.ValueObjects; + +namespace Algowars.Domain.Submissions.Entities; + +public sealed class Submission : AggregateRoot +{ + public Submission( + Guid userId, + Guid problemVersionId, + Guid languageVersionId, + SubmissionType type, + SourceCode sourceCode, + IEnumerable testCaseIds) + { + UserId = userId; + ProblemVersionId = problemVersionId; + LanguageVersionId = languageVersionId; + Type = type; + SourceCode = sourceCode ?? throw new ArgumentNullException(nameof(sourceCode)); + Status = SubmissionStatus.Queued; + + foreach (Guid testCaseId in testCaseIds) + _results.Add(new SubmissionResult(testCaseId)); + } + + public void Complete() + { + if (_results.Any(r => !r.IsTerminal)) + throw new SubmissionNotCompleteException(); + + Status = _results.All(r => r.Status == SubmissionResultStatus.Accepted) + ? SubmissionStatus.Accepted + : SubmissionStatus.WrongAnswer; + } + + public void StartRunning() + { + if (Status != SubmissionStatus.Queued) + throw new InvalidSubmissionStateException("Only a queued submission can be started."); + + Status = SubmissionStatus.Running; + } + + public void UpdateResult( + Guid testCaseId, + SubmissionResultStatus status, + int? runtime = null, + int? memoryUsed = null, + string? actualOutput = null, + string? standardOutput = null, + string? standardError = null, + string? compileOutput = null) + { + SubmissionResult result = _results.FirstOrDefault(r => r.TestCaseId == testCaseId) + ?? throw new SubmissionResultNotFoundException(testCaseId); + + result.Update(status, runtime, memoryUsed, actualOutput, standardOutput, standardError, compileOutput); + } + + private Submission() { } + + public Guid LanguageVersionId { get; private set; } + public Guid ProblemVersionId { get; private set; } + public IReadOnlyCollection Results => _results.AsReadOnly(); + public SourceCode SourceCode { get; private set; } = null!; + public SubmissionStatus Status { get; private set; } + public SubmissionType Type { get; private set; } + public Guid UserId { get; private set; } + + private readonly List _results = []; +} diff --git a/Algowars.Domain/Submissions/Entities/SubmissionResult.cs b/Algowars.Domain/Submissions/Entities/SubmissionResult.cs new file mode 100644 index 0000000..1984dec --- /dev/null +++ b/Algowars.Domain/Submissions/Entities/SubmissionResult.cs @@ -0,0 +1,45 @@ +using Algowars.Domain.Submissions.Enums; +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Submissions.Entities; + +public sealed class SubmissionResult : Entity +{ + internal SubmissionResult(Guid testCaseId) + { + TestCaseId = testCaseId; + Status = SubmissionResultStatus.Pending; + } + + public void Update( + SubmissionResultStatus status, + int? runtime = null, + int? memoryUsed = null, + string? actualOutput = null, + string? standardOutput = null, + string? standardError = null, + string? compileOutput = null) + { + Status = status; + Runtime = runtime; + MemoryUsed = memoryUsed; + ActualOutput = actualOutput; + StandardOutput = standardOutput; + StandardError = standardError; + CompileOutput = compileOutput; + } + + public bool IsTerminal => Status is not SubmissionResultStatus.Pending + and not SubmissionResultStatus.Processing; + + private SubmissionResult() { } + + public string? ActualOutput { get; private set; } + public string? CompileOutput { get; private set; } + public int? MemoryUsed { get; private set; } + public int? Runtime { get; private set; } + public string? StandardError { get; private set; } + public string? StandardOutput { get; private set; } + public SubmissionResultStatus Status { get; private set; } + public Guid TestCaseId { get; private set; } +} diff --git a/Algowars.Domain/Submissions/Enums/SubmissionResultStatus.cs b/Algowars.Domain/Submissions/Enums/SubmissionResultStatus.cs new file mode 100644 index 0000000..5ec86cd --- /dev/null +++ b/Algowars.Domain/Submissions/Enums/SubmissionResultStatus.cs @@ -0,0 +1,13 @@ +namespace Algowars.Domain.Submissions.Enums; + +public enum SubmissionResultStatus +{ + Pending, + Processing, + Accepted, + WrongAnswer, + TimeLimitExceeded, + MemoryLimitExceeded, + RuntimeError, + CompileError +} diff --git a/Algowars.Domain/Submissions/Enums/SubmissionStatus.cs b/Algowars.Domain/Submissions/Enums/SubmissionStatus.cs new file mode 100644 index 0000000..b507222 --- /dev/null +++ b/Algowars.Domain/Submissions/Enums/SubmissionStatus.cs @@ -0,0 +1,9 @@ +namespace Algowars.Domain.Submissions.Enums; + +public enum SubmissionStatus +{ + Queued, + Running, + Accepted, + WrongAnswer +} diff --git a/Algowars.Domain/Submissions/Enums/SubmissionType.cs b/Algowars.Domain/Submissions/Enums/SubmissionType.cs new file mode 100644 index 0000000..8c8aa9d --- /dev/null +++ b/Algowars.Domain/Submissions/Enums/SubmissionType.cs @@ -0,0 +1,7 @@ +namespace Algowars.Domain.Submissions.Enums; + +public enum SubmissionType +{ + Run, + Submit +} diff --git a/Algowars.Domain/Submissions/Exceptions/InvalidSourceCodeException.cs b/Algowars.Domain/Submissions/Exceptions/InvalidSourceCodeException.cs new file mode 100644 index 0000000..c00bb35 --- /dev/null +++ b/Algowars.Domain/Submissions/Exceptions/InvalidSourceCodeException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Submissions.Exceptions; + +public sealed class InvalidSourceCodeException : DomainException +{ + public InvalidSourceCodeException(string reason) + : base($"Source code is invalid: {reason}") { } +} diff --git a/Algowars.Domain/Submissions/Exceptions/InvalidSubmissionStateException.cs b/Algowars.Domain/Submissions/Exceptions/InvalidSubmissionStateException.cs new file mode 100644 index 0000000..48fcb9b --- /dev/null +++ b/Algowars.Domain/Submissions/Exceptions/InvalidSubmissionStateException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Submissions.Exceptions; + +public sealed class InvalidSubmissionStateException : DomainException +{ + public InvalidSubmissionStateException(string reason) + : base($"Submission state transition is invalid: {reason}") { } +} diff --git a/Algowars.Domain/Submissions/Exceptions/SubmissionNotCompleteException.cs b/Algowars.Domain/Submissions/Exceptions/SubmissionNotCompleteException.cs new file mode 100644 index 0000000..f4f559d --- /dev/null +++ b/Algowars.Domain/Submissions/Exceptions/SubmissionNotCompleteException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Submissions.Exceptions; + +public sealed class SubmissionNotCompleteException : DomainException +{ + public SubmissionNotCompleteException() + : base("Submission cannot be completed because one or more results are still pending or processing.") { } +} diff --git a/Algowars.Domain/Submissions/Exceptions/SubmissionResultNotFoundException.cs b/Algowars.Domain/Submissions/Exceptions/SubmissionResultNotFoundException.cs new file mode 100644 index 0000000..a266b83 --- /dev/null +++ b/Algowars.Domain/Submissions/Exceptions/SubmissionResultNotFoundException.cs @@ -0,0 +1,9 @@ +using Algowars.Domain.SeedWork; + +namespace Algowars.Domain.Submissions.Exceptions; + +public sealed class SubmissionResultNotFoundException : DomainException +{ + public SubmissionResultNotFoundException(Guid testCaseId) + : base($"Submission result for test case '{testCaseId}' was not found.") { } +} diff --git a/Algowars.Domain/Submissions/ISubmissionRepository.cs b/Algowars.Domain/Submissions/ISubmissionRepository.cs new file mode 100644 index 0000000..cc3e5cf --- /dev/null +++ b/Algowars.Domain/Submissions/ISubmissionRepository.cs @@ -0,0 +1,10 @@ +using Algowars.Domain.Submissions.Entities; + +namespace Algowars.Domain.Submissions; + +public interface ISubmissionRepository +{ + Task AddAsync(Submission submission, CancellationToken cancellationToken = default); + Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task UpdateAsync(Submission submission, CancellationToken cancellationToken = default); +} diff --git a/Algowars.Domain/Submissions/ValueObjects/SourceCode.cs b/Algowars.Domain/Submissions/ValueObjects/SourceCode.cs new file mode 100644 index 0000000..35bb4a4 --- /dev/null +++ b/Algowars.Domain/Submissions/ValueObjects/SourceCode.cs @@ -0,0 +1,23 @@ +using Algowars.Domain.Submissions.Exceptions; + +namespace Algowars.Domain.Submissions.ValueObjects; + +public sealed record SourceCode +{ + public SourceCode(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidSourceCodeException("Source code cannot be empty."); + + if (value.Length > MaxLength) + throw new InvalidSourceCodeException($"Source code cannot exceed {MaxLength} characters."); + + Value = value; + } + + public static implicit operator string(SourceCode code) => code.Value; + public override string ToString() => Value; + + public static readonly int MaxLength = 65536; + public string Value { get; } +} diff --git a/Algowars.Domain/User/Entities/User.cs b/Algowars.Domain/Users/Entities/User.cs similarity index 90% rename from Algowars.Domain/User/Entities/User.cs rename to Algowars.Domain/Users/Entities/User.cs index 73781e3..76c5099 100644 --- a/Algowars.Domain/User/Entities/User.cs +++ b/Algowars.Domain/Users/Entities/User.cs @@ -1,8 +1,8 @@ using Algowars.Domain.SeedWork; -using Algowars.Domain.User.Exceptions; -using Algowars.Domain.User.ValueObjects; +using Algowars.Domain.Users.Exceptions; +using Algowars.Domain.Users.ValueObjects; -namespace Algowars.Domain.User.Entities; +namespace Algowars.Domain.Users.Entities; public sealed class User(Username username, string sub) : AggregateRoot { diff --git a/Algowars.Domain/User/Exceptions/InvalidBioException.cs b/Algowars.Domain/Users/Exceptions/InvalidBioException.cs similarity index 66% rename from Algowars.Domain/User/Exceptions/InvalidBioException.cs rename to Algowars.Domain/Users/Exceptions/InvalidBioException.cs index 2332557..98d271c 100644 --- a/Algowars.Domain/User/Exceptions/InvalidBioException.cs +++ b/Algowars.Domain/Users/Exceptions/InvalidBioException.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.SeedWork; +using Algowars.Domain.SeedWork; -namespace Algowars.Domain.User.Exceptions; +namespace Algowars.Domain.Users.Exceptions; public sealed class InvalidBioException : DomainException { diff --git a/Algowars.Domain/User/Exceptions/InvalidImageUrlException.cs b/Algowars.Domain/Users/Exceptions/InvalidImageUrlException.cs similarity index 60% rename from Algowars.Domain/User/Exceptions/InvalidImageUrlException.cs rename to Algowars.Domain/Users/Exceptions/InvalidImageUrlException.cs index f7cb4db..997d0f1 100644 --- a/Algowars.Domain/User/Exceptions/InvalidImageUrlException.cs +++ b/Algowars.Domain/Users/Exceptions/InvalidImageUrlException.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.SeedWork; +using Algowars.Domain.SeedWork; -namespace Algowars.Domain.User.Exceptions; +namespace Algowars.Domain.Users.Exceptions; public sealed class InvalidImageUrlException(string reason) : DomainException($"Image URL is invalid: {reason}") { diff --git a/Algowars.Domain/User/Exceptions/InvalidUserSubException.cs b/Algowars.Domain/Users/Exceptions/InvalidUserSubException.cs similarity index 65% rename from Algowars.Domain/User/Exceptions/InvalidUserSubException.cs rename to Algowars.Domain/Users/Exceptions/InvalidUserSubException.cs index a8110fe..8b7c9ae 100644 --- a/Algowars.Domain/User/Exceptions/InvalidUserSubException.cs +++ b/Algowars.Domain/Users/Exceptions/InvalidUserSubException.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.SeedWork; +using Algowars.Domain.SeedWork; -namespace Algowars.Domain.User.Exceptions; +namespace Algowars.Domain.Users.Exceptions; public sealed class InvalidUserSubException : DomainException { diff --git a/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs b/Algowars.Domain/Users/Exceptions/InvalidUsernameException.cs similarity index 60% rename from Algowars.Domain/User/Exceptions/InvalidUsernameException.cs rename to Algowars.Domain/Users/Exceptions/InvalidUsernameException.cs index 3a92afd..371f27a 100644 --- a/Algowars.Domain/User/Exceptions/InvalidUsernameException.cs +++ b/Algowars.Domain/Users/Exceptions/InvalidUsernameException.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.SeedWork; +using Algowars.Domain.SeedWork; -namespace Algowars.Domain.User.Exceptions; +namespace Algowars.Domain.Users.Exceptions; public sealed class InvalidUsernameException(string reason) : DomainException($"Username is invalid: {reason}") { diff --git a/Algowars.Domain/User/Exceptions/UsernameCooldownException.cs b/Algowars.Domain/Users/Exceptions/UsernameCooldownException.cs similarity index 81% rename from Algowars.Domain/User/Exceptions/UsernameCooldownException.cs rename to Algowars.Domain/Users/Exceptions/UsernameCooldownException.cs index 557aa30..8b392a4 100644 --- a/Algowars.Domain/User/Exceptions/UsernameCooldownException.cs +++ b/Algowars.Domain/Users/Exceptions/UsernameCooldownException.cs @@ -1,8 +1,8 @@ - + using Algowars.Domain.SeedWork; -namespace Algowars.Domain.User.Exceptions; +namespace Algowars.Domain.Users.Exceptions; public sealed class UsernameCooldownException(DateTime lastChangedAt) : DomainException($"Username can only be changed once every 30 days. Last changed at: {lastChangedAt}.") { diff --git a/Algowars.Domain/User/ValueObjects/Bio.cs b/Algowars.Domain/Users/ValueObjects/Bio.cs similarity index 86% rename from Algowars.Domain/User/ValueObjects/Bio.cs rename to Algowars.Domain/Users/ValueObjects/Bio.cs index dbdf4b5..bb5ddc5 100644 --- a/Algowars.Domain/User/ValueObjects/Bio.cs +++ b/Algowars.Domain/Users/ValueObjects/Bio.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.User.Exceptions; +using Algowars.Domain.Users.Exceptions; -namespace Algowars.Domain.User.ValueObjects; +namespace Algowars.Domain.Users.ValueObjects; public sealed record Bio { diff --git a/Algowars.Domain/User/ValueObjects/ImageUrl.cs b/Algowars.Domain/Users/ValueObjects/ImageUrl.cs similarity index 90% rename from Algowars.Domain/User/ValueObjects/ImageUrl.cs rename to Algowars.Domain/Users/ValueObjects/ImageUrl.cs index 639fd89..e5b1f42 100644 --- a/Algowars.Domain/User/ValueObjects/ImageUrl.cs +++ b/Algowars.Domain/Users/ValueObjects/ImageUrl.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.User.Exceptions; +using Algowars.Domain.Users.Exceptions; -namespace Algowars.Domain.User.ValueObjects; +namespace Algowars.Domain.Users.ValueObjects; public sealed record ImageUrl { diff --git a/Algowars.Domain/User/ValueObjects/Username.cs b/Algowars.Domain/Users/ValueObjects/Username.cs similarity index 88% rename from Algowars.Domain/User/ValueObjects/Username.cs rename to Algowars.Domain/Users/ValueObjects/Username.cs index fb27c95..ea0d521 100644 --- a/Algowars.Domain/User/ValueObjects/Username.cs +++ b/Algowars.Domain/Users/ValueObjects/Username.cs @@ -1,6 +1,6 @@ -using Algowars.Domain.User.Exceptions; +using Algowars.Domain.Users.Exceptions; -namespace Algowars.Domain.User.ValueObjects; +namespace Algowars.Domain.Users.ValueObjects; public sealed record Username { From 7ebac885efbb172c67447522e3933902593a21eb Mon Sep 17 00:00:00 2001 From: admclamb Date: Fri, 12 Jun 2026 12:28:03 -0400 Subject: [PATCH 07/34] update command handler --- .../Algowars.Application.csproj | 11 +++++ Algowars.Application/Class1.cs | 7 --- .../Commands/AbstractCommandHandler.cs | 44 +++++++++++++++++++ Algowars.Application/Commands/ICommand.cs | 8 ++++ .../Commands/ICommandHandler.cs | 13 ++++++ .../User/CreateUser/CreateUserCommand.cs | 3 ++ .../User/CreateUser/CreateUserHandler.cs | 16 +++++++ .../User/CreateUser/CreateUserValidator.cs | 26 +++++++++++ .../Enums/SubmissionResultStatus.cs | 0 .../Submission/Enums/SubmissionStatus.cs | 0 .../Submission/Enums/SubmissionType.cs | 0 .../Exceptions/InvalidSourceCodeException.cs | 0 .../InvalidSubmissionStateException.cs | 0 .../SubmissionResultNotFoundException.cs | 0 .../Submission/ISubmissionRepository.cs | 0 .../Submission/ValueObjects/SourceCode.cs | 0 .../Submissions/ISubmissionRepository.cs | 6 +-- Algowars.Domain/Users/IUserRepository.cs | 13 ++++++ .../Users/ValueObjects/Username.cs | 2 + Directory.Packages.props | 26 ++++++----- 20 files changed, 153 insertions(+), 22 deletions(-) delete mode 100644 Algowars.Application/Class1.cs create mode 100644 Algowars.Application/Commands/AbstractCommandHandler.cs create mode 100644 Algowars.Application/Commands/ICommand.cs create mode 100644 Algowars.Application/Commands/ICommandHandler.cs create mode 100644 Algowars.Application/Commands/User/CreateUser/CreateUserCommand.cs create mode 100644 Algowars.Application/Commands/User/CreateUser/CreateUserHandler.cs create mode 100644 Algowars.Application/Commands/User/CreateUser/CreateUserValidator.cs create mode 100644 Algowars.Domain/Submission/Enums/SubmissionResultStatus.cs create mode 100644 Algowars.Domain/Submission/Enums/SubmissionStatus.cs create mode 100644 Algowars.Domain/Submission/Enums/SubmissionType.cs create mode 100644 Algowars.Domain/Submission/Exceptions/InvalidSourceCodeException.cs create mode 100644 Algowars.Domain/Submission/Exceptions/InvalidSubmissionStateException.cs create mode 100644 Algowars.Domain/Submission/Exceptions/SubmissionResultNotFoundException.cs create mode 100644 Algowars.Domain/Submission/ISubmissionRepository.cs create mode 100644 Algowars.Domain/Submission/ValueObjects/SourceCode.cs create mode 100644 Algowars.Domain/Users/IUserRepository.cs diff --git a/Algowars.Application/Algowars.Application.csproj b/Algowars.Application/Algowars.Application.csproj index b760144..a87fd0d 100644 --- a/Algowars.Application/Algowars.Application.csproj +++ b/Algowars.Application/Algowars.Application.csproj @@ -6,4 +6,15 @@ enable + + + + + + + + + + + diff --git a/Algowars.Application/Class1.cs b/Algowars.Application/Class1.cs deleted file mode 100644 index 279cebc..0000000 --- a/Algowars.Application/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Algowars.Application -{ - public class Class1 - { - - } -} diff --git a/Algowars.Application/Commands/AbstractCommandHandler.cs b/Algowars.Application/Commands/AbstractCommandHandler.cs new file mode 100644 index 0000000..6547da4 --- /dev/null +++ b/Algowars.Application/Commands/AbstractCommandHandler.cs @@ -0,0 +1,44 @@ +using Algowars.Application.Commands; +using Ardalis.Result; +using FluentValidation; + +namespace ApplicationCore.Commands; + +public abstract class AbstractCommandHandler( + IValidator? validator = null +) : ICommandHandler + where TCommand : ICommand +{ + private readonly IValidator? _validator = validator; + + public async Task> Handle(TCommand request, CancellationToken cancellationToken) + { + if (_validator is not null) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = validationResult + .Errors.Select(e => + { + string identifier = e.FormattedMessagePlaceholderValues + .TryGetValue("PropertyName", out object? displayName) + ? displayName?.ToString() ?? e.PropertyName + : e.PropertyName; + + return new ValidationError(identifier, e.ErrorMessage); + }) + .ToList(); + + return Result.Invalid(errors); + } + } + + return await HandleValidated(request, cancellationToken); + } + + protected abstract Task> HandleValidated( + TCommand request, + CancellationToken cancellationToken + ); +} \ No newline at end of file diff --git a/Algowars.Application/Commands/ICommand.cs b/Algowars.Application/Commands/ICommand.cs new file mode 100644 index 0000000..e66698c --- /dev/null +++ b/Algowars.Application/Commands/ICommand.cs @@ -0,0 +1,8 @@ +using Ardalis.Result; +using MediatR; + +namespace Algowars.Application.Commands; + +public interface ICommand : IRequest> { } + +public interface ICommand : IRequest { } \ No newline at end of file diff --git a/Algowars.Application/Commands/ICommandHandler.cs b/Algowars.Application/Commands/ICommandHandler.cs new file mode 100644 index 0000000..0895fa6 --- /dev/null +++ b/Algowars.Application/Commands/ICommandHandler.cs @@ -0,0 +1,13 @@ +using Ardalis.Result; +using MediatR; + +namespace Algowars.Application.Commands; + +public interface ICommandHandler + : IRequestHandler> + where TCommand : ICommand +{ } + +public interface ICommandHandler : IRequestHandler + where TCommand : ICommand +{ } \ No newline at end of file diff --git a/Algowars.Application/Commands/User/CreateUser/CreateUserCommand.cs b/Algowars.Application/Commands/User/CreateUser/CreateUserCommand.cs new file mode 100644 index 0000000..cc1d348 --- /dev/null +++ b/Algowars.Application/Commands/User/CreateUser/CreateUserCommand.cs @@ -0,0 +1,3 @@ +namespace Algowars.Application.Commands.User.CreateUser; + +internal sealed record CreateUserCommand(string Username, string Sub, string? ImageUrl) : ICommand; diff --git a/Algowars.Application/Commands/User/CreateUser/CreateUserHandler.cs b/Algowars.Application/Commands/User/CreateUser/CreateUserHandler.cs new file mode 100644 index 0000000..efa99bf --- /dev/null +++ b/Algowars.Application/Commands/User/CreateUser/CreateUserHandler.cs @@ -0,0 +1,16 @@ +using Algowars.Domain.Users; +using ApplicationCore.Commands; +using Ardalis.Result; +using FluentValidation; + +namespace Algowars.Application.Commands.User.CreateUser; + +internal sealed partial class CreateUserHandler(IValidator validator, IUserRepository userRepository) : AbstractCommandHandler(validator) +{ + protected override async Task> HandleValidated(CreateUserCommand request, CancellationToken cancellationToken) + { + var foundUserBySub = await userRepository.FindBySubAsync(request.Sub, cancellationToken); + + + } +} diff --git a/Algowars.Application/Commands/User/CreateUser/CreateUserValidator.cs b/Algowars.Application/Commands/User/CreateUser/CreateUserValidator.cs new file mode 100644 index 0000000..03c38c0 --- /dev/null +++ b/Algowars.Application/Commands/User/CreateUser/CreateUserValidator.cs @@ -0,0 +1,26 @@ +using Algowars.Domain.Users.ValueObjects; +using FluentValidation; + +namespace Algowars.Application.Commands.User.CreateUser; + +internal sealed class CreateUserValidator : AbstractValidator +{ + public CreateUserValidator() + { + RuleFor(x => x.Username) + .NotEmpty() + .MaximumLength(Username.MaxLength) + .MinimumLength(Username.MinLength) + .Must(IsUsernameValid) + .WithMessage("Username contains invalid characters"); + + RuleFor(x => x.Sub) + .NotEmpty(); + + RuleFor(x => x.ImageUrl) + .MaximumLength(ImageUrl.MaxLength); + } + + private static bool IsUsernameValid(string username) => + username.All(c => char.IsLetterOrDigit(c) || c == '_' || c == '-'); +} diff --git a/Algowars.Domain/Submission/Enums/SubmissionResultStatus.cs b/Algowars.Domain/Submission/Enums/SubmissionResultStatus.cs new file mode 100644 index 0000000..e69de29 diff --git a/Algowars.Domain/Submission/Enums/SubmissionStatus.cs b/Algowars.Domain/Submission/Enums/SubmissionStatus.cs new file mode 100644 index 0000000..e69de29 diff --git a/Algowars.Domain/Submission/Enums/SubmissionType.cs b/Algowars.Domain/Submission/Enums/SubmissionType.cs new file mode 100644 index 0000000..e69de29 diff --git a/Algowars.Domain/Submission/Exceptions/InvalidSourceCodeException.cs b/Algowars.Domain/Submission/Exceptions/InvalidSourceCodeException.cs new file mode 100644 index 0000000..e69de29 diff --git a/Algowars.Domain/Submission/Exceptions/InvalidSubmissionStateException.cs b/Algowars.Domain/Submission/Exceptions/InvalidSubmissionStateException.cs new file mode 100644 index 0000000..e69de29 diff --git a/Algowars.Domain/Submission/Exceptions/SubmissionResultNotFoundException.cs b/Algowars.Domain/Submission/Exceptions/SubmissionResultNotFoundException.cs new file mode 100644 index 0000000..e69de29 diff --git a/Algowars.Domain/Submission/ISubmissionRepository.cs b/Algowars.Domain/Submission/ISubmissionRepository.cs new file mode 100644 index 0000000..e69de29 diff --git a/Algowars.Domain/Submission/ValueObjects/SourceCode.cs b/Algowars.Domain/Submission/ValueObjects/SourceCode.cs new file mode 100644 index 0000000..e69de29 diff --git a/Algowars.Domain/Submissions/ISubmissionRepository.cs b/Algowars.Domain/Submissions/ISubmissionRepository.cs index cc3e5cf..cf53792 100644 --- a/Algowars.Domain/Submissions/ISubmissionRepository.cs +++ b/Algowars.Domain/Submissions/ISubmissionRepository.cs @@ -4,7 +4,7 @@ namespace Algowars.Domain.Submissions; public interface ISubmissionRepository { - Task AddAsync(Submission submission, CancellationToken cancellationToken = default); - Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default); - Task UpdateAsync(Submission submission, CancellationToken cancellationToken = default); + Task AddAsync(Submission submission, CancellationToken cancellationToken); + Task FindByIdAsync(Guid id, CancellationToken cancellationToken); + Task UpdateAsync(Submission submission, CancellationToken cancellationToken); } diff --git a/Algowars.Domain/Users/IUserRepository.cs b/Algowars.Domain/Users/IUserRepository.cs new file mode 100644 index 0000000..9d6fc40 --- /dev/null +++ b/Algowars.Domain/Users/IUserRepository.cs @@ -0,0 +1,13 @@ +using Algowars.Domain.Users.Entities; +using Algowars.Domain.Users.ValueObjects; + +namespace Algowars.Domain.Users; + +public interface IUserRepository +{ + Task FindByIdAsync(Guid id, CancellationToken cancellationToken); + + Task FindByUsrenameAsync(Username usrename, CancellationToken cancellationToken); + + Task FindBySubAsync(string sub, CancellationToken cancellationToken); +} diff --git a/Algowars.Domain/Users/ValueObjects/Username.cs b/Algowars.Domain/Users/ValueObjects/Username.cs index ea0d521..9cf6d4b 100644 --- a/Algowars.Domain/Users/ValueObjects/Username.cs +++ b/Algowars.Domain/Users/ValueObjects/Username.cs @@ -12,6 +12,8 @@ public Username(string value) if (value.Length < MinLength || value.Length > MaxLength) throw new InvalidUsernameException($"Username must be between {MinLength} and {MaxLength} characters."); + if (!value.All(c => char.IsLetterOrDigit(c) || c == '_' || c == '-')) + throw new InvalidUsernameException("Username can only contain letters, digits, underscores, or hyphens."); Value = value; } diff --git a/Directory.Packages.props b/Directory.Packages.props index 6800179..c18f1e8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,17 +1,19 @@ - true + true - - - - - - - - - - + + + + + + + + + + + + - + \ No newline at end of file From c20c776947193adf7ecafc17361c8bbd085a74e0 Mon Sep 17 00:00:00 2001 From: admclamb Date: Sat, 13 Jun 2026 02:00:12 -0400 Subject: [PATCH 08/34] Setting up account handler --- .../User/CreateUser/CreateUserHandler.cs | 16 --------- .../CreateUser/CreateUserCommand.cs | 2 +- .../Users/CreateUser/CreateUserHandler.cs | 36 +++++++++++++++++++ .../CreateUser/CreateUserValidator.cs | 2 +- Algowars.Domain/SeedWork/IAggregateFactory.cs | 7 ++++ .../Users/Factories/UserFactory.cs | 19 ++++++++++ Algowars.Domain/Users/IUserRepository.cs | 5 ++- 7 files changed, 66 insertions(+), 21 deletions(-) delete mode 100644 Algowars.Application/Commands/User/CreateUser/CreateUserHandler.cs rename Algowars.Application/Commands/{User => Users}/CreateUser/CreateUserCommand.cs (63%) create mode 100644 Algowars.Application/Commands/Users/CreateUser/CreateUserHandler.cs rename Algowars.Application/Commands/{User => Users}/CreateUser/CreateUserValidator.cs (92%) create mode 100644 Algowars.Domain/SeedWork/IAggregateFactory.cs create mode 100644 Algowars.Domain/Users/Factories/UserFactory.cs diff --git a/Algowars.Application/Commands/User/CreateUser/CreateUserHandler.cs b/Algowars.Application/Commands/User/CreateUser/CreateUserHandler.cs deleted file mode 100644 index efa99bf..0000000 --- a/Algowars.Application/Commands/User/CreateUser/CreateUserHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Algowars.Domain.Users; -using ApplicationCore.Commands; -using Ardalis.Result; -using FluentValidation; - -namespace Algowars.Application.Commands.User.CreateUser; - -internal sealed partial class CreateUserHandler(IValidator validator, IUserRepository userRepository) : AbstractCommandHandler(validator) -{ - protected override async Task> HandleValidated(CreateUserCommand request, CancellationToken cancellationToken) - { - var foundUserBySub = await userRepository.FindBySubAsync(request.Sub, cancellationToken); - - - } -} diff --git a/Algowars.Application/Commands/User/CreateUser/CreateUserCommand.cs b/Algowars.Application/Commands/Users/CreateUser/CreateUserCommand.cs similarity index 63% rename from Algowars.Application/Commands/User/CreateUser/CreateUserCommand.cs rename to Algowars.Application/Commands/Users/CreateUser/CreateUserCommand.cs index cc1d348..b1d7bd3 100644 --- a/Algowars.Application/Commands/User/CreateUser/CreateUserCommand.cs +++ b/Algowars.Application/Commands/Users/CreateUser/CreateUserCommand.cs @@ -1,3 +1,3 @@ -namespace Algowars.Application.Commands.User.CreateUser; +namespace Algowars.Application.Commands.Users.CreateUser; internal sealed record CreateUserCommand(string Username, string Sub, string? ImageUrl) : ICommand; diff --git a/Algowars.Application/Commands/Users/CreateUser/CreateUserHandler.cs b/Algowars.Application/Commands/Users/CreateUser/CreateUserHandler.cs new file mode 100644 index 0000000..e0911f1 --- /dev/null +++ b/Algowars.Application/Commands/Users/CreateUser/CreateUserHandler.cs @@ -0,0 +1,36 @@ +using Algowars.Domain.SeedWork; +using Algowars.Domain.Users; +using Algowars.Domain.Users.Factories; +using Algowars.Domain.Users.ValueObjects; +using ApplicationCore.Commands; +using Ardalis.Result; +using FluentValidation; +using UserEntity = Algowars.Domain.Users.Entities.User; + +namespace Algowars.Application.Commands.Users.CreateUser; + +internal sealed partial class CreateUserHandler( + IValidator validator, + IUserRepository userRepository, + IAggregateFactory userFactory) + : AbstractCommandHandler(validator) +{ + protected override async Task> HandleValidated(CreateUserCommand request, CancellationToken cancellationToken) + { + var user = userFactory.Create(new CreateUserParams(request.Username, request.Sub, request.ImageUrl)); + + var foundUserBySub = await userRepository.FindBySubAsync(user.Sub, cancellationToken); + if (foundUserBySub is not null) + return Result.Conflict("A user with this account already exists."); + + var foundUserByUsername = await userRepository.FindByUsername(user.Username, cancellationToken); + if (foundUserByUsername is not null) + return Result.Conflict("Username is already taken."); + + + + await userRepository.AddAsync(user, cancellationToken); + + return Result.Success(user.Id); + } +} diff --git a/Algowars.Application/Commands/User/CreateUser/CreateUserValidator.cs b/Algowars.Application/Commands/Users/CreateUser/CreateUserValidator.cs similarity index 92% rename from Algowars.Application/Commands/User/CreateUser/CreateUserValidator.cs rename to Algowars.Application/Commands/Users/CreateUser/CreateUserValidator.cs index 03c38c0..f30e2eb 100644 --- a/Algowars.Application/Commands/User/CreateUser/CreateUserValidator.cs +++ b/Algowars.Application/Commands/Users/CreateUser/CreateUserValidator.cs @@ -1,7 +1,7 @@ using Algowars.Domain.Users.ValueObjects; using FluentValidation; -namespace Algowars.Application.Commands.User.CreateUser; +namespace Algowars.Application.Commands.Users.CreateUser; internal sealed class CreateUserValidator : AbstractValidator { diff --git a/Algowars.Domain/SeedWork/IAggregateFactory.cs b/Algowars.Domain/SeedWork/IAggregateFactory.cs new file mode 100644 index 0000000..589f135 --- /dev/null +++ b/Algowars.Domain/SeedWork/IAggregateFactory.cs @@ -0,0 +1,7 @@ +namespace Algowars.Domain.SeedWork; + +public interface IAggregateFactory + where TAggregate : AggregateRoot +{ + TAggregate Create(TParams parameters); +} diff --git a/Algowars.Domain/Users/Factories/UserFactory.cs b/Algowars.Domain/Users/Factories/UserFactory.cs new file mode 100644 index 0000000..6ef613e --- /dev/null +++ b/Algowars.Domain/Users/Factories/UserFactory.cs @@ -0,0 +1,19 @@ +using Algowars.Domain.SeedWork; +using Algowars.Domain.Users.Entities; +using Algowars.Domain.Users.ValueObjects; + +namespace Algowars.Domain.Users.Factories; + +public sealed record CreateUserParams(string Username, string Sub, string? ImageUrl); + +public sealed class UserFactory : IAggregateFactory +{ + public User Create(CreateUserParams parameters) + { + var username = new Username(parameters.Username); + var imageUrl = parameters.ImageUrl is not null ? new ImageUrl(parameters.ImageUrl) : null; + var user = new User(username, parameters.Sub); + user.UpdateImageUrl(imageUrl); + return user; + } +} diff --git a/Algowars.Domain/Users/IUserRepository.cs b/Algowars.Domain/Users/IUserRepository.cs index 9d6fc40..28d44b5 100644 --- a/Algowars.Domain/Users/IUserRepository.cs +++ b/Algowars.Domain/Users/IUserRepository.cs @@ -5,9 +5,8 @@ namespace Algowars.Domain.Users; public interface IUserRepository { + Task AddAsync(User user, CancellationToken cancellationToken); Task FindByIdAsync(Guid id, CancellationToken cancellationToken); - - Task FindByUsrenameAsync(Username usrename, CancellationToken cancellationToken); - Task FindBySubAsync(string sub, CancellationToken cancellationToken); + Task FindByUsername(Username username, CancellationToken cancellationToken); } From ddd3db8267ee9597e850c6f69def163a8b2c3883 Mon Sep 17 00:00:00 2001 From: admclamb Date: Sat, 13 Jun 2026 22:17:38 -0400 Subject: [PATCH 09/34] Update entities --- Algowars.Api/Algowars.Api.csproj | 12 +++- Algowars.Api/Controllers/UsersController.cs | 26 ++++++++ Algowars.Api/Program.cs | 5 ++ .../ApplicationServiceRegistration.cs | 22 +++++++ .../Users/CreateUser/CreateUserHandler.cs | 3 +- .../Services/Users/IUserService.cs | 8 +++ .../Services/Users/UserService.cs | 13 ++++ .../Algowars.Infrastructure.csproj | 16 +++++ Algowars.Infrastructure/Class1.cs | 7 --- .../InfrastructureServiceRegistration.cs | 40 +++++++++++++ .../Persistence/AlgoWarsDbContext.cs | 20 +++++++ .../Persistence/Models/Users/UserDataModel.cs | 32 ++++++++++ .../Repositories/UserRepository.cs | 59 +++++++++++++++++++ Directory.Packages.props | 10 ++++ 14 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 Algowars.Api/Controllers/UsersController.cs create mode 100644 Algowars.Application/ApplicationServiceRegistration.cs create mode 100644 Algowars.Application/Services/Users/IUserService.cs create mode 100644 Algowars.Application/Services/Users/UserService.cs delete mode 100644 Algowars.Infrastructure/Class1.cs create mode 100644 Algowars.Infrastructure/InfrastructureServiceRegistration.cs create mode 100644 Algowars.Infrastructure/Persistence/AlgoWarsDbContext.cs create mode 100644 Algowars.Infrastructure/Persistence/Models/Users/UserDataModel.cs create mode 100644 Algowars.Infrastructure/Repositories/UserRepository.cs diff --git a/Algowars.Api/Algowars.Api.csproj b/Algowars.Api/Algowars.Api.csproj index 0057157..8c1dfd6 100644 --- a/Algowars.Api/Algowars.Api.csproj +++ b/Algowars.Api/Algowars.Api.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -7,7 +7,17 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Algowars.Api/Controllers/UsersController.cs b/Algowars.Api/Controllers/UsersController.cs new file mode 100644 index 0000000..fb7ea8e --- /dev/null +++ b/Algowars.Api/Controllers/UsersController.cs @@ -0,0 +1,26 @@ +using Algowars.Application.Services.Users; +using Ardalis.Result.AspNetCore; +using Microsoft.AspNetCore.Mvc; + +namespace Algowars.Api.Controllers; + +[ApiController] +[Route("api/v{version:apiVersion}/[controller]")] +public class UsersController(IUserService userService) : ControllerBase +{ + [HttpPost] + [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> CreateUser( + [FromBody] CreateUserRequest request, + CancellationToken cancellationToken) + => this.ToActionResult(await userService.CreateUserAsync( + request.Username, + request.Sub, + request.ImageUrl, + cancellationToken)); + +} + +public sealed record CreateUserRequest(string Username, string Sub, string? ImageUrl); diff --git a/Algowars.Api/Program.cs b/Algowars.Api/Program.cs index 666a9c5..841636d 100644 --- a/Algowars.Api/Program.cs +++ b/Algowars.Api/Program.cs @@ -1,8 +1,13 @@ +using Algowars.Application; +using Algowars.Infrastructure; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); +builder.Services.AddApplication(); +builder.AddInfrastructure(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); diff --git a/Algowars.Application/ApplicationServiceRegistration.cs b/Algowars.Application/ApplicationServiceRegistration.cs new file mode 100644 index 0000000..2019f6c --- /dev/null +++ b/Algowars.Application/ApplicationServiceRegistration.cs @@ -0,0 +1,22 @@ +using Algowars.Application.Services.Users; +using Algowars.Domain.SeedWork; +using Algowars.Domain.Users.Entities; +using Algowars.Domain.Users.Factories; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +namespace Algowars.Application; + +public static class ApplicationServiceRegistration +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ApplicationServiceRegistration).Assembly)); + services.AddValidatorsFromAssembly(typeof(ApplicationServiceRegistration).Assembly); + + services.AddScoped, UserFactory>(); + services.AddScoped(); + + return services; + } +} diff --git a/Algowars.Application/Commands/Users/CreateUser/CreateUserHandler.cs b/Algowars.Application/Commands/Users/CreateUser/CreateUserHandler.cs index e0911f1..c7efb76 100644 --- a/Algowars.Application/Commands/Users/CreateUser/CreateUserHandler.cs +++ b/Algowars.Application/Commands/Users/CreateUser/CreateUserHandler.cs @@ -1,7 +1,6 @@ using Algowars.Domain.SeedWork; using Algowars.Domain.Users; using Algowars.Domain.Users.Factories; -using Algowars.Domain.Users.ValueObjects; using ApplicationCore.Commands; using Ardalis.Result; using FluentValidation; @@ -24,7 +23,7 @@ protected override async Task> HandleValidated(CreateUserCommand re return Result.Conflict("A user with this account already exists."); var foundUserByUsername = await userRepository.FindByUsername(user.Username, cancellationToken); - if (foundUserByUsername is not null) + if (foundUserByUsername is not null) return Result.Conflict("Username is already taken."); diff --git a/Algowars.Application/Services/Users/IUserService.cs b/Algowars.Application/Services/Users/IUserService.cs new file mode 100644 index 0000000..a9e882d --- /dev/null +++ b/Algowars.Application/Services/Users/IUserService.cs @@ -0,0 +1,8 @@ +using Ardalis.Result; + +namespace Algowars.Application.Services.Users; + +public interface IUserService +{ + Task> CreateUserAsync(string username, string sub, string? imageUrl, CancellationToken cancellationToken = default); +} diff --git a/Algowars.Application/Services/Users/UserService.cs b/Algowars.Application/Services/Users/UserService.cs new file mode 100644 index 0000000..26a5b69 --- /dev/null +++ b/Algowars.Application/Services/Users/UserService.cs @@ -0,0 +1,13 @@ +using Algowars.Application.Commands.Users.CreateUser; +using Ardalis.Result; +using MediatR; + +namespace Algowars.Application.Services.Users; + +internal sealed class UserService(ISender sender) : IUserService +{ + public async Task> CreateUserAsync(string username, string sub, string? imageUrl, CancellationToken cancellationToken = default) + { + return await sender.Send(new CreateUserCommand(username, sub, imageUrl), cancellationToken); + } +} diff --git a/Algowars.Infrastructure/Algowars.Infrastructure.csproj b/Algowars.Infrastructure/Algowars.Infrastructure.csproj index b760144..d2fe8ae 100644 --- a/Algowars.Infrastructure/Algowars.Infrastructure.csproj +++ b/Algowars.Infrastructure/Algowars.Infrastructure.csproj @@ -4,6 +4,22 @@ net10.0 enable enable + 02c70788-01ef-4b09-ae11-4908d9c98a34 + + + + + + + + + + + + + + + diff --git a/Algowars.Infrastructure/Class1.cs b/Algowars.Infrastructure/Class1.cs deleted file mode 100644 index c25b286..0000000 --- a/Algowars.Infrastructure/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Algowars.Infrastructure -{ - public class Class1 - { - - } -} diff --git a/Algowars.Infrastructure/InfrastructureServiceRegistration.cs b/Algowars.Infrastructure/InfrastructureServiceRegistration.cs new file mode 100644 index 0000000..514a8b2 --- /dev/null +++ b/Algowars.Infrastructure/InfrastructureServiceRegistration.cs @@ -0,0 +1,40 @@ +using Algowars.Domain.Users; +using Algowars.Infrastructure.Persistence; +using Algowars.Infrastructure.Repositories; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Algowars.Infrastructure; + +public static class InfrastructureServiceRegistration +{ + public static IHostApplicationBuilder AddInfrastructure(this IHostApplicationBuilder builder) + { + builder.AddNpgsqlDbContext("algowars-db"); + + builder.Services.AddScoped(); + + builder.Services.AddMassTransit(x => + { + if (builder.Environment.IsDevelopment()) + { + x.UsingRabbitMq((ctx, cfg) => + { + cfg.Host(builder.Configuration["ConnectionStrings:algowars-mq"]); + cfg.ConfigureEndpoints(ctx); + }); + } + else + { + x.UsingAzureServiceBus((ctx, cfg) => + { + cfg.Host(builder.Configuration["ConnectionStrings:algowars-mq"]); + cfg.ConfigureEndpoints(ctx); + }); + } + }); + + return builder; + } +} diff --git a/Algowars.Infrastructure/Persistence/AlgoWarsDbContext.cs b/Algowars.Infrastructure/Persistence/AlgoWarsDbContext.cs new file mode 100644 index 0000000..65be0c9 --- /dev/null +++ b/Algowars.Infrastructure/Persistence/AlgoWarsDbContext.cs @@ -0,0 +1,20 @@ +using Algowars.Infrastructure.Persistence.Models.Users; +using Microsoft.EntityFrameworkCore; + +namespace Algowars.Infrastructure.Persistence; + +internal sealed class AlgoWarsDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Users => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasIndex(u => u.Sub).IsUnique(); + + modelBuilder.Entity() + .HasIndex(u => u.Username).IsUnique(); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/Algowars.Infrastructure/Persistence/Models/Users/UserDataModel.cs b/Algowars.Infrastructure/Persistence/Models/Users/UserDataModel.cs new file mode 100644 index 0000000..acdb848 --- /dev/null +++ b/Algowars.Infrastructure/Persistence/Models/Users/UserDataModel.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Algowars.Infrastructure.Persistence.Models.Users; + +[Table("users")] +internal sealed class UserDataModel +{ + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Column("sub")] + [Required] + public required string Sub { get; set; } + + [Column("username")] + [Required] + [MaxLength(20)] + public required string Username { get; set; } + + [Column("bio")] + [MaxLength(500)] + public string? Bio { get; set; } + + [Column("image_url")] + [MaxLength(2048)] + public string? ImageUrl { get; set; } + + [Column("username_last_changed_at")] + public DateTime? UsernameLastChangedAt { get; set; } +} diff --git a/Algowars.Infrastructure/Repositories/UserRepository.cs b/Algowars.Infrastructure/Repositories/UserRepository.cs new file mode 100644 index 0000000..505c873 --- /dev/null +++ b/Algowars.Infrastructure/Repositories/UserRepository.cs @@ -0,0 +1,59 @@ +using Algowars.Domain.Users; +using Algowars.Domain.Users.Entities; +using Algowars.Domain.Users.ValueObjects; +using Algowars.Infrastructure.Persistence; +using Algowars.Infrastructure.Persistence.Models.Users; +using Microsoft.EntityFrameworkCore; + +namespace Algowars.Infrastructure.Repositories; + +internal sealed class UserRepository(AlgoWarsDbContext context) : IUserRepository +{ + public async Task AddAsync(User user, CancellationToken cancellationToken) + { + var model = ToModel(user); + await context.Users.AddAsync(model, cancellationToken); + } + + public async Task FindByIdAsync(Guid id, CancellationToken cancellationToken) + { + var model = await context.Users.FindAsync([id], cancellationToken); + return model is null ? null : ToDomain(model); + } + + public async Task FindBySubAsync(string sub, CancellationToken cancellationToken) + { + var model = await context.Users.FirstOrDefaultAsync(u => u.Sub == sub, cancellationToken); + return model is null ? null : ToDomain(model); + } + + public async Task FindByUsername(Username username, CancellationToken cancellationToken) + { + var model = await context.Users.FirstOrDefaultAsync(u => u.Username == username.Value, cancellationToken); + return model is null ? null : ToDomain(model); + } + + private static UserDataModel ToModel(User user) => new() + { + Id = user.Id, + Sub = user.Sub, + Username = user.Username.Value, + Bio = user.Bio?.Value, + ImageUrl = user.ImageUrl?.Value, + UsernameLastChangedAt = user.UsernameLastChangedAt + }; + + private static User ToDomain(UserDataModel model) + { + var user = new User(new Username(model.Username), model.Sub); + + if (model.Bio is not null) + user.UpdateBio(new Bio(model.Bio)); + + if (model.ImageUrl is not null) + user.UpdateImageUrl(new ImageUrl(model.ImageUrl)); + + return user; + } +} + diff --git a/Directory.Packages.props b/Directory.Packages.props index c18f1e8..7fac50f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,13 +5,23 @@ + + + + + + + + + + From 3008982af00445bbf59159816e994f1203b7d804 Mon Sep 17 00:00:00 2001 From: admclamb Date: Sun, 14 Jun 2026 13:54:46 -0400 Subject: [PATCH 10/34] add aspire --- .agents/skills/aspire-deployment/SKILL.md | 222 ++++++++++ .../aspire-deployment/references/aws.md | 176 ++++++++ .../aspire-deployment/references/azure.md | 316 ++++++++++++++ .../aspire-deployment/references/cicd.md | 342 +++++++++++++++ .../references/docker-compose.md | 155 +++++++ .../github-actions-azure-csharp.yml | 53 +++ .../github-actions-azure-typescript.yml | 53 +++ .../references/javascript.md | 126 ++++++ .../references/kubernetes.md | 236 +++++++++++ .../aspire-deployment/references/preflight.md | 189 +++++++++ .agents/skills/aspire-init/SKILL.md | 146 +++++++ .../aspire-init/references/init-workflow.md | 123 ++++++ .../aspire-init/references/templates.md | 92 ++++ .agents/skills/aspire-monitoring/SKILL.md | 197 +++++++++ .../references/diagnostics-bridge.md | 209 ++++++++++ .../references/monitoring.md | 161 +++++++ .../references/playwright-handoff.md | 21 + .agents/skills/aspire-orchestration/SKILL.md | 204 +++++++++ .../references/agent-workflows.md | 119 ++++++ .../references/app-commands.md | 123 ++++++ .../references/detection.md | 160 +++++++ .../references/resource-management.md | 38 ++ .../references/safety-guardrails.md | 272 ++++++++++++ .agents/skills/aspire/SKILL.md | 159 +++++++ .../aspire-13-3-breaking-changes.md | 110 +++++ .agents/skills/aspireify/SKILL.md | 329 +++++++++++++++ .../aspireify/references/apphost-wiring.md | 393 ++++++++++++++++++ .../aspireify/references/csharp-authoring.md | 179 ++++++++ .../aspireify/references/docker-compose.md | 214 ++++++++++ .../references/full-solution-apphosts.md | 332 +++++++++++++++ .../aspireify/references/javascript-apps.md | 150 +++++++ .../aspireify/references/opentelemetry.md | 112 +++++ .../aspireify/references/scan-and-propose.md | 121 ++++++ .../aspireify/references/service-defaults.md | 114 +++++ .../references/typescript-authoring.md | 246 +++++++++++ .../skills/aspireify/references/validation.md | 97 +++++ .agents/skills/dotnet-inspect/SKILL.md | 120 ++++++ Algowars.Api/Algowars.Api.csproj | 1 + Algowars.Api/Program.cs | 4 +- Algowars.Api/appsettings.Development.json | 3 + Algowars.AppHost/Algowars.AppHost.csproj | 22 + Algowars.AppHost/AppHost.cs | 18 + Algowars.AppHost/appsettings.Development.json | 8 + Algowars.AppHost/appsettings.json | 9 + Algowars.AppHost/aspire.config.json | 5 + .../ApplicationServiceRegistration.cs | 2 +- .../20260614173828_InitialCreate.Designer.cs | 73 ++++ .../20260614173828_InitialCreate.cs | 50 +++ .../AlgoWarsDbContextModelSnapshot.cs | 70 ++++ .../Algowars.ServiceDefaults.csproj | 22 + Algowars.ServiceDefaults/Extensions.cs | 127 ++++++ Directory.Packages.props | 10 + README.md | 81 ++++ Server.slnx | 6 + aspire.config.json | 5 + nuget.config | 12 + 56 files changed, 6933 insertions(+), 4 deletions(-) create mode 100644 .agents/skills/aspire-deployment/SKILL.md create mode 100644 .agents/skills/aspire-deployment/references/aws.md create mode 100644 .agents/skills/aspire-deployment/references/azure.md create mode 100644 .agents/skills/aspire-deployment/references/cicd.md create mode 100644 .agents/skills/aspire-deployment/references/docker-compose.md create mode 100644 .agents/skills/aspire-deployment/references/github-actions-azure-csharp.yml create mode 100644 .agents/skills/aspire-deployment/references/github-actions-azure-typescript.yml create mode 100644 .agents/skills/aspire-deployment/references/javascript.md create mode 100644 .agents/skills/aspire-deployment/references/kubernetes.md create mode 100644 .agents/skills/aspire-deployment/references/preflight.md create mode 100644 .agents/skills/aspire-init/SKILL.md create mode 100644 .agents/skills/aspire-init/references/init-workflow.md create mode 100644 .agents/skills/aspire-init/references/templates.md create mode 100644 .agents/skills/aspire-monitoring/SKILL.md create mode 100644 .agents/skills/aspire-monitoring/references/diagnostics-bridge.md create mode 100644 .agents/skills/aspire-monitoring/references/monitoring.md create mode 100644 .agents/skills/aspire-monitoring/references/playwright-handoff.md create mode 100644 .agents/skills/aspire-orchestration/SKILL.md create mode 100644 .agents/skills/aspire-orchestration/references/agent-workflows.md create mode 100644 .agents/skills/aspire-orchestration/references/app-commands.md create mode 100644 .agents/skills/aspire-orchestration/references/detection.md create mode 100644 .agents/skills/aspire-orchestration/references/resource-management.md create mode 100644 .agents/skills/aspire-orchestration/references/safety-guardrails.md create mode 100644 .agents/skills/aspire/SKILL.md create mode 100644 .agents/skills/aspire/references/aspire-13-3-breaking-changes.md create mode 100644 .agents/skills/aspireify/SKILL.md create mode 100644 .agents/skills/aspireify/references/apphost-wiring.md create mode 100644 .agents/skills/aspireify/references/csharp-authoring.md create mode 100644 .agents/skills/aspireify/references/docker-compose.md create mode 100644 .agents/skills/aspireify/references/full-solution-apphosts.md create mode 100644 .agents/skills/aspireify/references/javascript-apps.md create mode 100644 .agents/skills/aspireify/references/opentelemetry.md create mode 100644 .agents/skills/aspireify/references/scan-and-propose.md create mode 100644 .agents/skills/aspireify/references/service-defaults.md create mode 100644 .agents/skills/aspireify/references/typescript-authoring.md create mode 100644 .agents/skills/aspireify/references/validation.md create mode 100644 .agents/skills/dotnet-inspect/SKILL.md create mode 100644 Algowars.AppHost/Algowars.AppHost.csproj create mode 100644 Algowars.AppHost/AppHost.cs create mode 100644 Algowars.AppHost/appsettings.Development.json create mode 100644 Algowars.AppHost/appsettings.json create mode 100644 Algowars.AppHost/aspire.config.json create mode 100644 Algowars.Infrastructure/Migrations/20260614173828_InitialCreate.Designer.cs create mode 100644 Algowars.Infrastructure/Migrations/20260614173828_InitialCreate.cs create mode 100644 Algowars.Infrastructure/Migrations/AlgoWarsDbContextModelSnapshot.cs create mode 100644 Algowars.ServiceDefaults/Algowars.ServiceDefaults.csproj create mode 100644 Algowars.ServiceDefaults/Extensions.cs create mode 100644 README.md create mode 100644 aspire.config.json create mode 100644 nuget.config diff --git a/.agents/skills/aspire-deployment/SKILL.md b/.agents/skills/aspire-deployment/SKILL.md new file mode 100644 index 0000000..5a7ebe0 --- /dev/null +++ b/.agents/skills/aspire-deployment/SKILL.md @@ -0,0 +1,222 @@ +--- +name: aspire-deployment +description: "**WORKFLOW SKILL** — Deploy Aspire apps from AppHost models to Docker Compose, Kubernetes, Azure, or AWS. WHEN: \"deploy Aspire app\", \"publish Aspire artifacts\", \"deploy to Azure Container Apps\", \"generate Kubernetes artifacts\", \"tear down Aspire deployment\". INVOKES: aspire CLI, Aspire docs, target cloud/container CLIs. FOR SINGLE OPERATIONS: use generic Azure, Kubernetes, Docker, or AWS tools only when no Aspire AppHost exists." +license: MIT +metadata: + author: Microsoft + version: "0.0.1" +--- + +# Aspire Deployment + +Use this skill when the task is to publish, preview, validate, deploy, or tear down an Aspire application deployment. This skill owns Aspire deployment routing. Do not start with a generic Azure, Docker, Kubernetes, Helm, or Bicep workflow until you have checked whether the workspace is an Aspire app. + +Aspire deployment starts from the AppHost model. Treat `aspire deploy`, `aspire publish`, `aspire destroy`, `aspire do`, and the deployment environment resources in the AppHost as the primary path. + +Keep this as one skill with target-specific references. Load only the reference files that match the target you discover or the user requests. + +## Routing precedence + +This skill wins over generic cloud deployment skills when both conditions are true: + +1. The user asks to deploy, publish, generate deployment artifacts, create Bicep/Helm/Compose/CDK output, host on Azure or AWS, deploy to Azure or AWS, deploy to Kubernetes, deploy to Docker Compose, tear down deployed resources, or validate a deployment. +2. The workspace has Aspire markers: + - Aspire workspace configuration + - C# or TypeScript AppHost files + - an AppHost project + - AppHost code using Aspire distributed application builder APIs + +If Aspire markers are present but this skill was not automatically invoked, switch to this skill before continuing. Prefer Aspire CLI commands such as `aspire ls`, `aspire config list`, `aspire ps`, and `aspire describe` for workspace orientation. + +## Guiding principles + +### Use Aspire docs before changing deployment code + +Before adding target packages, editing the AppHost, or using an unfamiliar deployment API, use Aspire docs: + +```bash +aspire docs search "deploy with Aspire" +aspire docs search "Docker Compose deployment" +aspire docs search "Kubernetes deployment" +aspire docs search "Azure Container Apps deployment" +aspire docs search "Azure App Service deployment" +aspire docs search "Azure Kubernetes Service deployment" +aspire docs get "deploy-to-azure-kubernetes-service-aks" +aspire docs get "" +``` + +When you need exact C# or TypeScript API shape, use API docs too. Search both languages when you are not sure which AppHost language the repo uses: + +```bash +aspire docs api search "" --language csharp +aspire docs api search "" --language typescript +aspire docs api get "" +``` + +Do not invent package names, builder methods, overloads, or deployment commands. API shapes differ between C# and TypeScript AppHosts. + +### Prefer Aspire-native deployment + +Use Aspire deployment targets and CLI commands first: + +```bash +aspire publish --list-steps +aspire deploy --list-steps +aspire publish +aspire deploy +aspire destroy +aspire do +``` + +Use target-specific tooling only after Aspire has generated artifacts or when the target docs call for it: + +- Docker Compose: inspect generated `aspire-output/docker-compose.yaml` and `.env*`; Aspire can also run `docker compose up` through `aspire deploy`. +- Kubernetes: inspect generated Helm chart output; use Helm/kubectl when applying published artifacts yourself. +- Azure: use `aspire add `, `aspire publish`, and `aspire deploy` through the AppHost deployment environment. +- AWS: use `aspire add aws` to add the integration, inspect generated CDK/CloudFormation output, and follow the AWS integrations repository guidance. + +### Ask where to deploy only when ambiguous + +Do not ask for target selection when the user already chose a target such as Docker Compose, Kubernetes, Azure Container Apps, Azure App Service, Azure Kubernetes Service (AKS), or AWS. Use the chosen target and continue with its reference. + +If the user did not explicitly choose a deployment target and the AppHost does not already contain exactly one deployment environment, ask where they want to deploy before adding integrations, editing the AppHost, publishing artifacts, or deploying. Use a single multiple-choice question: + +> Where do you want to deploy this Aspire app? + +Show these choices: + +| Choice | Aspire add command | Use when | +|--------|--------------------|----------| +| Docker Compose | `aspire add docker` | The user wants local/server container deployment artifacts for Docker or Podman. | +| Kubernetes | `aspire add kubernetes` | The user has an existing Kubernetes cluster and wants Helm/Kubernetes artifacts or direct cluster deployment. | +| Azure Container Apps | `aspire add azure-appcontainers` | The user wants an Azure-managed container platform for distributed apps and services. | +| Azure App Service | `aspire add azure-appservice` | The user wants Azure website hosting for web apps/APIs that fit the App Service model. | +| Azure Kubernetes Service (AKS) | `aspire add azure-kubernetes` | The user wants Aspire to provision and deploy to Azure-managed Kubernetes. | +| AWS | `aspire add aws` | The user wants Aspire to publish/deploy through the AWS Aspire integrations and AWS CDK. | + +If the user says only "Azure", ask again with just the Azure choices: Azure Container Apps, Azure App Service, or Azure Kubernetes Service (AKS). If the AppHost already contains exactly one deployment environment and the user did not ask to change targets, use that target and tell the user what was detected. + +### Ask before creating cloud resources when intent is not explicit + +Cloud deploys can create billable resources. If the user asked for a plan, preview, validation, or "make this deployable", stop after the deployment plan/artifacts and ask before running the command that provisions resources. + +If the user explicitly asked to deploy now, continue through preflight and deployment, but still surface any target choice, subscription/resource group ambiguity, or missing parameter decisions before provisioning. + +### Keep Azure deployment Aspire-native + +The Azure deployment path in this skill is `aspire add `, AppHost environment configuration, `aspire publish`, and `aspire deploy`. Do not route Azure deployment work through a separate Azure deployment tool or generated infrastructure workflow. + +## Default workflow + +1. **Orient to the Aspire workspace.** + - Start with `aspire ls` to list AppHosts in the current scope, then use `aspire.config.json`, AppHost project files, or `aspire ps` if more context is needed. + - If no AppHost exists, stop deployment work and invoke the `aspireify` skill to initialize/wire the AppHost before continuing. + - Identify C# vs TypeScript AppHost. + - Prefer Aspire CLI commands for discovery and state inspection. +2. **Clarify or infer the deployment target.** + - If the user named Docker Compose, Kubernetes, Azure Container Apps, Azure App Service, Azure Kubernetes Service (AKS), or AWS, load that target reference without asking again. + - If they only said "deploy", inspect existing AppHost target environment resources. + - If exactly one target environment already exists, use it and state what was detected. + - If multiple targets exist, none exists, or the user says only "Azure", ask where to deploy using the choices above. +3. **Load target and app-type references.** + - Docker Compose: [references/docker-compose.md](references/docker-compose.md) + - Kubernetes and Azure Kubernetes Service (AKS): [references/kubernetes.md](references/kubernetes.md) + - Azure Container Apps/App Service/Azure Kubernetes Service (AKS): [references/azure.md](references/azure.md) + - AWS: [references/aws.md](references/aws.md) + - JavaScript app resources: [references/javascript.md](references/javascript.md) + - CI/CD or GitHub Actions automation: [references/cicd.md](references/cicd.md) +4. **Use Aspire docs search for current guidance.** + - Search and get the target deployment docs. + - Search API docs before editing AppHost code. +5. **Apply the target code changes.** + - Run the target's `aspire add ...` command if the integration is missing. + - Add the deployment environment resource to the AppHost. + - Do not add explicit compute-environment assignment for the common single-environment case. Only disambiguate when the AppHost has multiple deployment environments. In C# this is usually `WithComputeEnvironment(...)`; for TypeScript AppHosts, verify the current language-specific docs before assuming an equivalent. + - Add only the target-specific customization APIs the deployment needs, such as endpoint exposure, Helm settings, Compose file customization, Azure site/container app customization, or AWS publish target overrides. +6. **Preflight the deployment model.** + - Confirm the target integration package exists in the AppHost. + - Confirm the AppHost has the target environment resource. + - Confirm compute resources are assigned to the target environment only when multiple compute environments exist. A single compute environment is the common case and can be inferred. + - Inventory parameters, secrets, connection strings, external endpoints, container registries, and target-specific prerequisites. + - For Azure or AWS, confirm auth, target account/subscription, region/location, and resource group/stack context. +7. **Preview before applying.** + - Run `aspire publish --list-steps` or `aspire deploy --list-steps`. + - Use `aspire publish -o ` when artifact review is requested. + - Treat published artifacts as a preview/handoff. `aspire deploy` resolves values and applies the deployment from the AppHost model; it does not consume a previously published output directory. + - Summarize resources, endpoints, parameters, secrets, identities, and generated artifacts. +8. **Deploy or hand off.** + - Run `aspire deploy` when the user asked to deploy and preflight is complete. + - Run a named step with `aspire do ` only when the user asked for a specific pipeline step. + - For published artifacts, explain the target-native apply step. +9. **Destroy only when explicitly requested.** + - Run `aspire destroy` to execute the selected AppHost/environment's target destroy pipeline. + - Confirm the AppHost, environment, target account/subscription/cluster, and destructive intent before running it. + - Use `--yes` only when the user or CI workflow already made teardown intent explicit. + - Prefer `aspire destroy` over target-native delete commands unless you are troubleshooting failed teardown or cleaning up unmanaged leftovers. +10. **Verify the outcome.** + - Use target output, `aspire describe`, cloud CLI, Docker Compose, kubectl, or endpoint checks appropriate to the target. + - After destroy, verify target resources are removed or record any leftovers that require manual cleanup. + +## AppHost target detection + +Search the AppHost for deployment environment resources: + +| Target | Aspire add command | Integration | AppHost environment concept | +|--------|--------------------|-------------|-----------------------------| +| Docker Compose | `aspire add docker` | Docker hosting | Docker Compose environment | +| Kubernetes | `aspire add kubernetes` | Kubernetes hosting | Kubernetes environment | +| Azure Container Apps | `aspire add azure-appcontainers` | Azure Container Apps hosting | Azure Container Apps environment | +| Azure App Service | `aspire add azure-appservice` | Azure App Service hosting | Azure App Service environment | +| Azure Kubernetes Service (AKS) | `aspire add azure-kubernetes` | Azure Kubernetes hosting | Azure Kubernetes Service (AKS) environment | +| AWS | `aspire add aws` | AWS hosting | AWS CDK environment | + +Use this table only for orientation. Before editing code, verify the current API in Aspire docs for the AppHost language. + +## Parameter and secret preflight + +Parameters are deployment inputs. They may be supplied by configuration files, user secrets, environment variables, command-line args, interactive prompts, or CI/CD secret stores depending on the target and command. + +Before deployment, report: + +- Parameter name from AppHost parameter APIs, including config-backed parameters +- Whether it is secret +- Expected provider syntax such as `Parameters__name`; for parameter names with dashes, use underscores in environment variables, for example `registry-endpoint` becomes `Parameters__registry_endpoint` +- Where it flows, such as a project environment variable, connection string, Key Vault secret, Helm Secret, Compose `.env`, or Azure app setting +- Whether a value appears configured or missing + +Use `aspire secret list` for AppHost user secrets when appropriate, but do not print secret values. For deployment artifacts, inspect generated placeholders and mappings, not raw secret content. + +## Target references + +- [references/docker-compose.md](references/docker-compose.md) - Docker Compose target, generated files, environment variables, cleanup. +- [references/kubernetes.md](references/kubernetes.md) - Kubernetes and Azure Kubernetes Service (AKS) target selection, Helm output, registry requirements, kubectl/Helm checks. +- [references/azure.md](references/azure.md) - Azure target selection, Azure settings, Container Apps, App Service, and Azure Kubernetes Service (AKS). +- [references/aws.md](references/aws.md) - AWS target selection, AWS CDK prerequisites, publish/deploy workflow, and AWS integration docs. +- [references/javascript.md](references/javascript.md) - JavaScript app deployment models, including Vite/static assets, Node/SSR servers, Next.js, and gateway/backend serving patterns. +- [references/cicd.md](references/cicd.md) - CI/CD and GitHub Actions workflow guidance for Aspire publish/deploy, parameters, secrets, registry auth, and cloud auth. +- [references/preflight.md](references/preflight.md) - Common preflight, preview, parameter, destroy, and validation checklist. + +## Agent execution + +When running unattended (CI, scripted, agent-driven), append `--non-interactive` to every Aspire CLI invocation that may prompt — most importantly `aspire publish`, `aspire deploy`, and `aspire destroy`. For `aspire destroy`, also pass `--yes` only after the user has explicitly confirmed teardown intent (or a CI workflow already encodes that intent). + +Prefer surfacing prompt-driving values up front (target subscription/region/resource group, parameters, secrets, registry credentials) so the unattended run does not stall. See [references/preflight.md](references/preflight.md) for the full preflight checklist. + +## Handoff Rules + +| Scenario | Route To | +|----------|----------| +| Start, stop, wait, or restart the AppHost / its resources | `aspire-orchestration` skill | +| Logs, traces, metrics, dashboard for a running or deployed app | `aspire-monitoring` skill | +| AppHost authoring — adding integrations, wiring resources, environment setup | `aspireify` skill | +| Deployed-app diagnostics — App Insights, ACA logs, AKS Container Insights | `azure-diagnostics` skill (azure-skills) | + +> Never hand deployment off to azure-skills. Aspire handles publish, deploy, and destroy +> end-to-end across Docker Compose / Kubernetes / Azure / AWS via the AppHost model. + +## Project-Local Skill Override + +If `.agents/skills/aspire-deployment/SKILL.md` exists (dropped by `aspire agent init`), +prefer it over this plugin skill — it is the authoritative project-local version with +content version-aligned to the consumer's Aspire CLI. This plugin skill is the always-on +safety net for repos that have not yet run `aspire agent init`. diff --git a/.agents/skills/aspire-deployment/references/aws.md b/.agents/skills/aspire-deployment/references/aws.md new file mode 100644 index 0000000..e00750a --- /dev/null +++ b/.agents/skills/aspire-deployment/references/aws.md @@ -0,0 +1,176 @@ +# AWS deployment + +Use this reference when the user asks to deploy an Aspire app to AWS or to generate AWS CDK/CloudFormation deployment artifacts. + +AWS deployment is currently a preview path driven by the AWS Aspire integrations. Treat the AWS repository as the source of truth, verify the installed package/API shape before editing, and do not invent TypeScript deployment APIs. AWS deployment has no TypeScript AppHost support yet. + +## Source of truth and docs lookup + +Use the AWS Aspire integrations repository for AWS-specific deployment guidance: + +https://github.com/aws/integrations-on-dotnet-aspire-for-aws + +Use these repository docs when available: + +- `README.md` for the deployment overview and prerequisites. +- `src/Aspire.Hosting.AWS/README.md` for package-level setup. +- `docs/deployment-design.md` for publish target overrides, CDK constructs, and advanced customization. + +Do not use `aspire docs search` for AWS deployment guidance. The AWS deployment docs are not in the Aspire docs index; use the AWS repository and deployment design document instead. + +## Target setup + +Add the AWS integration with: + +```bash +aspire add aws +``` + +Then configure the C# AppHost with an AWS CDK environment using the API shape supported by the installed integration. Current documented setup uses an AWS CDK environment, a preview defaults provider, and preview publisher APIs. Expect preview diagnostics such as `ASPIREAWSPUBLISHERS001`; follow the AWS repository guidance for the required suppressions instead of hiding warnings broadly. + +## Code changes to make + +Make these changes in the C# AppHost when the AWS integration docs still show the C#-only preview API: + +1. Run `aspire add aws` if the AppHost does not already reference `Aspire.Hosting.AWS`. +2. Add the AWS CDK environment near the top of the AppHost, before resources that should deploy: + + ```csharp + #pragma warning disable ASPIREAWSPUBLISHERS001 + + var builder = DistributedApplication.CreateBuilder(args); + + builder.AddAWSCDKEnvironment( + name: "", + cdkDefaultsProviderFactory: CDKDefaultsProviderFactory.Preview_V1); + ``` + +3. Keep existing Aspire resources in the AppHost. Default AWS deployment mapping handles common resources without per-resource publish calls. +4. Keep `WithReference(...)` relationships between resources. The AWS deployment system uses references for environment variables, VPC attachment, and security group connectivity where supported. +5. Add AWS-specific resources only when the app actually needs them and the AWS docs show the current shape, such as `AddAWSLambdaFunction` for Lambda projects/handlers. +6. Add publish override methods only when the user asks for a specific AWS shape or the default mapping is wrong. For example, use an ECS Fargate/ALB publish target only after verifying the current method name in the AWS deployment design document. +7. Do not invent a TypeScript AWS deployment environment. If the AppHost is TypeScript, stop and report that the current AWS deployment integration is C# AppHost-focused. + +Do not add AWS CDK constructs directly as the first approach. Start with Aspire resources and references, then use AWS construct callbacks or custom CDK stacks only for explicit infrastructure customization. + +## Prerequisites + +Check: + +- AWS credentials are available for the target account. +- The AWS region is known and matches the user's intended deployment target. +- Node.js 22.x is installed when the AWS CDK tooling requires it. +- AWS CDK is installed and available. +- The target account and region are bootstrapped for CDK before first deployment: + + ```bash + cdk bootstrap aws:/// + ``` + +- Docker is available when compute resource images need to be built. +- Required AppHost parameters are configured or can be prompted. +- If the AppHost project references unsigned AWS CDK packages and the build reports package signing diagnostics, follow the AWS repository guidance for the narrow project warning suppression. + +Use AWS CLI to verify identity and region before deploying: + +```bash +aws sts get-caller-identity +aws configure list +aws configure get region +cdk --version +``` + +Do not print access keys, secret keys, session tokens, or resolved secret parameter values. + +## Preview and publish + +Use Aspire publish to generate deployment artifacts before applying them when the user wants review or validation: + +```bash +aspire publish --list-steps +aspire publish -o aws-artifacts +``` + +For AWS CDK deployment, the AWS integration transforms Aspire resources into CDK constructs and synthesizes CloudFormation templates under `cdk.out` in the output location. Inspect the generated CDK output before deploying when the user asked for a preview: + +```bash +find aws-artifacts -maxdepth 3 -type f | sort +cdk ls --app aws-artifacts/cdk.out +cdk diff --app aws-artifacts/cdk.out +``` + +The exact output path can vary with the installed integration; inspect the publish output and generated files instead of assuming a fixed directory layout. + +## Deploy + +Deploy with: + +```bash +aspire deploy +``` + +The AWS integration runs the publish step and then uses AWS CDK deployment against the configured AWS account and region. + +Use `aspire deploy --list-steps` before applying changes when the user asked for validation or when the AppHost has custom AWS publish targets. + +## Destroy + +Use Aspire to run the AWS target's destroy pipeline: + +```bash +aspire destroy --environment +``` + +For AWS, `aspire destroy` delegates to the AWS deployment target for the selected AppHost/environment. Confirm the AWS account, region, CDK stack name, AppHost, and environment before running destroy. Use `--yes` only after destructive intent is explicit. Use AWS CLI or CDK destroy commands only to investigate failed teardown or clean up resources that the Aspire AWS target did not manage. + +## Resource mapping and customization + +Current AWS guidance says resources are mapped to AWS services by default and can be overridden with publish extension methods. Keep these rules in mind: + +- Web projects can map to ECS Fargate-style targets by default. +- Lambda project resources can map to AWS Lambda. +- Redis can map to ElastiCache. +- `WithReference()` drives connectivity such as environment variables, VPC attachment, and security groups. +- Custom CDK stacks and construct callbacks are advanced customization points; use them only when the user asks for infrastructure customization and verify the exact API in the AWS design document. + +Do not assume every Aspire resource has an AWS publish target. If a resource has no supported mapping, call that out before deployment rather than implying it will be provisioned. + +## Troubleshooting live AWS state + +Use AWS CLI to inspect live resources that Aspire deployed through CDK/CloudFormation: + +```bash +aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE UPDATE_ROLLBACK_COMPLETE +aws cloudformation describe-stacks --stack-name "" +aws cloudformation describe-stack-events --stack-name "" --max-items 20 +aws cloudformation list-stack-resources --stack-name "" +``` + +Use service-specific commands based on the resources in the generated CDK output: + +```bash +# ECS/Fargate +aws ecs list-clusters +aws ecs list-services --cluster "" +aws ecs describe-services --cluster "" --services "" + +# Lambda +aws lambda list-functions +aws lambda get-function --function-name "" + +# ElastiCache +aws elasticache describe-cache-clusters --show-cache-node-info +``` + +When a deploy fails, inspect CloudFormation stack events first, then the specific service that failed. Match live stack/resource names back to the AWS CDK environment name, Aspire deployment output, and generated `cdk.out` templates. + +## Common checks + +- Confirm the generated AWS stack name and region before deployment. +- Confirm credentials match the intended account. +- Confirm CDK bootstrap has run for the account and region. +- Treat preview deployment APIs as subject to change; re-check the AWS repository before making assumptions. +- When customization is needed, use the AWS repository's deployment design document and verify the exact API for the AppHost language before editing. +- Confirm any AppHost parameters that become CloudFormation/CDK inputs, especially names containing punctuation, by inspecting the synthesized output. +- If an ECS service is unhealthy, inspect ECS service events, task status, logs, image pull permissions, and security groups. +- If CloudFormation rolls back, read the first failed stack event rather than only the final rollback event. diff --git a/.agents/skills/aspire-deployment/references/azure.md b/.agents/skills/aspire-deployment/references/azure.md new file mode 100644 index 0000000..e8a9dd8 --- /dev/null +++ b/.agents/skills/aspire-deployment/references/azure.md @@ -0,0 +1,316 @@ +# Azure deployment + +Use this reference when the user asks to deploy an Aspire app to Azure, Azure Container Apps, Azure App Service, or Azure Kubernetes Service (AKS). + +## Use Aspire-native Azure deployment + +For Aspire apps, start from the AppHost deployment model. Azure deployment should go through the Azure hosting integrations, AppHost environment resources, `aspire publish`, and `aspire deploy`. + +Do not generate or hand-edit Azure infrastructure as the source of truth unless the user explicitly wants a published artifact handoff. The AppHost Azure environment determines what gets provisioned. + +## Choose Azure target + +| Target | Aspire add command | Best fit | Integration | Environment concept | +|--------|--------------------|----------|-------------|---------------------| +| Azure Container Apps | `aspire add azure-appcontainers` | Distributed/containerized services with internal/external ingress | Azure Container Apps hosting | Azure Container Apps environment | +| Azure App Service | `aspire add azure-appservice` | Public web apps/APIs that fit the App Service website model | Azure App Service hosting | Azure App Service environment | +| Azure Kubernetes Service (AKS) | `aspire add azure-kubernetes` | Kubernetes workloads where Aspire should provision Azure Kubernetes infrastructure | Azure Kubernetes hosting | Azure Kubernetes Service (AKS) environment | + +If the user only says "Azure", ask which target unless the AppHost already contains exactly one Azure environment resource or the scenario strongly implies one. + +Use these choices: + +| Choice | Aspire add command | Use when | +|--------|--------------------|----------| +| Azure Container Apps | `aspire add azure-appcontainers` | The user wants an Azure-managed container platform for distributed apps and services. | +| Azure App Service | `aspire add azure-appservice` | The user wants Azure website hosting for web apps/APIs that fit the App Service model. | +| Azure Kubernetes Service (AKS) | `aspire add azure-kubernetes` | The user wants Aspire to provision and deploy to Azure-managed Kubernetes. | + +## Docs to load + +Always start with current docs: + +```bash +aspire docs search "Azure Container Apps deployment" +aspire docs search "Azure App Service deployment" +aspire docs search "Azure Kubernetes Service deployment" +aspire docs search "Azure Kubernetes Service hosting integration" +aspire docs get "deploy-to-azure-container-apps" +aspire docs get "configure-azure-container-apps-environments" +aspire docs get "deploy-to-azure-app-service" +aspire docs get "set-up-azure-app-service-in-the-apphost" +aspire docs get "deploy-to-azure-kubernetes-service-aks" +aspire docs get "external-parameters" +``` + +Some Azure deployment searches can return noisy integration results. Search first so you can find renamed pages, then prefer the known deployment slugs above when they are available. If one of these known slugs is not available in the installed Aspire docs, inspect the closest current replacement before editing. + +Use API docs before editing. Search for the target environment, endpoint, and customization concepts in the AppHost language you detected: + +```bash +aspire docs api search "AddAzureContainerAppEnvironment" --language csharp +aspire docs api search "AddAzureContainerAppEnvironment" --language typescript +aspire docs api search "AddAzureAppServiceEnvironment" --language csharp +aspire docs api search "AddAzureAppServiceEnvironment" --language typescript +aspire docs api search "AddAzureKubernetesEnvironment" --language csharp +aspire docs api search "AddAzureKubernetesEnvironment" --language typescript +aspire docs api search "WithExternalHttpEndpoints" --language csharp +aspire docs api search "WithExternalHttpEndpoints" --language typescript +``` + +## Shared Azure preflight + +Check: + +- Azure CLI is installed when local deploy uses Azure CLI credentials. +- The user is authenticated (`az login`) or another `Azure:CredentialSource` is configured. +- Target subscription, location, and resource group are known. +- Required AppHost parameters are configured or can be prompted. +- The AppHost has the correct Azure target integration and environment resource. +- Production-only resources are not hidden behind run-mode-only execution context checks. +- Compute resources are assigned to the intended Azure environment when multiple compute environments exist. + +Azure deployment settings: + +| Setting | Environment variable | Purpose | +|---------|----------------------|---------| +| `Azure:SubscriptionId` | `Azure__SubscriptionId` | Target subscription | +| `Azure:Location` | `Azure__Location` | Azure region | +| `Azure:ResourceGroup` | `Azure__ResourceGroup` | Resource group | +| `Azure:CredentialSource` | `Azure__CredentialSource` | Credential source override | + +For local development, `aspire secret set` can store these values for the AppHost: + +```bash +aspire secret set "Azure:SubscriptionId" "" +aspire secret set "Azure:Location" "" +aspire secret set "Azure:ResourceGroup" "" +``` + +Do not use `aspire secret set` as the deployment parameter mechanism. It is a local/dev convenience today. For publish/deploy, TypeScript AppHosts, CI, and other non-interactive deploys, set deployment settings and AppHost parameters as environment variables on the `aspire deploy` process: + +```bash +Azure__SubscriptionId="" \ +Azure__Location="westus2" \ +Azure__ResourceGroup="my-app-rg" \ +Parameters__api_key="" \ +aspire deploy --apphost ./apphost.ts --environment Production --non-interactive +``` + +Do not print secret values. Subscription/resource group/location are not secrets, but still summarize them carefully. + +If a parameter name contains `-`, use `_` in the environment variable name. For example, AppHost parameter `registry-endpoint` maps to `Parameters__registry_endpoint`. + +If `az account show` reports a tenant but `aspire deploy` later prompts during `fetch-tenant`, do not assume `aspire secret set "Azure:TenantId" ...` will answer that prompt. Tenant selection can still be a pipeline prompt. Run the deploy in a real interactive terminal/PTY, or make the Azure CLI login context unambiguous before deploying, for example with `az login --tenant ` or `azure/login`'s `tenant-id` input in GitHub Actions. + +## Azure Container Apps + +Setup: + +```bash +aspire add azure-appcontainers +``` + +Code changes: + +1. Add an Azure Container Apps environment resource. + + C# AppHost shape: + + ```csharp + var aca = builder.AddAzureContainerAppEnvironment("aca"); + ``` + + TypeScript AppHost shape: + + ```typescript + const aca = await builder.addAzureContainerAppEnvironment("aca"); + ``` + +2. Add `.WithExternalHttpEndpoints()` to C# compute resources that should be publicly reachable, such as projects, JavaScript/Python executables, containers, Dockerfiles, and similar workloads. For TypeScript AppHosts, use the endpoint/external endpoint API returned by docs. +3. Do not add explicit compute-environment assignment for the common single-environment case. Only if the AppHost has multiple compute environments, disambiguate each Azure Container Apps workload; in C#, add `.WithComputeEnvironment(aca)` to each compute resource that should deploy there. For TypeScript AppHosts, verify the current language-specific docs before assuming an equivalent assignment API. +4. Do not add `PublishAsAzureContainerApp(...)` / `publishAsAzureContainerApp(...)` for a default Container App deployment. Add it only when the user needs per-resource Container App customization. Use Container App Job APIs only for worker/job resources that should run as jobs. +5. Add managed Azure resources for production dependencies when appropriate, then keep normal `WithReference` / `withReference` connections so Aspire wires app settings, identities, and connection details. + +Key checks: + +- One external HTTP/HTTP2 target-port group becomes the main ingress. +- Multiple external endpoint groups are not supported. +- External non-HTTP endpoints are not supported. +- HTTP/HTTP2 and TCP endpoints cannot be mixed on the same target port. +- The external HTTP endpoints API marks endpoints reachable outside the Container Apps environment; verify the exact API name for C# or TypeScript before editing. +- Internal services should not be made external unless the user wants public access. +- HTTP endpoints are upgraded to HTTPS by default for deployed endpoint URLs; only disable the upgrade when the user explicitly wants HTTP. +- Volumes and bind mounts become Azure Files-backed mounts, with generated storage accounts, file shares, and managed environment storage. +- Aspire provisions or attaches ACR, builds images, pushes images, and grants pull permissions with managed identity. +- The Aspire dashboard is included by default unless the environment disables it. + +Use the per-resource Container Apps customization API only when customization is required, and verify the exact API name for the AppHost language. + +## Azure App Service + +Setup: + +```bash +aspire add azure-appservice +``` + +Code changes: + +1. Add an Azure App Service environment resource. + + C# AppHost shape: + + ```csharp + var appService = builder.AddAzureAppServiceEnvironment("appservice"); + ``` + + TypeScript AppHost shape: + + ```typescript + const appService = await builder.addAzureAppServiceEnvironment("appservice"); + ``` + +2. Add `.WithExternalHttpEndpoints()` to the C# web-facing compute resources that should become websites, such as projects, JavaScript/Python executables, containers, Dockerfiles, and similar workloads. For TypeScript AppHosts, use the endpoint/external endpoint API returned by docs. +3. Do not add explicit compute-environment assignment for the common single-environment case. Only if the AppHost has multiple compute environments, disambiguate each App Service workload; in C#, add `.WithComputeEnvironment(appService)` to each website compute resource that should deploy there. +4. Keep background workers, infrastructure containers, and internal-only services out of App Service unless the docs say the resource is supported. Move dependencies to managed Azure resources or choose Container Apps/Azure Kubernetes Service (AKS). +5. Do not add `PublishAsAzureAppServiceWebsite(...)` / `publishAsAzureAppServiceWebsite(...)` for a default website deployment. Add it only when the user needs website customization such as app settings, deployment slots, tags, or infrastructure callbacks. +6. Use `SkipEnvironmentVariableNameChecks()` only when the user intentionally accepts App Service's dashed-setting behavior and the app does not depend on the original dashed key at runtime. + +Key checks: + +- App Service is a public website model. +- Supported workloads are web-facing project resources and Dockerfile-backed web containers. +- External HTTP/HTTPS endpoints are required for deployed websites. +- Only HTTP/HTTPS endpoints are supported. +- App Service supports a single target port for a deployed website. Multi-port public apps do not fit this target. +- Internal-only endpoints and arbitrary infrastructure containers do not fit this target. +- App Service upgrades external HTTP endpoint URLs to HTTPS by default. +- The environment creates an App Service Plan with default SKU `P0V3`/Premium Linux, a container registry, managed identity, and the dashboard by default. +- Application Insights is optional and must be enabled/configured intentionally. +- Environment variable names containing `-` fail validation because App Service removes dashes at runtime; prefer dash-free connection names rather than bypassing validation. +- Use managed Azure resources for databases, caches, and brokers. + +Use the per-site App Service customization API only for app settings, deployment slots, tags, validation overrides, or other per-site changes, and verify the exact API name for the AppHost language. + +## Azure Kubernetes Service (AKS) + +Setup: + +```bash +aspire add azure-kubernetes +``` + +Code changes: + +1. Add an Azure Kubernetes Service (AKS) environment resource. + + C# AppHost shape: + + ```csharp + var aks = builder.AddAzureKubernetesEnvironment("aks"); + ``` + + TypeScript AppHost shape: + + ```typescript + const aks = await builder.addAzureKubernetesEnvironment("aks"); + ``` + +2. Do not add explicit compute-environment assignment for the common single-environment case. Only if the AppHost has multiple compute environments, disambiguate each Azure Kubernetes Service (AKS) workload; in C#, add `.WithComputeEnvironment(aks)` to each compute resource that should deploy there, such as projects, JavaScript/Python executables, containers, Dockerfiles, and similar workloads. +3. Do not add a separate plain Kubernetes environment for the same Azure Kubernetes Service (AKS) target. The Azure Kubernetes environment owns the Kubernetes/Helm deployment path. +4. Use Azure Kubernetes Service (AKS) customization APIs only when needed, such as node pools, system node pool SKU/count, subnet integration, workload identity, custom ACR, or Application Gateway for Containers. +5. Use Kubernetes Gateway/Ingress/service customization APIs for public exposure. Do not assume adding the environment makes every workload public. + +For Azure Kubernetes Service (AKS) details, also load [kubernetes.md](kubernetes.md). + +Azure Kubernetes Service (AKS) uses the Azure Kubernetes integration to provision Azure Kubernetes Service (AKS) infrastructure and then deploys through an inner Kubernetes/Helm environment. It auto-creates ACR by default, supports explicit ACR replacement, and can customize node pools, subnets, workload identity, and Application Gateway for Containers. Verify exact APIs before editing. + +## Preview and deploy + +List pipeline steps: + +```bash +aspire publish --list-steps +aspire deploy --list-steps +``` + +Generate artifacts without applying: + +```bash +aspire publish -o azure-artifacts +``` + +Deploy: + +```bash +aspire deploy +``` + +For local Azure deploys, prefer a real interactive terminal for the first apply. Azure deployment can prompt for values that are not AppHost parameters, such as tenant selection when multiple tenants are available. Use `--non-interactive` only after configuring deploy-time values with environment variables and confirming the Azure CLI login context is unambiguous. + +Do not pipe an interactive `aspire deploy` through `tee`, `tail`, or another command when prompts may appear. The pipe can make the current terminal non-interactive and selection prompts will fail. Use the attached terminal output and the Aspire CLI log path printed by the command instead of capturing the transcript with a pipe. + +Use `--environment ` for staging/production context: + +```bash +aspire deploy --environment staging +``` + +Published Azure artifacts are for preview or handoff. `aspire deploy` resolves parameters and applies the Azure deployment from the AppHost model. + +## Destroy + +Use Aspire to run the Azure target's destroy pipeline: + +```bash +aspire destroy --environment +``` + +For Azure, `aspire destroy` delegates to the Azure deployment target for the selected AppHost/environment. Confirm the Azure subscription, resource group, environment name, and AppHost before running destroy. Use `--yes` only after the user has explicitly approved teardown or in an environment-protected cleanup workflow. Use Azure CLI after destroy to verify resource removal or investigate leftovers; do not start with `az group delete` or target-specific delete commands for resources managed by the Aspire Azure target unless `aspire destroy` cannot complete. + +## Common troubleshooting + +Use Azure CLI to inspect live Azure state that Aspire deployed, while keeping deployment changes in the AppHost and `aspire deploy`. + +Start by confirming the active subscription and resource group: + +```bash +az account show --query "{name:name, id:id, tenantId:tenantId}" --output table +az group show --name "" --query "{name:name, location:location, provisioningState:properties.provisioningState}" --output table +az deployment group list --resource-group "" --query "[].{name:name, state:properties.provisioningState, timestamp:properties.timestamp}" --output table +az resource list --resource-group "" --query "[].{name:name, type:type, location:location}" --output table +``` + +Use target-specific inspection commands: + +```bash +# Azure Container Apps +az containerapp list --resource-group "" --query "[].{name:name, state:properties.runningStatus, fqdn:properties.configuration.ingress.fqdn}" --output table +az containerapp revision list --resource-group "" --name "" --query "[].{name:name, active:properties.active, healthState:properties.healthState}" --output table +az containerapp logs show --resource-group "" --name "" --tail 100 + +# Azure App Service +az webapp list --resource-group "" --query "[].{name:name, state:state, hostNames:defaultHostName}" --output table +az webapp config appsettings list --resource-group "" --name "" --query "[].name" --output table +az webapp log tail --resource-group "" --name "" + +# Azure Kubernetes Service (AKS) +az aks show --resource-group "" --name "" --query "{name:name, state:provisioningState, fqdn:fqdn, kubernetesVersion:kubernetesVersion}" --output table +az aks get-credentials --resource-group "" --name "" +kubectl get pods,svc,ingress --all-namespaces +helm list --all-namespaces +``` + +When troubleshooting generated Azure resources, match the live resource names and tags back to the Aspire deployment summary, AppHost environment resource name, and selected `--environment` value. Do not print secret values; for Key Vault or app settings, inspect key names and references rather than values. + +- Missing Azure settings: configure `Azure__SubscriptionId`, `Azure__Location`, and optionally `Azure__ResourceGroup` on the deploy process. +- Wrong subscription: check `az account show` and compare to `Azure__SubscriptionId` or AppHost config. +- Failed resource group deployment: inspect the failed deployment with `az deployment group show --resource-group "" --name ""` and then inspect operation errors with `az deployment operation group list --resource-group "" --name "" --query "[?properties.provisioningState=='Failed']"`. +- Parameter prompts in CI: provide `Parameters__*` environment variables through pipeline secrets/variables. +- Secrets to Key Vault: use the Azure Key Vault hosting integration and secret APIs only after confirming docs and user intent. +- Public endpoint surprise: inspect external endpoint configuration and target endpoint rules before deployment. +- Container Apps revision unhealthy: check revision health, ingress, image pull, managed identity, and logs with `az containerapp revision list` and `az containerapp logs show`. +- App Service unsupported resource: move backing services to managed Azure resources or choose Container Apps/Azure Kubernetes Service (AKS) instead. +- App Service dashed setting failure: rename the connection/environment key if possible; bypass validation only when the app does not depend on the original dashed key at runtime. +- Azure Kubernetes Service (AKS) workload not reachable: refresh credentials with `az aks get-credentials`, then inspect pods, services, ingress/gateway resources, and Helm release status with `kubectl` and `helm`. diff --git a/.agents/skills/aspire-deployment/references/cicd.md b/.agents/skills/aspire-deployment/references/cicd.md new file mode 100644 index 0000000..2634335 --- /dev/null +++ b/.agents/skills/aspire-deployment/references/cicd.md @@ -0,0 +1,342 @@ +# CI/CD and GitHub Actions deployment + +Use this reference when the user asks to automate Aspire publish/deploy in CI/CD, create a GitHub Actions workflow, publish release artifacts, push container images, or run deployment validation in a pipeline. + +CI/CD should still start from the AppHost model. The pipeline should install the Aspire CLI, install the AppHost/resource toolchains needed by the repo, restore/build the workspace, provide AppHost parameters through CI secrets/variables, run Aspire publish/deploy/list-step commands, and then either upload generated artifacts or deploy with the target's credentials. + +## Docs to load + +Use these searches and docs: + +```bash +aspire docs search "ci" +aspire docs search "github actions" +aspire docs search "external parameters deployment" +aspire docs get "testing-in-cicd-pipelines" +aspire docs get "example-app-lifecycle-workflow" +aspire docs get "external-parameters" +``` + +`testing-in-cicd-pipelines` is primarily about tests, but it includes useful CI facts: Linux runners have Docker available, CI needs explicit timeouts for tests, Azure credentials must be configured explicitly, and secrets should come from CI variables. `example-app-lifecycle-workflow` is a worked GitHub Actions example for publishing Aspire artifacts and pushing images. + +## Decide publish, deploy, or handoff + +Ask which CI/CD outcome the user wants if it is not clear: + +| Outcome | Use when | Typical command | +|---------|----------|-----------------| +| Validate deployment model | PR or pre-merge check should prove the AppHost can produce deployment steps/artifacts | `aspire publish --list-steps` or `aspire deploy --list-steps` | +| Publish artifacts | CI should produce Compose/Helm/CDK/Bicep or other target artifacts for review or later apply | `aspire publish -o ` | +| Push images only | CI should build/push project images but not deploy infrastructure | `aspire do ` after checking `aspire deploy --list-steps` | +| Deploy from CI | Protected branch/environment should provision/update target infrastructure | `aspire deploy --environment ` | +| Destroy from CI | Explicit cleanup workflow should tear down an Aspire-owned deployment | `aspire destroy --environment --yes` | + +Do not assume `aspire deploy` consumes a previous `aspire publish` output. `aspire deploy` applies directly from the AppHost model and resolves values for the selected environment. + +## GitHub Actions workflow shape + +Use this baseline shape, then add the setup block that matches the AppHost/resource graph and layer target-specific auth and parameters on top: + +```yaml +name: Deploy + +on: + workflow_dispatch: + push: + branches: [main] + +permissions: + contents: read + packages: write + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Install Aspire CLI + run: | + curl -sSL https://aspire.dev/install.sh | bash + echo "$HOME/.aspire/bin" >> "$GITHUB_PATH" + + - name: Inspect Aspire deployment steps + run: | + aspire ls + aspire publish --list-steps + aspire deploy --list-steps +``` + +Add one or both setup blocks before Aspire commands: + +| AppHost/resources | Setup guidance | +|-------------------|----------------| +| C# AppHost or .NET project resources | Install .NET with `actions/setup-dotnet`, then use the repo's restore/build command. | +| TypeScript AppHost or JavaScript resources | Install Node with `actions/setup-node`, then run the repo's package-manager install/build commands. | +| Mixed C#/TypeScript graph | Use both setup blocks so the AppHost and all compute resources can be restored, built, and published. | + +C# AppHost setup: + +```yaml +- name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + +- name: Restore and build .NET workspace + run: | + dotnet restore + dotnet build --no-restore +``` + +TypeScript AppHost setup: + +```yaml +- name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22.x + +- name: Install and build TypeScript workspace + run: | + npm ci + npm run build --if-present +``` + +Use repository-specific setup instead of the generic snippets when the repo already has wrapper scripts, a local SDK restore step, a package manager other than npm, an AppHost `package.json` in a subdirectory, or a required build command. + +## Parameters and secrets + +Map AppHost parameters through workflow `env:` using Aspire configuration environment-variable conventions: + +```yaml +env: + Parameters__registry_endpoint: ghcr.io + Parameters__registry_repository: ${{ github.repository }} + Parameters__api_key: ${{ secrets.API_KEY }} +``` + +Rules: + +- AppHost parameter `name` maps to `Parameters__name`. +- Parameter names with dashes use underscores in environment variables, for example `registry-endpoint` becomes `Parameters__registry_endpoint`. +- Secret parameters should come from GitHub Actions secrets or environment secrets. +- Do not print secret values. Prefer listing parameter names/status before deployment. +- Use GitHub Environments for production secrets and required reviewers when `aspire deploy` provisions cloud resources. + +## Container registry auth + +For GitHub Container Registry, authenticate before Aspire push/publish/deploy steps that build and push images: + +```yaml +- name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} +``` + +Then provide registry parameters if the AppHost models them: + +```yaml +- name: Push images with Aspire + env: + Parameters__registry_endpoint: ghcr.io + Parameters__registry_repository: ${{ github.repository }} + run: aspire do +``` + +Get the exact push/build step from `aspire deploy --list-steps` or `aspire publish --list-steps`; do not hardcode `push` unless the target docs/listed steps show it. + +## Publish artifacts in GitHub Actions + +Use publish when the pipeline should produce artifacts for review or later apply: + +```yaml +- name: Publish Aspire artifacts + env: + Parameters__registry_endpoint: ghcr.io + Parameters__registry_repository: ${{ github.repository }} + run: aspire publish -o ./aspire-output + +- name: Upload Aspire artifacts + uses: actions/upload-artifact@v4 + with: + name: aspire-output + path: ./aspire-output +``` + +Treat published output as potentially sensitive. Docker Compose `.env.`, Helm values/secrets, CDK output, and target-specific deployment state can include resolved parameter values or secret references. Upload only what the user intends to retain. + +## Deploy from GitHub Actions + +Use deploy only when the workflow is intentionally allowed to modify infrastructure: + +```yaml +- name: Deploy with Aspire + env: + Parameters__registry_endpoint: ghcr.io + Parameters__registry_repository: ${{ github.repository }} + run: aspire deploy --environment production --non-interactive +``` + +Add target-specific authentication before this step: + +- Docker Compose: runner needs Docker/Podman and access to the target Docker host if deploying remotely. +- Kubernetes: configure `kubectl` context and registry pull auth before `aspire deploy`. +- Azure: authenticate Azure CLI or Azure SDK credentials before `aspire deploy`; set `Azure__SubscriptionId`, `Azure__Location`, and optionally `Azure__ResourceGroup`. +- AWS: configure AWS credentials/region, install CDK prerequisites, and bootstrap the account/region before `aspire deploy`. + +Never route Azure deployment through a separate Azure deployment tool from this skill. Keep Azure deployment Aspire-native with `aspire deploy`, and use Azure CLI only for authentication and live state inspection. + +## Destroy from CI/CD + +Use `aspire destroy` for teardown workflows that intentionally run the AppHost deployment target's destroy pipeline: + +```yaml +- name: Destroy with Aspire + if: ${{ github.event_name == 'workflow_dispatch' }} + env: + Azure__SubscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + Azure__Location: ${{ vars.AZURE_LOCATION }} + Azure__ResourceGroup: ${{ vars.AZURE_RESOURCE_GROUP }} + run: aspire destroy --environment production --yes --non-interactive +``` + +Keep destroy jobs manually triggered or gated by a protected GitHub Environment. Reuse the same target authentication, AppHost path, environment, and parameter conventions as deploy. Do not put destroy into normal validation or deploy jobs unless the workflow owns temporary infrastructure and teardown is part of the tested lifecycle. If CI also created external infrastructure that is outside the Aspire deployment target, such as a temporary Kubernetes cluster or registry, clean that up in separate explicit provider-specific steps after the Aspire destroy step. + +## Azure GitHub Actions auth + +Use the repository's preferred Azure auth pattern. For OIDC with `azure/login`, ensure the workflow has `id-token: write` and the cloud app/federated credential is configured: + +```yaml +- name: Azure login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} +``` + +Then provide Aspire Azure settings: + +```yaml +env: + Azure__SubscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + Azure__Location: ${{ vars.AZURE_LOCATION }} + Azure__ResourceGroup: ${{ vars.AZURE_RESOURCE_GROUP }} +``` + +If the workflow uses service-principal secrets instead of OIDC, set `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_CLIENT_SECRET` as secrets for Azure SDK authentication. Do not echo these values. + +## Azure GitHub Actions deployment pattern + +A real Azure Aspire deployment workflow can be as small as checkout, AppHost toolchain setup, Aspire CLI install, Azure login, and `aspire deploy`. Use these checked-in workflow references when the user wants GitHub Actions to deploy directly to Azure through Aspire and gate it with a GitHub Environment: + +- C# AppHost: [github-actions-azure-csharp.yml](github-actions-azure-csharp.yml) +- TypeScript AppHost: [github-actions-azure-typescript.yml](github-actions-azure-typescript.yml) + +The C# AppHost reference has this shape: + +```yaml +name: Aspire Deploy CI/CD + +on: + push: + branches: [live] + +permissions: + id-token: write + contents: read + +jobs: + aspire_deploy: + runs-on: ubuntu-latest + name: Deploy with Aspire + environment: + name: production + env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + submodules: true + lfs: false + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + dotnet-quality: preview + + - name: Install Aspire CLI + run: curl -sSL https://aspire.dev/install.sh | bash + + - name: Add Aspire CLI to PATH + run: echo "$HOME/.aspire/bin" >> "$GITHUB_PATH" + + - name: Azure Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy with Aspire + run: aspire deploy --apphost ./src/apphost.cs --environment Production --non-interactive + env: + Azure__SubscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + Azure__Location: ${{ vars.AZURE_LOCATION || 'eastus' }} + Azure__ResourceGroup: ${{ vars.AZURE_RESOURCE_GROUP }} + Parameters__admin_password: ${{ secrets.ADMIN_PASSWORD }} +``` + +Create a GitHub Environment named `production` and store deployment values there so approvals, branch rules, environment variables, and environment secrets apply to the deployment job: + +| GitHub Environment value | Example names | +|--------------------------|---------------| +| Variables | `AZURE_LOCATION`, `AZURE_RESOURCE_GROUP`, app build variables that are safe to expose | +| Secrets | `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`, AppHost parameter secrets such as `ADMIN_PASSWORD` | + +Adapt the example instead of copying it blindly: + +- Start from the external `.yml` reference that matches the AppHost language, then adjust paths, package manager commands, package-manager caching, target branch, and parameter names. +- Use `--apphost ` when the workflow should pin a specific AppHost, such as a single-file `apphost.cs`, `apphost.ts`, or an AppHost project file. +- For TypeScript AppHosts, replace the .NET setup with Node/package-manager setup, deploy with `--apphost --non-interactive`, and provide deployment settings and AppHost parameters through the deploy step's `env:`. +- Keep `id-token: write` for Azure OIDC login. +- Put non-secret deployment settings in GitHub Environment variables when possible, such as `AZURE_LOCATION` and `AZURE_RESOURCE_GROUP`. +- Put secret AppHost parameters in GitHub Environment secrets and pass them as `Parameters__*`. +- Add app-specific build metadata only when the app consumes it. For example, Vite apps need a `VITE_` prefix for values intended for client-side build-time exposure. +- Add extra Azure settings only when the AppHost declares them, such as `Azure__PostgresLocation` for a custom PostgreSQL region. +- Add a separate preflight step with `aspire ls` and `aspire deploy --list-steps` when the workflow should show planned deployment steps before applying changes. + +## Validation and troubleshooting + +Before applying changes: + +```bash +aspire ls +aspire publish --list-steps +aspire deploy --list-steps +``` + +After deployment, use the target reference's validation commands: + +- Docker Compose: `docker compose ps` against generated files or the target Compose project. +- Kubernetes: `kubectl get pods`, `kubectl get svc`, and `helm status`. +- Azure: `az account show`, `az resource list`, target-specific `az containerapp`, `az webapp`, or `az aks` commands. +- AWS: `aws cloudformation describe-stacks`, stack events, and service-specific AWS CLI commands. + +Common CI/CD failures: + +- Missing parameter: add `Parameters__*` env vars or GitHub Environment secrets. +- Wrong registry path: compare `Parameters__registry_endpoint` and `Parameters__registry_repository` to generated image names. +- Docker unavailable: use `ubuntu-*` GitHub-hosted runners for Linux containers or a configured self-hosted runner. +- Cloud auth uses the wrong identity/subscription/account: print identity metadata only, not secrets. +- Publish output contains sensitive values: reduce uploaded paths or use environment-protected artifacts. +- Deployment step name changed: rerun `aspire deploy --list-steps` and update `aspire do `. diff --git a/.agents/skills/aspire-deployment/references/docker-compose.md b/.agents/skills/aspire-deployment/references/docker-compose.md new file mode 100644 index 0000000..8db92ca --- /dev/null +++ b/.agents/skills/aspire-deployment/references/docker-compose.md @@ -0,0 +1,155 @@ +# Docker Compose deployment + +Use this reference when the user asks for Docker Compose deployment, local container deployment artifacts, or a non-cloud deployment package. + +## Docs to load + +Always start with current docs: + +```bash +aspire docs search "Docker Compose deployment" +aspire docs search "Docker hosting integration" +aspire docs search "Docker Compose environment variables" +aspire docs get "deploy-to-docker-compose" +aspire docs get "docker-integration" +aspire docs get "" +``` + +Use API docs before editing. Search in the AppHost language you detected: + +```bash +aspire docs api search "Docker Compose environment" --language csharp +aspire docs api search "Docker Compose environment" --language typescript +aspire docs api search "Docker Compose service customization" --language csharp +aspire docs api search "Docker Compose service customization" --language typescript +``` + +## Target setup + +Expected package and AppHost environment: + +```bash +aspire add docker +``` + +Add a Docker Compose environment resource using the C# or TypeScript API shape returned by Aspire docs. When a Docker Compose environment exists, compatible resources are automatically included in generated Compose output. Use the per-resource Docker Compose customization API only for customization. + +## Code changes to make + +Make these changes in the AppHost, not in the generated Compose output: + +1. Run `aspire add docker` if the AppHost does not already reference the Docker hosting integration. +2. Add a Docker Compose environment resource. + + C# AppHost shape: + + ```csharp + var compose = builder.AddDockerComposeEnvironment("docker-compose"); + ``` + + TypeScript AppHost shape: + + ```typescript + const compose = await builder.addDockerComposeEnvironment("docker-compose"); + ``` + +3. Do not add explicit compute-environment assignment for the common single-environment case. Only if the AppHost has multiple compute environments, disambiguate each Docker Compose workload; in C#, add `.WithComputeEnvironment(compose)` to each compute resource that should land in Compose. +4. For TypeScript AppHosts, verify the current language-specific docs before assuming an equivalent assignment API. +5. Keep normal app model relationships such as `WithReference`, endpoints, parameters, and connection strings in the AppHost. They flow into Compose environment variables and service dependencies. +6. Use customization APIs only for real Compose customization: + - C#: `compose.ConfigureComposeFile(...)`, `compose.ConfigureEnvFile(...)`, and `resource.PublishAsDockerComposeService(...)`. + - TypeScript: `compose.configureComposeFile(...)`, `compose.configureEnvFile(...)`, and `resource.publishAsDockerComposeService(...)`. + +Do not hand-edit `docker-compose.yaml` as the durable fix unless the user explicitly wants to eject generated artifacts. + +## Preflight + +Check: + +- Docker or Podman is installed and running. +- Docker must be at least 28.0.0, and Podman must be at least 5.0.0 for current Aspire CLI environment checks. +- The AppHost has a Docker Compose environment resource. +- The repo does not rely on local bind mounts that will be invalid on the target Docker host. +- Parameters and secrets are represented as placeholders in `.env` after `aspire publish` and resolved in `.env.` after prepare/deploy. +- Any fixed ports are intentional and do not conflict on the deployment host. + +Use `ASPIRE_CONTAINER_RUNTIME=docker` or `ASPIRE_CONTAINER_RUNTIME=podman` only when the user needs to force a runtime. + +## Preview and publish + +Generate artifacts without starting containers: + +```bash +aspire publish +``` + +Expected output includes: + +- `aspire-output/docker-compose.yaml` +- `aspire-output/.env` +- resource Dockerfiles when needed + +For environment-specific output and image build without running the whole deploy, use the target's prepare step if docs/list-steps show it: + +```bash +aspire deploy --list-steps +aspire do prepare-docker-compose --environment staging +``` + +The exact step name depends on the Docker Compose environment resource name. Use the step shown by `aspire deploy --list-steps`: for an environment resource named `docker-compose`, the prepare step is `prepare-docker-compose`; for one named `compose`, it is `prepare-compose`. + +## Deploy and destroy + +Deploy: + +```bash +aspire deploy +``` + +Aspire generates Compose output, builds images, writes environment-specific `.env` files, and runs Compose. + +Run the Docker Compose target's destroy pipeline only when requested: + +```bash +aspire destroy +``` + +For Docker Compose, `aspire destroy` delegates to the Compose deployment target for the selected AppHost/environment. Use Docker or Compose commands after destroy only to verify cleanup or investigate leftover containers, networks, volumes, or generated files. + +## Common decisions + +### Publish-only vs deploy + +Use `aspire publish` when the user wants files to review or hand to another deployment system. Use `aspire deploy` when they want Aspire to start the Compose deployment. + +`aspire publish` writes `docker-compose.yaml` and `.env` with blank placeholders for captured values. Prepare/deploy writes `.env.` with resolved values. Do not expect `aspire deploy` to consume a previously published directory. + +### Customizing generated Compose + +Use docs-backed APIs: + +- The Compose file customization API for global Compose model changes. +- The environment file customization API for generated `.env` changes. +- The per-resource Docker Compose service customization API for service-level changes. + +Do not hand-edit generated `docker-compose.yaml` as the source of truth unless the user explicitly wants to eject the artifact. + +### Environment files and bind mounts + +Generated `.env` values are intentionally separated from environment-specific `.env.` values: + +- `.env` is a publish-time placeholder file and preserves existing user values when possible. +- `.env.` is written by prepare/deploy with resolved parameter, image, and bind-mount values. +- Project images are represented through image placeholders such as `_IMAGE` until prepare/deploy resolves them. +- Bind mount source paths are replaced by `_BINDMOUNT_` placeholders because local paths often do not exist on another Docker host. +- Docker socket mounts are left as the platform socket path instead of being placeholderized. + +Treat `.env.` as potentially sensitive. + +### Compose project name + +Aspire uses a generated Compose project name based on the environment resource name and, when available, the AppHost path hash. This prevents common collisions between different AppHosts using the same environment name. + +### Service host names + +When a service needs another service's Compose host name, use the Docker Compose environment's host address expression API from docs. Do not hardcode generated service names unless the docs or artifact prove them. diff --git a/.agents/skills/aspire-deployment/references/github-actions-azure-csharp.yml b/.agents/skills/aspire-deployment/references/github-actions-azure-csharp.yml new file mode 100644 index 0000000..76abb90 --- /dev/null +++ b/.agents/skills/aspire-deployment/references/github-actions-azure-csharp.yml @@ -0,0 +1,53 @@ +name: Aspire Deploy CSharp + +on: + workflow_dispatch: + push: + branches: [live] + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy CSharp AppHost with Aspire + environment: + name: production + env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: true + steps: + - name: Checkout + # actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + - name: Setup .NET + # actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 + with: + dotnet-version: 10.0.x + dotnet-quality: preview + + - name: Install Aspire CLI + run: | + curl -sSL https://aspire.dev/install.sh | bash + echo "$HOME/.aspire/bin" >> "$GITHUB_PATH" + + - name: Azure Login + # azure/login@v2 + uses: azure/login@1384c340ab2dda50fed2bee3041d1d87018aa5e8 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy with Aspire + run: aspire deploy --apphost ./src/AppHost/AppHost.csproj --environment Production + env: + Azure__SubscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + Azure__Location: ${{ vars.AZURE_LOCATION || 'eastus' }} + Azure__ResourceGroup: ${{ vars.AZURE_RESOURCE_GROUP }} + Parameters__admin_password: ${{ secrets.ADMIN_PASSWORD }} diff --git a/.agents/skills/aspire-deployment/references/github-actions-azure-typescript.yml b/.agents/skills/aspire-deployment/references/github-actions-azure-typescript.yml new file mode 100644 index 0000000..6517dd3 --- /dev/null +++ b/.agents/skills/aspire-deployment/references/github-actions-azure-typescript.yml @@ -0,0 +1,53 @@ +name: Aspire Deploy TypeScript + +on: + workflow_dispatch: + push: + branches: [live] + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy TypeScript AppHost with Aspire + environment: + name: production + steps: + - name: Checkout + # actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + - name: Setup Node + # actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 22.x + + - name: Install and build workspace + run: | + npm ci + npm run build --if-present + + - name: Install Aspire CLI + run: | + curl -sSL https://aspire.dev/install.sh | bash + echo "$HOME/.aspire/bin" >> "$GITHUB_PATH" + + - name: Azure Login + # azure/login@v2 + uses: azure/login@1384c340ab2dda50fed2bee3041d1d87018aa5e8 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy with Aspire + run: aspire deploy --apphost ./apphost.ts --environment Production --non-interactive + env: + Azure__SubscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + Azure__Location: ${{ vars.AZURE_LOCATION || 'eastus' }} + Azure__ResourceGroup: ${{ vars.AZURE_RESOURCE_GROUP }} + Parameters__admin_password: ${{ secrets.ADMIN_PASSWORD }} diff --git a/.agents/skills/aspire-deployment/references/javascript.md b/.agents/skills/aspire-deployment/references/javascript.md new file mode 100644 index 0000000..c82f911 --- /dev/null +++ b/.agents/skills/aspire-deployment/references/javascript.md @@ -0,0 +1,126 @@ +# JavaScript app deployment + +Use this reference whenever an Aspire deployment includes JavaScript app resources, such as Vite, React, Vue, Angular, Astro, Next.js, Nuxt, or plain Node.js apps. + +JavaScript resources are not just another deployment target. They need an explicit production serving model before the target reference can be trusted. A Vite dev server that works during `aspire run` is often build-only during publish/deploy unless the AppHost says who serves the built files. + +## Integration to add + +```bash +aspire add javascript +``` + +This adds `Aspire.Hosting.JavaScript`, which provides `AddJavaScriptApp`, `AddNodeApp`, `AddViteApp`, `AddNextJsApp`, package-manager helpers, and JavaScript publish modes. + +## Docs to load + +Always start with current docs: + +```bash +aspire docs search "javascript deployment" +aspire docs get "deploy-javascript-apps" +aspire docs get "set-up-javascript-apps-in-the-apphost" +``` + +Use API docs before editing. Search in the AppHost language you detected: + +```bash +aspire docs api search "AddJavaScriptApp" --language csharp +aspire docs api search "AddJavaScriptApp" --language typescript +aspire docs api search "AddNodeApp" --language csharp +aspire docs api search "AddNodeApp" --language typescript +aspire docs api search "AddViteApp" --language csharp +aspire docs api search "AddViteApp" --language typescript +aspire docs api search "AddNextJsApp" --language csharp +aspire docs api search "AddNextJsApp" --language typescript +aspire docs api search "PublishAsStaticWebsite" --language csharp +aspire docs api search "PublishAsStaticWebsite" --language typescript +aspire docs api search "PublishAsNodeServer" --language csharp +aspire docs api search "PublishAsNodeServer" --language typescript +aspire docs api search "PublishAsPackageScript" --language csharp +aspire docs api search "PublishAsPackageScript" --language typescript +aspire docs api search "PublishWithContainerFiles" --language csharp +aspire docs api search "PublishWithContainerFiles" --language typescript +aspire docs api search "PublishWithStaticFiles" --language csharp +aspire docs api search "PublishWithStaticFiles" --language typescript +``` + +## Setup + +Use `Aspire.Hosting.JavaScript` for Aspire 13+ apps. `Aspire.Hosting.NodeJs` is the old package name; do not add it for new guidance unless the target repo is intentionally pinned to older Aspire. + +## Resource choice + +Choose the AppHost resource based on what the JavaScript app is during local development: + +| Resource | Use when | C# shape | TypeScript shape | +|----------|----------|----------|------------------| +| JavaScript app | Generic package-script-driven app | `AddJavaScriptApp(...)` | `addJavaScriptApp(...)` | +| Node app | A Node process starts a specific script file | `AddNodeApp(...)` | `addNodeApp(...)` | +| Vite app | Vite-based browser app or framework dev server | `AddViteApp(...)` | `addViteApp(...)` | +| Next.js app | Next.js app using the dedicated helper | `AddNextJsApp(...)` | `addNextJsApp(...)` | + +`AddNextJsApp` and JavaScript publish methods are experimental. In C# AppHosts, follow docs for the `ASPIREJAVASCRIPT001` suppression instead of suppressing warnings broadly. + +## Production serving model + +Choose the production entrypoint before deploying: + +| Production shape | Use when | AppHost pattern | +|------------------|----------|-----------------| +| Static frontend served by a backend | The backend should serve built frontend files from `wwwroot`, `static`, or similar | Backend resource uses `PublishWithContainerFiles(frontend, "")` | +| Static frontend served by a gateway/BFF | YARP or another proxy is the public entrypoint and serves the built frontend | Gateway uses `PublishWithStaticFiles(frontend)` | +| Static frontend served by the JavaScript resource | The frontend should deploy as its own static website/container | JavaScript resource uses `PublishAsStaticWebsite(...)` | +| SSR or Node.js server with built output | The framework emits a server entrypoint/artifact | JavaScript resource uses `PublishAsNodeServer(...)` | +| SSR or Node.js server started by package script | Runtime needs package manager scripts and runtime dependencies | JavaScript resource uses `PublishAsPackageScript(...)` | +| Next.js standalone app | Next.js is configured for standalone output | Use `AddNextJsApp(...)` | + +Do not assume the Vite dev server is the production server. During publish/deploy, Vite resources usually build static files unless the AppHost selects a JavaScript publish mode. + +## Code changes to make + +1. Run `aspire add javascript` if the AppHost does not already reference the JavaScript hosting integration. +2. Add the appropriate JavaScript resource. + + C# AppHost examples: + + ```csharp + var frontend = builder.AddViteApp("frontend", "./frontend"); + var api = builder.AddNodeApp("api", "./api", "server.js") + .WithHttpEndpoint(env: "PORT"); + ``` + + TypeScript AppHost examples: + + ```typescript + const frontend = await builder.addViteApp("frontend", "./frontend"); + const api = await builder + .addNodeApp("api", "./api", "server.js") + .withHttpEndpoint({ env: "PORT" }); + ``` + +3. Configure package manager and scripts only when defaults are wrong: + - C#: `WithNpm(...)`, `WithYarn(...)`, `WithPnpm(...)`, `WithBun(...)`, `WithBuildScript(...)`, `WithRunScript(...)`. + - TypeScript: `withNpm(...)`, `withYarn(...)`, `withPnpm(...)`, `withBun(...)`, `withBuildScript(...)`, `withRunScript(...)`. +4. Add a production serving model from the table above. This is required for build-only browser apps. +5. Add `WithExternalHttpEndpoints()` / `withExternalHttpEndpoints()` only to the resource that owns the public production HTTP surface, such as the backend, gateway, static website, Node server, or Next.js app. +6. Keep dev-only gateway/proxy wiring behind run-mode checks when it depends on a dev server URL. Production routing must be configured on the production-serving resource, not assumed from Vite dev proxy settings. +7. After choosing the production-serving resource, apply the target reference for Docker Compose, Kubernetes, Azure, or AWS. + +## Target-specific notes + +- Docker Compose: JavaScript resources can publish as containers or static-file-carrying build resources. Inspect generated Dockerfiles, `docker-compose.yaml`, and `.env` placeholders. +- Kubernetes and Azure Kubernetes Service (AKS): make sure the production-serving resource becomes the workload with the service/ingress/gateway exposure. Build-only resources must be consumed by another deployable resource or converted with a JavaScript publish method. +- Azure Container Apps: only the production-serving compute resource should be external. For static browser apps, `PublishAsStaticWebsite(...)` can be the external resource; for backend-served or gateway-served apps, the backend/gateway is the external resource. +- Azure App Service: use this only when the JavaScript app fits the public website model and single target-port constraints. SSR/Node apps need the right startup script/output and external HTTP endpoint. +- AWS: verify support in the AWS integrations repo. Do not assume the AWS preview deployment supports every JavaScript publish mode. + +## Common pitfalls + +- A Vite resource that works locally can fail deployment validation if it is still build-only and no deployed resource consumes its files. +- Vite dev-server proxy configuration does not automatically become production routing. +- `PublishWithContainerFiles(...)` copies built files into a destination container; the destination app still needs to serve those files. +- `PublishWithStaticFiles(...)` is for gateway/BFF resources such as YARP serving frontend files. +- `PublishAsPackageScript(...)` keeps runtime package dependencies and is larger than a built Node server output; prefer `PublishAsNodeServer(...)` when the framework emits a runnable server artifact. +- Next.js standalone deployment requires Next.js standalone output configuration. Validate this before deploying. +- Do not expose both a dev frontend resource and the production-serving gateway/backend unless the user intentionally wants two public surfaces. diff --git a/.agents/skills/aspire-deployment/references/kubernetes.md b/.agents/skills/aspire-deployment/references/kubernetes.md new file mode 100644 index 0000000..32108c9 --- /dev/null +++ b/.agents/skills/aspire-deployment/references/kubernetes.md @@ -0,0 +1,236 @@ +# Kubernetes and Azure Kubernetes Service (AKS) deployment + +Use this reference when the user asks for Kubernetes, Helm, kubectl, Azure Kubernetes Service (AKS), or cluster deployment. + +## Choose Kubernetes vs Azure Kubernetes Service (AKS) + +Aspire has two Kubernetes paths: + +| Target | Use when | Integration | Environment concept | +|--------|----------|-------------|---------------------| +| Existing or externally-managed Kubernetes cluster | The cluster already exists, or the user explicitly wants a provider-managed cluster outside Aspire such as DigitalOcean Kubernetes (DOKS). Current `kubectl` context should point at the target cluster. | Kubernetes hosting | Kubernetes environment | +| Azure Kubernetes Service (AKS) | Aspire should provision Azure Kubernetes Service (AKS), ACR, identity, and Azure dependencies | Azure Kubernetes hosting | Azure Kubernetes Service (AKS) environment | + +If the user says "Kubernetes" but not "Azure Kubernetes Service (AKS)", ask whether they want an existing/external cluster or a new Azure Kubernetes Service (AKS) cluster. + +If the user names a non-Azure Kubernetes provider, such as DigitalOcean Kubernetes, use the existing/external Kubernetes path. Aspire will deploy into that cluster with Helm, but it does not own the provider's cluster or registry lifecycle unless a target-specific Aspire integration exists. + +If the user says "Azure Kubernetes Service (AKS)", do not ask the existing-cluster question; use the Azure Kubernetes Service (AKS) path and also load the Azure reference for shared Azure settings. + +## Docs to load + +Always start with current docs: + +```bash +aspire docs search "Kubernetes deployment" +aspire docs search "Kubernetes hosting integration" +aspire docs get "deploy-to-kubernetes-clusters" +aspire docs get "kubernetes-integration" +aspire docs get "deploy-to-azure-kubernetes-service-aks" +aspire docs get "azure-kubernetes-service-aks-integration" +aspire docs search "Azure Kubernetes Service hosting integration" +aspire docs get "" +``` + +Use API docs before editing. Search in the AppHost language you detected: + +```bash +aspire docs api search "Kubernetes environment" --language csharp +aspire docs api search "Kubernetes environment" --language typescript +aspire docs api search "Helm" --language csharp +aspire docs api search "Helm" --language typescript +aspire docs api search "Kubernetes service customization" --language csharp +aspire docs api search "Kubernetes service customization" --language typescript +``` + +## Existing or external Kubernetes cluster setup + +Expected package and AppHost environment: + +```bash +aspire add kubernetes +``` + +Add a Kubernetes environment resource using the C# or TypeScript API shape returned by Aspire docs. + +For vanilla Kubernetes and externally-managed clusters, a container registry is required for project/container image deployment because Aspire has no local-registry fallback for Kubernetes. Cluster nodes must pull the built images. + +Add a container registry resource to the AppHost using the language-specific API shape returned by Aspire docs. Aspire can use a single registry as the default target; use per-resource registry assignment only when different workloads must use different registries. + +Verify the registry is reachable from both the agent machine and cluster nodes. + +When the user asks to create a provider-managed cluster or registry outside Aspire, such as a DigitalOcean Kubernetes cluster and DigitalOcean Container Registry, confirm the billable resource choice first. After creation, configure `kubectl`, registry authentication, and any provider-specific registry-to-cluster integration before running `aspire deploy`. + +### Existing/external cluster code changes + +Make these changes in the AppHost: + +1. Run `aspire add kubernetes` if the AppHost does not already reference the Kubernetes hosting integration. +2. Add a Kubernetes environment resource. + + C# AppHost shape: + + ```csharp + var k8s = builder.AddKubernetesEnvironment("k8s"); + ``` + + TypeScript AppHost shape: + + ```typescript + const k8s = await builder.addKubernetesEnvironment("k8s"); + ``` + +3. Add a container registry for project/container images, for example `builder.AddContainerRegistry(...)` in C# or the TypeScript equivalent returned by API docs. If one registry exists, Aspire can use it as the default target; use `WithContainerRegistry(...)` / `withContainerRegistry(...)` only when a specific resource should use a specific registry. +4. Do not add explicit compute-environment assignment for the common single-environment case. Only if the AppHost has multiple compute environments, disambiguate each Kubernetes workload; in C#, add `.WithComputeEnvironment(k8s)` to each compute resource that should deploy to this cluster. +5. For TypeScript AppHosts, verify the current language-specific docs before assuming an equivalent assignment API. +6. Use `k8s.WithHelm(...)` / `k8s.withHelm(...)` only when the user needs chart name, release name, namespace, chart version, or other Helm settings. +7. Use `k8s.AddGateway(...)`, `k8s.AddIngress(...)`, or the TypeScript equivalents when public exposure is required. Otherwise services remain internal by default. For a simple public web frontend on a cloud Kubernetes provider, a per-resource `LoadBalancer` Service can be the direct exposure model; keep backend/internal services as `ClusterIP`. + - **Routed endpoints must be marked external.** Any endpoint exposed by an Ingress (`WithPath(...)`) or a Gateway (`WithRoute(...)`), or wired up via `WithDefaultBackend(...)`, must come from a resource that opts in with `.WithExternalHttpEndpoints()` (C#) or `isExternal: true` on the endpoint annotation. `aspire publish` fails fast with an `InvalidOperationException` from `EndpointRoutingValidation` if a non-external endpoint is routed, so always pair `AddIngress`/`AddGateway` plumbing with explicit external opt-in on the target resource. This applies to AKS through `AzureKubernetesEnvironment` as well. +8. Use `PublishAsKubernetesService(...)` / `publishAsKubernetesService(...)` only for per-resource Kubernetes manifest customization. + +## Azure Kubernetes Service (AKS) setup + +Expected package and AppHost environment: + +```bash +aspire add azure-kubernetes +``` + +Add an Azure Kubernetes Service (AKS) environment resource using the C# or TypeScript API shape returned by Aspire docs. + +Azure Kubernetes Service (AKS) deployment provisions Azure infrastructure, including Azure Kubernetes Service (AKS), ACR, managed identity, and Azure resources modeled in the AppHost. + +Azure Kubernetes Service (AKS) creates an inner Kubernetes environment for Helm deployment and auto-creates an Azure Container Registry unless a registry is explicitly configured. + +### Azure Kubernetes Service (AKS) code changes + +Make these changes in the AppHost: + +1. Run `aspire add azure-kubernetes` if the AppHost does not already reference the Azure Kubernetes hosting integration. +2. Add an Azure Kubernetes Service (AKS) environment resource. + + C# AppHost shape: + + ```csharp + var aks = builder.AddAzureKubernetesEnvironment("aks"); + ``` + + TypeScript AppHost shape: + + ```typescript + const aks = await builder.addAzureKubernetesEnvironment("aks"); + ``` + +3. Do not add explicit compute-environment assignment for the common single-environment case. Only if the AppHost has multiple compute environments, disambiguate each Azure Kubernetes Service (AKS) workload; in C#, add `.WithComputeEnvironment(aks)` to each compute resource that should deploy there. +4. Do not add a plain `AddKubernetesEnvironment` next to `AddAzureKubernetesEnvironment` for the same target. The Azure Kubernetes Service (AKS) environment owns the inner Kubernetes/Helm environment. +5. Let the integration create the default Azure Container Registry unless the user needs a specific registry. If they do, use the Azure Kubernetes registry customization API from current docs. +6. Use node pool/subnet/customization APIs only when required, for example `WithSystemNodePool(...)`, `AddNodePool(...)`, `WithSubnet(...)`, or the TypeScript equivalents returned by API docs. +7. Use Kubernetes Gateway/Ingress APIs for public exposure; do not assume every service becomes public. + +## Preflight + +For all Kubernetes targets: + +- `kubectl` is installed. +- `helm` is installed. +- AppHost has the correct Kubernetes environment. +- AppHost parameters are configured or can be prompted. +- External exposure is explicit through Ingress, Gateway API, service customization, or target-specific defaults. +- Helm is the default deployment engine; record any customized namespace, release name, chart name, or chart version. +- Storage defaults are understood. Kubernetes defaults to `emptyDir`; persistent storage needs explicit storage type/class/size decisions. + +For existing/external clusters: + +- `kubectl config current-context` points to the intended cluster. +- Container registry is configured and reachable. +- Registry authentication is configured for image push from the agent machine. +- Image pull secret or registry auth is configured when the cluster cannot pull from the registry anonymously. +- Provider-specific registry attachment is configured when required, for example attaching a provider registry to a managed cluster. +- Namespace/release/chart settings are understood if Helm settings are customized. + +For Azure Kubernetes Service (AKS): + +- Azure CLI auth is available (`az login` for local deploy). +- Subscription, location, and resource group source are known. +- `Azure:SubscriptionId` / `Azure__SubscriptionId` and `Azure:Location` / `Azure__Location` are configured or can be prompted. +- Node pool defaults fit the target subscription/region, or the AppHost customizes node pools. + +## Preview and publish + +Generate Helm artifacts without applying them: + +```bash +aspire publish -o k8s-artifacts +``` + +For Azure Kubernetes Service (AKS), publish can also generate Azure infrastructure artifacts. + +Inspect expected output: + +- `Chart.yaml` +- `values.yaml` +- `templates/` +- generated environment values for deploy-time parameter and image resolution +- Bicep infrastructure for Azure Kubernetes Service (AKS) targets when generated + +Use list steps before deploy: + +```bash +aspire deploy --list-steps +``` + +## Deploy + +Existing/external cluster: + +```bash +aspire deploy +``` + +Aspire uses current `kubectl` context and Helm. It deploys application resources into the cluster; it does not create or delete an externally-managed Kubernetes cluster or provider registry. + +Azure Kubernetes Service (AKS): + +```bash +aspire deploy +``` + +Aspire provisions Azure resources, builds/pushes images to ACR, generates Helm charts, and installs them into Azure Kubernetes Service (AKS). + +External Helm charts added to a Kubernetes environment install after the main app chart. They are not uninstalled by default during destroy because they may be shared; only treat them as owned by the Aspire app when the AppHost explicitly opts into destroy-time uninstall. + +## Destroy + +Run the destroy pipeline for this AppHost/environment: + +```bash +aspire destroy --environment +``` + +For an existing/external cluster, the destroy step runs against the configured cluster context for the selected AppHost/environment. Confirm the current `kubectl` context, namespace/release settings, and AppHost environment before running destroy. It does not delete an externally-created Kubernetes cluster, node pool, provider load balancer account resource, or container registry. Delete provider infrastructure with the provider CLI only when the user explicitly asks to remove that infrastructure too. For Azure Kubernetes Service (AKS), also confirm the Azure subscription and resource group because that target can own Azure infrastructure as well as cluster resources. Use `--yes` only after destructive intent is explicit. Use kubectl, cloud CLI, provider delete commands, or Helm only to diagnose failed teardown or remove leftovers that are not managed by the Aspire deployment target. + +## Native artifact handoff + +If the user wants to apply published artifacts themselves: + +```bash +helm install ./k8s-artifacts +helm upgrade ./k8s-artifacts +``` + +Use values files or `--set` for environment-specific image tags and settings. + +## Exposure and TLS + +By default, generated services use the Kubernetes environment's default service type, which is `ClusterIP`. Public access requires an explicit exposure mechanism such as Ingress, Gateway API, or service type customization. For cloud Kubernetes providers without an ingress controller configured, a frontend `LoadBalancer` Service is often the fastest public smoke-test path, while backend services should usually remain `ClusterIP`. Verify exact APIs in docs before editing because C# and TypeScript AppHost shapes differ. + +Gateway/Ingress TLS can add extra deployment steps for bootstrap secrets, FQDN discovery, and field ownership cleanup. If a deployment fails around TLS or Gateway resources, inspect the listed pipeline steps and the generated manifests before changing the AppHost. + +## Troubleshooting + +- Empty connection strings: inspect generated ConfigMaps and Secrets and verify env var names in pod specs. +- Password/auth failures: inspect Kubernetes Secrets and references; do not print secret values. +- ImagePullBackOff: verify image registry, pull secret/identity, and image tags. +- Azure Kubernetes Service (AKS) cluster unreachable: refresh credentials with `az aks get-credentials`. +- ACR pull issues on Azure Kubernetes Service (AKS): check ACR role assignment with `az aks check-acr`. +- Helm conflict on Gateway/TLS resources: inspect `helm status`, `kubectl describe gateway`, and the generated Gateway/Ingress manifests. diff --git a/.agents/skills/aspire-deployment/references/preflight.md b/.agents/skills/aspire-deployment/references/preflight.md new file mode 100644 index 0000000..52361d8 --- /dev/null +++ b/.agents/skills/aspire-deployment/references/preflight.md @@ -0,0 +1,189 @@ +# Common deployment preflight + +Use this reference for every Aspire deployment target. + +## AppHost discovery + +Find the AppHost before choosing commands: + +1. Run `aspire ls` first. It lists all AppHosts in the current scope and is the preferred discovery command. +2. If `aspire ls` shows exactly one AppHost, use it. +3. If `aspire ls` shows no AppHosts, stop deployment work and invoke the `aspireify` skill to initialize/wire the AppHost before continuing. +4. If `aspire ls` shows multiple AppHosts or discovery is still ambiguous, inspect `aspire.config.json`, `*.AppHost.csproj`, `apphost.cs`, or `apphost.ts`. +5. For C# project AppHosts, confirm the project references Aspire AppHost support. +6. For C# single-file AppHosts, look for the Aspire AppHost SDK directive. +7. For TypeScript AppHosts, look for the AppHost file and generated module support. + +Use `--apphost ` when discovery is ambiguous, multiple AppHosts exist, or CI/CD should pin a specific AppHost. The path can point to an AppHost project file or supported single-file AppHost, such as `apphost.cs`. + +## Docs lookup checklist + +Use Aspire docs search before changing target configuration: + +```bash +aspire docs search "ci" +aspire docs search "github actions" +aspire docs search "deployment overview" +aspire docs search "deploy with Aspire" +aspire docs search "external parameters deployment" +aspire docs search " deployment" +aspire docs get "" +``` + +Use API docs before writing AppHost code. Search both languages until the AppHost language is known: + +```bash +aspire docs api search "" --language csharp +aspire docs api search "" --language typescript +aspire docs api get "" +``` + +If `aspire docs` is unavailable, use official `aspire.dev` docs through the available web/documentation tools. Do not use outdated blog posts or workload-era docs as authority. + +## Target detection checklist + +Inspect AppHost code for existing target environments. API names differ by AppHost language, so use these as concepts rather than exact names: + +- Docker Compose environment +- Kubernetes environment +- Azure Container Apps environment +- Azure App Service environment +- Azure Kubernetes Service (AKS) environment +- AWS CDK environment + +If none exists, ask for the deployment target unless the user's request clearly names one. + +If more than one exists, ask which one to use or whether to deploy all. + +## Compute environment assignment + +Aspire can infer the compute environment only when there is exactly one compute environment in the model. If multiple deployment environments exist, verify each compute resource is explicitly assigned to the intended environment before publishing or deploying. + +Do not assume a resource deploys just because it appears in run mode. Resources hidden behind run-mode-only conditionals or assigned to a different compute environment will not be part of the target deployment. + +## Preview commands + +Use these before applying changes: + +```bash +aspire publish --list-steps +aspire deploy --list-steps +``` + +Treat `--list-steps` as a structural preview, not proof that a later deploy can run unattended. Some targets can emit deploy-time selection prompts that are not AppHost parameters, such as Azure tenant selection. If a prompt appears, answer it in a real interactive terminal/PTY; do not try to satisfy it by setting unrelated AppHost secrets unless the target docs explicitly define that mapping. + +Use publish when the user wants to inspect generated artifacts: + +```bash +aspire publish -o ./aspire-output +``` + +The output path can be a scratch path if the user only asked for a preview. Do not commit generated deployment artifacts unless the user explicitly asks to keep them in source control. + +When running a command that may prompt, do not pipe it through `tee`, `tail`, or similar output filters. Pipes can remove the interactive terminal that selection prompts require. Use the shell/session transcript or the CLI log file path printed by Aspire for diagnostics. + +`aspire publish` and `aspire deploy` are related but not a two-step apply pipeline: + +- `aspire publish` generates target artifacts for review or handoff and leaves unresolved values as target-specific placeholders where possible. +- `aspire deploy` resolves parameters and applies deployment steps directly from the AppHost model. +- Running `aspire deploy` later does not consume the directory produced by a previous `aspire publish`. + +Use `--environment ` when the user wants a staging/production context other than the default. Deployment state and cached values are scoped by AppHost and environment, so changing the environment changes which cached values are used. + +## Destroying deployments + +Use `aspire destroy` to run the AppHost deployment environment's destroy pipeline: + +```bash +aspire destroy --environment +``` + +Treat destroy as a destructive deployment operation: + +- Run it only when the user explicitly asks to tear down or clean up a deployment, or when a test workflow owns temporary infrastructure and teardown is part of that workflow. +- Use `aspire destroy --list-steps` when practical to preview the teardown pipeline before applying it. +- Confirm the AppHost, environment, target account/subscription/cluster, and resource group/namespace/stack context before applying. +- Use the same `--apphost ` and `--environment ` values used for deployment when discovery or environment scope could be ambiguous. +- Use `--yes` only for non-interactive teardown when destructive intent has already been approved, such as an explicit cleanup job. +- Do not describe destroy as a Helm, Kubernetes, Docker, Azure, or AWS command. It is an Aspire command that delegates to the selected deployment target's destroy step. +- Keep target-native delete commands as troubleshooting or manual-leftover cleanup, not the primary teardown path for resources managed by the Aspire deployment target. + +## Parameters and secrets + +Inventory parameter declarations. API casing differs by AppHost language: + +- direct parameters +- secret parameters +- config-backed parameters +- connection string parameters +- target-specific parameter APIs + +Report each parameter without revealing values: + +| Field | What to report | +|-------|----------------| +| Name | AppHost parameter name | +| Secret | whether it is marked secret | +| Source | environment variable, appsettings, user secrets, command line, prompt, CI secret | +| Consumer | project env var, connection string, Key Vault secret, Compose env, Helm Secret, Azure app setting | +| Status | configured, missing, generated, or unknown | + +Use these conventions: + +- AppHost parameter config key: `Parameters:name` +- Environment variable provider key: `Parameters__name` +- Parameter names with dashes use underscores in environment variables, for example `registry-endpoint` becomes `Parameters__registry_endpoint` +- Local/dev-only AppHost secret command: `aspire secret set "Parameters:name" ""` + +`aspire secret set` is a local/dev convenience today, not the deployment parameter path. For TypeScript AppHosts and non-interactive runs, set deploy-time values as environment variables on the `aspire deploy` process: + +```bash +Parameters__name="" \ +aspire deploy --apphost ./apphost.ts --environment Production --non-interactive +``` + +Never print secret values. If a command prints secrets, redact them in any summary. + +Deployment state can include resolved parameter values. Treat local deployment cache files and generated environment-specific artifacts as sensitive unless the target docs prove otherwise. + +For CI/CD and GitHub Actions, load `references/cicd.md` before adding workflow YAML or pipeline commands. + +## External endpoints and production topology + +Flag production-only endpoint behavior: + +- External HTTP endpoint configuration can change what becomes public in deployment. +- JavaScript app resources need an explicit production serving model. Load `references/javascript.md` when the AppHost contains JavaScript, Vite, Node, or Next.js resources. +- Azure Container Apps supports internal and external endpoints, with one main HTTP ingress group. +- Azure App Service supports a public website model with external HTTP/HTTPS endpoints only. +- Kubernetes exposes services through ClusterIP by default unless Ingress, Gateway API, or service type customization is configured. + +Report what will be public, internal, or not exposed based on the target docs and AppHost code. + +## Run vs publish/deploy branching + +Check for AppHost conditionals around execution context and environment: + +- run mode vs publish/deploy mode +- development, staging, production, or custom environment checks + +Explain when resources exist only locally or only in publish/deploy mode. If a resource is behind a run-mode-only branch, do not tell the user it will deploy. + +## Target-specific tools + +Use target-native tools only after Aspire has produced artifacts or when verifying the result: + +- Docker Compose: inspect generated Compose files or run Compose status commands against the generated project/files. +- Kubernetes: inspect Helm output and use kubectl/Helm against the target cluster. +- Azure: use Azure CLI for authentication and resource inspection, while keeping deployment through Aspire target environments. +- AWS: use AWS/CDK tooling required by the AWS Aspire integration. + +## Validation + +After deployment, verify with target-appropriate checks: + +- Docker Compose: `docker compose ps` against generated files or endpoint checks. +- Kubernetes: `kubectl get pods`, `kubectl get svc`, `helm status`, endpoint checks. +- Azure: CLI output, Azure CLI resource inspection, endpoint checks, and dashboard URL when available. + +Do not mark cloud deployment complete until provisioning, deployment, and at least one target-specific health or endpoint check succeeded. diff --git a/.agents/skills/aspire-init/SKILL.md b/.agents/skills/aspire-init/SKILL.md new file mode 100644 index 0000000..75a894b --- /dev/null +++ b/.agents/skills/aspire-init/SKILL.md @@ -0,0 +1,146 @@ +--- +name: aspire-init +description: >- + **WORKFLOW SKILL** - First-run flow for adding Aspire to a repo. Picks `aspire new` + (greenfield) or `aspire init` (existing repo), drops the AppHost skeleton, then hands + off to `aspireify` for resource wiring. + USE FOR: aspire init, aspire new, aspire-starter, aspire-ts-starter, aspire-py-starter, + add Aspire to existing repo, scaffold Aspire app, bootstrap Aspire, no AppHost detected, + install aspireify, generated .aspire/modules. + DO NOT USE FOR: AppHost wiring on an existing AppHost (use aspireify), start/stop/wait + (use aspire-orchestration), deploy/publish (use aspire-deployment), logs/traces (use + aspire-monitoring), repo that already has an AppHost. + INVOKES: aspire CLI (init, new, doctor), aspireify (handoff after skeleton drop). + FOR SINGLE OPERATIONS: Run `aspire init` or `aspire new TEMPLATE` directly. +license: MIT +metadata: + author: Microsoft + version: "0.0.1" +--- + +# Aspire Init + +> **First-run only.** This skill owns the skeleton drop and template choice for repositories +> that do not yet have an Aspire AppHost. Once the skeleton is in place, hand off to +> [`aspireify`](../aspireify/SKILL.md) for the actual resource wiring. + +## Prerequisites + +| Requirement | Install | +|-------------|---------| +| .NET 10.0 SDK | https://dotnet.microsoft.com/download | +| Aspire CLI (curl installer) | `curl -sSL https://aspire.dev/install.sh \| bash` | +| Aspire CLI (NativeAOT global tool) | `dotnet tool install -g Aspire.Cli` (.NET 10 required) | +| Diagnose missing prerequisites | `aspire doctor` | + +> Aspire ships the CLI as a NativeAOT .NET global tool — instant startup, no JIT warmup. +> The curl/PowerShell installer remains supported for environments without .NET 10. + +## Detection + +Activate **only** when adding Aspire to a workspace that does not yet have one. Confirm ALL +of the following before running `aspire init`: + +| Signal | How to Detect | Meaning | +|--------|---------------|---------| +| No C# AppHost | No `.csproj` containing `Aspire.AppHost.Sdk` | OK to init | +| No file-based AppHost | No `apphost.cs` with `#:sdk Aspire.AppHost.Sdk` | OK to init | +| No TypeScript AppHost | No `apphost.ts` in repo root | OK to init | +| No Aspire config | No `aspire.config.json` in repo root | OK to init | +| User intent | Explicit "add Aspire", "scaffold Aspire", "aspire init" | OK to init | + +If **any** AppHost signal is already present, **do not run `aspire init`**. Route to +[`aspireify`](../aspireify/SKILL.md) (re-wire) or +[`aspire-orchestration`](../aspire-orchestration/SKILL.md) (lifecycle). + +## Decision: `aspire new` vs `aspire init` + +| Situation | Command | Why | +|-----------|---------|-----| +| Empty directory or brand-new project | `aspire new