Skip to content

Feat/fight rabbit merge#82

Merged
KietPham-VN merged 19 commits into
mainfrom
feat/fight-rabbit-merge
May 12, 2026
Merged

Feat/fight rabbit merge#82
KietPham-VN merged 19 commits into
mainfrom
feat/fight-rabbit-merge

Conversation

@EricN2907

@EricN2907 EricN2907 commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features

    • Added mentor assignment and management endpoints
    • Added question answer validation and random question selection
    • Added user profile retrieval endpoint
    • Implemented background email notifications for mentor assignments
  • Refactor

    • Migrated course identifiers from integers to GUIDs
    • Replaced teacher role with mentor role throughout the system
    • Redesigned user data transfer objects and improved pagination performance
    • Simplified role system (Administrator/Mentor/Student)
  • Bug Fixes

    • Added defensive null-checks for judge results
    • Improved error handling in profile updates

EricN2907 and others added 18 commits May 5, 2026 21:54
- Updated service response handling in CompilerEndpoints, CourseEndpoints, EnrollmentEndpoints, ExamEndpoints, ExamParticipationEndpoints, ExamRoomEndpoints, HealthApi, LogBridgeEndpoints, MentorEndpoints, QuizletEndpoints, StudentSubmissionEndpoints, StudentTestCaseEndpoints, TeacherEndpoints, TestCaseEndpoints, and TestRoomEndpoints to enhance clarity and maintainability.
- Removed unnecessary using directives and organized namespaces across various files.
- Simplified response status code handling in EndpointExtensions and OpenApiConventionExtensions.
- Introduced global usings for common namespaces in Presentation, Domain, and Persistence layers to reduce boilerplate code.
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@EricN2907 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 36 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9aa48d29-80df-4dac-a845-e417e9966574

📥 Commits

Reviewing files that changed from the base of the PR and between ce66c22 and 173732c.

📒 Files selected for processing (2)
  • backend/Src/PIED_LMS.Contract/Services/Course/Command.cs
  • backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs
📝 Walkthrough

Walkthrough

This PR transitions the LMS from a teacher-centric to mentor-centric role model, modernizes course identifiers from int to Guid, introduces background email processing infrastructure, consolidates user response types into a unified UserDto, and adds question validation and randomized quiz features across the application, domain, contract, and presentation layers.

Changes

Role System and Entity Refactoring: Teacher → Mentor

Layer / File(s) Summary
Domain constants and entity definitions
backend/Src/PIED_LMS.Domain/Constants/RoleConstants.cs, backend/Src/PIED_LMS.Domain/Entities/Course.cs
RoleConstants removes Teacher and introduces Administrator constant; Course replaces Teachers navigation collection with Mentors.
Persistence configuration
backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs, backend/Src/PIED_LMS.Persistence/Services/CourseLockingService.cs
EF Core mapping updates the join table from course_teachers to course_mentors; ICourseLockingService parameter signature aligns with Guid course IDs.
Database seeding and infrastructure setup
backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs, backend/Src/PIED_LMS.Infrastructure/DependencyInjection.cs, backend/Src/PIED_LMS.Infrastructure/Email/SmtpEmailService.cs
DbInitializer removes teacher role seeding and updates mentor seed logic; email template text updated to refer to mentors instead of teachers.
Application command handlers
backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignTeachersHandler.cs (deleted)
New AssignMentorsHandler replaces deleted AssignTeachersHandler; same operational flow (course load, user validation, role verification, mentor list update, email enqueue) with mentor-specific role checks and messaging.
Application query handlers (Mentors)
backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/*, backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/* (deleted)
New mentors query handlers (GetMentorsHandler, GetMentorByIdHandler, GetAllMentorsSimpleHandler) replace deleted teacher handlers; retrieve/paginate/filter mentors using role-based lookups.
Query handlers (Course retrieval)
backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCoursesHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetStudentAvailableCoursesHandler.cs
Course query handlers update eager-loading from Teachers to Mentors; DTO mapping changed to project CourseMentorDto instead of CourseTeacherDto.
Contract DTOs and endpoints
backend/Src/PIED_LMS.Contract/Services/Course/Response.cs, backend/Src/PIED_LMS.Contract/Services/Mentor/Response.cs, backend/Src/PIED_LMS.Presentation/APIs/MentorEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/TeacherEndpoints.cs (deleted)
Course response DTOs introduce CourseMentorDto and remove CourseTeacherDto; new mentor contract DTOs (MentorDto, MentorSimpleDto) defined; new MentorEndpoints replaces deleted TeacherEndpoints with admin-scoped mentor management routes.
API authorization tightening
backend/Src/PIED_LMS.Presentation/APIs/ExamEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/ExamParticipationEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/TestCaseEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/TestRoomEndpoints.cs
Endpoint authorizations updated from hardcoded role strings to RoleConstants.Administrator/RoleConstants.Mentor; teacher role removed from all authorization policies.

Course Identifier Modernization: int → Guid

Layer / File(s) Summary
Domain entity updates
backend/Src/PIED_LMS.Domain/Entities/Course.cs, backend/Src/PIED_LMS.Domain/Entities/Enrollment.cs, backend/Src/PIED_LMS.Domain/Abstractions/ICourseLockingService.cs
Course.Id and Enrollment.CourseId changed from int to Guid; ICourseLockingService.GetCourseForUpdateAsync parameter updated to Guid.
Application settings and handlers
backend/Src/PIED_LMS.Application/Options/CourseManagementSettings.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Room/CreateRoomHandler.cs
GetCourseUrl method parameter changed to Guid; CreateCourseHandler return type and success path updated to return ServiceResponse<Guid> (previously int); test room handler aligns with mentor-based creation identifier.
Contract updates
backend/Src/PIED_LMS.Contract/Services/Course/Command.cs, backend/Src/PIED_LMS.Contract/Services/Course/Query.cs, backend/Src/PIED_LMS.Contract/Services/Course/Response.cs, backend/Src/PIED_LMS.Contract/Services/Enrollment/*
Command/query records (CreateCourseCommand, UpdateCourseCommand, DeleteCourseCommand, GetCourseByIdQuery, etc.) and response DTOs (CourseDto, PrerequisiteDto, StudentAvailableCourseDto, EnrollmentResponse) use Guid course identifiers.
Endpoint route definitions
backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/EnrollmentEndpoints.cs
Route constraints and handler signatures updated to use {id:guid} patterns; form-binding and command construction reflect Guid course IDs.

Background Email Processing Infrastructure

Layer / File(s) Summary
Email queue abstraction and implementation
backend/Src/PIED_LMS.Contract/Abstractions/BackgroundTasks/IBackgroundEmailQueue.cs, backend/Src/PIED_LMS.Infrastructure/BackgroundTasks/BackgroundEmailQueue.cs
New IBackgroundEmailQueue interface and EmailJob record define email job model with recipient/course metadata and retry configuration; BackgroundEmailQueue implements bounded channel-based async queue.
Email background service
backend/Src/PIED_LMS.Infrastructure/BackgroundTasks/EmailBackgroundService.cs
New BackgroundService continuously dequeues EmailJob items, retries up to MaxRetryAttempts with configurable delay, resolves IEmailService per scope, and processes course assignment emails; logs startup/shutdown and handles cancellation gracefully.
Dependency injection wiring
backend/Src/PIED_LMS.Infrastructure/DependencyInjection.cs
Registers IBackgroundEmailQueue singleton and EmailBackgroundService as hosted service.

User DTO Consolidation and Query Optimization

Layer / File(s) Summary
User response DTO standardization
backend/Src/PIED_LMS.Contract/Services/Identity/Response.cs, backend/Src/PIED_LMS.Contract/Services/Identity/Query.cs
Single UserDto record replaces scattered response types; query contracts (GetUserByIdQuery, GetAllUsersQuery, GetAllStudentsQuery) updated to return ServiceResponse<UserDto> or ServiceResponse<PaginatedResponse<UserDto>>.
Query handler refactoring (all users)
backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllUsersHandler.cs
Constructor adds IUnitOfWork; user retrieval orders by CreatedAt descending before pagination; role resolution moved from per-user N+1 queries to single batched lookup via IdentityUserRole<Guid> join with ApplicationRole.
Query handler refactoring (students)
backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllStudentsHandler.cs
Constructor adds RoleManager<ApplicationRole> and IUnitOfWork; student query built via role-id subquery with pagination in EF; roles batch-loaded once per page instead of per-student.
New "GetMe" query handler
backend/Src/PIED_LMS.Application/UserCases/Queries/GetMeHandler.cs
New handler retrieves current user by UserId, resolves roles, optionally fetches profile picture URL with fallback on error, and returns ServiceResponse<UserDto>.
Query handler updates (single user)
backend/Src/PIED_LMS.Application/UserCases/Queries/GetUserByIdHandler.cs
Return type changed to ServiceResponse<UserDto>; exception handling expanded with dedicated catch branches for UnauthorizedAccessException and IOException.
Endpoint wiring
backend/Src/PIED_LMS.Presentation/APIs/AuthenticationEndpoints.cs
New authorized GET routes (/me, /users/{id}, /users, /students) registered; all user query handlers now accept HttpContext and return via unified ToActionResult(context) mapping; student route authorization limited to Administrator/Mentor.

Question and Quiz Feature Additions

Layer / File(s) Summary
Question answer checking
backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionQuery.cs, backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionDto.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Question/CheckQuestionAnswerHandler.cs
New CheckQuestionAnswerCommand carries QuestionId and SelectedOptions; CheckAnswerResponse returns correctness, correct answers list, and explanation; handler loads question, evaluates set equality against correct answers, returns success/failure ServiceResponse.
Random question retrieval
backend/Src/PIED_LMS.Application/UserCases/Queries/Question/GetRandomQuestionHandler.cs
New query handler uses EF.Functions.Random() to fetch single random visible question from a published quizlet; returns RandomQuestionResponse with metadata and answer content list.
Quiz endpoint integration
backend/Src/PIED_LMS.Presentation/APIs/QuestionEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/QuizletEndpoints.cs
New /api/questions Carter module with GET /random and POST /check routes; QuizletEndpoints refactored to accept [AsParameters] CreateQuestionQuizRequest and use RoleConstants for authorization.
Question type resolution
backend/Src/PIED_LMS.Application/UserCases/Commands/Quiz/UpdateQuestionQuizHandler.cs
Question entity instantiation uses fully qualified Domain.Entities.Question type.

API Endpoint Refactoring and Result Mapping

Layer / File(s) Summary
Course endpoint modernization
backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs
Endpoints updated to use Guid route constraints; CreateCourse/UpdateCourse handlers accept single [AsParameters] request DTO instead of discrete form fields; new /mentors assignment endpoint with null/duplicate validation; authorization tightened to Administrator role for write operations and all GET operations now require authorization.
Enrollment endpoint consolidation
backend/Src/PIED_LMS.Presentation/APIs/EnrollmentEndpoints.cs
Course enrollment binds courseId as Guid; GetAvailableCourses/GetEnrollments handlers now accept [AsParameters] request DTOs (GetAvailableCoursesRequest, GetEnrollmentsRequest) instead of scattered [FromQuery] parameters.
Unified result mapping
backend/Src/PIED_LMS.Presentation/Extensions/EndpointExtensions.cs
ToActionResult<T> now returns NotFound when result.Success && result.Data == null for reference types; error detection broadened to include message text containing "not found" (case-insensitive).
Compilation handler safety
backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs
Null-check added for judgeResult after successful compiler judge; logs error and returns failure ServiceResponse instead of proceeding with null-forgiving access.
Profile update handler clarity
backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs
UpdateAsync result handling refactored to check !result.Succeeded first, log error details, and return failure; success return moved outside the condition block for clarity.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Admin
    participant Endpoint as Course Mentor<br/>Endpoint
    participant Handler as AssignMentors<br/>Handler
    participant Repo as IUnitOfWork<br/>Repository
    participant Queue as Email<br/>Queue
    participant Service as Email<br/>Background Service

    User->>Endpoint: POST /courses/{id}/mentors<br/>{mentors: [id1, id2]}
    Endpoint->>Endpoint: Validate non-null,<br/>no duplicates
    Endpoint->>Handler: AssignMentorsCommand
    Handler->>Repo: Load course by ID
    Handler->>Repo: Fetch mentor users by IDs
    Handler->>Handler: Validate all IDs exist
    Handler->>Handler: Validate all have Mentor role
    Handler->>Repo: Clear/repopulate course.Mentors
    Handler->>Repo: CommitAsync()
    Handler->>Queue: EnqueueEmailAsync (per mentor)
    Handler->>Endpoint: ServiceResponse<string> success
    Endpoint->>User: 200 OK
    
    Queue->>Service: Dequeue EmailJob
    Service->>Service: Retry loop (up to MaxRetries)
    Service->>Service: Create DI scope
    Service->>Service: Resolve IEmailService
    Service->>Service: SendCourseAssignmentAsync
    Service->>Queue: Success / Retry with delay
Loading
sequenceDiagram
    participant User as User
    participant Endpoint as Question<br/>Endpoint
    participant Handler as CheckAnswer<br/>Handler
    participant Repo as IUnitOfWork<br/>Repository
    participant Logic as Answer<br/>Evaluation

    User->>Endpoint: POST /api/questions/check<br/>{questionId, selectedOptions}
    Endpoint->>Handler: CheckQuestionAnswerCommand
    Handler->>Repo: Load question with answers
    Handler->>Handler: Return NOT_FOUND if missing
    Handler->>Logic: Compute set equality:<br/>selectedOptions vs correct answers
    Logic->>Logic: IsCorrect = exact match?
    Handler->>Handler: Build CheckAnswerResponse<br/>(isCorrect, correctAnswers, explanation)
    Handler->>Endpoint: ServiceResponse<CheckAnswerResponse>
    Endpoint->>User: 200 OK (with evaluation result)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PiedTeam/pied-lms#76: Modifies the same domain persistence and service signatures for courses and enrollments (Course.Id, Enrollment.CourseId, ICourseLockingService).
  • PiedTeam/pied-lms#81: Makes the same code-level change to CourseManagementSettings.GetCourseUrl and includes overlapping mentor/course identifier refactoring.
  • PiedTeam/pied-lms#17: Modifies the same application query handler files (GetAllUsersHandler, GetUserByIdHandler) and evolves the same application/contract layers and user response types.

Suggested reviewers

  • KietPham-VN

🎓 A Mentor's Ascendance 🎓
Teachers fade to memory, mentors rise with pride,
Int courses now in Guid, refactored far and wide.
Background queues hum emails soft and slow,
Questions checked with answers bright—the system's glow. ✨


Critical Issues & Observations

1. Incomplete Role Constant Replacement (SERIOUS)

The RoleConstants file removes Teacher and adds Administrator = "Admin", yet scattered references to role names remain inconsistent:

  • RoleConstants.Mentor is correctly used in most endpoints, but the hardcoded "Admin" string appears in several seeding/configuration paths (e.g., DbInitializer).
  • Violation: Role constant Administrator should be used uniformly instead of the literal "Admin" string in database seeding and role lookups.

2. Null Reference Risk in Email Enqueue (MEDIUM)

The AssignMentorsHandler enqueues email jobs only when mentor.Email is non-empty, but the EmailJob record does not validate non-null/non-empty email at the contract level:

if (!string.IsNullOrEmpty(mentor.Email))
    await backgroundEmailQueue.EnqueueEmailAsync(new EmailJob { Email = mentor.Email, ... });

The IBackgroundEmailQueue.EnqueueEmailAsync performs ArgumentNullException.ThrowIfNull(job) but does not validate job.Email. This could allow malformed jobs into the queue.

3. Duplicate Mentor Validation Logic (LOW)

The AssignMentorsCommand handler validates duplicate mentor IDs at the application layer, but also requires client-side validation in the endpoint. While defense-in-depth is appropriate, this creates two separate validation points that must be kept in sync. The endpoint returns 400 BadRequest for duplicates, but the handler may receive them from a direct CQRS dispatch and also validate—consider whether the handler should be the single source of truth.

4. Profile Picture Resolution Inconsistency (MEDIUM)

The GetMeHandler, GetUserByIdHandler, and GetAllUsersHandler all resolve profile picture URLs via IFileStorageService, but error handling differs:

  • GetMeHandler: Logs warning, falls back to null
  • GetUserByIdHandler: Dedicated try/catch with specific exception handling
  • GetAllUsersHandler: Silent fallback (no explicit error logging shown)

Recommendation: Standardize profile picture error handling across all user query handlers with consistent logging and fallback behavior.

5. Course Authorization Tightened Without Migration Path (HIGH)

The CourseEndpoints now require authorization for all GET operations (list, detail, curriculum, insight), changing from previously public or minimally-restricted endpoints. This is a breaking change for any unauthenticated or lower-role clients. Verify that this is intentional and that API documentation has been updated accordingly.

6. N+1 Query Optimization Incomplete (MEDIUM)

While GetAllUsersHandler and GetAllStudentsHandler now batch-load roles via a single join query, the GetMeHandler still calls userManager.GetRolesAsync(user) inside the handler—a single-user operation that performs an implicit database query. Consider whether to batch-load here as well for consistency, or document why this single-user path differs.

7. Database Seeding Role Assignment Logic (MEDIUM)

The DbInitializer refactored mentor role-assignment from the prior teacher setup, but the control flow for handling "already in role" vs. failure now uses conditional branching with else:

if (!await userManager.IsInRoleAsync(mentor, mentorRole.Name))
    await userManager.AddToRoleAsync(mentor, mentorRole.Name);
else
    _logger.LogInformation($"User {mentor.Id} is already a Mentor.");

This prevents exceptions from being re-thrown if role assignment fails for other reasons (e.g., database errors). Verify that all exceptions on role assignment should be caught and logged rather than propagated.

8. Question/Quiz Type Safety (LOW)

The UpdateQuestionQuizHandler now uses fully qualified Domain.Entities.Question type reference instead of the short form. While this fixes ambiguity, consider whether a using alias or consistent import pattern would improve readability across the codebase.


Recommendation: Address issues #1 (role constant uniformity), #2 (email validation), #5 (authorization breaking change), and #7 (exception handling on role assignment) before merging. Issues #3, #4, and #6 are architectural refinements suitable for follow-up PRs.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fight-rabbit-merge

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 21

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs (1)

100-108: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve mentor profile picture URLs before returning DTOs.

Line 107 passes t.ProfilePictureUrl through directly. This can leak storage keys/relative paths instead of consumable URLs and will produce inconsistent API output.

Suggested fix
-        var mentorDtos = course.Mentors.Select(t => new CourseMentorDto(
-            t.Id,
-            t.FirstName ?? string.Empty,
-            t.LastName ?? string.Empty,
-            t.Email ?? string.Empty,
-            t.Bio,
-            t.ProfilePictureUrl
-        )).ToList();
+        var mentorDtos = new List<CourseMentorDto>();
+        foreach (var t in course.Mentors)
+        {
+            var profilePictureUrl = string.IsNullOrWhiteSpace(t.ProfilePictureUrl)
+                ? null
+                : await fileStorageService.GetFileUrlAsync(t.ProfilePictureUrl);
+
+            mentorDtos.Add(new CourseMentorDto(
+                t.Id,
+                t.FirstName ?? string.Empty,
+                t.LastName ?? string.Empty,
+                t.Email ?? string.Empty,
+                t.Bio,
+                profilePictureUrl
+            ));
+        }

Also applies to: 120-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs`
around lines 100 - 108, The mentor mapping is returning raw storage keys via
t.ProfilePictureUrl; update the mapping in GetCourseByIdHandler (the
course.Mentors.Select(...) that constructs CourseMentorDto) to resolve each
mentor's ProfilePictureUrl into a public/consumable URL before passing it to
CourseMentorDto (e.g., call your existing storage/url helper or service method
such as ResolveUrl/GetPublicUrl on t.ProfilePictureUrl). Apply the same change
to the other mentor mapping site referenced (the second mapping around line 120)
so all CourseMentorDto instances receive resolved URLs rather than raw
keys/relative paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs`:
- Around line 73-93: Assigning mentors currently commits DB changes before
calling backgroundEmailQueue.EnqueueEmailAsync in AssignMentorsHandler, which
can leave mentors assigned without notifications if enqueueing fails; fix by
either (A) moving the enqueue calls into the same transaction scope as the DB
updates (invoke EnqueueEmailAsync for each mentor before SaveChangesAsync /
transaction.Commit so enqueueing occurs inside the transaction), or (B)
implement compensating tracking: create and persist a PendingNotification entity
when assigning mentors (e.g., AddPendingNotification for each mentor) before
commit, then after commit attempt EnqueueEmailAsync and on success remove the
corresponding PendingNotification (or keep a retry worker to process
PendingNotification records until successful); reference
backgroundEmailQueue.EnqueueEmailAsync, AssignMentorsHandler, and the DB
save/transaction methods to locate where to apply the change.
- Around line 116-120: The check for a missing Mentor role in
AssignMentorsHandler (mentorRole == null) currently logs and returns 'users',
which misclassifies a system configuration error as a validation failure; change
this to fail fast: when mentorRole is null, log the error with context (e.g.,
"Mentor role not found in the system") and then either throw an appropriate
exception (e.g., InvalidOperationException or a custom
SystemConfigurationException) from Handle, or return an error result/response
directly from Handle indicating system misconfiguration; update callers/tests to
expect this failure instead of treating it as a user validation issue.
- Around line 63-68: The Clear()+foreach pattern on course.Mentors causes many
change notifications; in AssignMentorsHandler replace that block with a single
assignment to the collection (e.g., course.Mentors = mentors.ToList() or
course.Mentors = new List<Mentor>(mentors)) so the navigation property is
swapped in one operation; ensure the target property type matches
(ICollection<T>/List<T>) and that your EF entity configuration supports direct
collection replacement before committing this change.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs`:
- Around line 49-58: The two identical catch blocks in CreateCourseHandler
(catching ArgumentException and InvalidOperationException) should be
consolidated into a single handler to avoid duplication; replace them with one
conditional catch that logs via logger.LogError(ex, "...") and returns the same
new ServiceResponse<Guid>(false, "Failed to upload thumbnail image. Please try
again.") — for example use a single catch (Exception ex) when (ex is
ArgumentException || ex is InvalidOperationException) { ... } so the identical
behavior around logging and returning is centralized.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs`:
- Around line 58-64: The handler currently logs result.Errors but returns only a
generic message; update UpdateProfileHandler to include the specific error
descriptions from result.Errors (the errors variable built from
result.Errors.Select(...)) in the ServiceResponse<string> returned on failure so
callers get actionable feedback (either by passing the errors collection into
ServiceResponse<string> if it has an errors/details field or by appending
string.Join(", ", errors) to the message); keep the existing
logger.LogWarning(user.Id, errors) behavior but ensure the returned
ServiceResponse contains the actual error descriptions instead of just "Failed
to update profile".

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Question/CheckQuestionAnswerHandler.cs`:
- Around line 15-17: The query in CheckQuestionAnswerHandler retrieves a
question by ID without enforcing visibility/publication, which can expose hidden
or unpublished content; update the
unitOfWork.Repository<Domain.Entities.Question>().FindAll(...) predicate to
include the same visibility/publication gates used by your random-question
retrieval (add the published/visible conditions—e.g., the same q.IsVisible &&
q.IsPublished or q.Visible && q.Published predicate used there) so the handler
only loads questions that are public and visible when evaluating answers for
request.QuestionId.
- Around line 30-32: The code in CheckQuestionAnswerHandler computes isCorrect
by dereferencing request.SelectedOptions without a null guard; add a null check
for request.SelectedOptions before the isCorrect calculation (in the same
handler/method that declares isCorrect) and handle the null case by returning a
controlled client error (e.g., BadRequest/validation result) or treating it as
an empty selection per business rules; ensure references to
request.SelectedOptions and the isCorrect calculation are updated so the
comparison only runs when SelectedOptions is non-null.

In `@backend/Src/PIED_LMS.Application/UserCases/Queries/GetMeHandler.cs`:
- Around line 19-20: The failure branch in GetMeHandler returns new
ServiceResponse<UserDto>(false, "User not found") but doesn't mark the response
as NotFound; update this branch to set the not-found indicator on the
ServiceResponse (e.g., use the constructor overload or property to set
NotFound/IsNotFound = true) so API translators can return HTTP 404; modify the
return in GetMeHandler accordingly using ServiceResponse<UserDto>'s not-found
parameter or property.

In
`@backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetMentorsHandler.cs`:
- Around line 25-30: The handler GetMentorsHandler currently treats a missing
mentor role (result of _roleManager.FindByNameAsync(RoleConstants.Mentor)) as a
successful empty result; change this to return a failure ServiceResponse
(success = false) with an appropriate error message and no page payload (or
null) so configuration errors are surfaced; update the block handling mentorRole
== null to construct and return ServiceResponse<PagedResult<MentorDto>> with
success false and a descriptive error, and consider logging the missing role
before returning.

In
`@backend/Src/PIED_LMS.Application/UserCases/Queries/Submission/GetSubmissionByIdQueryHandler.cs`:
- Around line 41-46: The handler currently only rejects Students/Mentors when
they don’t own the submission but implicitly allows all other roles; change
GetSubmissionByIdQueryHandler to implement deny-by-default by replacing the two
separate checks with an explicit allow-list: if roleClaim ==
RoleConstants.Student then require submission.StudentId == currentUserId, if
roleClaim == RoleConstants.Mentor then require submission.Exam.CreatedBy ==
currentUserId, otherwise deny; if none of the allow conditions match return a
ServiceResponse<SubmissionDetailResponse>(false, "Forbidden", ...) so any
missing or unexpected roleClaim cannot read submissions.

In `@backend/Src/PIED_LMS.Contract/Services/Enrollment/Command.cs`:
- Line 11: The parameter declaration uses a fully qualified type
Microsoft.AspNetCore.Http.IFormFile for PaymentProof even though
Microsoft.AspNetCore.Http is already imported; change the parameter type to the
unqualified IFormFile (i.e., update the PaymentProof parameter in the Command
class/constructor signature) to remove the redundant namespace qualification
while leaving the using directive intact.

In `@backend/Src/PIED_LMS.Contract/Services/Enrollment/Request.cs`:
- Around line 6-17: The pagination params accept invalid (zero/negative) values;
add validation attributes to enforce minimum bounds by decorating PageIndex and
PageSize in both GetAvailableCoursesRequest and GetEnrollmentsRequest with a
Range attribute (e.g., [Range(1, int.MaxValue)]) and import
System.ComponentModel.DataAnnotations so the contract rejects values below 1;
ensure you apply the attribute to both occurrences of PageIndex and PageSize in
the two record definitions.

In `@backend/Src/PIED_LMS.Infrastructure/BackgroundTasks/BackgroundEmailQueue.cs`:
- Around line 10-17: The constructor in BackgroundEmailQueue currently hardcodes
capacity=100; make the queue capacity configurable by reading
BackgroundTaskSettings.EmailQueueCapacity (or an injected options/settings
instance) and using that value as the BoundedChannelOptions capacity with a
sensible fallback (e.g., existing default) if the config is missing or invalid;
update the BackgroundEmailQueue constructor signature to accept the
settings/options or IConfiguration, validate/parse
BackgroundTaskSettings.EmailQueueCapacity, and use it when creating
Channel.CreateBounded<EmailJob>(options) so capacity can be tuned via
configuration rather than a hardcoded literal.

In
`@backend/Src/PIED_LMS.Infrastructure/BackgroundTasks/EmailBackgroundService.cs`:
- Around line 52-63: The code unconditionally sets emailSent = true after
calling SendCourseAssignmentAsync and ignores its Task<bool> result; change it
to capture the bool returned by SendCourseAssignmentAsync (assign to emailSent
like in EnrollStudentsHandler), and if the result is false throw an
InvalidOperationException to trigger retry logic; also remove the misleading
comment that rationalizes treating non-throwing as success. Reference:
SendCourseAssignmentAsync, emailSent, EmailBackgroundService and the job.*
parameters used in the call.

In `@backend/Src/PIED_LMS.Presentation/APIs/AuthenticationEndpoints.cs`:
- Line 76: The AuthorizeAttribute usage in the RequireAuthorization call uses
unnecessary string interpolation; replace
$"{RoleConstants.Administrator},{RoleConstants.Mentor}" with a plain string
concatenation or, better, a reusable constant for the combined roles (e.g.,
create a Roles.AdminAndMentor constant) and use new AuthorizeAttribute { Roles =
"<combined-roles>" } in the RequireAuthorization call to remove the
interpolation while keeping the same values.

In `@backend/Src/PIED_LMS.Presentation/APIs/EnrollmentEndpoints.cs`:
- Around line 79-83: Refactor the repeated inline pagination default logic into
a reusable extension by adding a static class (e.g., PaginationExtensions) with
methods GetPageIndexOrDefault(this int) and GetPageSizeOrDefault(this int) that
return pageIndex > 0 ? pageIndex : 1 and pageSize > 0 ? pageSize : 10
respectively, then update EnrollmentEndpoints where queries are created (e.g.,
the call constructing
PIED_LMS.Contract.Services.Course.GetStudentAvailableCoursesQuery and the other
occurrences around lines 93-96) to call
request.PageIndex.GetPageIndexOrDefault() and
request.PageSize.GetPageSizeOrDefault() instead of the duplicated ternary
expressions.

In `@backend/Src/PIED_LMS.Presentation/APIs/QuestionEndpoints.cs`:
- Around line 41-49: The CheckAnswer endpoint does not accept or forward a
CancellationToken, so add a CancellationToken parameter to the method signature
(e.g., CancellationToken cancellationToken), accept it from the framework, and
propagate it when sending the MediatR command and converting the result: pass
cancellationToken into sender.Send(...) for the CheckQuestionAnswerCommand and
into result.ToActionResult(context, cancellationToken) (or the equivalent
overload) so the request can be cancelled if the client disconnects.
- Around line 35-39: GetRandomQuestion currently doesn't accept or propagate a
CancellationToken, so update the GetRandomQuestion method signature to accept a
CancellationToken (e.g., CancellationToken cancellationToken) and pass that
token into the Send call (sender.Send(new GetRandomQuestionQuery(),
cancellationToken)); also propagate the token into the result conversion if
available (e.g., result.ToActionResult(context, cancellationToken)) so the
request can be cancelled when the client disconnects. Ensure you update any
callers or route bindings that invoke GetRandomQuestion to supply the
cancellation token from the HttpContext or framework binding.

In `@backend/Src/PIED_LMS.Presentation/APIs/StudentSubmissionEndpoints.cs`:
- Around line 30-34: The MapGet endpoint for "/submissions/{id:guid}" currently
calls RequireAuthorization() which allows any authenticated user; restrict it at
the API boundary by specifying the allowed roles on the endpoint (e.g., require
Student, Mentor and Admin roles) instead of the generic call — update the
authorization on the MapGet("/submissions/{id:guid}", GetSubmissionById) chain
(the RequireAuthorization call) to require explicit roles or a dedicated policy
(keeping the existing GetSubmissionById handler checks as defense-in-depth).

In `@backend/Src/PIED_LMS.Presentation/Extensions/EndpointExtensions.cs`:
- Around line 9-15: The current branch inside the success path checks
result.Success and then returns Results.NotFound when result.Data is null (and
typeof(T).IsClass), which incorrectly maps a successful operation to HTTP 404;
update the success handling in the method that inspects result.Success so that
when result.Success == true you always return Results.Ok(result) (remove the
Results.NotFound branch), and ensure callers/handlers set result.Success = false
and appropriate error info when data is truly missing so that NotFound (or other
error responses) are produced only from non-success results.
- Around line 19-21: The NotFound detection currently relies on message text
(result.Message.Contains("not found")), which is fragile; update the check in
the EndpointExtensions code to remove the Message-based check and rely on
result.IsNotFound and a canonical ErrorCode check instead (e.g., inspect
result.ErrorCode for a NOT_FOUND marker using a null-safe comparison on
result.ErrorCode, such as result.ErrorCode is not null &&
result.ErrorCode.Contains("NOT_FOUND", StringComparison.OrdinalIgnoreCase)), so
only IsNotFound or ErrorCode determine 404 handling and any Message inspection
is eliminated.

---

Outside diff comments:
In
`@backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs`:
- Around line 100-108: The mentor mapping is returning raw storage keys via
t.ProfilePictureUrl; update the mapping in GetCourseByIdHandler (the
course.Mentors.Select(...) that constructs CourseMentorDto) to resolve each
mentor's ProfilePictureUrl into a public/consumable URL before passing it to
CourseMentorDto (e.g., call your existing storage/url helper or service method
such as ResolveUrl/GetPublicUrl on t.ProfilePictureUrl). Apply the same change
to the other mentor mapping site referenced (the second mapping around line 120)
so all CourseMentorDto instances receive resolved URLs rather than raw
keys/relative paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a42ccd50-dbce-4063-a4c5-49a0f65062dd

📥 Commits

Reviewing files that changed from the base of the PR and between 099f5de and ce66c22.

⛔ Files ignored due to path filters (7)
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510085819_RenameTeacherToMentor.Designer.cs is excluded by !**/*.Designer.cs, !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510085819_RenameTeacherToMentor.cs is excluded by !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510091502_ChangeCourseFromIntegerToGuid.Designer.cs is excluded by !**/*.Designer.cs, !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510091502_ChangeCourseFromIntegerToGuid.cs is excluded by !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510102335_ChangeCourseIntegerToGUID.Designer.cs is excluded by !**/*.Designer.cs, !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510102335_ChangeCourseIntegerToGUID.cs is excluded by !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/PiedLmsDbContextModelSnapshot.cs is excluded by !**/Migrations/** and included by backend/Src/**
📒 Files selected for processing (69)
  • backend/Src/PIED_LMS.Application/Options/CourseManagementSettings.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignTeachersHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Question/CheckQuestionAnswerHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Quiz/UpdateQuestionQuizHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Room/CreateRoomHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCoursesHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetStudentAvailableCoursesHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllStudentsHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllUsersHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetMeHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetUserByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetAllMentorsSimpleHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetMentorByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetMentorsHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Question/GetRandomQuestionHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Submission/GetSubmissionByIdQueryHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetAllTeachersSimpleHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeacherByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeachersHandler.cs
  • backend/Src/PIED_LMS.Contract/Abstractions/BackgroundTasks/IBackgroundEmailQueue.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Command.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Request.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/Enrollment/Command.cs
  • backend/Src/PIED_LMS.Contract/Services/Enrollment/Request.cs
  • backend/Src/PIED_LMS.Contract/Services/Enrollment/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/ExamParticipation/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/ExamParticipation/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/Identity/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Identity/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/Identity/Validators/UpdateProfileValidator.cs
  • backend/Src/PIED_LMS.Contract/Services/Mentor/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Mentor/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionDto.cs
  • backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionQuery.cs
  • backend/Src/PIED_LMS.Contract/Services/QuestionQuiz/Request.cs
  • backend/Src/PIED_LMS.Contract/Services/Teacher/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Teacher/Response.cs
  • backend/Src/PIED_LMS.Domain/Abstractions/ICourseLockingService.cs
  • backend/Src/PIED_LMS.Domain/Constants/RoleConstants.cs
  • backend/Src/PIED_LMS.Domain/Entities/Course.cs
  • backend/Src/PIED_LMS.Domain/Entities/Enrollment.cs
  • backend/Src/PIED_LMS.Infrastructure/BackgroundTasks/BackgroundEmailQueue.cs
  • backend/Src/PIED_LMS.Infrastructure/BackgroundTasks/EmailBackgroundService.cs
  • backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs
  • backend/Src/PIED_LMS.Infrastructure/DependencyInjection.cs
  • backend/Src/PIED_LMS.Infrastructure/Email/SmtpEmailService.cs
  • backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs
  • backend/Src/PIED_LMS.Persistence/Services/CourseLockingService.cs
  • backend/Src/PIED_LMS.Presentation/APIs/AuthenticationEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/EnrollmentEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/ExamEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/ExamParticipationEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/MentorEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/QuestionEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/QuizletEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/StudentSubmissionEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/TeacherEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/TestCaseEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/TestRoomEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/Extensions/EndpointExtensions.cs
💤 Files with no reviewable changes (8)
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignTeachersHandler.cs
  • backend/Src/PIED_LMS.Contract/Services/Teacher/Response.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeachersHandler.cs
  • backend/Src/PIED_LMS.Contract/Services/Teacher/Query.cs
  • backend/Src/PIED_LMS.Domain/Constants/RoleConstants.cs
  • backend/Src/PIED_LMS.Presentation/APIs/TeacherEndpoints.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeacherByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetAllTeachersSimpleHandler.cs

Comment on lines +63 to +68
course.Mentors.Clear();

foreach (var mentor in mentors)
{
course.Mentors.Add(mentor);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Collection modification pattern is inefficient.

Using Clear() followed by individual Add() calls triggers multiple collection modification notifications in Entity Framework. Consider replacing the collection directly if the EF configuration supports it.

♻️ More efficient collection replacement
-            course.Mentors.Clear();
-
-            foreach (var mentor in mentors)
-            {
-                course.Mentors.Add(mentor);
-            }
+            course.Mentors.Clear();
+            foreach (var mentor in mentors)
+                course.Mentors.Add(mentor);

Or if your EF configuration allows collection replacement:

-            course.Mentors.Clear();
-            foreach (var mentor in mentors)
-            {
-                course.Mentors.Add(mentor);
-            }
+            course.Mentors = mentors;

Verify your entity configuration supports direct collection assignment before applying the second option.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
course.Mentors.Clear();
foreach (var mentor in mentors)
{
course.Mentors.Add(mentor);
}
course.Mentors = mentors;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs`
around lines 63 - 68, The Clear()+foreach pattern on course.Mentors causes many
change notifications; in AssignMentorsHandler replace that block with a single
assignment to the collection (e.g., course.Mentors = mentors.ToList() or
course.Mentors = new List<Mentor>(mentors)) so the navigation property is
swapped in one operation; ensure the target property type matches
(ICollection<T>/List<T>) and that your EF entity configuration supports direct
collection replacement before committing this change.

Comment on lines +73 to +93
foreach (var mentor in mentors)
{
if (string.IsNullOrWhiteSpace(mentor.Email))
{
logger.LogWarning(
"Skipping email notification for mentor {MentorId} - email is null or empty",
mentor.Id);
continue;
}

await backgroundEmailQueue.EnqueueEmailAsync(new EmailJob(
mentor.Email,
$"{mentor.FirstName} {mentor.LastName}",
course.Title,
course.StartDate,
course.EndDate,
_courseManagementSettings.GetCourseUrl(course.Id),
_courseManagementSettings.EmailRetryAttempts,
_courseManagementSettings.EmailRetryDelayMs
));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Email enqueueing occurs after database commit.

The email notifications are enqueued after the database transaction commits. If enqueueing fails (e.g., queue service unavailable), mentors will be assigned without receiving notifications. This creates a consistency gap between assignment and notification.

Consider either:

  1. Moving email enqueueing before the commit and wrapping in a transaction scope, or
  2. Implementing compensating logic to track un-notified assignments for retry
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs`
around lines 73 - 93, Assigning mentors currently commits DB changes before
calling backgroundEmailQueue.EnqueueEmailAsync in AssignMentorsHandler, which
can leave mentors assigned without notifications if enqueueing fails; fix by
either (A) moving the enqueue calls into the same transaction scope as the DB
updates (invoke EnqueueEmailAsync for each mentor before SaveChangesAsync /
transaction.Commit so enqueueing occurs inside the transaction), or (B)
implement compensating tracking: create and persist a PendingNotification entity
when assigning mentors (e.g., AddPendingNotification for each mentor) before
commit, then after commit attempt EnqueueEmailAsync and on success remove the
corresponding PendingNotification (or keep a retry worker to process
PendingNotification records until successful); reference
backgroundEmailQueue.EnqueueEmailAsync, AssignMentorsHandler, and the DB
save/transaction methods to locate where to apply the change.

Comment on lines +116 to +120
if (mentorRole == null)
{
logger.LogError("Mentor role not found in the system");
return users;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Handle missing Mentor role as a system error, not validation failure.

If the Mentor role doesn't exist in the system, returning all users as invalid will produce the misleading error "One or more users do not have the Mentor role." This is a system configuration error, not a validation failure. The operation should fail fast with an appropriate error message indicating system misconfiguration.

🐛 Fail fast on missing Mentor role
         if (mentorRole == null)
         {
             logger.LogError("Mentor role not found in the system");
-            return users;
+            throw new InvalidOperationException("Mentor role is not configured in the system. Please contact system administrator.");
         }

Alternatively, if throwing is not desired, return an error response directly from the Handle method after detecting this condition.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (mentorRole == null)
{
logger.LogError("Mentor role not found in the system");
return users;
}
if (mentorRole == null)
{
logger.LogError("Mentor role not found in the system");
throw new InvalidOperationException("Mentor role is not configured in the system. Please contact system administrator.");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs`
around lines 116 - 120, The check for a missing Mentor role in
AssignMentorsHandler (mentorRole == null) currently logs and returns 'users',
which misclassifies a system configuration error as a validation failure; change
this to fail fast: when mentorRole is null, log the error with context (e.g.,
"Mentor role not found in the system") and then either throw an appropriate
exception (e.g., InvalidOperationException or a custom
SystemConfigurationException) from Handle, or return an error result/response
directly from Handle indicating system misconfiguration; update callers/tests to
expect this failure instead of treating it as a user validation issue.

Comment on lines 49 to 58
catch (ArgumentException ex)
{
logger.LogError(ex, "Failed to upload thumbnail to S3 for course creation");
return new ServiceResponse<int>(false, "Failed to upload thumbnail image. Please try again.");
return new ServiceResponse<Guid>(false, "Failed to upload thumbnail image. Please try again.");
}
catch (InvalidOperationException ex)
{
logger.LogError(ex, "Failed to upload thumbnail to S3 for course creation");
return new ServiceResponse<int>(false, "Failed to upload thumbnail image. Please try again.");
return new ServiceResponse<Guid>(false, "Failed to upload thumbnail image. Please try again.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consolidate duplicate exception handling.

Both ArgumentException and InvalidOperationException catch blocks log and return identical error messages. This violates the DRY principle.

♻️ Consolidate exception handlers
-                catch (ArgumentException ex)
-                {
-                    logger.LogError(ex, "Failed to upload thumbnail to S3 for course creation");
-                    return new ServiceResponse<Guid>(false, "Failed to upload thumbnail image. Please try again.");
-                }
-                catch (InvalidOperationException ex)
-                {
-                    logger.LogError(ex, "Failed to upload thumbnail to S3 for course creation");
-                    return new ServiceResponse<Guid>(false, "Failed to upload thumbnail image. Please try again.");
-                }
+                catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
+                {
+                    logger.LogError(ex, "Failed to upload thumbnail to S3 for course creation");
+                    return new ServiceResponse<Guid>(false, "Failed to upload thumbnail image. Please try again.");
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs`
around lines 49 - 58, The two identical catch blocks in CreateCourseHandler
(catching ArgumentException and InvalidOperationException) should be
consolidated into a single handler to avoid duplication; replace them with one
conditional catch that logs via logger.LogError(ex, "...") and returns the same
new ServiceResponse<Guid>(false, "Failed to upload thumbnail image. Please try
again.") — for example use a single catch (Exception ex) when (ex is
ArgumentException || ex is InvalidOperationException) { ... } so the identical
behavior around logging and returning is centralized.

Comment on lines +58 to +64
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description).ToList();
logger.LogWarning("Failed to update profile for user {UserId}: {Errors}", user.Id,
string.Join(", ", errors));
return new ServiceResponse<string>(false, "Failed to update profile");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Error details are not surfaced to the user.

The error descriptions are collected on line 60 and logged on line 62, but the response on line 63 returns only a generic "Failed to update profile" message. Users cannot determine what went wrong with their profile update (e.g., validation failures, username conflicts, etc.).

Return the specific error messages to help users correct their input.

💡 Include error details in the response
             if (!result.Succeeded)
             {
                 var errors = result.Errors.Select(e => e.Description).ToList();
                 logger.LogWarning("Failed to update profile for user {UserId}: {Errors}", user.Id,
                     string.Join(", ", errors));
-                return new ServiceResponse<string>(false, "Failed to update profile");
+                return new ServiceResponse<string>(false, $"Failed to update profile: {string.Join("; ", errors)}");
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs`
around lines 58 - 64, The handler currently logs result.Errors but returns only
a generic message; update UpdateProfileHandler to include the specific error
descriptions from result.Errors (the errors variable built from
result.Errors.Select(...)) in the ServiceResponse<string> returned on failure so
callers get actionable feedback (either by passing the errors collection into
ServiceResponse<string> if it has an errors/details field or by appending
string.Join(", ", errors) to the message); keep the existing
logger.LogWarning(user.Id, errors) behavior but ensure the returned
ServiceResponse contains the actual error descriptions instead of just "Failed
to update profile".

Comment on lines +35 to +39
private static async Task<IResult> GetRandomQuestion(ISender sender, HttpContext context)
{
var result = await sender.Send(new GetRandomQuestionQuery());
return result.ToActionResult(context);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing CancellationToken propagation.

Lines 35-39 do not accept or propagate a CancellationToken. This prevents proper request cancellation—if the client disconnects, the query continues executing, wasting resources.

🛡️ Proposed fix to add cancellation support
-private static async Task<IResult> GetRandomQuestion(ISender sender, HttpContext context)
+private static async Task<IResult> GetRandomQuestion(
+    ISender sender, 
+    HttpContext context,
+    CancellationToken cancellationToken)
 {
-    var result = await sender.Send(new GetRandomQuestionQuery());
+    var result = await sender.Send(new GetRandomQuestionQuery(), cancellationToken);
     return result.ToActionResult(context);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/QuestionEndpoints.cs` around lines 35
- 39, GetRandomQuestion currently doesn't accept or propagate a
CancellationToken, so update the GetRandomQuestion method signature to accept a
CancellationToken (e.g., CancellationToken cancellationToken) and pass that
token into the Send call (sender.Send(new GetRandomQuestionQuery(),
cancellationToken)); also propagate the token into the result conversion if
available (e.g., result.ToActionResult(context, cancellationToken)) so the
request can be cancelled when the client disconnects. Ensure you update any
callers or route bindings that invoke GetRandomQuestion to supply the
cancellation token from the HttpContext or framework binding.

Comment on lines +41 to +49
private static async Task<IResult> CheckAnswer(
[FromBody] CheckAnswerRequest request,
ISender sender,
HttpContext context)
{
var command = new CheckQuestionAnswerCommand(request.QuestionId, request.SelectedOptions);
var result = await sender.Send(command);
return result.ToActionResult(context);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing CancellationToken propagation.

Lines 41-49 do not accept or propagate a CancellationToken. This prevents proper request cancellation, causing the command to execute even after the client disconnects.

🛡️ Proposed fix to add cancellation support
 private static async Task<IResult> CheckAnswer(
     [FromBody] CheckAnswerRequest request, 
     ISender sender, 
-    HttpContext context)
+    HttpContext context,
+    CancellationToken cancellationToken)
 {
     var command = new CheckQuestionAnswerCommand(request.QuestionId, request.SelectedOptions);
-    var result = await sender.Send(command);
+    var result = await sender.Send(command, cancellationToken);
     return result.ToActionResult(context);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static async Task<IResult> CheckAnswer(
[FromBody] CheckAnswerRequest request,
ISender sender,
HttpContext context)
{
var command = new CheckQuestionAnswerCommand(request.QuestionId, request.SelectedOptions);
var result = await sender.Send(command);
return result.ToActionResult(context);
}
private static async Task<IResult> CheckAnswer(
[FromBody] CheckAnswerRequest request,
ISender sender,
HttpContext context,
CancellationToken cancellationToken)
{
var command = new CheckQuestionAnswerCommand(request.QuestionId, request.SelectedOptions);
var result = await sender.Send(command, cancellationToken);
return result.ToActionResult(context);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/QuestionEndpoints.cs` around lines 41
- 49, The CheckAnswer endpoint does not accept or forward a CancellationToken,
so add a CancellationToken parameter to the method signature (e.g.,
CancellationToken cancellationToken), accept it from the framework, and
propagate it when sending the MediatR command and converting the result: pass
cancellationToken into sender.Send(...) for the CheckQuestionAnswerCommand and
into result.ToActionResult(context, cancellationToken) (or the equivalent
overload) so the request can be cancelled if the client disconnects.

Comment on lines +30 to +34
// Admins/Mentors could also use this if they have the ID
group.MapGet("/submissions/{id:guid}", GetSubmissionById)
.WithName("GetSubmissionById")
.RequireAuthorization() // general auth since both teacher and student can view
.WithServiceResponseOpenApi<
SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
.RequireAuthorization() // general auth since both mentor and student can view
.WithServiceResponseOpenApi<SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict endpoint roles at the API boundary.

Line 33 currently allows any authenticated role. That is an authorization scope regression for a sensitive resource-by-id endpoint. Enforce explicit roles here and keep handler checks as defense-in-depth.

Suggested fix
-        group.MapGet("/submissions/{id:guid}", GetSubmissionById)
-            .WithName("GetSubmissionById")
-            .RequireAuthorization() // general auth since both mentor and student can view
-            .WithServiceResponseOpenApi<SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
+        group.MapGet("/submissions/{id:guid}", GetSubmissionById)
+            .WithName("GetSubmissionById")
+            .RequireAuthorization(policy => policy.RequireRole("Student", "Mentor", "Administrator"))
+            .WithServiceResponseOpenApi<SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Admins/Mentors could also use this if they have the ID
group.MapGet("/submissions/{id:guid}", GetSubmissionById)
.WithName("GetSubmissionById")
.RequireAuthorization() // general auth since both teacher and student can view
.WithServiceResponseOpenApi<
SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
.RequireAuthorization() // general auth since both mentor and student can view
.WithServiceResponseOpenApi<SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
// Admins/Mentors could also use this if they have the ID
group.MapGet("/submissions/{id:guid}", GetSubmissionById)
.WithName("GetSubmissionById")
.RequireAuthorization(policy => policy.RequireRole("Student", "Mentor", "Administrator"))
.WithServiceResponseOpenApi<SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/StudentSubmissionEndpoints.cs` around
lines 30 - 34, The MapGet endpoint for "/submissions/{id:guid}" currently calls
RequireAuthorization() which allows any authenticated user; restrict it at the
API boundary by specifying the allowed roles on the endpoint (e.g., require
Student, Mentor and Admin roles) instead of the generic call — update the
authorization on the MapGet("/submissions/{id:guid}", GetSubmissionById) chain
(the RequireAuthorization call) to require explicit roles or a dedicated policy
(keeping the existing GetSubmissionById handler checks as defense-in-depth).

Comment on lines +9 to +15
if (result.Success)
{
if (result.Data is null && typeof(T).IsClass)
return Results.NotFound(result);

return Results.Ok(result);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Success response with null data should not return NotFound.

Lines 11-12 return NotFound when result.Success == true but result.Data is null. This creates a semantic mismatch: a successful operation should not yield an HTTP 404 status.

This logic masks handler implementation errors. Handlers should explicitly set Success = false with an appropriate error code when data is not found, rather than relying on this fallback.

Returning 404 for a "successful" response degrades error signaling and creates inconsistent API behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/Extensions/EndpointExtensions.cs` around
lines 9 - 15, The current branch inside the success path checks result.Success
and then returns Results.NotFound when result.Data is null (and
typeof(T).IsClass), which incorrectly maps a successful operation to HTTP 404;
update the success handling in the method that inspects result.Success so that
when result.Success == true you always return Results.Ok(result) (remove the
Results.NotFound branch), and ensure callers/handlers set result.Success = false
and appropriate error info when data is truly missing so that NotFound (or other
error responses) are produced only from non-success results.

Comment on lines +19 to +21
if (result.IsNotFound ||
(result.ErrorCode is not null && result.ErrorCode.Contains("NOT_FOUND")) ||
(result.Message is not null && result.Message.Contains("not found", StringComparison.OrdinalIgnoreCase)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Message-based NotFound detection is fragile.

Line 21 checks if result.Message contains "not found" to determine HTTP 404 status. This creates implicit coupling between message text and HTTP status codes, which is unreliable.

Message content can vary and may legitimately contain "not found" in contexts where 404 is inappropriate. Use ErrorCode as the authoritative source for error classification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/Extensions/EndpointExtensions.cs` around
lines 19 - 21, The NotFound detection currently relies on message text
(result.Message.Contains("not found")), which is fragile; update the check in
the EndpointExtensions code to remove the Message-based check and rely on
result.IsNotFound and a canonical ErrorCode check instead (e.g., inspect
result.ErrorCode for a NOT_FOUND marker using a null-safe comparison on
result.ErrorCode, such as result.ErrorCode is not null &&
result.ErrorCode.Contains("NOT_FOUND", StringComparison.OrdinalIgnoreCase)), so
only IsNotFound or ErrorCode determine 404 handling and any Message inspection
is eliminated.

Comment on lines +30 to +33
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error in Email Background Service loop.");
}
Comment on lines +72 to +88
catch (Exception ex)
{
if (attempt >= job.MaxRetryAttempts)
{
logger.LogError(ex,
"Failed to send course assignment email to {Email} after {Attempts} attempts",
job.Email, job.MaxRetryAttempts);
}
else
{
logger.LogWarning(ex,
"Failed to send course assignment email to {Email} on attempt {Attempt}. Retrying in {DelayMs}ms",
job.Email, attempt, job.RetryDelayMs);

await Task.Delay(job.RetryDelayMs, stoppingToken);
}
}
@KietPham-VN KietPham-VN merged commit 4b50132 into main May 12, 2026
7 checks passed
@KietPham-VN KietPham-VN deleted the feat/fight-rabbit-merge branch May 12, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants