Implemented Short Course APIs#7
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughIntroduces a complete application service layer for short course management, consisting of a data transfer object (DTO) for ShortCourse with AutoMapper mapping, a service interface defining CRUD and domain-specific operations, and a concrete implementation handling validation, persistence, and error management. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
mblebelo
left a comment
There was a problem hiding this comment.
Code RabbitAI: All checks passed and conversations resolved ✅
mblebelo
left a comment
There was a problem hiding this comment.
Code RabbitAI: All checks passed and conversations resolved ✅
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (6)
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/Dto/ShortCourseDto.cs (1)
26-26: Remove commented code or convert to a TODO.If the
Applicationsproperty is planned for future implementation, convert the comment to a proper TODO. Otherwise, remove it to keep the codebase clean.aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/ShortCourseAppService.cs (5)
56-81: Consider eager loading for EnrolledStudents to prevent N+1 queries.If the
EnrolledStudentscollection should be included in the result, add.Include()to avoid N+1 query problems when AutoMapper accesses the navigation property.Apply this diff if EnrolledStudents should be loaded:
+using Microsoft.EntityFrameworkCore; + // ... in GetAllAsync method: - var query = Repository.GetAll(); + var query = Repository.GetAll() + .Include(sc => sc.EnrolledStudents);Alternatively, if EnrolledStudents should not be loaded here, ensure the AutoMapper configuration ignores this property or the DTO property is nullable/initialized.
95-103: Redundant null check after repository GetAsync.The ABP
Repository.GetAsync()method throws an exception if the entity is not found, so the null check on lines 97-103 is unreachable. You can safely remove this check or useFirstOrDefaultAsync()if you need to handle the null case explicitly.Apply this diff to remove the redundant check:
var shortCourse = await _shortCourseRepository.GetAsync(input.Id); - if (shortCourse == null) - { - throw new UserFriendlyException( - $"Short course with ID {input.Id} not found.", - Abp.Logging.LogSeverity.Warn - ); - } - return ObjectMapper.Map<ShortCourseDto>(shortCourse);
174-182: Redundant null check after repository GetAsync.Similar to the
GetAsyncmethod, the null check here is redundant sinceRepository.GetAsync()throws if the entity doesn't exist.Apply this diff:
var shortCourse = await _shortCourseRepository.GetAsync(input.Id); - if (shortCourse == null) - { - throw new UserFriendlyException( - $"Short course with ID {input.Id} not found.", - Abp.Logging.LogSeverity.Warn - ); - } - await _shortCourseRepository.DeleteAsync(input.Id);
212-216: Consider using SingleOrDefaultAsync if Code is unique.If the
Codeproperty is unique (enforced by database constraint or domain logic), useSingleOrDefaultAsync()instead ofFirstOrDefaultAsync()to catch data integrity issues where multiple courses share the same code.Apply this diff:
- var shortCourse = await AsyncQueryableExecuter.FirstOrDefaultAsync( + var shortCourse = await AsyncQueryableExecuter.SingleOrDefaultAsync( _shortCourseRepository .GetAll() .Where(sc => sc.Code == code) );
365-386: Consider adding pagination to GetOpenCoursesAsync.The method returns all active courses without pagination, which could cause performance issues if the number of active courses grows large. Consider accepting a paging parameter or adding a reasonable limit.
Apply this diff to add pagination:
- public async Task<List<ShortCourseDto>> GetOpenCoursesAsync() + public async Task<PagedResultDto<ShortCourseDto>> GetOpenCoursesAsync(PagedAndSortedResultRequestDto input) { try { + var query = _shortCourseRepository + .GetAll() + .Where(sc => sc.IsActive); + + var totalCount = await AsyncQueryableExecuter.CountAsync(query); + var shortCourses = await AsyncQueryableExecuter.ToListAsync( - _shortCourseRepository - .GetAll() - .Where(sc => sc.IsActive) + query .OrderBy(sc => sc.StartDate) + .Skip(input.SkipCount) + .Take(input.MaxResultCount) ); - return ObjectMapper.Map<List<ShortCourseDto>>(shortCourses); + var result = ObjectMapper.Map<List<ShortCourseDto>>(shortCourses); + return new PagedResultDto<ShortCourseDto>(totalCount, result); }Note: This would require updating the interface signature as well.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/Dto/ShortCourseDto.cs(1 hunks)aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/IShortCourseAppService.cs(1 hunks)aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/ShortCourseAppService.cs(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: blebelo
Repo: blebelo/moipone PR: 6
File: aspnet-core/src/Moipone.PublicSite.Application/Students/Dto/StudentDto.cs:13-26
Timestamp: 2025-12-17T20:15:26.321Z
Learning: In the Moipone.PublicSite project, the [Required] attributes on domain entities like Student are used for database schema generation and object creation validation, not for DTO-level validation. This allows DTOs to remain flexible with nullable properties for API operations while the domain layer enforces data integrity.
📚 Learning: 2025-12-17T20:11:46.178Z
Learnt from: blebelo
Repo: blebelo/moipone PR: 6
File: aspnet-core/src/Moipone.PublicSite.Application/Students/Dto/StudentDto.cs:13-26
Timestamp: 2025-12-17T20:11:46.178Z
Learning: In the Moipone.PublicSite project, StudentDto uses nullable properties for all fields to support partial updates and single field updates in the update operations, even though the underlying Student domain entity has [Required] attributes on many fields.
Applied to files:
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/Dto/ShortCourseDto.cs
📚 Learning: 2025-12-17T20:15:26.321Z
Learnt from: blebelo
Repo: blebelo/moipone PR: 6
File: aspnet-core/src/Moipone.PublicSite.Application/Students/Dto/StudentDto.cs:13-26
Timestamp: 2025-12-17T20:15:26.321Z
Learning: In ASP.NET Core projects with a layered architecture, avoid using [Required] on DTOs for API contracts; reserve [Required] for domain entities that participate in persistence. DTOs can have nullable properties to support optional fields in API operations, while the domain layer enforces data integrity. Ensure the domain models (e.g., Student) have appropriate [Required] attributes, and mapping from DTOs to domain should validate required fields before domain creation.
Applied to files:
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/Dto/ShortCourseDto.cs
🪛 GitHub Check: Build Stage
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/Dto/ShortCourseDto.cs
[warning] 18-18:
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[warning] 17-17:
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[warning] 15-15:
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[warning] 14-14:
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Test Stage
- GitHub Check: auto-pr
🔇 Additional comments (4)
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/IShortCourseAppService.cs (1)
9-16: LGTM! Clean interface definition.The interface properly extends
IAsyncCrudAppServiceand adds domain-specific operations. Method signatures are clear and consistent.aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/ShortCourseAppService.cs (3)
20-24: LGTM! Standard constructor pattern.The constructor follows ABP conventions by accepting the repository and passing it to the base class.
26-54: Add validation attributes to ShortCourseDto to match domain entity constraints.The
ShortCoursedomain entity already enforces validation via [Required], [StringLength], [Range], and [RegularExpression] attributes. However,ShortCourseDtolacks these validation attributes—all properties are nullable with no constraints. ABP will not validate the DTO automatically without these attributes, allowing invalid data to be mapped and persisted. Add matching validation attributes to the DTO properties to ensure validation occurs at the service layer before mapping to the domain entity.⛔ Skipped due to learnings
Learnt from: blebelo Repo: blebelo/moipone PR: 6 File: aspnet-core/src/Moipone.PublicSite.Application/Students/Dto/StudentDto.cs:13-26 Timestamp: 2025-12-17T20:15:26.321Z Learning: In the Moipone.PublicSite project, the [Required] attributes on domain entities like Student are used for database schema generation and object creation validation, not for DTO-level validation. This allows DTOs to remain flexible with nullable properties for API operations while the domain layer enforces data integrity.Learnt from: blebelo Repo: blebelo/moipone PR: 6 File: aspnet-core/src/Moipone.PublicSite.Application/Students/Dto/StudentDto.cs:13-26 Timestamp: 2025-12-17T20:11:46.178Z Learning: In the Moipone.PublicSite project, StudentDto uses nullable properties for all fields to support partial updates and single field updates in the update operations, even though the underlying Student domain entity has [Required] attributes on many fields.
141-144: Partial update concern: Nullable DTO properties may unintentionally set fields to null.
ObjectMapper.Map(input, shortCourse)will map all properties from the DTO, including null values. SinceShortCourseDtohas nullable properties, any field not provided in the input will be mapped as null, overwriting existing entity values. This pattern exists across StudentAppService and AddressAppService with the same issue. Configure AutoMapper to skip null values using.ForAllMembers(opt => opt.Condition((src, dest, propValue, context) => propValue != null))or handle partial updates explicitly in the Update method.⛔ Skipped due to learnings
Learnt from: blebelo Repo: blebelo/moipone PR: 6 File: aspnet-core/src/Moipone.PublicSite.Application/Students/Dto/StudentDto.cs:13-26 Timestamp: 2025-12-17T20:11:46.178Z Learning: In the Moipone.PublicSite project, StudentDto uses nullable properties for all fields to support partial updates and single field updates in the update operations, even though the underlying Student domain entity has [Required] attributes on many fields.Learnt from: blebelo Repo: blebelo/moipone PR: 6 File: aspnet-core/src/Moipone.PublicSite.Application/Students/Dto/StudentDto.cs:13-26 Timestamp: 2025-12-17T20:15:26.321Z Learning: In the Moipone.PublicSite project, the [Required] attributes on domain entities like Student are used for database schema generation and object creation validation, not for DTO-level validation. This allows DTOs to remain flexible with nullable properties for API operations while the domain layer enforces data integrity.
| public string? Title { get; set; } | ||
| public string? Description { get; set; } | ||
| public int? Capacity { get; set; } | ||
| public string? Requirements { get; set; } | ||
| public string? Code { get; set; } |
There was a problem hiding this comment.
Enable nullable reference context to resolve compiler warnings.
The static analysis correctly flags that nullable annotations require a #nullable context. Add #nullable enable at the top of the file (after usings) to properly enable nullable reference types.
Apply this diff:
using System.Collections.Generic;
namespace Moipone.PublicSite.ShortCourses.Dto
{
+#nullable enable
[AutoMap(typeof(ShortCourse))]
public class ShortCourseDto : EntityDto<Guid>Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 GitHub Check: Build Stage
[warning] 18-18:
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[warning] 17-17:
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[warning] 15-15:
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[warning] 14-14:
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
🤖 Prompt for AI Agents
In
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/Dto/ShortCourseDto.cs
around lines 14 to 18, nullable reference annotations are used but the file
lacks a nullable context; add a "#nullable enable" directive near the top of the
file (immediately after the using statements) so the compiler recognizes the
nullable annotations and clears the related warnings.
| #endregion | ||
|
|
||
| #region Enrollment Data | ||
| public ICollection<StudentDto> EnrolledStudents { get; set; } |
There was a problem hiding this comment.
Initialize the EnrolledStudents collection to prevent NullReferenceException.
The non-nullable EnrolledStudents property is not initialized, which will cause a NullReferenceException when accessed if AutoMapper doesn't populate it from the source entity.
Apply this diff to initialize the collection:
- public ICollection<StudentDto> EnrolledStudents { get; set; }
+ public ICollection<StudentDto> EnrolledStudents { get; set; } = new List<StudentDto>();Alternatively, if the property should support null values when no students are enrolled, make it nullable:
- public ICollection<StudentDto> EnrolledStudents { get; set; }
+ public ICollection<StudentDto>? EnrolledStudents { get; set; }📝 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.
| public ICollection<StudentDto> EnrolledStudents { get; set; } | |
| public ICollection<StudentDto> EnrolledStudents { get; set; } = new List<StudentDto>(); |
| public ICollection<StudentDto> EnrolledStudents { get; set; } | |
| public ICollection<StudentDto>? EnrolledStudents { get; set; } |
🤖 Prompt for AI Agents
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/Dto/ShortCourseDto.cs
around line 25: the non-nullable EnrolledStudents property is not initialized
which can cause NullReferenceException; either initialize it to an empty
collection (e.g., assign new List<StudentDto>() in the property declaration or
constructor) or change its type to nullable (ICollection<StudentDto>?) if null
is an acceptable state—update the property accordingly and ensure any callers
handle the chosen non-null or nullable contract.
| { | ||
| Logger.Error("Error creating short course", ex); | ||
| throw new UserFriendlyException( | ||
| $"Could not create Short Course. Error: {ex.Message}", |
There was a problem hiding this comment.
Avoid exposing internal exception details to users.
Including ex.Message in the UserFriendlyException may expose sensitive internal implementation details (e.g., database connection strings, stack traces). Consider using a generic error message instead.
Apply this diff:
Logger.Error("Error creating short course", ex);
throw new UserFriendlyException(
- $"Could not create Short Course. Error: {ex.Message}",
+ "Could not create Short Course. Please try again or contact support.",
Abp.Logging.LogSeverity.Error
);📝 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.
| $"Could not create Short Course. Error: {ex.Message}", | |
| Logger.Error("Error creating short course", ex); | |
| throw new UserFriendlyException( | |
| "Could not create Short Course. Please try again or contact support.", | |
| Abp.Logging.LogSeverity.Error | |
| ); |
🤖 Prompt for AI Agents
In
aspnet-core/src/Moipone.PublicSite.Application/ShortCourses/ShortCourseAppService.cs
around line 50, the code puts ex.Message into a UserFriendlyException exposing
internal error details; change it to throw a UserFriendlyException with a
generic, non-sensitive message (e.g., "An error occurred while creating the
short course. Please try again or contact support.") and move the full exception
details into server-side logging (e.g., Logger.LogError(ex, "...")) so the
exception is recorded for diagnostics but not shown to the user.
| public async Task<ShortCourseDto> OpenApplicationsAsync(Guid id) | ||
| { | ||
| try | ||
| { | ||
| if (id == Guid.Empty) | ||
| { | ||
| throw new UserFriendlyException( | ||
| "Invalid short course ID.", | ||
| Abp.Logging.LogSeverity.Warn | ||
| ); | ||
| } | ||
|
|
||
| var shortCourse = await _shortCourseRepository.GetAsync(id); | ||
|
|
||
| if (shortCourse == null) | ||
| { | ||
| throw new UserFriendlyException( | ||
| $"Short course with ID {id} not found.", | ||
| Abp.Logging.LogSeverity.Warn | ||
| ); | ||
| } | ||
|
|
||
| shortCourse.IsActive = true; | ||
| var updatedShortCourse = await _shortCourseRepository.UpdateAsync(shortCourse); | ||
|
|
||
| return ObjectMapper.Map<ShortCourseDto>(updatedShortCourse); | ||
| } | ||
| catch (UserFriendlyException) | ||
| { | ||
| throw; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Logger.Error($"Error opening applications for short course with ID {id}", ex); | ||
| throw new UserFriendlyException( | ||
| $"Could not open applications. Error: {ex.Message}", | ||
| Abp.Logging.LogSeverity.Error | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
OpenApplicationsAsync and ReopenApplicationsAsync are identical.
Both methods perform exactly the same operation (setting IsActive = true). This is code duplication that should be addressed.
Option 1: If the methods should have identical behavior, consolidate them:
public async Task<ShortCourseDto> OpenApplicationsAsync(Guid id)
{
- try
- {
- if (id == Guid.Empty)
- {
- throw new UserFriendlyException(
- "Invalid short course ID.",
- Abp.Logging.LogSeverity.Warn
- );
- }
-
- var shortCourse = await _shortCourseRepository.GetAsync(id);
-
- if (shortCourse == null)
- {
- throw new UserFriendlyException(
- $"Short course with ID {id} not found.",
- Abp.Logging.LogSeverity.Warn
- );
- }
-
- shortCourse.IsActive = true;
- var updatedShortCourse = await _shortCourseRepository.UpdateAsync(shortCourse);
-
- return ObjectMapper.Map<ShortCourseDto>(updatedShortCourse);
- }
- catch (UserFriendlyException)
- {
- throw;
- }
- catch (Exception ex)
- {
- Logger.Error($"Error opening applications for short course with ID {id}", ex);
- throw new UserFriendlyException(
- $"Could not open applications. Error: {ex.Message}",
- Abp.Logging.LogSeverity.Error
- );
- }
+ return await SetApplicationStatusAsync(id, true, "open");
}
public async Task<ShortCourseDto> ReopenApplicationsAsync(Guid id)
{
- // ... identical code ...
+ return await SetApplicationStatusAsync(id, true, "reopen");
}
+
+ private async Task<ShortCourseDto> SetApplicationStatusAsync(Guid id, bool isActive, string action)
+ {
+ try
+ {
+ if (id == Guid.Empty)
+ {
+ throw new UserFriendlyException(
+ "Invalid short course ID.",
+ Abp.Logging.LogSeverity.Warn
+ );
+ }
+
+ var shortCourse = await _shortCourseRepository.GetAsync(id);
+ shortCourse.IsActive = isActive;
+ var updatedShortCourse = await _shortCourseRepository.UpdateAsync(shortCourse);
+
+ return ObjectMapper.Map<ShortCourseDto>(updatedShortCourse);
+ }
+ catch (UserFriendlyException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Error {action}ing applications for short course with ID {id}", ex);
+ throw new UserFriendlyException(
+ $"Could not {action} applications.",
+ Abp.Logging.LogSeverity.Error
+ );
+ }
+ }Option 2: If they should behave differently (e.g., tracking state history, business rules), add domain methods to the ShortCourse entity:
// In ShortCourse entity:
public void OpenApplications() { /* domain logic */ }
public void ReopenApplications() { /* different domain logic */ }Also applies to: 324-363
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.