Skip to content

feat(course): refactor curriculum and insight to use Clean Architectu…#87

Merged
KietPham-VN merged 5 commits into
mainfrom
feat/refactor-api-response-course
May 31, 2026
Merged

feat(course): refactor curriculum and insight to use Clean Architectu…#87
KietPham-VN merged 5 commits into
mainfrom
feat/refactor-api-response-course

Conversation

@EricN2907

@EricN2907 EricN2907 commented May 31, 2026

Copy link
Copy Markdown
Collaborator

…re and DDD

  • Introduce CurriculumSection Value Object with domain validations

  • Migrate Course Curriculum from string to List

  • Use EF Core HasConversion for Curriculum persistence

  • Update Command/Query Handlers and DTOs to use strongly typed collections

  • Remove deprecated CourseContentHelper

Summary by CodeRabbit

  • New Features

    • Course curriculum is now structured into sections with title, summary, and content for improved organization.
  • Bug Fixes

    • Enhanced validation for course curriculum and insight during creation and updates.
  • Refactor

    • Improved course content handling logic for better data integrity and maintainability.

…re and DDD

- Introduce CurriculumSection Value Object with domain validations

- Migrate Course Curriculum from string to List<CurriculumSection>

- Use EF Core HasConversion for Curriculum persistence

- Update Command/Query Handlers and DTOs to use strongly typed collections

- Remove deprecated CourseContentHelper
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@EricN2907, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 34 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b74c3359-e056-4836-ad82-79b08f746620

📥 Commits

Reviewing files that changed from the base of the PR and between 5c13f83 and 2026c06.

📒 Files selected for processing (7)
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/UpdateCourseHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseCurriculumHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCoursesHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetUserByIdHandler.cs
  • backend/Src/PIED_LMS.Domain/Entities/CurriculumSection.cs
📝 Walkthrough

Walkthrough

This PR refactors course curriculum and insight management from a utility-based JSON serialization pattern to a domain-driven approach. CourseContentHelper is removed entirely; curriculum is now a strongly-typed CurriculumSection collection on the Course entity, with validation performed in constructors and mutator methods. Contracts, handlers, and EF Core persistence are updated to operate on domain objects directly.

Changes

Course Curriculum Domain Refactoring

Layer / File(s) Summary
Domain model: CurriculumSection entity and Course mutations
backend/Src/PIED_LMS.Domain/Entities/CurriculumSection.cs, backend/Src/PIED_LMS.Domain/Entities/Course.cs
New CurriculumSection domain entity with constructor validation for title, summary, and content items. Course.Curriculum type changes from string? to List<CurriculumSection> with private setter. Course.Insight privatized. New SetCurriculum and SetInsight mutator methods with validation logic and ArgumentException on invalid input.
Request and Command contract updates
backend/Src/PIED_LMS.Contract/Services/Course/Command.cs, backend/Src/PIED_LMS.Contract/Services/Course/Request.cs
CreateCourseCommand and UpdateCourseCommand change Curriculum from string? to List<CurriculumSectionDto>?; Insight becomes non-nullable string. Corresponding request types update form-binding from string? to List<CurriculumSectionDto>?.
Command handlers: Create and Update with setter calls
backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Course/UpdateCourseHandler.cs
Removes CourseContentHelper-based validation; calls course.SetCurriculum(...) and course.SetInsight(...) after entity construction. Catches ArgumentException to return failed responses. UpdateCourseHandler repositions UpdatedAt timestamp and adjusts change-tracking logging.
Query handlers: Direct curriculum projection
backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseCurriculumHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCoursesHandler.cs
Removes CourseContentHelper.DeserializeCurriculum calls and projects course.Curriculum directly into CurriculumSectionDto using LINQ with null-safe fallbacks to empty lists. Removes obsolete using PIED_LMS.Application.UserCases; directives.
EF Core persistence: JSON conversion for Curriculum
backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs
Adds System.Text.Json import and configures Course.Curriculum to persist via JSON conversion with case-insensitive deserialization. Null/whitespace fallback to empty List<CurriculumSection>. Marks Insight as not required.

Possibly Related PRs

  • PiedTeam/pied-lms#85: Modifies the same course curriculum/insight pipeline in handlers and DTO mapping, though uses legacy CourseContentHelper validation while this PR removes it entirely.
  • PiedTeam/pied-lms#75: Course handler and domain model implementation that this refactor depends upon and restructures.
  • PiedTeam/pied-lms#77: Adds GetCourseCurriculumHandler and DTO serialization that this PR refactors to use direct projection instead of helper-based deserialization.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


A curriculum born anew in types so clear,
No JSON strings to parse and fear,
Domain entities stand strong and tall,
Validation close, no helpers at all. 🎓
The course refactors, structured and bright! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is truncated and incomplete. It reads 'feat(course): refactor curriculum and insight to use Clean Architectu…' but fails to convey the actual main objective, which is refactoring to use Clean Architecture and DDD principles. Provide the complete, untruncated title. The intended message appears to be 'feat(course): refactor curriculum and insight to use Clean Architecture and DDD' but the ellipsis obscures the actual scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/refactor-api-response-course

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 5

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.Domain/Entities/Course.cs (1)

24-40: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Stop exposing the aggregate’s curriculum as a mutable List<>.

Lines 24-40 still allow callers to mutate Course.Curriculum directly or mutate the same list instance after SetCurriculum returns, which bypasses the new domain validation entirely. Store a copy and expose a read-only collection/backing field instead.

🤖 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.Domain/Entities/Course.cs` around lines 24 - 40,
Course.Curriculum is exposed as a mutable List allowing external mutation and
bypassing domain validation; change it to expose a read-only collection
(IReadOnlyList<CurriculumSection> or ReadOnlyCollection<CurriculumSection>)
backed by a private List<CurriculumSection> field (e.g., _curriculum), and
update SetCurriculum to assign a defensive copy (new
List<CurriculumSection>(curriculum ?? Enumerable.Empty<CurriculumSection>())) to
that private field; have the public Curriculum getter return the read-only view
(e.g., _curriculum.AsReadOnly() or the IReadOnlyList) so callers cannot mutate
the aggregate or the original list after SetCurriculum returns.
🤖 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/CreateCourseHandler.cs`:
- Around line 83-97: CreateCourseHandler currently calls SaveFileAsync before
validating CurriculumSection and Insight, so if course.SetCurriculum or
course.SetInsight throws (ArgumentException) an uploaded thumbnail is orphaned;
move the validation logic so you construct/validate the domainCurriculum (using
Domain.Entities.CurriculumSection) and validate request.Insight (or call
SetCurriculum/SetInsight on a temp Course object) before invoking SaveFileAsync,
or, if you prefer to keep upload first, catch exceptions after
SetCurriculum/SetInsight and delete the uploaded file by calling the same
storage delete routine used elsewhere; update the try/catch around SetCurriculum
and SetInsight to ensure uploaded file is removed on failure and retain the
logger.LogWarning/ServiceResponse behavior.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/UpdateCourseHandler.cs`:
- Around line 131-137: The handler currently calls course.SetCurriculum and
course.SetInsight after performing thumbnail side effects, which can delete the
old thumbnail and upload a new one before validation fails; move all domain
validation (invoke SetCurriculum, SetInsight and any other domain setters that
can throw, e.g., Validate... on the Course aggregate) to run before any
thumbnail Delete/Upload operations so that ArgumentException is raised prior to
making storage changes, and only if validation succeeds proceed to call the
thumbnail methods (e.g., DeleteThumbnail/UploadThumbnail) and then
persist/commit the changes.
- Around line 121-130: The handler currently treats a null
UpdateCourseCommand.Curriculum as an instruction to clear the course curriculum;
change UpdateCourseHandler so that when request.Curriculum is null it does
nothing (preserve existing curriculum), and only call course.SetCurriculum(...)
when request.Curriculum is non-null — mapping items to
Domain.Entities.CurriculumSection for a non-empty list, and explicitly clearing
(e.g. course.SetCurriculum(null) or an agreed “clear” value) only when
request.Curriculum is an empty list; update the branch around request.Curriculum
and calls to course.SetCurriculum accordingly.

In `@backend/Src/PIED_LMS.Domain/Entities/CurriculumSection.cs`:
- Around line 7-25: The Content property currently exposes a mutable
List<string> and the constructor stores the caller's list reference directly;
change CurriculumSection.Content to expose an immutable/read-only type (e.g.,
IReadOnlyList<string> or ReadOnlyCollection<string>) and in the
CurriculumSection(string title, string summary, List<string> content)
constructor make a defensive copy of the incoming list (new
List<string>(content)) and assign a read-only wrapper (e.g., AsReadOnly or cast
to IReadOnlyList) so consumers cannot mutate the internal collection after
construction while preserving EF compatibility via the private parameterless
constructor.

In `@backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs`:
- Line 1: Course.Curriculum is mapped via JSON conversion but lacks a
ValueComparer, so EF Core will track snapshots by reference and miss in-place
edits; in CourseConfiguration add a ValueComparer for the converted type (e.g.,
ValueComparer<List<CurriculumSection>>) and attach it to the property metadata
(via .Metadata.SetValueComparer(...) or the fluent API). Implement the comparer
to compare value equality (either by comparing serialized JSON strings or deep
sequence equality on CurriculumSection items) and to produce snapshots by
cloning (e.g., via JsonSerializer serialize/deserialize) so mutations of
CurriculumSection.Content are detected; update the CourseConfiguration mapping
for the Curriculum property to use this comparer alongside the existing
HasConversion/JsonSerializer usage.

---

Outside diff comments:
In `@backend/Src/PIED_LMS.Domain/Entities/Course.cs`:
- Around line 24-40: Course.Curriculum is exposed as a mutable List allowing
external mutation and bypassing domain validation; change it to expose a
read-only collection (IReadOnlyList<CurriculumSection> or
ReadOnlyCollection<CurriculumSection>) backed by a private
List<CurriculumSection> field (e.g., _curriculum), and update SetCurriculum to
assign a defensive copy (new List<CurriculumSection>(curriculum ??
Enumerable.Empty<CurriculumSection>())) to that private field; have the public
Curriculum getter return the read-only view (e.g., _curriculum.AsReadOnly() or
the IReadOnlyList) so callers cannot mutate the aggregate or the original list
after SetCurriculum returns.
🪄 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: 792bdc10-0b6c-4eed-8c9f-505fe0255a2d

📥 Commits

Reviewing files that changed from the base of the PR and between 7f2577f and 5c13f83.

📒 Files selected for processing (11)
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/UpdateCourseHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/CourseContentHelper.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseCurriculumHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCoursesHandler.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Command.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Request.cs
  • backend/Src/PIED_LMS.Domain/Entities/Course.cs
  • backend/Src/PIED_LMS.Domain/Entities/CurriculumSection.cs
  • backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs
💤 Files with no reviewable changes (1)
  • backend/Src/PIED_LMS.Application/UserCases/CourseContentHelper.cs

Comment thread backend/Src/PIED_LMS.Application/UserCases/Commands/Course/UpdateCourseHandler.cs Outdated
Comment thread backend/Src/PIED_LMS.Application/UserCases/Commands/Course/UpdateCourseHandler.cs Outdated
Comment thread backend/Src/PIED_LMS.Domain/Entities/CurriculumSection.cs Outdated
@@ -1,3 +1,4 @@
using System.Text.Json;

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'Property\(c => c\.Curriculum\)|HasConversion|SetValueComparer|ValueComparer<|public List<CurriculumSection> Curriculum|List<string> Content' backend/Src

Repository: PiedTeam/pied-lms

Length of output: 6280


Add a ValueComparer for the JSON-converted Course.Curriculum collection (missing today).

CourseConfiguration maps Course.Curriculum as a mutable List<CurriculumSection> via HasConversion(...JsonSerializer...), but there is no ValueComparer/.Metadata.SetValueComparer(...) configured (no such configuration exists in backend/Src). EF Core will then track snapshots by reference, so in-place edits can be missed—including edits to CurriculumSection.Content, since that list is serialized inside the Course.Curriculum JSON payload. (https://learn.microsoft.com/en-us/ef/core/modeling/value-comparers)

Proposed fix
+using Microsoft.EntityFrameworkCore.ChangeTracking;
 using System.Text.Json;
 using PIED_LMS.Domain.Entities;
@@
-        var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+        var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+        var curriculumComparer = new ValueComparer<List<CurriculumSection>>(
+            (left, right) =>
+                JsonSerializer.Serialize(left ?? new List<CurriculumSection>(), jsonOptions) ==
+                JsonSerializer.Serialize(right ?? new List<CurriculumSection>(), jsonOptions),
+            value =>
+                JsonSerializer.Serialize(value ?? new List<CurriculumSection>(), jsonOptions).GetHashCode(),
+            value =>
+                JsonSerializer.Deserialize<List<CurriculumSection>>(
+                    JsonSerializer.Serialize(value ?? new List<CurriculumSection>(), jsonOptions),
+                    jsonOptions) ?? new List<CurriculumSection>());
+
         builder.Property(c => c.Curriculum)
             .HasConversion(
                 v => JsonSerializer.Serialize(v, jsonOptions),
                 v => string.IsNullOrWhiteSpace(v) 
                     ? new List<CurriculumSection>() 
                     : JsonSerializer.Deserialize<List<CurriculumSection>>(v, jsonOptions) ?? new List<CurriculumSection>()
-            );
+            )
+            .Metadata.SetValueComparer(curriculumComparer);
🤖 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.Persistence/Configurations/CourseConfiguration.cs` at
line 1, Course.Curriculum is mapped via JSON conversion but lacks a
ValueComparer, so EF Core will track snapshots by reference and miss in-place
edits; in CourseConfiguration add a ValueComparer for the converted type (e.g.,
ValueComparer<List<CurriculumSection>>) and attach it to the property metadata
(via .Metadata.SetValueComparer(...) or the fluent API). Implement the comparer
to compare value equality (either by comparing serialized JSON strings or deep
sequence equality on CurriculumSection items) and to produce snapshots by
cloning (e.g., via JsonSerializer serialize/deserialize) so mutations of
CurriculumSection.Content are detected; update the CourseConfiguration mapping
for the Curriculum property to use this comparer alongside the existing
HasConversion/JsonSerializer usage.

Comment on lines +104 to +107
catch (Exception deleteEx)
{
logger.LogError(deleteEx, "Failed to delete orphaned thumbnail {ThumbnailPath}", thumbnailPath);
}
@KietPham-VN KietPham-VN merged commit 74fecc6 into main May 31, 2026
7 checks passed
@KietPham-VN KietPham-VN deleted the feat/refactor-api-response-course branch May 31, 2026 11:36
This was referenced Jun 2, 2026
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