-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Sub-Direction layer for user-facing Directions #1670
base: develop
Are you sure you want to change the base?
Conversation
…titutionHierarchy
…pdate mapping related with Directions
WalkthroughThis pull request refactors the management of directional data across the codebase. The changes primarily replace the use of “Directions” with “SubDirections” in queries, mappings, entity models, and test data. Service methods, interfaces, and controller endpoints (including a new GetByFilter method) have been updated to return a structured result, with enhanced error handling and logging. Additionally, new configuration classes and test data generators for SubDirection have been introduced, and related tests have been modified accordingly. Changes
Sequence Diagram(s)sequenceDiagram
participant C as Client
participant DC as DirectionController
participant DS as DirectionService
participant DB as Database
C->>DC: GET /v1/direction/filter?params
DC->>DS: GetByFilter(filter)
DS->>DB: Query using SubDirections
DB-->>DS: Return filtered results
DS-->>DC: Result (Success/NoContent)
alt Valid result
DC->>C: 200 OK with data
else No data found
DC->>C: 404 Not Found with message
end
sequenceDiagram
participant C as Client
participant DC as DirectionController
participant DS as DirectionService
participant DB as Database
C->>DC: POST /v1/direction with DirectionDto
DC->>DS: Create(DirectionDto)
DS->>DB: Save new Direction (with SubDirections)
DB-->>DS: Persisted entity
DS-->>DC: Result<DirectionDto>
alt Creation successful
DC->>C: 201 Created with entity data
else Failure encountered
DC->>C: Error response (wrapped in result)
end
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🔭 Outside diff range comments (2)
OutOfSchool/OutOfSchool.WebApi.Tests/Services/StatisticServiceTest.cs (1)
545-551
:⚠️ Potential issueFix incomplete test data in Application test case.
The SubDirection object in this test case is missing the DirectionId and Direction properties that are present in all other test cases. This inconsistency could affect test reliability.
Apply this diff to complete the test data:
new SubDirection { Id = 2, + DirectionId = 2, + Direction = new Direction + { + Id = 2 + } },OutOfSchool/OutOfSchool.BusinessLogic/Services/SubordinationStructure/InstitutionHierarchyService.cs (1)
54-54
:⚠️ Potential issueUpdate Create and Update methods to use SubDirections.
The Create and Update methods need to be updated to use the new SubDirection relationship instead of directly using Directions.
Apply this diff to fix the inconsistency:
- var newInstitutionHierarchy = await repository.Create(institutionHierarchy, dto.Directions.Select(d => d.Id).ToList()).ConfigureAwait(false); + var newInstitutionHierarchy = await repository.Create(institutionHierarchy, dto.SubDirections.Select(s => s.Direction.Id).ToList()).ConfigureAwait(false); - .Update( - institutionHierarchy, - dto.Directions.Select(d => d.Id).ToList()) + .Update( + institutionHierarchy, + dto.SubDirections.Select(s => s.Direction.Id).ToList())Also applies to: 236-236
🧹 Nitpick comments (27)
OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/AdminControllerTests.cs (1)
219-239
: Consider adding test coverage for failed results.While the tests cover successful updates and invalid model states, they don't verify how the controller handles failed results from the service layer (e.g., when the service returns
Result<DirectionDto>.Failed()
).Consider adding a test case like this:
+[Test] +public async Task UpdateDirections_WhenServiceReturnsFailed_ReturnsBadRequestObjectResult() +{ + // Arrange + controller.ControllerContext.HttpContext = fakeHttpContext; + controller.ControllerContext.HttpContext.SetContextUser(Role.TechAdmin); + var changedDirection = new DirectionDto() + { + Id = 1, + Title = "ChangedTitle", + }; + var returnedResult = Result<DirectionDto>.Failed(new OperationError + { + Code = "400", + Description = "Update failed", + }); + sensitiveDirectionService.Setup(x => x.Update(changedDirection)).ReturnsAsync(returnedResult); + + // Act + var result = await controller.UpdateDirections(changedDirection).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.TypeOf<BadRequestObjectResult>()); + Assert.That((result as BadRequestObjectResult).StatusCode, Is.EqualTo(400)); +}Also applies to: 241-255
OutOfSchool/OutOfSchool.WebApi.Tests/Services/StatisticServiceTest.cs (3)
59-80
: Enhance test coverage for SubDirection functionality.The existing tests verify the workshop retrieval but don't explicitly validate the new SubDirection layer. Consider adding test cases that verify:
- Correct mapping of SubDirection to Direction
- Proper filtering based on SubDirection properties
Also applies to: 82-104
248-476
: Reduce test data duplication using helper methods.The SubDirection test data structure is repeated across WithWorkshops, WithWorkshopsIncludingCATOTTG, and WithApplications methods. Consider extracting the common SubDirection setup into a helper method.
Example helper method:
private SubDirection CreateSubDirection(int id) { return new SubDirection { Id = id, DirectionId = id, Direction = new Direction { Id = id } }; }Also applies to: 488-626
106-156
: Enhance assertions to verify SubDirection properties.The current assertions verify the overall results but don't explicitly check the SubDirection properties. Consider adding specific assertions to verify the SubDirection-Direction relationship.
Example assertion:
result .Should() .Match<Workshop>(w => w.InstitutionHierarchy.SubDirections.All(sd => sd.DirectionId == sd.Direction.Id));OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/DirectionControllerTests.cs (1)
153-165
: Add test coverage for Result failure scenarios.The Create test only covers the success case. Consider adding tests for failure scenarios using Result, such as:
- Business rule violations
- Validation failures
- Conflict scenarios
Example test:
[Test] public async Task Create_WhenBusinessRuleViolated_ReturnsBadRequest() { // Arrange var error = "Business rule violated"; var returnedResult = Result<DirectionDto>.Failed(error); service.Setup(x => x.Create(direction)).ReturnsAsync(returnedResult); // Act var result = await controller.Create(direction).ConfigureAwait(false) as BadRequestObjectResult; // Assert Assert.That(result, Is.Not.Null); Assert.That(result.StatusCode, Is.EqualTo(400)); Assert.That(result.Value, Is.EqualTo(error)); }OutOfSchool/OutOfSchool.BusinessLogic/Util/Mapping/ElasticProfile.cs (1)
156-156
: Consider adding null checks for robustness.The mapping assumes that both SubDirection and Direction entities are not null.
Add null checks to prevent potential NullReferenceException:
-opt => opt.MapFrom(src => src.InstitutionHierarchy.SubDirections.Where(x => !x.Direction.IsDeleted).Select(d => d.DirectionId))) +opt => opt.MapFrom(src => src.InstitutionHierarchy?.SubDirections?.Where(x => x.Direction != null && !x.Direction.IsDeleted).Select(d => d.DirectionId) ?? Enumerable.Empty<long>()))OutOfSchool/OutOfSchool.WebApi.Tests/Services/DirectionServiceTests.cs (3)
103-103
: Consider a more specific test name.The test name could be more descriptive about the specific failure condition being tested.
Consider renaming to
Create_DuplicateTitle_ReturnsFailedResult
to better describe the exact validation being tested.
223-225
: Enhance test coverage for SubDirections relationship.While the filter logic has been updated to use SubDirections, the test assertions only verify the Direction properties.
Consider adding assertions to verify the SubDirections relationship:
// Assert Assert.True(result.Entities.All(d => d.Title == expectedDto.Title)); +Assert.That(expected.FirstOrDefault().SubDirections, Is.Not.Empty, "Direction should have associated SubDirections"); +Assert.That(expected.FirstOrDefault().SubDirections.SelectMany(s => s.InstitutionHierarchies), Is.Not.Empty, "SubDirections should have associated InstitutionHierarchies");Also applies to: 260-262
313-330
: Enhance test data coverage.The test data could be more comprehensive. Currently, only one Direction has SubDirections, which might miss testing important scenarios.
Consider adding SubDirections to other test Directions to ensure proper testing of various scenarios:
new Direction() { Title = "Test1", Description = "Test1", + SubDirections = new List<SubDirection>() + { + new SubDirection() + { + Title = "Test1Sub", + Description = "Test1Sub", + InstitutionHierarchies = new List<InstitutionHierarchy>() + { + new InstitutionHierarchy() + { + Id = Guid.NewGuid(), + Title = "Title1", + HierarchyLevel = 1, + InstitutionId = new Guid("af475193-6a1e-4a75-9ba3-439c4300f771"), + }, + }, + } + } },OutOfSchool/OutOfSchool.BusinessLogic/Util/MappingProfile.cs (2)
505-507
: Consider adding XML documentation for Direction mapping.The mapping configuration for
Direction
has been updated to ignoreSubDirections
. Consider adding XML documentation to explain the relationship betweenDirection
andSubDirection
entities, similar to the documentation provided for other mappings (e.g., lines 243-252).+ /// <summary> + /// Maps Direction entity to DirectionDto. + /// Note: SubDirections are ignored in the reverse mapping as they are managed through the SubDirection entity. + /// </summary> CreateSoftDeletedMap<DirectionDto, Direction>() .ForMember(dest => dest.SubDirections, opt => opt.Ignore()) .ForMember(c => c.UpdatedAt, m => m.Ignore());
493-494
: Remove or address the warning comment.The warning comment indicates that the mapping for
ShortUserDto
toAdminDto
is temporary and needs to be removed or refactored. Consider creating a new issue to track this technical debt.Would you like me to create a new issue to track the removal or refactoring of this temporary mapping?
OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs (5)
110-110
: Typo: “succesfully” → “successfully.”
Please correct the misspelling to maintain professional logging output.- logger.LogDebug("Direction with Id = {id} succesfully deleted.", id); + logger.LogDebug("Direction with Id = {id} successfully deleted.", id);
116-117
: Reconsider concurrency exception usage.
Throwing aDbUpdateConcurrencyException
for a missing record can be misleading. Consider a more descriptive custom exception or aResult
failure to ensure clarity.
160-160
: Redundant cast.
Castingg.Count()
toint?
is superfluous sinceCount()
is already anint
. Consider removing it unless needed for a future requirement.- WorkshopsCount = g.Count() as int?, + WorkshopsCount = g.Count(),
230-232
: Typo: “succesfully” → “successfully.”
Please fix the spelling to maintain consistency in logs.- logger.LogDebug("Direction with Id = {id} updated succesfully.", direction?.Id); + logger.LogDebug("Direction with Id = {id} updated successfully.", direction?.Id);
287-287
: Keep block style consistent.
Theelse {
statement uses an inline brace, whereas other blocks use new lines. For readability, consider a consistent code style.OutOfSchool/OutOfSchool.DataAccess/Models/Direction.cs (1)
24-24
: Replacing InstitutionHierarchies with SubDirections.
This transition suggests a pivot in data modeling towards a more granular sub-direction structure. EnsureSubDirections
is initialized (= new();
) if you want to avoid null references.- public virtual List<SubDirection> SubDirections { get; set; } + public virtual List<SubDirection> SubDirections { get; set; } = new();OutOfSchool/OutOfSchool.DataAccess/Models/SubDirection.cs (1)
18-20
: Description property usage.
The optional 500-length description is a practical limit, but consider validating user input to avoid large manual expansions.OutOfSchool/OutOfSchool.DataAccess/Models/Configurations/SubDirectionConfiguration.cs (1)
1-23
: LGTM! Consider adding XML documentation.The entity configuration is well-structured with proper relationship definitions and performance considerations (index on IsDeleted). The use of DeleteBehavior.Restrict is appropriate to prevent unintended cascading deletes.
Consider adding XML documentation to describe the purpose of this configuration class and its relationships:
+/// <summary> +/// Entity Framework configuration for the SubDirection entity. +/// Configures relationships with Direction and InstitutionHierarchy entities. +/// </summary> public class SubDirectionConfiguration : IEntityTypeConfiguration<SubDirection>OutOfSchool/OutOfSchool.DataAccess/Models/SubordinationStructure/InstitutionHierarchy.cs (1)
30-30
: Initialize the collection property.The virtual collection property should be initialized to prevent potential null reference exceptions.
- public virtual List<SubDirection> SubDirections { get; set; } + public virtual List<SubDirection> SubDirections { get; set; } = new();OutOfSchool/Tests/OutOfSchool.Tests.Common/TestDataGenerators/SubDirectionsGenerator.cs (2)
9-11
: Enhance test data generation rules.The current implementation might benefit from additional rules and a larger Id range:
private static readonly Faker<SubDirection> faker = new Faker<SubDirection>() - .RuleFor(x => x.Id, f => f.Random.Long(min: 1, max: 1_000_000)) + .RuleFor(x => x.Id, f => f.Random.Long(min: 1, max: long.MaxValue)) + .RuleFor(x => x.IsDeleted, f => f.Random.Bool(0.1f)) // 10% chance of being deleted + .RuleFor(x => x.Description, f => f.Lorem.Paragraph()) .RuleFor(x => x.Title, f => f.Company.CompanyName());
15-16
: Consider adding parallel generation for large datasets.For large lists of direction IDs, consider using parallel processing:
public static List<SubDirection> Generate(List<long> directionIds) => - directionIds.Select(id => Generate(id)).ToList(); + directionIds.AsParallel() + .Select(id => Generate(id)) + .ToList();OutOfSchool/Tests/OutOfSchool.Tests.Common/TestDataGenerators/InstitutionHierarchyGenerator.cs (1)
41-41
: Consider adding null check and filtering deleted SubDirections.The flattening operation might throw if any Direction in the list is null or if SubDirections is null. Additionally, consider filtering out deleted SubDirections if applicable.
- hierarchy.SubDirections = directions.SelectMany(d => d.SubDirections).ToList(); + hierarchy.SubDirections = directions + ?.Where(d => d != null) + .SelectMany(d => d.SubDirections?.Where(sd => !sd.IsDeleted) ?? Enumerable.Empty<SubDirection>()) + .ToList() ?? [];OutOfSchool/OutOfSchool.BusinessLogic/Util/Mapping/ExternalExportMappingProfile.cs (1)
82-82
: Consider adding filtering for non-deleted Directions.For consistency with the Workshop mapping, consider filtering out deleted Directions here as well.
- .ForMember(dest => dest.DirectionIds, opt => opt.MapFrom(src => src.SubDirections.Select(x => x.DirectionId))); + .ForMember(dest => dest.DirectionIds, opt => opt.MapFrom(src => src.SubDirections.Where(x => !x.Direction.IsDeleted).Select(x => x.DirectionId)));OutOfSchool/OutOfSchool.WebApi/Controllers/V1/DirectionController.cs (2)
130-152
: Consider consolidating exception handling.The exception handling could be simplified by combining the catch blocks since they perform the same action.
try { directionDto.Id = default; var response = await service.Create(directionDto).ConfigureAwait(false); if (response != null && response.Succeeded) { return CreatedAtAction( nameof(GetById), new { id = response.Value.Id, }, response.Value); } else { return HandleServiceRespone(response); } } - catch (ArgumentException ex) - { - return BadRequest(ex.Message); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } + catch (Exception ex) when (ex is ArgumentException || ex is Exception) + { + return BadRequest(ex.Message); + }
154-174
: Fix typo in method name.The method name contains a typo:
HandleServiceRespone
should beHandleServiceResponse
.- private IActionResult HandleServiceRespone<T>(Result<T> response) + private IActionResult HandleServiceResponse<T>(Result<T> response)OutOfSchool/OutOfSchool.DataAccess/Repository/InstitutionHierarchyRepository.cs (1)
29-59
: Consider moving business logic to a service layer.The repository is currently handling complex business logic related to Direction-SubDirection relationships. This violates the Single Responsibility Principle and makes the code harder to maintain and test.
Consider:
- Moving the Direction-SubDirection relationship logic to a dedicated service
- Keeping the repository focused on basic CRUD operations
- Using a Unit of Work pattern for transaction management
Example service structure:
public interface IInstitutionHierarchyService { Task<InstitutionHierarchy> CreateWithDirections(InstitutionHierarchy entity, List<long> directionsIds); Task<InstitutionHierarchy> UpdateWithDirections(InstitutionHierarchy entity, List<long> directionsIds); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
OutOfSchool/OutOfSchool.BusinessLogic/Services/ApplicationService.cs
(1 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/Database/WorkshopService.cs
(1 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs
(7 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/ExternalExportService.cs
(1 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/IDirectionService.cs
(2 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/ISensitiveDirectionService.cs
(1 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/StatisticService.cs
(2 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/SubordinationStructure/InstitutionHierarchyService.cs
(1 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Util/Mapping/ElasticProfile.cs
(1 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Util/Mapping/ExternalExportMappingProfile.cs
(2 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Util/MappingProfile.cs
(6 hunks)OutOfSchool/OutOfSchool.DataAccess/Models/Configurations/SubDirectionConfiguration.cs
(1 hunks)OutOfSchool/OutOfSchool.DataAccess/Models/Direction.cs
(1 hunks)OutOfSchool/OutOfSchool.DataAccess/Models/SubDirection.cs
(1 hunks)OutOfSchool/OutOfSchool.DataAccess/Models/SubordinationStructure/InstitutionHierarchy.cs
(1 hunks)OutOfSchool/OutOfSchool.DataAccess/OutOfSchoolDbContext.cs
(2 hunks)OutOfSchool/OutOfSchool.DataAccess/Repository/InstitutionHierarchyRepository.cs
(2 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/AdminControllerTests.cs
(2 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/DirectionControllerTests.cs
(4 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Services/Database/InstitutionHierarchyRepositoryTests.cs
(1 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Services/DirectionSensitiveServiceTests.cs
(3 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Services/DirectionServiceTests.cs
(7 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Services/ExternalExportSubDirectionsTests.cs
(1 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Services/InstitutionHierarchyServiceTest.cs
(5 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Services/StatisticServiceTest.cs
(12 hunks)OutOfSchool/OutOfSchool.WebApi/Controllers/V1/DirectionController.cs
(3 hunks)OutOfSchool/Tests/OutOfSchool.Tests.Common/TestDataGenerators/InstitutionHierarchyGenerator.cs
(3 hunks)OutOfSchool/Tests/OutOfSchool.Tests.Common/TestDataGenerators/SubDirectionsGenerator.cs
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: test (windows-latest)
🔇 Additional comments (48)
OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/AdminControllerTests.cs (2)
230-231
: LGTM! The test correctly handles the new Result type.The test has been updated to use
Result<DirectionDto>
instead of directly returningDirectionDto
, which aligns with the service layer changes.
268-269
: LGTM! The test maintains consistency with the service layer changes.The test correctly uses
Result<DirectionDto>
for the non-admin user scenario, maintaining consistency with the service layer changes.OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/DirectionControllerTests.cs (4)
13-13
: LGTM!The addition of the
OutOfSchool.BusinessLogic.Common
namespace is necessary to support the Result return type changes.
139-150
: LGTM! Improved error handling with proper HTTP status codes.The test now correctly expects a NotFound response instead of null, which is a better REST practice.
156-157
: LGTM! Properly handling Result wrapper.The test correctly sets up the service mock to return a Result and verifies the success case.
196-209
: LGTM! Added necessary IDs to test data.The addition of sequential IDs to the test data enables proper testing of ID-based operations.
OutOfSchool/OutOfSchool.BusinessLogic/Util/Mapping/ElasticProfile.cs (1)
156-156
: LGTM! The mapping change aligns with the SubDirection layer introduction.The mapping correctly retrieves direction IDs through the new SubDirection relationship.
Please verify that:
- All SubDirections have valid DirectionId values
- The deletion check on Direction (parent) instead of SubDirection (child) maintains the expected filtering behavior
Run this script to verify the relationships and values:
✅ Verification successful
🏁 Script executed:
#!/bin/bash # Description: Verify SubDirection relationships and values # Check for SubDirections with null or invalid DirectionId ast-grep --pattern 'class SubDirection { $$$ DirectionId $_ $$$ }' # Check for SubDirection usage patterns rg -A 5 'SubDirection.*Direction.*IsDeleted'Length of output: 2704
Mapping configuration update for SubDirections Approved
The change to map direction IDs through the
SubDirections
relationship—with a deletion check on the parentDirection
—is consistent across the codebase. Usage in both theWorkshopService.cs
andExternalExportMappingProfile.cs
confirms this pattern is intentional.
- Ensure all SubDirections indeed have valid
DirectionId
values.- Verify that checking
!x.Direction.IsDeleted
on the parent yields the desired filtering behavior.Overall, the modifications meet the PR objectives.
OutOfSchool/OutOfSchool.BusinessLogic/Util/MappingProfile.cs (2)
46-47
: Consistent mapping of DirectionIds across entities.The mapping configurations for
Workshop
,WorkshopCreateRequestDto
, andCompetitiveEvent
have been consistently updated to sourceDirectionIds
fromSubDirections.DirectionId
instead ofDirections.Id
. This change aligns with the new entity relationships.Also applies to: 166-167, 780-781
471-472
: Verify the impact of InstitutionHierarchy mapping changes.The mapping configuration for
InstitutionHierarchy
has been updated to:
- Map
Directions
fromSubDirections.Direction
- Ignore
SubDirections
in the reverse mappingWhile the changes look correct, ensure that:
- The
Direction
navigation property is properly loaded when retrievingSubDirections
- Any code that depends on the
Directions
property is updated to handle the new mappingAlso applies to: 474-475
❌ Verification inconclusive
🏁 Script executed:
#!/bin/bash # Description: Check for direct usage of InstitutionHierarchy.Directions property # to identify potential breaking changes # Search for direct property access echo "Searching for direct property access..." rg "InstitutionHierarchy\.Directions" -A 5 # Search for LINQ queries echo "Searching for LINQ queries..." rg "from\s+.*\s+in\s+.*\.InstitutionHierarchy\.Directions" -A 5Length of output: 1180
Action: Review EF Query Configuration and ExternalExportService Usage of InstitutionHierarchy.Directions
The mapping update in the profile is correct in mapping Directions from SubDirections by selecting the nested Direction property. Our static search confirms that ExternalExportService directly references
InstitutionHierarchy.Directions
(as found in the string concatenation for export). However, we have not seen any clear evidence (via eager‑loading configurations in search results) that the retrieval of InstitutionHierarchy ensures its SubDirections—and thus their nested Direction properties—are fully loaded.
- Impact Point: ExternalExportService depends on
InstitutionHierarchy.Directions
to be populated.- Next Step: Please verify that the EF queries retrieving InstitutionHierarchy include appropriate eager-loading (e.g. using
.Include(s => s.SubDirections).ThenInclude(sd => sd.Direction)
) so that the Direction navigation property is available at runtime.OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs (20)
58-60
: Refactored return type to a wrapped result.
Switching from returning a raw DTO toResult<DirectionDto>
is a solid approach for structured error handling and success/failure encapsulation.
64-68
: Validation errors are now surfaced appropriately.
ReturningResult<DirectionDto>.Failed
upon encountering validation errors is a clean way to notify callers of specific issues without throwing exceptions.
72-74
: Successful creation logging.
Logging the newly created entity’s ID is helpful for debugging. The finalResult<DirectionDto>.Success
result looks consistent with the new pattern.
80-80
: Consistent debug-level logging.
Lowering the log level from Information to Debug is acceptable if you prefer less verbosity in production.
94-94
: Verify EF query for SubDirections usage.
Using.Any(d => d.DirectionId == id)
is correct for a simple existence check. However, ensure your database indexing covers frequent lookups byDirectionId
to prevent potential performance bottlenecks.
124-128
: Retrieval logging is consistent.
Debug-level logging for fetching all directions is consistent with the rest of the service.
133-136
: Introduce new filtering for directions.
Returning aSearchResult<DirectionDto>
while logging at Debug level is a good practice for tracking advanced queries without cluttering logs.
148-148
: Loading SubDirections eagerly.
UsingincludeProperties: "SubDirections"
ensures related data is available, but confirm if this is always needed to optimize performance.
153-153
: Check potential performance for nested ‘Any’ calls.
Further filtering with.Any(...)
can lead to multiple queries or more complex SQL. Make sure heavy usage is tested for performance.
170-170
: Debug logging for final results.
Good to see the total count of returned directions, which aids in verifying query correctness.
200-202
: Refactored update method to return a ‘Result’.
Aligning the update method with the same return signature improves consistency for error and success handling.
204-212
: Guarding against null DTO.
This ensures immediate feedback if the caller passes a null object, avoiding potential null reference issues downstream.
219-224
: Handling non-existent Direction on update.
Returning a 404 within theResult
object is consistent with RESTful patterns, making it clear that the resource is not found.
235-237
: Validation function returning a collection of errors.
Returning a list instead of throwing allows for graceful handling of multiple validation failures.
239-239
: Check concurrency for title uniqueness.
Using.Any()
checks if a direction with the same title already exists. Ensure concurrency is handled if multiple create/update calls happen simultaneously.
241-247
: Warning log for duplicate direction data.
The approach of logging and adding to an error list is consistent with the newly introducedResult
error pattern.
250-250
: Returning validation errors.
Short, straightforward approach to packaging validation failures.
260-261
: Filter expansion to SubDirections.
Includingdirection.SubDirections.Any(...)
in the filter broadens the search. Double-check indexing on the related tables for consistent performance.
272-273
: Ministry Admin filter on SubDirections.
Chaining.Any(...)
calls is logical. Just confirm performance is acceptable for large sets of SubDirections.
281-282
: Region Admin filter on SubDirections.
Similar approach as the ministry admin logic. Keep an eye on performance with large data.OutOfSchool/OutOfSchool.DataAccess/Models/SubDirection.cs (3)
1-5
: Introduction of SubDirection entity.
Defining a separate class for SubDirections is an effective approach to modularizing the direction model.
6-16
: Validation attributes on Title.
Marking the title as required and imposing length constraints ensures the user can’t enter invalid or excessively long titles.
21-24
: Navigation properties for relationships.
AssociateSubDirection
withDirection
andInstitutionHierarchies
ensures a well-defined data model. Confirm your mappings are correct in EF configurations.OutOfSchool/Tests/OutOfSchool.Tests.Common/TestDataGenerators/InstitutionHierarchyGenerator.cs (1)
15-15
: LGTM! Initialization of SubDirections collection.The change correctly initializes an empty SubDirections collection using the new C# collection expression syntax.
OutOfSchool/OutOfSchool.WebApi.Tests/Services/Database/InstitutionHierarchyRepositoryTests.cs (1)
60-60
: LGTM! Updated test to use SubDirections.The test correctly reflects the transition from Directions to SubDirections by selecting DirectionId from the SubDirections collection.
OutOfSchool/OutOfSchool.BusinessLogic/Util/Mapping/ExternalExportMappingProfile.cs (1)
41-41
: LGTM! Added proper filtering for non-deleted Directions.The mapping correctly filters out deleted Directions when selecting DirectionIds from SubDirections.
OutOfSchool/OutOfSchool.WebApi.Tests/Services/DirectionSensitiveServiceTests.cs (3)
88-88
: LGTM! Assertion updated to handle Result wrapper.The assertion has been correctly updated to access the Title property through the Result wrapper's Value property.
93-104
: LGTM! Test updated to verify new error handling.The test has been properly updated to:
- Check for failed result status
- Verify the correct error code is returned
This aligns well with the service's new error handling approach using Result.
131-148
: LGTM! Data seeding updated for new entity model.The test data setup has been correctly updated to reflect the new entity relationships:
- Direction contains SubDirections
- SubDirection contains InstitutionHierarchies
OutOfSchool/OutOfSchool.WebApi.Tests/Services/ExternalExportSubDirectionsTests.cs (1)
143-147
: LGTM! Test data setup enhanced with SubDirections.The test data generation has been properly updated to:
- Generate SubDirections using Direction IDs
- Add SubDirections to the context before Directions to maintain referential integrity
OutOfSchool/OutOfSchool.WebApi/Controllers/V1/DirectionController.cs (2)
57-73
: LGTM! Well-structured filter endpoint added.The new GetByFilter endpoint is concisely implemented with proper:
- Documentation
- Response type definitions
- Result handling using ProtectAndMap
81-99
: LGTM! Enhanced error handling for GetById.The method has been improved with:
- Proper null check
- Specific error message for NotFound response
- Updated API documentation
OutOfSchool/OutOfSchool.BusinessLogic/Services/StatisticService.cs (2)
86-89
: LGTM! Query updated for new entity model.The query has been properly updated to:
- Use SubDirections relationship
- Group by DirectionId maintaining data consistency
191-191
: Verify performance impact of additional includes.The addition of SubDirections and Direction includes will generate additional joins. Consider monitoring query performance with larger datasets.
✅ Verification successful
🏁 Script executed:
#!/bin/bash # Description: Check for potential N+1 queries in workshop loading # Search for related repository methods that might trigger lazy loading rg -A 5 "workshopRepository.Get|popularWorkshops"Length of output: 25811
Performance Impact Verification for Extra Includes
The extra include chain in the query now joins InstitutionHierarchy → SubDirections → Direction in a single query. This approach avoids N+1 issues since EF Core generates one complex query rather than multiple round trips; however, the additional joins can increase query complexity and memory consumption. No evidence of an N+1 query pattern was found in the repository searches, but with larger datasets the join cost may negatively impact performance.
- The eager loading is correctly implemented in a single query.
- Additional joins are expected with this pattern; monitor execution time with sizeable data.
- Consider performance profiling (e.g., via EF Core logging) in a production-like environment to ensure acceptable performance.
OutOfSchool/OutOfSchool.DataAccess/OutOfSchoolDbContext.cs (1)
149-149
: LGTM!The changes correctly introduce the new
SubDirection
entity by adding the DbSet property and applying its configuration.Also applies to: 204-204
OutOfSchool/OutOfSchool.BusinessLogic/Services/ExternalExportService.cs (1)
229-229
: LGTM!The property inclusion is correctly updated to use "SubDirections" instead of "Directions", aligning with the new entity structure.
OutOfSchool/OutOfSchool.WebApi.Tests/Services/InstitutionHierarchyServiceTest.cs (1)
68-68
: LGTM!The test cases are correctly updated to use
SubDirections
instead ofDirections
while maintaining the same test coverage and assertions.Also applies to: 233-233, 251-251, 275-275, 284-284
OutOfSchool/OutOfSchool.BusinessLogic/Services/ApplicationService.cs (1)
362-362
: LGTM!The entity inclusion chain is correctly updated to load
SubDirections
and their associatedDirection
, maintaining proper data relationships.OutOfSchool/OutOfSchool.BusinessLogic/Services/Database/WorkshopService.cs (1)
927-927
: LGTM! Consider monitoring query performance.The change correctly implements filtering through the new SubDirection entity while maintaining proper soft delete checks.
Since this change adds an additional level of navigation (SubDirections -> Direction), consider monitoring query performance. You can use the following script to analyze the generated SQL:
✅ Verification successful
🏁 Script executed:
#!/bin/bash # Description: Search for similar query patterns to identify potential performance impacts # Look for similar navigation patterns that might benefit from optimization ast-grep --pattern 'SubDirections.Any(d => !d.Direction.IsDeleted' # Look for potential eager loading configurations that might help optimize the query rg -A 5 'includeProperties.*SubDirections'Length of output: 2171
The SubDirection filtering condition is implemented correctly and aligns with similar patterns in the codebase.
- The usage of
InstitutionHierarchy.SubDirections.Any(d => !d.Direction.IsDeleted && d.DirectionId == direction)
is consistent with the refactoring observed inDirectionService.cs
and its tests.- As highlighted, the additional navigation layer through
SubDirections
should be monitored for performance impacts, but its implementation is sound.
public async Task GetById_WhenIdIsInvalid_ThrowsArgumentOutOfRangeException(long id) | ||
{ | ||
// Act and Assert | ||
Assert.ThrowsAsync<ArgumentOutOfRangeException>( | ||
async () => await service.GetById(id).ConfigureAwait(false)); | ||
// Act | ||
var result = await service.GetById(id).ConfigureAwait(false); | ||
|
||
// Assert | ||
Assert.IsNull(result); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update test name to match new behavior.
The test name suggests it's expecting an exception, but the implementation checks for a null result.
Rename the test to match the actual behavior being tested:
-public async Task GetById_WhenIdIsInvalid_ThrowsArgumentOutOfRangeException(long id)
+public async Task GetById_WhenIdIsInvalid_ReturnsNull(long id)
📝 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 async Task GetById_WhenIdIsInvalid_ThrowsArgumentOutOfRangeException(long id) | |
{ | |
// Act and Assert | |
Assert.ThrowsAsync<ArgumentOutOfRangeException>( | |
async () => await service.GetById(id).ConfigureAwait(false)); | |
// Act | |
var result = await service.GetById(id).ConfigureAwait(false); | |
// Assert | |
Assert.IsNull(result); | |
} | |
public async Task GetById_WhenIdIsInvalid_ReturnsNull(long id) | |
{ | |
// Act | |
var result = await service.GetById(id).ConfigureAwait(false); | |
// Assert | |
Assert.IsNull(result); | |
} |
var institutionHierarchies = await repository.Get(asNoTracking: true) | ||
.Include(x => x.Directions) | ||
.Include(x => x.SubDirections).ThenInclude(s => s.Direction) | ||
.Include(x => x.Institution) | ||
.ToListAsync() | ||
.ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Update data fetching to use new SubDirection relationship.
The GetAll method now correctly includes SubDirections and their related Direction entities. However, the Create and Update methods still use the old Directions relationship, which could lead to runtime errors or data inconsistency.
Let's verify the impact of this change:
🏁 Script executed:
#!/bin/bash
# Description: Check for inconsistencies in the usage of Directions vs SubDirections
# Search for Directions usage in the codebase
echo "Searching for Directions usage..."
rg -A 5 "Directions\." --type cs
# Search for SubDirections usage in the codebase
echo "Searching for SubDirections usage..."
rg -A 5 "SubDirections\." --type cs
# Search for Direction-related entity configurations
echo "Searching for Direction configurations..."
ast-grep --pattern $'class DirectionConfiguration : IEntityTypeConfiguration<Direction>
{
$$$
}'
# Search for SubDirection-related entity configurations
echo "Searching for SubDirection configurations..."
ast-grep --pattern $'class SubDirectionConfiguration : IEntityTypeConfiguration<SubDirection>
{
$$$
}'
Length of output: 47001
Inconsistency Identified Between Data Fetching and Modification Methods
The GetAll method now correctly loads the SubDirection relationship, but the Create/Update flows still depend on the old dto.Directions
—which are then translated in the repository via a lookup on dbContext.Directions
to retrieve SubDirections. This mismatch can lead to runtime surprises or data inconsistency if the DTO isn’t updated or the repository logic isn’t adjusted accordingly.
- Ensure the Create/Update methods use a DTO that reflects the new SubDirection entity.
- Confirm that the repository methods operating on these IDs correctly map to SubDirections as expected.
/// The task result contains a <see cref="DirectionCreateUpdateDto"/> that was updated.</returns> | ||
Task<Result<DirectionDto>> Update(DirectionDto dto); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix incorrect XML documentation.
The XML documentation references DirectionCreateUpdateDto
, but the method returns Result<DirectionDto>
.
- /// The task result contains a <see cref="DirectionCreateUpdateDto"/> that was updated.</returns>
+ /// The task result contains a <see cref="DirectionDto"/> that was updated.</returns>
📝 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.
/// The task result contains a <see cref="DirectionCreateUpdateDto"/> that was updated.</returns> | |
Task<Result<DirectionDto>> Update(DirectionDto dto); | |
/// The task result contains a <see cref="DirectionDto"/> that was updated.</returns> | |
Task<Result<DirectionDto>> Update(DirectionDto dto); |
/// The task result contains a <see cref="DirectionCreateUpdateDto"/> that was created.</returns> | ||
Task<Result<DirectionDto>> Create(DirectionDto dto); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documentation and return type mismatch.
The documentation mentions DirectionCreateUpdateDto
but the method returns Result<DirectionDto>
. This inconsistency should be fixed.
- /// The task result contains a <see cref="DirectionCreateUpdateDto"/> that was created.</returns>
+ /// The task result contains a <see cref="Result{DirectionDto}"/> that was created.</returns>
📝 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.
/// The task result contains a <see cref="DirectionCreateUpdateDto"/> that was created.</returns> | |
Task<Result<DirectionDto>> Create(DirectionDto dto); | |
/// The task result contains a <see cref="Result{DirectionDto}"/> that was created.</returns> | |
Task<Result<DirectionDto>> Create(DirectionDto dto); |
@@ -28,7 +28,7 @@ public InstitutionHierarchyRepository(OutOfSchoolDbContext dbContext) | |||
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns> | |||
public async Task<InstitutionHierarchy> Create(InstitutionHierarchy entity, List<long> directionsIds) | |||
{ | |||
entity.Directions = dbContext.Directions.Where(w => directionsIds.Contains(w.Id)).ToList(); | |||
entity.SubDirections = dbContext.Directions.Where(w => directionsIds.Contains(w.Id)).SelectMany(d => d.SubDirections).ToList(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Optimize the query and add validation.
The current implementation has potential performance issues and lacks validation:
- Consider using a more efficient query to avoid loading unnecessary data
- Add null/empty check for directionsIds
- Consider explicitly setting the entity state
Here's a suggested improvement:
+ if (directionsIds == null || !directionsIds.Any())
+ {
+ throw new ArgumentException("Direction IDs cannot be null or empty", nameof(directionsIds));
+ }
- entity.SubDirections = dbContext.Directions.Where(w => directionsIds.Contains(w.Id)).SelectMany(d => d.SubDirections).ToList();
+ entity.SubDirections = await dbContext.SubDirections
+ .Where(sd => directionsIds.Contains(sd.DirectionId))
+ .ToListAsync();
+ dbContext.Entry(entity).State = EntityState.Added;
📝 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.
entity.SubDirections = dbContext.Directions.Where(w => directionsIds.Contains(w.Id)).SelectMany(d => d.SubDirections).ToList(); | |
if (directionsIds == null || !directionsIds.Any()) | |
{ | |
throw new ArgumentException("Direction IDs cannot be null or empty", nameof(directionsIds)); | |
} | |
entity.SubDirections = await dbContext.SubDirections | |
.Where(sd => directionsIds.Contains(sd.DirectionId)) | |
.ToListAsync(); | |
dbContext.Entry(entity).State = EntityState.Added; |
newEntity.SubDirections.RemoveAll(x => !directionsIds.Contains(x.DirectionId)); | ||
var exceptDirectionsIds = directionsIds.Where(p => newEntity.SubDirections.TrueForAll(x => x.DirectionId != p)); | ||
newEntity.SubDirections.AddRange(dbContext.Directions.Where(w => exceptDirectionsIds.Contains(w.Id)).SelectMany(d => d.SubDirections).ToList()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Simplify the update logic and improve performance.
The current implementation has several concerns:
- Multiple database operations could impact performance
- No concurrency handling
- Complex and hard-to-read logic
- Potential memory issues with large datasets
Consider this more efficient approach:
+ if (directionsIds == null || !directionsIds.Any())
+ {
+ throw new ArgumentException("Direction IDs cannot be null or empty", nameof(directionsIds));
+ }
+
+ // Add concurrency check
+ if (entity.Version != null)
+ {
+ dbContext.Entry(newEntity).OriginalValues["Version"] = entity.Version;
+ }
+
- newEntity.SubDirections.RemoveAll(x => !directionsIds.Contains(x.DirectionId));
- var exceptDirectionsIds = directionsIds.Where(p => newEntity.SubDirections.TrueForAll(x => x.DirectionId != p));
- newEntity.SubDirections.AddRange(dbContext.Directions.Where(w => exceptDirectionsIds.Contains(w.Id)).SelectMany(d => d.SubDirections).ToList());
+ // Efficiently update the SubDirections collection
+ var currentSubDirectionIds = newEntity.SubDirections.Select(sd => sd.Id).ToHashSet();
+ var newSubDirections = await dbContext.SubDirections
+ .Where(sd => directionsIds.Contains(sd.DirectionId))
+ .ToListAsync();
+ var newSubDirectionIds = newSubDirections.Select(sd => sd.Id).ToHashSet();
+
+ // Remove SubDirections that are no longer needed
+ newEntity.SubDirections.RemoveAll(sd => !newSubDirectionIds.Contains(sd.Id));
+
+ // Add new SubDirections
+ foreach (var subDirection in newSubDirections.Where(sd => !currentSubDirectionIds.Contains(sd.Id)))
+ {
+ newEntity.SubDirections.Add(subDirection);
+ }
This approach:
- Uses HashSet for efficient lookups
- Adds concurrency check
- Reduces database roundtrips
- Improves readability
- Adds input validation
📝 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.
newEntity.SubDirections.RemoveAll(x => !directionsIds.Contains(x.DirectionId)); | |
var exceptDirectionsIds = directionsIds.Where(p => newEntity.SubDirections.TrueForAll(x => x.DirectionId != p)); | |
newEntity.SubDirections.AddRange(dbContext.Directions.Where(w => exceptDirectionsIds.Contains(w.Id)).SelectMany(d => d.SubDirections).ToList()); | |
if (directionsIds == null || !directionsIds.Any()) | |
{ | |
throw new ArgumentException("Direction IDs cannot be null or empty", nameof(directionsIds)); | |
} | |
// Add concurrency check | |
if (entity.Version != null) | |
{ | |
dbContext.Entry(newEntity).OriginalValues["Version"] = entity.Version; | |
} | |
// Efficiently update the SubDirections collection | |
var currentSubDirectionIds = newEntity.SubDirections.Select(sd => sd.Id).ToHashSet(); | |
var newSubDirections = await dbContext.SubDirections | |
.Where(sd => directionsIds.Contains(sd.DirectionId)) | |
.ToListAsync(); | |
var newSubDirectionIds = newSubDirections.Select(sd => sd.Id).ToHashSet(); | |
// Remove SubDirections that are no longer needed | |
newEntity.SubDirections.RemoveAll(sd => !newSubDirectionIds.Contains(sd.Id)); | |
// Add new SubDirections | |
foreach (var subDirection in newSubDirections.Where(sd => !currentSubDirectionIds.Contains(sd.Id))) | |
{ | |
newEntity.SubDirections.Add(subDirection); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (6)
OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/DirectionControllerTests.cs (1)
181-198
: Consider verifying the error message content.While the test properly verifies the response type and status code, it would be more robust to also assert that the error description matches the expected message.
// Assert Assert.That(result, Is.TypeOf<BadRequestObjectResult>()); Assert.That(result.StatusCode, Is.EqualTo(400)); +var error = (result.Value as Result<DirectionDto>)?.Error; +Assert.That(error?.Description, Is.EqualTo("There is already a Direction with such a data."));OutOfSchool/OutOfSchool.WebApi.Tests/Services/DirectionServiceTests.cs (1)
223-225
: Enhance test coverage for the SubDirection relationship.While the query has been updated to use the new SubDirection relationship, consider adding assertions to verify:
- The complete hierarchy (Direction -> SubDirection -> InstitutionHierarchy)
- The correct mapping of SubDirection properties
// Assert Assert.True(result.Entities.All(d => d.Title == expectedDto.Title)); +Assert.That(result.Entities.All(d => d.SubDirections != null && d.SubDirections.Any())); +Assert.That(result.Entities.All(d => d.SubDirections.All(s => s.InstitutionHierarchies != null && s.InstitutionHierarchies.Any())));Also applies to: 260-262
OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs (4)
58-74
: LGTM! Consider updating XML documentation.The changes improve error handling by using the Result pattern and collecting validation errors. The logging level change to Debug is appropriate for this context.
Update the XML documentation to reflect the new return type:
/// <summary> /// Creates new direction. /// </summary> /// <param name="dto">Direction entity to add.</param> -/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns> +/// <returns>A <see cref="Task{Result{DirectionDto}}"/> representing the result of the asynchronous operation, containing either the created direction or validation errors.</returns>
148-154
: Extract magic strings and improve query readability.Consider extracting "SubDirections" as a constant and using method chaining for better readability.
+ private const string SubDirectionsInclude = "SubDirections"; - .Get(skip: filter.From, take: filter.Size, whereExpression: predicate, orderBy: sortExpression, includeProperties: "SubDirections") + .Get( + skip: filter.From, + take: filter.Size, + whereExpression: predicate, + orderBy: sortExpression, + includeProperties: SubDirectionsInclude)
235-251
: Add input validation.Consider validating the input dto before checking for duplicates.
private List<OperationError> DirectionValidation(DirectionDto dto) { var errors = new List<OperationError>(); + if (dto == null) + { + errors.Add(new OperationError + { + Code = "400", + Description = localizer["Direction data cannot be null."] + }); + return errors; + } + if (string.IsNullOrWhiteSpace(dto.Title)) + { + errors.Add(new OperationError + { + Code = "400", + Description = localizer["Direction title cannot be empty."] + }); + } if (repository.Get(whereExpression: x => x.Title == dto.Title).Any()) { // ... existing duplicate check ... } return errors; }
272-283
: Extract complex predicates into separate methods.Consider extracting the institution hierarchy predicates into separate methods for better readability and maintainability.
+ private Expression<Func<Direction, bool>> BuildInstitutionHierarchyPredicate(long institutionId) + { + return PredicateBuilder.True<Direction>() + .And<Direction>(d => d.SubDirections + .Any(s => s.InstitutionHierarchies.Any(h => h.InstitutionId == institutionId))); + } if (currentUserService.IsMinistryAdmin()) { var ministryAdmin = await ministryAdminService.GetByUserId(currentUserService.UserId); - predicate = predicate - .And<Direction>(d => d.SubDirections. - Any(s => s.InstitutionHierarchies.Any(h => h.InstitutionId == ministryAdmin.InstitutionId))); + predicate = predicate.And(BuildInstitutionHierarchyPredicate(ministryAdmin.InstitutionId)); // ... rest of the code }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs
(7 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/IDirectionService.cs
(2 hunks)OutOfSchool/OutOfSchool.BusinessLogic/Services/ISensitiveDirectionService.cs
(1 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/DirectionControllerTests.cs
(5 hunks)OutOfSchool/OutOfSchool.WebApi.Tests/Services/DirectionServiceTests.cs
(7 hunks)OutOfSchool/OutOfSchool.WebApi/Controllers/V1/DirectionController.cs
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- OutOfSchool/OutOfSchool.BusinessLogic/Services/IDirectionService.cs
- OutOfSchool/OutOfSchool.BusinessLogic/Services/ISensitiveDirectionService.cs
- OutOfSchool/OutOfSchool.WebApi/Controllers/V1/DirectionController.cs
🔇 Additional comments (10)
OutOfSchool/OutOfSchool.WebApi.Tests/Controllers/DirectionControllerTests.cs (4)
13-13
: LGTM!The added import is necessary to support the new
Result<T>
type usage in the tests.
139-149
: Great improvement in error handling!The test now correctly expects a 404 NotFound response for invalid IDs, which aligns with RESTful best practices. This is a better approach than returning null.
156-157
: LGTM!The test now properly handles the new
Result<T>
pattern, which provides better error handling and success state management.
215-227
: LGTM!The addition of IDs to the fake test data is necessary for proper testing of ID-based operations.
OutOfSchool/OutOfSchool.WebApi.Tests/Services/DirectionServiceTests.cs (5)
12-12
: LGTM!The addition of the BusinessLogic.Common namespace import is necessary to support the Result pattern implementation.
97-98
: LGTM!The test methods have been correctly updated to handle the Result return type:
- Assertions now properly access the Value property
- Failure case validation checks both the Result object and its error details
Also applies to: 103-120
163-169
: Update test name to match new behavior.The test name suggests it's expecting an exception, but the implementation checks for a null result.
198-198
: LGTM!The error message has been improved for better readability.
313-330
: LGTM!The seed data has been properly updated to reflect the new data model structure with SubDirection and its relationships.
OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs (1)
200-233
: LGTM! Comprehensive error handling improvements.The changes properly handle null input, non-existent entities, and use appropriate error codes and messages.
catch (DbUpdateConcurrencyException ex) | ||
{ | ||
logger.LogError($"Deleting failed. Direction with Id = {id} doesn't exist in the system."); | ||
throw; | ||
logger.LogError(ex, "Deleting failed. Direction with Id = {id} doesn't exist in the system.", id); | ||
throw new DbUpdateConcurrencyException(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Improve exception handling.
Re-throwing the exception loses the stack trace. Consider either:
- Just throwing the original exception:
throw;
- Or wrapping it in a custom exception with more context.
- throw new DbUpdateConcurrencyException();
+ throw; // Preserve the original stack trace
📝 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.
catch (DbUpdateConcurrencyException ex) | |
{ | |
logger.LogError($"Deleting failed. Direction with Id = {id} doesn't exist in the system."); | |
throw; | |
logger.LogError(ex, "Deleting failed. Direction with Id = {id} doesn't exist in the system.", id); | |
throw new DbUpdateConcurrencyException(); | |
} | |
catch (DbUpdateConcurrencyException ex) | |
{ | |
logger.LogError(ex, "Deleting failed. Direction with Id = {id} doesn't exist in the system.", id); | |
throw; // Preserve the original stack trace | |
} |
public async Task<DirectionDto> GetById(long id) | ||
{ | ||
logger.LogInformation($"Getting Direction by Id started. Looking Id = {id}."); | ||
logger.LogDebug("Getting Direction by Id started. Looking Id = {id}.", id); | ||
|
||
var direction = await repository.GetById((int)id).ConfigureAwait(false); | ||
|
||
if (direction == null) | ||
{ | ||
throw new ArgumentOutOfRangeException( | ||
nameof(id), | ||
localizer["The id cannot be greater than number of table entities."]); | ||
logger.LogError("Direction with Id = {id} doesn't exist in the system.", id); | ||
return null; | ||
} | ||
|
||
logger.LogInformation($"Successfully got a Direction with Id = {id}."); | ||
logger.LogDebug("Successfully got a Direction with Id = {id}.", id); | ||
|
||
return mapper.Map<DirectionDto>(direction); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider using Result pattern for consistency.
For consistency with other methods, consider returning Result instead of null.
- public async Task<DirectionDto> GetById(long id)
+ public async Task<Result<DirectionDto>> GetById(long id)
{
logger.LogDebug("Getting Direction by Id started. Looking Id = {id}.", id);
var direction = await repository.GetById((int)id).ConfigureAwait(false);
if (direction == null)
{
logger.LogError("Direction with Id = {id} doesn't exist in the system.", id);
- return null;
+ return Result<DirectionDto>.Failed(new OperationError
+ {
+ Code = "404",
+ Description = $"Direction with Id = {id} does not exist."
+ });
}
logger.LogDebug("Successfully got a Direction with Id = {id}.", id);
- return mapper.Map<DirectionDto>(direction);
+ return Result<DirectionDto>.Success(mapper.Map<DirectionDto>(direction));
}
📝 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 async Task<DirectionDto> GetById(long id) | |
{ | |
logger.LogInformation($"Getting Direction by Id started. Looking Id = {id}."); | |
logger.LogDebug("Getting Direction by Id started. Looking Id = {id}.", id); | |
var direction = await repository.GetById((int)id).ConfigureAwait(false); | |
if (direction == null) | |
{ | |
throw new ArgumentOutOfRangeException( | |
nameof(id), | |
localizer["The id cannot be greater than number of table entities."]); | |
logger.LogError("Direction with Id = {id} doesn't exist in the system.", id); | |
return null; | |
} | |
logger.LogInformation($"Successfully got a Direction with Id = {id}."); | |
logger.LogDebug("Successfully got a Direction with Id = {id}.", id); | |
return mapper.Map<DirectionDto>(direction); | |
} | |
public async Task<Result<DirectionDto>> GetById(long id) | |
{ | |
logger.LogDebug("Getting Direction by Id started. Looking Id = {id}.", id); | |
var direction = await repository.GetById((int)id).ConfigureAwait(false); | |
if (direction == null) | |
{ | |
logger.LogError("Direction with Id = {id} doesn't exist in the system.", id); | |
return Result<DirectionDto>.Failed(new OperationError | |
{ | |
Code = "404", | |
Description = $"Direction with Id = {id} does not exist." | |
}); | |
} | |
logger.LogDebug("Successfully got a Direction with Id = {id}.", id); | |
return Result<DirectionDto>.Success(mapper.Map<DirectionDto>(direction)); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs (3)
138-183
: Add pagination parameter validation.The method accepts pagination parameters (From, Size) but doesn't validate them. This could lead to performance issues with large page sizes.
Add validation at the beginning of the method:
public async Task<SearchResult<DirectionDto>> GetByFilter(DirectionFilter filter, bool isAdmins) { logger.LogDebug("Getting Directions by filter started."); + if (filter.Size <= 0 || filter.Size > 100) // Adjust max size as needed + { + filter.Size = 10; // Default size + } + + if (filter.From < 0) + { + filter.From = 0; + } + var (predicate, workshopCountFilter) = await BuildPredicate(filter, isAdmins).ConfigureAwait(false);
239-255
: Enhance validation rules.The validation only checks for duplicate titles. Consider adding validations for:
- Required properties (Title)
- String length limits
- Invalid characters
Add additional validations:
private List<OperationError> DirectionValidation(DirectionDto dto) { var errors = new List<OperationError>(); + if (string.IsNullOrWhiteSpace(dto.Title)) + { + errors.Add(new OperationError + { + Code = "400", + Description = localizer["Title is required."] + }); + } + else if (dto.Title.Length > 100) // Adjust max length as needed + { + errors.Add(new OperationError + { + Code = "400", + Description = localizer["Title exceeds maximum length."] + }); + } + if (repository.Get(whereExpression: x => x.Title == dto.Title).Any()) {
261-266
: Optimize string search performance.The current implementation performs case-insensitive contains check on both Direction and SubDirection titles. Consider normalizing the search term and using an indexed column for better performance.
Optimize the search:
if (!string.IsNullOrWhiteSpace(filter.Name)) { + var searchTerm = filter.Name.Trim().ToUpperInvariant(); predicate = predicate - .And(direction => direction.Title.Contains(filter.Name, StringComparison.InvariantCultureIgnoreCase) || - direction.SubDirections.Any(s => s.Title.Contains(filter.Name, StringComparison.InvariantCultureIgnoreCase))); + .And(direction => direction.NormalizedTitle.Contains(searchTerm) || + direction.SubDirections.Any(s => s.NormalizedTitle.Contains(searchTerm))); }Note: This assumes you'll add a normalized (uppercase) title column to both Direction and SubDirection entities, which should be maintained at insert/update time.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs
(7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: test (macOS-latest)
- GitHub Check: test (windows-latest)
- GitHub Check: SonarCloud
- GitHub Check: test (ubuntu-latest)
🔇 Additional comments (2)
OutOfSchool/OutOfSchool.BusinessLogic/Services/DirectionService.cs (2)
114-122
: Improve exception handling.Re-throwing the exception loses the stack trace. Consider either:
- Just throwing the original exception:
throw;
- Or wrapping it in a custom exception with more context.
186-201
: Consider using Result pattern for consistency.For consistency with other methods, consider returning Result instead of null.
public async Task<Result<DirectionDto>> Create(DirectionDto dto) | ||
{ | ||
logger.LogInformation("Direction creating was started."); | ||
logger.LogDebug("Direction creating was started."); | ||
|
||
var direction = mapper.Map<Direction>(dto); | ||
|
||
DirectionValidation(dto); | ||
var validationErrors = DirectionValidation(dto); | ||
if (validationErrors.Any()) | ||
{ | ||
return Result<DirectionDto>.Failed(validationErrors.ToArray()); | ||
} | ||
|
||
var newDirection = await repository.Create(direction).ConfigureAwait(false); | ||
|
||
logger.LogInformation($"Direction with Id = {newDirection?.Id} created successfully."); | ||
logger.LogDebug("Direction with Id = {id} created successfully.", newDirection?.Id); | ||
|
||
return mapper.Map<DirectionDto>(newDirection); | ||
return Result<DirectionDto>.Success(mapper.Map<DirectionDto>(newDirection)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add null check for input parameter.
The Create method should validate that the input dto is not null before proceeding with the operation.
Add this check at the beginning of the method:
public async Task<Result<DirectionDto>> Create(DirectionDto dto)
{
logger.LogDebug("Direction creating was started.");
+ if (dto == null)
+ {
+ logger.LogError("Creation failed. Dto is null");
+ return Result<DirectionDto>.Failed(new OperationError
+ {
+ Code = "400",
+ Description = "Direction dto cannot be null"
+ });
+ }
+
var direction = mapper.Map<Direction>(dto);
📝 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 async Task<Result<DirectionDto>> Create(DirectionDto dto) | |
{ | |
logger.LogInformation("Direction creating was started."); | |
logger.LogDebug("Direction creating was started."); | |
var direction = mapper.Map<Direction>(dto); | |
DirectionValidation(dto); | |
var validationErrors = DirectionValidation(dto); | |
if (validationErrors.Any()) | |
{ | |
return Result<DirectionDto>.Failed(validationErrors.ToArray()); | |
} | |
var newDirection = await repository.Create(direction).ConfigureAwait(false); | |
logger.LogInformation($"Direction with Id = {newDirection?.Id} created successfully."); | |
logger.LogDebug("Direction with Id = {id} created successfully.", newDirection?.Id); | |
return mapper.Map<DirectionDto>(newDirection); | |
return Result<DirectionDto>.Success(mapper.Map<DirectionDto>(newDirection)); | |
} | |
public async Task<Result<DirectionDto>> Create(DirectionDto dto) | |
{ | |
logger.LogDebug("Direction creating was started."); | |
if (dto == null) | |
{ | |
logger.LogError("Creation failed. Dto is null"); | |
return Result<DirectionDto>.Failed(new OperationError | |
{ | |
Code = "400", | |
Description = "Direction dto cannot be null" | |
}); | |
} | |
var direction = mapper.Map<Direction>(dto); | |
var validationErrors = DirectionValidation(dto); | |
if (validationErrors.Any()) | |
{ | |
return Result<DirectionDto>.Failed(validationErrors.ToArray()); | |
} | |
var newDirection = await repository.Create(direction).ConfigureAwait(false); | |
logger.LogDebug("Direction with Id = {id} created successfully.", newDirection?.Id); | |
return Result<DirectionDto>.Success(mapper.Map<DirectionDto>(newDirection)); | |
} |
public async Task<Result<DirectionDto>> Update(DirectionDto dto) | ||
{ | ||
logger.LogInformation($"Updating Direction with Id = {dto?.Id} started."); | ||
logger.LogDebug("Updating Direction with Id = {id} started.", dto?.Id); | ||
|
||
ArgumentNullException.ThrowIfNull(dto); | ||
if (dto == null) | ||
{ | ||
logger.LogError("Updating failed. Dto is null"); | ||
return Result<DirectionDto>.Failed(new OperationError | ||
{ | ||
Code = "400", | ||
Description = $"Dto is null.", | ||
}); | ||
} | ||
|
||
|
||
var direction = await repository.GetById(dto.Id).ConfigureAwait(false); | ||
|
||
if (direction is null) | ||
{ | ||
logger.LogError($"Updating failed. Direction with Id = {dto?.Id} doesn't exist in the system."); | ||
throw new DbUpdateConcurrencyException($"Updating failed. Direction with Id = {dto?.Id} doesn't exist in the system."); | ||
logger.LogError("Updating failed. Direction with Id = {id} doesn't exist in the system.", dto.Id); | ||
return Result<DirectionDto>.Failed(new OperationError | ||
{ | ||
Code = "404", | ||
Description = $"Direction with Id = {dto.Id} does not exist.", | ||
}); | ||
} | ||
|
||
mapper.Map(dto, direction); | ||
direction = await repository.Update(direction).ConfigureAwait(false); | ||
|
||
logger.LogInformation($"Direction with Id = {direction?.Id} updated succesfully."); | ||
logger.LogDebug("Direction with Id = {id} updated succesfully.", direction?.Id); | ||
|
||
return mapper.Map<DirectionDto>(direction); | ||
return Result<DirectionDto>.Success(mapper.Map<DirectionDto>(direction)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add property validation before update.
The method validates that dto is not null but doesn't validate its properties. Consider reusing DirectionValidation method.
Add validation before updating:
if (direction is null)
{
logger.LogError("Updating failed. Direction with Id = {id} doesn't exist in the system.", dto.Id);
return Result<DirectionDto>.Failed(new OperationError
{
Code = "404",
Description = $"Direction with Id = {dto.Id} does not exist.",
});
}
+ var validationErrors = DirectionValidation(dto);
+ if (validationErrors.Any())
+ {
+ return Result<DirectionDto>.Failed(validationErrors.ToArray());
+ }
+
mapper.Map(dto, direction);
📝 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 async Task<Result<DirectionDto>> Update(DirectionDto dto) | |
{ | |
logger.LogInformation($"Updating Direction with Id = {dto?.Id} started."); | |
logger.LogDebug("Updating Direction with Id = {id} started.", dto?.Id); | |
ArgumentNullException.ThrowIfNull(dto); | |
if (dto == null) | |
{ | |
logger.LogError("Updating failed. Dto is null"); | |
return Result<DirectionDto>.Failed(new OperationError | |
{ | |
Code = "400", | |
Description = $"Dto is null.", | |
}); | |
} | |
var direction = await repository.GetById(dto.Id).ConfigureAwait(false); | |
if (direction is null) | |
{ | |
logger.LogError($"Updating failed. Direction with Id = {dto?.Id} doesn't exist in the system."); | |
throw new DbUpdateConcurrencyException($"Updating failed. Direction with Id = {dto?.Id} doesn't exist in the system."); | |
logger.LogError("Updating failed. Direction with Id = {id} doesn't exist in the system.", dto.Id); | |
return Result<DirectionDto>.Failed(new OperationError | |
{ | |
Code = "404", | |
Description = $"Direction with Id = {dto.Id} does not exist.", | |
}); | |
} | |
mapper.Map(dto, direction); | |
direction = await repository.Update(direction).ConfigureAwait(false); | |
logger.LogInformation($"Direction with Id = {direction?.Id} updated succesfully."); | |
logger.LogDebug("Direction with Id = {id} updated succesfully.", direction?.Id); | |
return mapper.Map<DirectionDto>(direction); | |
return Result<DirectionDto>.Success(mapper.Map<DirectionDto>(direction)); | |
} | |
public async Task<Result<DirectionDto>> Update(DirectionDto dto) | |
{ | |
logger.LogDebug("Updating Direction with Id = {id} started.", dto?.Id); | |
if (dto == null) | |
{ | |
logger.LogError("Updating failed. Dto is null"); | |
return Result<DirectionDto>.Failed(new OperationError | |
{ | |
Code = "400", | |
Description = $"Dto is null.", | |
}); | |
} | |
var direction = await repository.GetById(dto.Id).ConfigureAwait(false); | |
if (direction is null) | |
{ | |
logger.LogError("Updating failed. Direction with Id = {id} doesn't exist in the system.", dto.Id); | |
return Result<DirectionDto>.Failed(new OperationError | |
{ | |
Code = "404", | |
Description = $"Direction with Id = {dto.Id} does not exist.", | |
}); | |
} | |
var validationErrors = DirectionValidation(dto); | |
if (validationErrors.Any()) | |
{ | |
return Result<DirectionDto>.Failed(validationErrors.ToArray()); | |
} | |
mapper.Map(dto, direction); | |
direction = await repository.Update(direction).ConfigureAwait(false); | |
logger.LogDebug("Direction with Id = {id} updated succesfully.", direction?.Id); | |
return Result<DirectionDto>.Success(mapper.Map<DirectionDto>(direction)); | |
} |
|
Summary by CodeRabbit
New Features
Improvements
Refactor