Feature/issue 309 changing photo logic from base64 to url approach - #318
Conversation
WalkthroughReplaces embedded Base64 payloads with URL-based image access, refactors local blob service to filesystem + URL generation, introduces a BlobStorageExceptions hierarchy, renames Image.Base64→Url (entity/DTO), updates mappings/handlers to use URLs and TransactionScope/SaveChangesAsync, adjusts DI/config (IHttpContextAccessor, RootPath/ImagesSubPath), and updates tests/seeders and .gitignore. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant API as WebAPI
participant Handler as CreateImageHandler
participant Repo as Repository
participant Blob as BlobService
note right of Handler #f7f7d9: create flow (DB first, then file)
Client->>API: POST /api/images (Base64, MimeType)
API->>Handler: Handle(command)
Handler->>Repo: Begin TransactionScope
Handler->>Repo: CreateAsync(image entity)
Handler->>Repo: SaveChangesAsync()
Handler->>Blob: SaveFileInStorageAsync(Base64, BlobName, MimeType)
Handler->>Repo: scope.Complete()
Handler->>Blob: GetFileUrl(BlobName, MimeType)
Handler->>API: Result<ImageDTO { Url, ... }>
API-->>Client: 200 OK + DTO
sequenceDiagram
autonumber
participant Client
participant API as WebAPI
participant Handler as GetImageByIdHandler
participant Repo as Repository
participant Blob as BlobService
Client->>API: GET /api/images/{id}
API->>Handler: Handle(query)
Handler->>Repo: Find image by id
alt image exists & BlobName valid
Handler->>Blob: GetFileUrl(BlobName, MimeType)
Handler->>API: Result<ImageDTO { Url }>
API-->>Client: 200 OK
else missing / invalid
Handler-->>API: Error Result (400/404)
API-->>Client: 400/404
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 17
🔭 Outside diff range comments (2)
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (2)
80-87: Remove unused image retrieval logic.This code fetches an image from the repository but doesn't use the result for anything. Since the blob service dependency was removed, this appears to be leftover code from the refactoring.
var resultDto = _mapper.Map<TeamMember, TeamMemberDto>(entityToUpdate); -if (entityToUpdate.ImageId != null) -{ - Image? image = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync( - new QueryOptions<Image>() - { - Filter = i => i.Id == entityToUpdate.ImageId - }); -}
95-98: Remove unreachable exception handler.The
BlobStorageExceptioncatch block is now unreachable since all blob storage operations were removed from this handler. This creates dead code that could confuse future maintainers.-catch (BlobStorageException e) -{ - return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}" ); -}
🧹 Nitpick comments (8)
VictoryCenter/VictoryCenter.DAL/Entities/Image.cs (1)
12-12: Consider usingSystem.Uritype for stronger type safety.The static analysis tool correctly identifies that URL properties should ideally use
System.Uriinstead ofstringfor better type safety and validation. However, this depends on your architecture decisions:Pros of using
Uri:
- Built-in URL validation
- Type safety prevents invalid URL assignments
- Clear semantic intent
Potential considerations:
- EF Core serialization behavior with
Uritypes- JSON serialization in API responses
- Consistency with existing codebase patterns
If you decide to keep
string, consider adding validation attributes or custom validation logic to ensure URL validity.- public string? Url { get; set; } + public Uri? Url { get; set; }VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (1)
6-6: Consider parameter naming consistency and return type.The new
GetFileUrlmethod has a few minor considerations:
- Parameter naming inconsistency:
mimeType(camelCase) vsmimetype(lowercase). For consistency with line 7, consider usingmimeType:- string GetFileUrl(string name, string mimeType); + string GetFileUrl(string name, string mimeType);
Synchronous method in async interface: While this method being synchronous makes sense for URL generation, it creates a mixed pattern. Consider documenting why this method doesn't need to be async.
Return type consideration: Similar to other files, consider
Uriinstead ofstringfor type safety, though this should be consistent across the codebase.The method design aligns well with the architectural changes - URL generation is typically a lightweight, synchronous operation.
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (1)
80-83: Consider removing obsolete exception handling.Since the handler no longer directly uses IBlobService, the BlobStorageException catch block may be unnecessary. The AutoMapper resolver might handle blob storage exceptions differently.
Verify if BlobStorageException can still be thrown in this context:
#!/bin/bash # Description: Check if BlobStorageException is still thrown by AutoMapper or other dependencies # Search for BlobStorageException usage in AutoMapper resolvers and related code ast-grep --pattern 'throw new BlobStorageException($$$)'VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs (2)
101-131: Consider adding proper cleanup mechanism for test dataThe cleanup operation at line 130 might not execute if any assertion fails, potentially leaving test data in the system. Consider wrapping the test logic in a try-finally block or using a more robust cleanup pattern.
[Fact] public async Task DeleteImage_MultipleImages_ShouldDeleteOnlySpecified() { // Arrange: Створюємо два тестових зображення var image1 = await CreateTestImageAsync("test-image-1"); var image2 = await CreateTestImageAsync("test-image-2"); - string filePath1 = Path.Combine(_blobEnvironment.FullPath, $"{image1.BlobName}.png"); - string filePath2 = Path.Combine(_blobEnvironment.FullPath, $"{image2.BlobName}.png"); - - // Act: Видаляємо тільки перше зображення - HttpResponseMessage response = await _client.DeleteAsync($"api/Image/{image1.Id}"); - - // Assert: Перевіряємо результат - Assert.True(response.IsSuccessStatusCode); - - // Перше зображення має бути видалене - var deletedImage1 = await _dbContext.Images.AsNoTracking() - .FirstOrDefaultAsync(e => e.Id == image1.Id); - Assert.Null(deletedImage1); - Assert.False(File.Exists(filePath1)); - - // Друге зображення має залишитися - var remainingImage2 = await _dbContext.Images.AsNoTracking() - .FirstOrDefaultAsync(e => e.Id == image2.Id); - Assert.NotNull(remainingImage2); - Assert.True(File.Exists(filePath2)); - - // Cleanup: Видаляємо друге зображення - await _client.DeleteAsync($"api/Image/{image2.Id}"); + try + { + string filePath1 = Path.Combine(_blobEnvironment.FullPath, $"{image1.BlobName}.png"); + string filePath2 = Path.Combine(_blobEnvironment.FullPath, $"{image2.BlobName}.png"); + + // Act: Видаляємо тільки перше зображення + HttpResponseMessage response = await _client.DeleteAsync($"api/Image/{image1.Id}"); + + // Assert: Перевіряємо результат + Assert.True(response.IsSuccessStatusCode); + + // Перше зображення має бути видалене + var deletedImage1 = await _dbContext.Images.AsNoTracking() + .FirstOrDefaultAsync(e => e.Id == image1.Id); + Assert.Null(deletedImage1); + Assert.False(File.Exists(filePath1)); + + // Друге зображення має залишитися + var remainingImage2 = await _dbContext.Images.AsNoTracking() + .FirstOrDefaultAsync(e => e.Id == image2.Id); + Assert.NotNull(remainingImage2); + Assert.True(File.Exists(filePath2)); + } + finally + { + // Cleanup: Видаляємо друге зображення + await _client.DeleteAsync($"api/Image/{image2.Id}"); + } }
133-136: Use long literals in InlineData attributesBased on the VictoryCenter project conventions, InlineData attributes should provide long literals when testing long parameters.
[Theory] -[InlineData(-1)] -[InlineData(0)] +[InlineData(-1L)] +[InlineData(0L)] public async Task DeleteImage_InvalidIdValues_ShouldReturnNotFound(long invalidId)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1)
68-71: Use long literals in InlineData attributesFor consistency with VictoryCenter project conventions, use long literals in InlineData attributes.
[Theory] -[InlineData(-1)] -[InlineData(0)] +[InlineData(-1L)] +[InlineData(0L)] public async Task GetImageById_InvalidIdValues_ShouldReturnNotFound(long invalidId)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (1)
230-249: Simplify code by removing redundant variableThe
subStringvariable at line 234 is unnecessary as it just copies thesubPathparameter without any transformation.private async Task<Image> CreateTestImageAsync(string? subPath, string blobName, string? customMimeType = null ) { var mimeType = customMimeType ?? "image/png"; var extension = GetExtensionFromMimeType(mimeType); - var subString = subPath; - var url = $"http://localhost/{subString}/{blobName}.{extension}"; + var url = $"http://localhost/{subPath}/{blobName}.{extension}"; var image = new Image { BlobName = blobName, MimeType = mimeType, Url = url, CreatedAt = DateTime.UtcNow }; _dbContext.Images.Add(image); await _dbContext.SaveChangesAsync(); return image; }VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs (1)
102-105: Use long literals in InlineData attributesFor consistency with VictoryCenter project conventions, use long literals in InlineData attributes.
[Theory] -[InlineData(-1)] -[InlineData(0)] +[InlineData(-1L)] +[InlineData(0L)] public async Task UpdateImage_InvalidIdValues_ShouldReturnNotFound(long invalidId)
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (37)
.gitignore(1 hunks)VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs(2 hunks)VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageResolver/ImageToUrlResolver.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImagesProfile.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Queries/Images/GetByName/GetImageByNameHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetPublished/GetPublishedTeamMembersHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs(5 hunks)VictoryCenter/VictoryCenter.DAL/Data/EntityTypeConfigurations/ImageConfig.cs(1 hunks)VictoryCenter/VictoryCenter.DAL/Entities/Image.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs(3 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs(3 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs(4 hunks)VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/ImageSeeder/ImagesSeeder.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/IntegrationTestsDatabaseSeeder.cs(1 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs(2 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs(3 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageByName.cs(5 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/UpdateImage.cs(2 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs(4 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs(5 hunks)VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs(2 hunks)VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs(2 hunks)VictoryCenter/VictoryCenter.WebAPI/Program.cs(2 hunks)VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json(1 hunks)VictoryCenter/VictoryCenter.WebAPI/appsettings.json(1 hunks)
💤 Files with no reviewable changes (6)
- VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs
- VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs
- VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs
- VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs
- VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetPublished/GetPublishedTeamMembersHandler.cs
- VictoryCenter/VictoryCenter.BLL/Queries/Images/GetByName/GetImageByNameHandler.cs
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: in the victorycenter project, the jwt secret key in appsettings.json is temporarily hard-coded for d...
Learnt from: NovickVitaliy
PR: ita-social-projects/VictoryCenter-Back#201
File: VictoryCenter/VictoryCenter.WebAPI/appsettings.json:16-16
Timestamp: 2025-06-26T08:26:15.124Z
Learning: In the VictoryCenter project, the JWT secret key in appsettings.json is temporarily hard-coded for development purposes and will be removed/replaced with secure configuration in future stages.
Applied to files:
VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.jsonVictoryCenter/VictoryCenter.WebAPI/appsettings.jsonVictoryCenter/VictoryCenter.WebAPI/Program.csVictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.csVictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.csVictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
📚 Learning: in the victorycenter project integration tests, using an empty claims array for createaccesstoken in...
Learnt from: NovickVitaliy
PR: ita-social-projects/VictoryCenter-Back#201
File: VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Base/IntegrationTestDbFixture.cs:48-48
Timestamp: 2025-06-27T08:50:52.032Z
Learning: In the VictoryCenter project integration tests, using an empty claims array for CreateAccessToken in the IntegrationTestDbFixture is sufficient for authentication purposes, as confirmed by the project maintainer.
Applied to files:
VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.jsonVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs
📚 Learning: in the victorycenter test seeder for teammember entities, the last category in the categories list i...
Learnt from: VladimirSushinsky
PR: ita-social-projects/VictoryCenter-Back#114
File: VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/TeamMembersSeeder/TeamMemberSeeder.cs:20-24
Timestamp: 2025-06-17T20:32:16.009Z
Learning: In the VictoryCenter test seeder for TeamMember entities, the last category in the categories list is intentionally excluded from having team members assigned to it (using `categories[i % (categories.Count - 1)].Id`). This design ensures that the last category remains available for delete tests without foreign key constraint violations, as delete operations require categories with no related team members.
Applied to files:
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/IntegrationTestsDatabaseSeeder.csVictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.csVictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/ImageSeeder/ImagesSeeder.cs
📚 Learning: in victorycenter project, when writing unit tests for deleteteammembercommand, the parameter types s...
Learnt from: VladimirSushinsky
PR: ita-social-projects/VictoryCenter-Back#177
File: VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/DeleteTeamMemberTests.cs:47-47
Timestamp: 2025-06-20T18:50:30.605Z
Learning: In VictoryCenter project, when writing unit tests for DeleteTeamMemberCommand, the parameter types should use `long` to match the command constructor and entity ID type. InlineData attributes should provide `long` literals (e.g., -1L, 0L) rather than int values.
Applied to files:
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.csVictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.csVictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.csVictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.csVictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs
📚 Learning: in the victorycenter project, team member categories typically contain about 10 members in productio...
Learnt from: Oleh-Bashtovyi
PR: ita-social-projects/VictoryCenter-Back#179
File: VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Reorder/ReorderTeamMembersHandler.cs:48-54
Timestamp: 2025-06-20T18:26:26.262Z
Learning: In the VictoryCenter project, team member categories typically contain about 10 members in production, so O(n²) operations on team members are not performance-critical and readability should be prioritized over micro-optimizations.
Applied to files:
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs
📚 Learning: in the victorycenter codebase, fluentvalidation is used for input validation in mediatr handlers. th...
Learnt from: Oleh-Bashtovyi
PR: ita-social-projects/VictoryCenter-Back#179
File: VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Reorder/ReorderTeamMembersHandler.cs:27-28
Timestamp: 2025-06-20T18:22:51.823Z
Learning: In the VictoryCenter codebase, FluentValidation is used for input validation in MediatR handlers. The handlers call ValidateAndThrowAsync() early in the Handle method, and validation exceptions are caught and converted to Result.Fail responses. This means validation logic should be kept in the validator classes rather than duplicated in the handlers.
Applied to files:
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.csVictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs
🪛 GitHub Check: Build and analyze
VictoryCenter/VictoryCenter.DAL/Entities/Image.cs
[warning] 12-12:
Change the 'Url' property type to 'System.Uri'. (https://rules.sonarsource.com/csharp/RSPEC-3996)
[warning] 12-12:
Change the 'Url' property type to 'System.Uri'. (https://rules.sonarsource.com/csharp/RSPEC-3996)
🔇 Additional comments (28)
.gitignore (1)
161-161: Confirmwwwrootexclusion won’t drop essential static assetsThe pattern
*wwwroot/will cause git to ignore every folder namedwwwrootat any depth.
If your web projects still rely on checked-in fallback images, CSS, or JS that must ship with the artifact, those files will silently become untracked. Please verify that all necessary static content is produced during build/deploy (or fetched from blob storage) before keeping this rule; otherwise narrow the pattern or add explicit negations for required files.VictoryCenter/VictoryCenter.DAL/Data/EntityTypeConfigurations/ImageConfig.cs (1)
29-29: LGTM! Configuration correctly updated to match entity changes.The update to ignore the
Urlproperty instead ofBase64is consistent with the entity model changes and maintains the correct behavior of not persisting dynamically generated URLs to the database.VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs (1)
40-55: Good refactoring to align with new architectureThe renaming from
BlobCryptographyExceptiontoImageProcessingExceptionproperly reflects the removal of encryption logic and the shift to general image processing. The exception structure maintains proper inheritance and constructor patterns.VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json (1)
8-8: Verify the ImagesSubPath value for potential typoThe value "IntegrationTekdnkdsts" appears to contain a typo. Was this intended to be "IntegrationTests"?
If this is intentional for uniqueness in testing, please confirm. Otherwise, consider correcting:
- "ImagesSubPath": "IntegrationTekdnkdsts" + "ImagesSubPath": "IntegrationTests"VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/IntegrationTestsDatabaseSeeder.cs (1)
23-23: Correct update to use new path propertyThe change from
BlobStorePathtoFullPathproperly aligns with the refactoredBlobEnvironmentVariablesstructure.VictoryCenter/VictoryCenter.WebAPI/appsettings.json (1)
8-8: Configuration simplification looks good!The replacement of separate
BlobStoreKeyandBlobStorePathwith a singleImagesSubPathproperty aligns well with the transition to URL-based image handling. This simplification makes the configuration more maintainable.VictoryCenter/VictoryCenter.WebAPI/Program.cs (2)
13-13: Essential addition for URL generation!Adding
HttpContextAccessoris crucial for the blob service to generate full URLs based on the current HTTP request context. This supports the transition from Base64 to URL-based image handling.
32-32: Static file serving correctly configured!The
UseStaticFiles()middleware enables direct serving of image files from the file system, which is essential for the new URL-based approach. The placement in the middleware pipeline is appropriate.VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/UpdateImage.cs (2)
42-42: Test data correctly updated for URL-based approach!The change from
Base64toUrlproperty in the test DTO aligns perfectly with the broader refactoring to URL-based image handling.
97-97: Assertion properly updated!The test assertion now correctly validates the
Urlproperty instead ofBase64, maintaining test coverage for the new image handling approach.VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs (2)
41-41: Test DTO correctly aligned with new architecture!The update from
Base64toUrlproperty in the test data properly reflects the transition to URL-based image handling.
93-93: Assertion updated appropriately!The test now validates the
Urlproperty instead ofBase64, ensuring continued test coverage for the refactored image handling logic.VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (2)
52-52: Good preservation of original MIME type!Capturing the previous MIME type before updating the entity is a smart approach that allows the blob service to properly handle the file type transition during updates.
64-64: Confirmed blob service signature matches usage
TheIBlobService.UpdateFileInStorageAsyncmethod inIBlobService.csindeed accepts the original MIME type (previousMimeType) followed by the new blob name and new MIME type, and theBlobServiceimplementation inBlobService.csaligns with this signature. The call inUpdateImageHandler.cs(line 64) is correct—no changes required.VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs (1)
66-66: LGTM! Assertion updated correctly.The test assertion properly validates the URL property instead of Base64, aligning with the refactoring objectives.
VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs (2)
221-235: LGTM! Ukrainian text properly encoded.The category data has been corrected with proper Ukrainian text encoding, improving readability and data quality.
293-299: LGTM! Proper configuration for local blob storage.The PostConfigure step correctly sets the RootPath from the web hosting environment, supporting the new blob storage structure with RootPath and ImagesSubPath properties.
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs (1)
5-8: LGTM! Well-designed configuration structure.The new properties properly support the blob storage refactoring:
- Required properties ensure configuration completeness
- Setters enable runtime configuration via PostConfigure
- Path.Combine ensures cross-platform compatibility
- FullPath provides convenient access to the combined path
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs (1)
75-75: LGTM! Test constructor calls updated consistently.All handler instantiations have been properly updated to remove the IBlobService dependency, aligning with the refactored CreateTeamMemberHandler constructor.
Also applies to: 91-91, 110-110, 140-140
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (1)
22-22: LGTM! Constructor updated correctly.The IBlobService dependency has been properly removed, aligning with the shift to URL-based image handling through AutoMapper configuration.
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageByName.cs (2)
29-32: LGTM! Test data properly updated for URL-based approach.The test setup correctly reflects the new architecture with
BlobNameexcluding the file extension and theUrlproperty containing the expected URL.
50-51: Mock setup correctly updated for synchronous URL generation.The change from
FindFileInStorageAsBase64AsynctoGetFileUrlaligns with the new URL-based approach and properly returns a URL string instead of Base64 content.VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (1)
124-124: Clean removal of blob service dependency.The handler constructor calls have been correctly updated to remove the
IBlobServiceparameter, maintaining consistency with the refactoredUpdateTeamMemberHandlerthat no longer requires blob storage functionality.Also applies to: 157-157, 183-183, 204-204, 223-223
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/ImageSeeder/ImagesSeeder.cs (2)
9-21: Well-structured data format for seeding.The tuple-based data structure clearly separates blob metadata (
blobName,base64Data,mimeType) and provides a clean foundation for the new seeding process.
27-45: Proper implementation of URL-based image seeding.The seeding logic correctly:
- Saves files to blob storage using the service
- Generates URLs through the blob service
- Creates
Imageentities with the generated URL- Uses proper async patterns
This aligns perfectly with the architectural shift from Base64 to URL-based image handling.
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (3)
23-31: Proper test setup for URL-based blob service.The configuration correctly uses the new
BlobEnvironmentVariablesstructure withRootPathandImagesSubPath, and includes the requiredIHttpContextAccessormock for URL generation.
56-71: Excellent test refactoring for URL generation.The test properly mocks the HTTP context and request to simulate URL generation, and verifies the expected HTTPS URL format including the subdirectory path. This thoroughly validates the new URL-based functionality.
43-43: Correct assertion for content verification.Changing from
Assert.NotEqualtoAssert.Equalmakes sense here - we want to verify that the stored file content matches the original content, not that it's different (which was likely the case when encryption was involved).
| public long Id { get; init; } | ||
| public string BlobName { get; init; } = null!; | ||
| public string Base64 { get; set; } = null!; | ||
| public string Url { get; set; } = null!; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Address property mutability inconsistency.
The Url property uses { get; set; } while other properties in this record use { get; init; }. This breaks the immutable pattern typically expected with record types.
If AutoMapper requires a setter for mapping, consider using { get; init; } and ensure the mapping configuration properly handles initialization. If mutability is truly required, document why this property needs to be mutable when others don't.
- public string Url { get; set; } = null!;
+ public string Url { get; init; } = null!;Additionally, consider the same type safety improvement mentioned for the entity:
- public string Url { get; init; } = null!;
+ public Uri Url { get; init; } = null!;📝 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 string Url { get; set; } = null!; | |
| public Uri Url { get; init; } = null!; |
🤖 Prompt for AI Agents
In VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs at line 6, the Url
property is declared with a mutable setter { get; set; } while other properties
use immutable setters { get; init; }, breaking the record's immutability
pattern. Change the Url property to use { get; init; } to maintain consistency
and immutability. If AutoMapper requires a setter, adjust the mapping
configuration to support init-only properties. Also, review and apply any
suggested type safety improvements for this property as mentioned for the
entity.
| private static string GetExtensionFromMimeType(string mimeType) | ||
| { | ||
| return mimeType.ToLower() switch | ||
| { | ||
| "image/jpeg" => "jpg", | ||
| "image/jpg" => "jpg", | ||
| "image/png" => "png", | ||
| "image/webp" => "webp", | ||
| _ => "jpg" | ||
| }; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Extract duplicated helper method to shared test utilities
The GetExtensionFromMimeType method is duplicated across all five test files. This violates the DRY principle and makes maintenance harder.
Create a shared test helper class:
// In a new file: VictoryCenter.IntegrationTests/ControllerTests/Images/Helpers/ImageTestHelpers.cs
namespace VictoryCenter.IntegrationTests.ControllerTests.Images.Helpers;
public static class ImageTestHelpers
{
public static string GetExtensionFromMimeType(string mimeType)
{
return mimeType.ToLower() switch
{
"image/jpeg" => "jpg",
"image/jpg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
_ => "jpg"
};
}
}Then update all test files to use the shared helper:
-private static string GetExtensionFromMimeType(string mimeType)
-{
- return mimeType.ToLower() switch
- {
- "image/jpeg" => "jpg",
- "image/jpg" => "jpg",
- "image/png" => "png",
- "image/webp" => "webp",
- _ => "jpg"
- };
-}
+// Use ImageTestHelpers.GetExtensionFromMimeType(mimeType) instead🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs
around lines 311 to 321, the GetExtensionFromMimeType method is duplicated
across multiple test files. To fix this, create a new static helper class
ImageTestHelpers in a new file
VictoryCenter.IntegrationTests/ControllerTests/Images/Helpers/ImageTestHelpers.cs
containing this method. Then remove the duplicate method from this file and all
other test files, and update their code to call
ImageTestHelpers.GetExtensionFromMimeType instead.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (1)
229-248: Helper method doesn't create physical files - inconsistent with other test classesUnlike the
CreateTestImageAsyncmethod inGetImageByIdTest.cs, this version only creates database records without corresponding physical files. This inconsistency could lead to false positive test results if the API expects files to exist in blob storage.Add physical file creation to match the implementation in other test classes:
_dbContext.Images.Add(image); await _dbContext.SaveChangesAsync(); +// Create physical test file for realistic testing +var filePath = Path.Combine(_blobEnv.FullPath, $"{blobName}.{extension}"); +Directory.CreateDirectory(_blobEnv.FullPath); +var testImageBytes = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII="); +await File.WriteAllBytesAsync(filePath, testImageBytes); return image;
🧹 Nitpick comments (2)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)
44-64: Consider using English for code comments for better international collaborationWhile the test implementation is solid, the Ukrainian comments (e.g., "Перевіряємо що відповідь успішна") might create barriers for international contributors. Consider using English to maintain consistency and improve maintainability across diverse teams.
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1)
54-66: Use a more robust approach for non-existent ID testingUsing
long.MaxValuecould theoretically collide with an actual ID if the database sequence reaches this value. Consider using a negative value or querying for the max ID and adding an offset to ensure the ID truly doesn't exist.- var nonExistentId = long.MaxValue; + // Use negative ID which is guaranteed to not exist + var nonExistentId = -999L;
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs(2 hunks)VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs(2 hunks)VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageResolver/ImageToUrlResolver.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImagesProfile.cs(2 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs(5 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs(3 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs(3 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs(3 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageByName.cs(4 hunks)
✅ Files skipped from review due to trivial changes (1)
- VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs
🚧 Files skipped from review as they are similar to previous changes (7)
- VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs
- VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImagesProfile.cs
- VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs
- VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageByName.cs
- VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageResolver/ImageToUrlResolver.cs
- VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: in the victorycenter project, the jwt secret key in appsettings.json is temporarily hard-coded for d...
Learnt from: NovickVitaliy
PR: ita-social-projects/VictoryCenter-Back#201
File: VictoryCenter/VictoryCenter.WebAPI/appsettings.json:16-16
Timestamp: 2025-06-26T08:26:15.124Z
Learning: In the VictoryCenter project, the JWT secret key in appsettings.json is temporarily hard-coded for development purposes and will be removed/replaced with secure configuration in future stages.
Applied to files:
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.csVictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs
⏰ 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: Build and analyze
- GitHub Check: Build and analyze
| private static string GetExtensionFromMimeType(string mimeType) | ||
| { | ||
| return mimeType.ToLower() switch | ||
| { | ||
| "image/jpeg" => "jpg", | ||
| "image/jpg" => "jpg", | ||
| "image/png" => "png", | ||
| "image/webp" => "webp", | ||
| _ => "jpg" | ||
| }; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Extract duplicate helper method to a shared test utility class
The GetExtensionFromMimeType method is duplicated across multiple test files (CreateImageTests.cs, GetImageByIdTest.cs, GetImageByNameTest.cs). Consider extracting this to a shared test utility class to follow the DRY principle and ensure consistent MIME type handling across all tests.
Create a shared utility class:
public static class ImageTestHelper
{
public static string GetExtensionFromMimeType(string mimeType)
{
return mimeType.ToLower() switch
{
"image/jpeg" => "jpg",
"image/jpg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
_ => "jpg"
};
}
}🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs
around lines 168 to 178, the GetExtensionFromMimeType method is duplicated in
multiple test files. Extract this method into a new shared static utility class,
for example ImageTestHelper, within the test project. Replace all occurrences of
this method in the test files with calls to
ImageTestHelper.GetExtensionFromMimeType to centralize the logic and avoid
duplication.
| [Fact] | ||
| public async Task GetImageByName_ImageWithEmptyBlobName_ShouldReturnError() | ||
| { | ||
| // Arrange: Створюємо зображення з порожнім BlobName | ||
| var imageWithEmptyBlobName = new Image | ||
| { | ||
| BlobName = "", // Порожнє значення | ||
| MimeType = "image/png", | ||
| Url = "http://test.com/empty.png", | ||
| CreatedAt = DateTime.UtcNow | ||
| }; | ||
|
|
||
| _dbContext.Images.Add(imageWithEmptyBlobName); | ||
| await _dbContext.SaveChangesAsync(); | ||
|
|
||
| // Act: Намагаємося отримати зображення за порожнім ім'ям | ||
| HttpResponseMessage response = await _client.GetAsync($"api/Image/by-name/"); | ||
|
|
||
| // Assert: Перевіряємо що повертається помилка (скоріше за все BadRequest через маршрутизацію) | ||
| Assert.False(response.IsSuccessStatusCode); | ||
| } |
There was a problem hiding this comment.
Test logic issue similar to GetImageByIdTest
This test has the same conceptual issue - it creates an image with an empty BlobName in the database and then expects retrieval to fail. Additionally, the test makes a request to /api/Image/by-name/ without a name parameter, which tests route handling rather than empty BlobName validation.
Consider restructuring this test to either:
- Verify that empty BlobName validation happens at creation time
- Test proper 404 response for genuinely missing names
- If testing route parameter validation, name the test accordingly
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
between lines 83 and 103, the test incorrectly expects retrieval failure for an
image with an empty BlobName by calling the API without a name parameter, which
tests routing rather than BlobName validation. To fix this, restructure the test
to either validate that empty BlobName is rejected during image creation, or
test that requesting a non-existent valid name returns a 404, or rename the test
to reflect it is checking route parameter validation. Adjust the test logic and
assertions accordingly to match the chosen approach.
| [Fact] | ||
| public async Task GetImageByName_CaseSensitivityTest_ShouldFindExactMatch() | ||
| { | ||
| // Arrange: Створюємо зображення з конкретним регістром | ||
| var originalBlobName = "TestImageName"; | ||
| var testImage = await CreateTestImageAsync(_blobEnv.ImagesSubPath, originalBlobName); | ||
|
|
||
| // Act & Assert: Тестуємо точне співпадіння | ||
| HttpResponseMessage exactResponse = await _client.GetAsync($"api/Image/by-name/{originalBlobName}"); | ||
| Assert.True(exactResponse.IsSuccessStatusCode); | ||
|
|
||
| // Act & Assert: Тестуємо різний регістр (має не знайти, якщо пошук case-sensitive) | ||
| var lowerCaseName = originalBlobName.ToLower(); | ||
| if (lowerCaseName != originalBlobName) | ||
| { | ||
| HttpResponseMessage caseResponse = await _client.GetAsync($"api/Image/by-name/{lowerCaseName}"); | ||
|
|
||
| // Результат залежить від налаштувань БД (case-sensitive чи ні) | ||
| // Зазвичай SQL Server case-insensitive, PostgreSQL case-sensitive | ||
| } | ||
| } |
There was a problem hiding this comment.
Incomplete test - missing assertion for case sensitivity behavior
The test performs a case-insensitive query but doesn't assert the result. This leaves the test incomplete and doesn't document the expected behavior. Either assert the expected outcome based on your database configuration or make this a parameterized test that handles both scenarios.
- // Результат залежить від налаштувань БД (case-sensitive чи ні)
- // Зазвичай SQL Server case-insensitive, PostgreSQL case-sensitive
+ // Assert based on expected database behavior
+ // For SQL Server (case-insensitive):
+ Assert.True(caseResponse.IsSuccessStatusCode);
+ // For PostgreSQL (case-sensitive):
+ // Assert.Equal(HttpStatusCode.NotFound, caseResponse.StatusCode);📝 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.
| [Fact] | |
| public async Task GetImageByName_CaseSensitivityTest_ShouldFindExactMatch() | |
| { | |
| // Arrange: Створюємо зображення з конкретним регістром | |
| var originalBlobName = "TestImageName"; | |
| var testImage = await CreateTestImageAsync(_blobEnv.ImagesSubPath, originalBlobName); | |
| // Act & Assert: Тестуємо точне співпадіння | |
| HttpResponseMessage exactResponse = await _client.GetAsync($"api/Image/by-name/{originalBlobName}"); | |
| Assert.True(exactResponse.IsSuccessStatusCode); | |
| // Act & Assert: Тестуємо різний регістр (має не знайти, якщо пошук case-sensitive) | |
| var lowerCaseName = originalBlobName.ToLower(); | |
| if (lowerCaseName != originalBlobName) | |
| { | |
| HttpResponseMessage caseResponse = await _client.GetAsync($"api/Image/by-name/{lowerCaseName}"); | |
| // Результат залежить від налаштувань БД (case-sensitive чи ні) | |
| // Зазвичай SQL Server case-insensitive, PostgreSQL case-sensitive | |
| } | |
| } | |
| [Fact] | |
| public async Task GetImageByName_CaseSensitivityTest_ShouldFindExactMatch() | |
| { | |
| // Arrange: Створюємо зображення з конкретним регістром | |
| var originalBlobName = "TestImageName"; | |
| var testImage = await CreateTestImageAsync(_blobEnv.ImagesSubPath, originalBlobName); | |
| // Act & Assert: Тестуємо точне співпадіння | |
| HttpResponseMessage exactResponse = await _client.GetAsync($"api/Image/by-name/{originalBlobName}"); | |
| Assert.True(exactResponse.IsSuccessStatusCode); | |
| // Act & Assert: Тестуємо різний регістр (має не знайти, якщо пошук case-sensitive) | |
| var lowerCaseName = originalBlobName.ToLower(); | |
| if (lowerCaseName != originalBlobName) | |
| { | |
| HttpResponseMessage caseResponse = await _client.GetAsync($"api/Image/by-name/{lowerCaseName}"); | |
| // Assert based on expected database behavior | |
| // For SQL Server (case-insensitive): | |
| Assert.True(caseResponse.IsSuccessStatusCode); | |
| // For PostgreSQL (case-sensitive): | |
| // Assert.Equal(HttpStatusCode.NotFound, caseResponse.StatusCode); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
around lines 105 to 125, the test performs a case-insensitive query but lacks an
assertion to verify the expected behavior. To fix this, add an assertion after
the case-insensitive GET request to explicitly check whether the response is
successful or not, depending on your database's case sensitivity settings.
Alternatively, refactor the test into a parameterized test that can handle both
case-sensitive and case-insensitive scenarios with appropriate assertions.
maxvonlancaster
left a comment
There was a problem hiding this comment.
Overall, LGTM!
There are some minor stuff, but I do not want to stop development with it, and we can discuss it on our synch ups
There was a problem hiding this comment.
Actionable comments posted: 2
🔭 Outside diff range comments (3)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs (1)
250-250: Potential data inconsistency in test fixture.There's a mismatch in the test data:
TeamMemberDtowithId = 2hasCategoryId = 12, but the correspondingTeamMemberentity hasCategoryId = 2. This inconsistency could lead to unreliable test results.- CategoryId = 12 + CategoryId = 2VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (1)
59-63: Critical: Blob name change isn’t persisted; SaveChanges is called too early and incorrectly treated as a failure on 0
- The early SaveChangesAsync means the later assignment imageEntity.BlobName = updatedBlobName is never saved to DB.
- SaveChangesAsync returns 0 when nothing changed; that’s not a failure. Using <= 0 can incorrectly abort updates where only the blob content changes (same MimeType).
Move SaveChangesAsync after you set BlobName and drop the <= 0 check.
Apply this diff:
- if (await _repositoryWrapper.SaveChangesAsync() <= 0) - { - return Result.Fail<ImageDTO>(ImageConstants.FailToUpdateImage); - } + // Defer persisting DB changes until both MimeType and BlobName are finalized. var updatedBlobName = await _blobService.UpdateFileInStorageAsync( imageEntity.BlobName, - previousType, + previousType, request.UpdateImageDto.Base64!, imageEntity.BlobName, request.UpdateImageDto.MimeType!); imageEntity.BlobName = updatedBlobName; + // Persist MimeType and BlobName together in a single transaction. + await _repositoryWrapper.SaveChangesAsync(); ImageDTO resultDto = _mapper.Map<Image, ImageDTO>(imageEntity); transaction.Complete();Also applies to: 71-77
VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (1)
63-67: Remove debug variable from production code.Line 65 contains a test variable assignment that appears to be left over from debugging. This should be removed.
catch (BlobStorageException e) { - var test = ErrorMessagesConstants.BlobStorageError(e.Message); return Result.Fail<ImageDTO>(ErrorMessagesConstants.BlobStorageError(e.Message)); }
♻️ Duplicate comments (1)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (1)
15-20: Harden directory creation in the constructor (wrap with try/catch).Creating directories can fail (permissions, IO issues). Wrap it and surface a BlobFileSystemException with context. This was previously suggested and remains applicable.
Apply this diff:
public BlobService(IOptions<BlobEnvironmentVariables> environment, IHttpContextAccessor httpContextAccessor) { _blobEnv = environment.Value; _httpContextAccessor = httpContextAccessor; - Directory.CreateDirectory(Path.Combine(_blobEnv.RootPath, _blobEnv.ImagesSubPath)); + try + { + Directory.CreateDirectory(Path.Combine(_blobEnv.RootPath, _blobEnv.ImagesSubPath)); + } + catch (Exception ex) + { + throw new BlobFileSystemException( + Path.Combine(_blobEnv.RootPath, _blobEnv.ImagesSubPath), + "Failed to create blob storage directory", + ex); + } }
🧹 Nitpick comments (34)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (5)
17-17: Remove unused IBlobService dependency from the handlerThe handler doesn’t use _blobService; AutoMapper resolvers can obtain IBlobService via DI independently. Dropping the field and ctor parameter reduces noise and avoids “assigned but never used” warnings.
Apply:
- private readonly IBlobService _blobService; private readonly IMapper _mapper; private readonly IRepositoryWrapper _repository; - public GetTeamMemberByIdHandler(IMapper mapper, IRepositoryWrapper repository, IBlobService blobService) + public GetTeamMemberByIdHandler(IMapper mapper, IRepositoryWrapper repository) { _mapper = mapper; _repository = repository; - _blobService = blobService; }Also applies to: 21-26
32-36: Consider NoTracking for this read-only queryIf your QueryOptions supports it, enable AsNoTracking/DisableTracking to avoid change-tracker overhead for a simple read.
45-47: Make the mapped DTO non-nullableMapping from a non-null entity should produce a non-null DTO. Avoid nullable result to reduce downstream null checks.
- TeamMemberDto? result = _mapper.Map<TeamMemberDto>(teamMember); + TeamMemberDto result = _mapper.Map<TeamMemberDto>(teamMember);
49-52: Sanitize blob storage error messagesBlobStorageError currently takes and echoes the raw exception message (
e.Message), which can leak internal/storage details. Instead, return a generic message to callers and log the full exception server-side:• Add a new overload or constant in
ErrorMessagesConstants, e.g.public static string BlobStorageError() => "An error occurred accessing blob storage.";• Inject
ILogger<GetTeamMemberByIdHandler>into the handler and log the exception:logger.LogError(e, "Failed to fetch team member blob for Id {TeamMemberId}", request.Id);• Update the catch to:
catch (BlobStorageException e) { logger.LogError(e, "…"); return Result.Fail<TeamMemberDto>(ErrorMessagesConstants.BlobStorageError()); }• Apply the same pattern to other handlers catching
BlobStorageException(e.g., image handlers).
28-39: Add a cancellation check in the handler
I’ve confirmed thatIRepositoryBase.GetFirstOrDefaultAsync(QueryOptions<T>?)has no overload accepting aCancellationToken. To avoid doing the database work when the operation is cancelled, short-circuit at the top of your handler:public async Task<Result<TeamMemberDto>> Handle(GetTeamMemberByIdQuery request, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); try { var queryOptions = new QueryOptions<TeamMember> { Filter = tm => tm.Id == request.Id, Include = t => t.Include(t => t.Image) }; TeamMember? teamMember = await _repository.TeamMembersRepository.GetFirstOrDefaultAsync(queryOptions); …Optionally, for full end-to-end support, consider extending your repository interface and implementations to accept and propagate
CancellationToken.VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (3)
83-87: Consider re-fetching the updated TeamMember with includes after SaveChanges (shorter transaction, richer DTO mapping)Right now, a separate query loads Image only when ImageId is set, and the mapping runs while the transaction is still open. A small refactor can:
- shorten the transaction window,
- avoid partial navigation loading risks (e.g., if TeamMemberDto mapping needs Category),
- and keep the post-update read consistent with the committed state.
Suggested flow:
- Save changes, call scope.Complete() as soon as DB work is done.
- Re-fetch the updated TeamMember with Include for Image (and Category if your mapping needs it).
- Map and return.
Example (illustrative; outside the selected lines):
// after SaveChangesAsync() > 0: scope.Complete(); var updated = await _repositoryWrapper.TeamMembersRepository.GetFirstOrDefaultAsync( new QueryOptions<TeamMember> { Filter = e => e.Id == request.Id, Include = q => q .Include(t => t.Image) .Include(t => t.Category) }); var resultDto = _mapper.Map<TeamMemberDto>(updated); return Result.Ok(resultDto);Also, confirm whether a missing Image entity should be tolerated or treated as NotFound — current logic silently returns null image in the DTO.
90-91: Make resultDto non-nullable; mapping here shouldn’t return null_mapper.Map for a non-null source returns a non-null DTO. Prefer a non-nullable local to reflect intent.
- TeamMemberDto? resultDto = _mapper.Map<TeamMember, TeamMemberDto>(entityToUpdate); + TeamMemberDto resultDto = _mapper.Map<TeamMember, TeamMemberDto>(entityToUpdate);
8-8: BlobStorageException import/catch likely stale — confirm necessity or removeGiven this handler no longer calls blob storage directly, the BlobStorageException catch (and using) might be dead code. If your AutoMapper resolver still uses IBlobService and may throw BlobStorageException, keeping the catch is fine — but consider tightening the error text to “team member image” for clarity. Otherwise, remove both to reduce coupling.
Option A — remove unused import and catch:
-using VictoryCenter.BLL.Exceptions.BlobStorageExceptions; ... - catch (BlobStorageException e) - { - return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}"); - }Option B — keep catch, adjust message:
- return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}"); + return Result.Fail<TeamMemberDto>($"Error with team member image: {e.Message}");Please confirm whether the mapping pipeline can throw BlobStorageException in this path.
Also applies to: 98-101
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (3)
35-39: Existence check could live in FluentValidation (optional)This category existence check works. Given this repo’s convention (validators handle input/business validation via ValidateAndThrowAsync early), consider moving the “CategoryId exists” check into the CreateTeamMemberCommand validator to keep handlers lean and consistent.
I can draft an async validator rule against CategoriesRepository if you’d like.
66-69: Guard against missing/invalid image (optional) and align behavior with GetImageByIdIf ImageId is provided but the image record is missing or lacks BlobName, the mapping may return null or trigger a resolver error. Decide whether creation should:
- succeed with null Image (current behavior), or
- fail fast with a clear error (similar to GetImageById).
If you choose to fail fast, consider:
- Image? imageResult = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync( + Image? imageResult = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync( new QueryOptions<Image> { Filter = i => i.Id == entity.ImageId }); - result.Image = _mapper.Map<ImageDTO>(imageResult); + if (imageResult is null) + { + return Result.Fail<TeamMemberDto>(ErrorMessagesConstants.NotFound(entity.ImageId, typeof(Image))); + } + if (string.IsNullOrEmpty(imageResult.BlobName)) + { + return Result.Fail<TeamMemberDto>(ImageConstants.ImageDataNotAvailable); + } + result.Image = _mapper.Map<ImageDTO>(imageResult);Alternatively, to keep mapping centralized, you could attach the image to the entity and map once after fetch.
Please confirm the intended business rule when a supplied ImageId points to a missing/invalid image.
82-85: Use centralized error formatter for blob errorsFor consistency with other handlers and to keep error messaging uniform, use ErrorMessagesConstants.BlobStorageError(e.Message) instead of a custom string.
Apply:
- return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}"); + return Result.Fail<TeamMemberDto>(ErrorMessagesConstants.BlobStorageError(e.Message));VictoryCenter/VictoryCenter.BLL/DTOs/Common/PaginationResult.cs (1)
3-4: Consider exposing Items as IReadOnlyList instead of T[].Using IReadOnlyList provides flexibility for callers and avoids exposing a mutable array. It also plays nicer with most serializers.
Apply this diff:
-public record PaginationResult<T>(T[] Items, long TotalItemsCount) +public record PaginationResult<T>(IReadOnlyList<T> Items, long TotalItemsCount) where T : class;If needed, ensure the namespace includes the generic collections:
using System.Collections.Generic;VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (1)
83-86: Make CountAsync more flexible and cancellation-aware.Two small improvements:
- Accept a nullable filter so callers can count all entities without passing e => true.
- Accept a CancellationToken and use AsNoTracking for consistency with read-only ops.
Apply this diff:
- public Task<long> CountAsync(Expression<Func<T, bool>> filter) - { - return _dbContext.Set<T>().LongCountAsync(filter); - } + public Task<long> CountAsync(Expression<Func<T, bool>>? filter = null, CancellationToken cancellationToken = default) + { + var query = _dbContext.Set<T>().AsNoTracking(); + if (filter != null) + { + query = query.Where(filter); + } + return query.LongCountAsync(cancellationToken); + }Note: If not already globally available, add:
using System.Threading;VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (1)
23-23: Broaden CountAsync signature for null filter and cancellation support.Allowing null (count all) and accepting a CancellationToken improves ergonomics and resilience, consistent with other async repository methods.
Apply this diff:
- Task<long> CountAsync(Expression<Func<T, bool>> filter); + Task<long> CountAsync(Expression<Func<T, bool>>? filter = null, CancellationToken cancellationToken = default);If needed, add:
using System.Threading;VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/TeamMembers/GetFiltered/GetFilteredTeamMembersTests.cs (1)
29-35: Good adaptation of the test to the paginated contract.Deserialization to PaginationResult and validating Items + TotalItemsCount are on point.
You can slightly strengthen the assertion to reflect pagination invariants.
Apply this diff:
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(responseContent); Assert.NotEmpty(responseContent.Items); Assert.True(responseContent.TotalItemsCount > 0); + Assert.True(responseContent.TotalItemsCount >= responseContent.Items.Length);VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (1)
3-14: Consider adding serialization and standard exception constructors to align with .NET guidelinesAdding [Serializable] and a protected serialization constructor improves compatibility (e.g., logging, cross-boundary scenarios) and satisfies common analyzers (CA1032/CA1064). A parameterless protected ctor is also conventional for base exception types.
Apply this diff:
public abstract class BlobStorageException : Exception { + protected BlobStorageException() + { + } + protected BlobStorageException(string message) : base(message) { } protected BlobStorageException(string message, Exception innerException) : base(message, innerException) { } + + protected BlobStorageException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + } }Additionally, consider decorating the class with the attribute:
- public abstract class BlobStorageException : Exception + [Serializable] + public abstract class BlobStorageException : ExceptionVictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (1)
3-12: Add inner-exception overload and serialization support (preserving FileName)This makes the exception more informative and analyzer-friendly when chaining exceptions and serializing. It also preserves FileName during serialization.
Apply this diff:
-public class BlobNotFoundException : BlobStorageException + [Serializable] + public class BlobNotFoundException : BlobStorageException { public BlobNotFoundException(string fileName, string message) : base(message) { FileName = fileName; } + public BlobNotFoundException(string fileName, string message, Exception innerException) + : base(message, innerException) + { + FileName = fileName; + } + + protected BlobNotFoundException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + FileName = info.GetString(nameof(FileName))!; + } + public string FileName { get; } + + public override void GetObjectData( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + { + if (info is null) throw new ArgumentNullException(nameof(info)); + info.AddValue(nameof(FileName), FileName); + base.GetObjectData(info, context); + } }VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (1)
3-14: Optional: add serialization constructor and [Serializable] for completenessEven though responses are now URL-based, this exception likely still appears on uploads. Adding the serialization constructor and attribute keeps consistency with the exception hierarchy.
Apply this diff:
-public class InvalidBase64FormatException : BlobStorageException + [Serializable] + public class InvalidBase64FormatException : BlobStorageException { public InvalidBase64FormatException(string message) : base(message) { } public InvalidBase64FormatException(string message, Exception innerException) : base(message, innerException) { } + + protected InvalidBase64FormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + } }VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (1)
3-18: Preserve FileName during serialization and add [Serializable]For parity with BlobNotFoundException and to keep diagnostic context, serialize FileName and add the serialization constructor.
Apply this diff:
-public class ImageProcessingException : BlobStorageException + [Serializable] + public class ImageProcessingException : BlobStorageException { public ImageProcessingException(string fileName, string message) : base(message) { FileName = fileName; } public ImageProcessingException(string fileName, string message, Exception innerException) : base(message, innerException) { FileName = fileName; } public string FileName { get; } + + protected ImageProcessingException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + FileName = info.GetString(nameof(FileName))!; + } + + public override void GetObjectData( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + { + if (info is null) throw new ArgumentNullException(nameof(info)); + info.AddValue(nameof(FileName), FileName); + base.GetObjectData(info, context); + } }VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (3)
57-57: Remove unused variable from repository Update callThe EntityEntry
result variable isn’t used and can be dropped for clarity.
Apply this diff:
- EntityEntry<Image> result = _repositoryWrapper.ImageRepository.Update(imageEntity); + _repositoryWrapper.ImageRepository.Update(imageEntity);
54-55: Name clarity: previousType → previousMimeTypeImproves readability and reduces ambiguity.
Apply this diff:
- var previousType = imageEntity.MimeType; + var previousMimeType = imageEntity.MimeType; @@ - previousType, + previousMimeType,Also applies to: 66-66
64-70: Pass CancellationToken to blob/storage operations if supportedTo make the handler truly cancellable, propagate cancellationToken to SaveChangesAsync and BlobService calls if their signatures allow it.
If available, prefer:
- var updatedBlobName = await _blobService.UpdateFileInStorageAsync( + var updatedBlobName = await _blobService.UpdateFileInStorageAsync( imageEntity.BlobName, - previousType, + previousMimeType, request.UpdateImageDto.Base64!, imageEntity.BlobName, - request.UpdateImageDto.MimeType!); + request.UpdateImageDto.MimeType!, + cancellationToken); @@ - await _repositoryWrapper.SaveChangesAsync(); + await _repositoryWrapper.SaveChangesAsync(cancellationToken);If these APIs don’t currently accept a CancellationToken, consider adding overloads.
VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs (1)
46-48: Consider null-checking the mapped result.While the mapper configuration should handle the mapping correctly, the result is marked as nullable (
ImageDTO?). For defensive programming, consider verifying the mapping succeeded before returning.ImageDTO? result = _mapper.Map<ImageDTO>(image); + +if (result is null) +{ + return Result.Fail<ImageDTO>(ImageConstants.FailedToMapImage); +} return Result.Ok(result);VictoryCenter/VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs (1)
39-54: Good transaction management, but consider error handling for blob deletion failures.The transaction scope usage is correct, and the database operations are properly wrapped. However, if blob deletion fails after the database commit, you'll have orphaned files in storage. Consider either:
- Moving blob deletion before database operations (if idempotent)
- Implementing compensating logic for cleanup
For better resilience against partial failures, consider implementing a two-phase approach or eventual consistency pattern for blob cleanup. You could also explore using a background job to clean up orphaned blobs if the immediate deletion fails.
VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (1)
38-56: Consider the transaction boundary for blob storage operations.The blob storage operation (Line 51) is inside the transaction scope but appears to be a filesystem operation that won't participate in the database transaction. If blob storage fails, the transaction will roll back the database changes, which is good. However, if the transaction fails to complete after blob storage succeeds, you'll have orphaned files.
Consider implementing a cleanup mechanism for orphaned blobs or using a two-phase commit pattern if your blob storage supports it. Alternatively, you could track blob cleanup requirements in a separate table within the same transaction.
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5)
36-39: Avoid leaking raw exception messages; use a consistent, user-safe message.Prefer a stable error message to keep logs consistent and avoid surfacing internals.
Apply this diff:
- throw new BlobFileSystemException(_blobEnv.FullPath, ex.Message, ex); + throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.FailToSaveImageInStorage, ex);
50-65: Sanitize URL construction: trim slashes and URL-encode the file name.Minor hardening: avoid double slashes if ImagesSubPath is configured with slashes and ensure the file name is URL-safe.
Apply this diff:
public string GetFileUrl(string name, string mimeType) { ValidateFileName(name); var extension = GetExtensionFromMimeType(mimeType); - var fileName = $"{name}.{extension}"; - HttpRequest? request = _httpContextAccessor.HttpContext?.Request; + var fileName = $"{name}.{extension}"; + var subPath = _blobEnv.ImagesSubPath.Trim('/'); + HttpRequest? request = _httpContextAccessor.HttpContext?.Request; if (request == null) { - throw new InvalidOperationException("HttpContext is not available."); + throw new InvalidOperationException("HttpContext is not available."); } - var baseUrl = $"{request.Scheme}://{request.Host}"; - return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}"; + var baseUrl = $"{request.Scheme}://{request.Host}"; + var safeFileName = Uri.EscapeDataString(fileName); + return $"{baseUrl}/{subPath}/{safeFileName}"; }
58-61: Consider a graceful fallback when HttpContext is unavailable.If this runs outside an HTTP request (jobs, background services), throwing may be undesirable. A relative URL fallback keeps the system functional.
Apply this diff if a relative URL is acceptable without HttpContext:
- if (request == null) - { - throw new InvalidOperationException("HttpContext is not available."); - } + if (request == null) + { + // Fallback to relative URL when HttpContext is not available + return $"/{subPath}/{Uri.EscapeDataString(fileName)}"; + }If absolute URLs are required, consider introducing a BaseUrl in BlobEnvironmentVariables and falling back to it when HttpContext is null.
94-122: Preserve inner exception when rethrowing InvalidBase64FormatException.Keeping the inner exception materially improves diagnostics without changing behavior.
Apply this diff:
catch (Exception ex) when (ex is not InvalidBase64FormatException) { - throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64); + throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64, ex); }
173-181: Tighten file name validation logic (IndexOfAny and explicit separators).Using IndexOfAny is more idiomatic and faster than Any(name.Contains). Explicitly disallowing path separators makes behavior OS-agnostic and URL-friendly.
Apply this diff:
private void ValidateFileName(string name) { - if (string.IsNullOrWhiteSpace(name) - || name.Contains("..") - || Path.GetInvalidFileNameChars().Any(name.Contains)) + if (string.IsNullOrWhiteSpace(name) + || name.Contains("..") + || name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 + || name.Contains('/') || name.Contains('\\')) { throw new BlobFileNameException(name, ImageConstants.CantGetFile(name)); } }VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs (4)
36-42: Use a realistic URL in the test double.The Url field currently holds base64 (“dGVzdA==”). Switching to a plausible URL better reflects the new model and improves test readability.
Apply this diff:
private readonly ImageDTO _testImageDto = new() { Id = 1, BlobName = "testblob", MimeType = "image/png", - Url = "dGVzdA==" + Url = "http://localhost/images/testblob.png" };
58-60: Remove unused local variable.fileName is declared but never used.
Apply this diff:
- var fileName = "testblob"; var fileWithExtension = "testblob.png";
160-186: Rename test for accuracy (it throws BlobFileSystemException, not IOException).Minor clarity improvement for future readers.
Apply this diff:
- public async Task Handle_ThrowsIOException_ShouldReturnFileCreatingFail() + public async Task Handle_FileSystemError_ShouldReturnFileCreatingFail()
131-134: Consider adding a unit test for InvalidBase64 flow.A test where the blob service throws InvalidBase64FormatException would complement coverage for the new base64 parsing path.
If you’d like, I can add a new test method that configures SaveFileInStorageAsync to throw InvalidBase64FormatException and asserts the handler returns a Failure with an appropriate error message.
Also applies to: 141-143, 155-158
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (25)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs(3 hunks)VictoryCenter/VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs(4 hunks)VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs(3 hunks)VictoryCenter/VictoryCenter.BLL/Commands/Payment/WayForPay/WayForPayPaymentCommandHandler.cs(2 hunks)VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs(4 hunks)VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs(5 hunks)VictoryCenter/VictoryCenter.BLL/DTOs/Common/PaginationResult.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs(4 hunks)VictoryCenter/VictoryCenter.BLL/Queries/Images/GetByName/GetImageByNameHandler.cs(4 hunks)VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs(4 hunks)VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersQuery.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs(3 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs(4 hunks)VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs(1 hunks)VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/TeamMembers/GetFiltered/GetFilteredTeamMembersTests.cs(2 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs(8 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs(6 hunks)
💤 Files with no reviewable changes (1)
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs
✅ Files skipped from review due to trivial changes (1)
- VictoryCenter/VictoryCenter.BLL/Commands/Payment/WayForPay/WayForPayPaymentCommandHandler.cs
🚧 Files skipped from review as they are similar to previous changes (1)
- VictoryCenter/VictoryCenter.BLL/Queries/Images/GetByName/GetImageByNameHandler.cs
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-20T18:22:51.823Z
Learnt from: Oleh-Bashtovyi
PR: ita-social-projects/VictoryCenter-Back#179
File: VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Reorder/ReorderTeamMembersHandler.cs:27-28
Timestamp: 2025-06-20T18:22:51.823Z
Learning: In the VictoryCenter codebase, FluentValidation is used for input validation in MediatR handlers. The handlers call ValidateAndThrowAsync() early in the Handle method, and validation exceptions are caught and converted to Result.Fail responses. This means validation logic should be kept in the validator classes rather than duplicated in the handlers.
Applied to files:
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.csVictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.csVictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs
📚 Learning: 2025-06-17T20:32:16.009Z
Learnt from: VladimirSushinsky
PR: ita-social-projects/VictoryCenter-Back#114
File: VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/TeamMembersSeeder/TeamMemberSeeder.cs:20-24
Timestamp: 2025-06-17T20:32:16.009Z
Learning: In the VictoryCenter test seeder for TeamMember entities, the last category in the categories list is intentionally excluded from having team members assigned to it (using `categories[i % (categories.Count - 1)].Id`). This design ensures that the last category remains available for delete tests without foreign key constraint violations, as delete operations require categories with no related team members.
Applied to files:
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.csVictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs
🧬 Code Graph Analysis (16)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(3-14)BlobStorageException(5-8)BlobStorageException(10-13)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (2)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs (1)
Task(29-50)VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (5)
Task(10-10)Task(12-12)Task(14-14)Task(20-21)Task(23-23)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(3-14)BlobStorageException(5-8)BlobStorageException(10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(3-14)BlobStorageException(5-8)BlobStorageException(10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(3-14)BlobStorageException(5-8)BlobStorageException(10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(3-14)BlobStorageException(5-8)BlobStorageException(10-13)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (2)
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryWrapper.cs (1)
TransactionScope(18-18)VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryWrapper.cs (1)
TransactionScope(40-43)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (1)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMemberById.cs (2)
TeamMemberDto(82-88)TeamMemberDto(90-93)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs (3)
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (5)
Task(10-10)Task(12-12)Task(14-14)Task(20-21)Task(23-23)VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (5)
Task(21-35)Task(37-48)Task(50-54)Task(66-81)Task(83-86)VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryWrapper.cs (1)
Task(35-38)
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (4)
VictoryCenter/VictoryCenter.BLL/Constants/ErrorMessagesConstants.cs (3)
ErrorMessagesConstants(3-66)NotFound(5-8)NotFound(10-18)VictoryCenter/VictoryCenter.DAL/Entities/Category.cs (1)
Category(3-14)VictoryCenter/VictoryCenter.DAL/Repositories/Options/QueryOptions.cs (1)
QueryOptions(6-15)VictoryCenter/VictoryCenter.DAL/Entities/TeamMember.cs (1)
TeamMember(5-30)
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (8)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs (1)
Task(24-61)VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (1)
Task(30-68)VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs (1)
Task(27-54)VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (1)
Task(28-53)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5)
Task(22-40)Task(42-48)Task(67-73)Task(136-150)Task(152-171)VictoryCenter/VictoryCenter.DAL/Entities/Category.cs (1)
Category(3-14)VictoryCenter/VictoryCenter.DAL/Repositories/Options/QueryOptions.cs (1)
QueryOptions(6-15)VictoryCenter/VictoryCenter.BLL/Constants/ErrorMessagesConstants.cs (3)
ErrorMessagesConstants(3-66)NotFound(5-8)NotFound(10-18)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs (6)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Media/ImageRepository.cs (2)
ImageRepository(8-14)ImageRepository(10-13)VictoryCenter/VictoryCenter.DAL/Repositories/Options/QueryOptions.cs (1)
QueryOptions(6-15)VictoryCenter/VictoryCenter.BLL/Constants/ErrorMessagesConstants.cs (4)
ErrorMessagesConstants(3-66)NotFound(5-8)NotFound(10-18)BlobStorageError(62-65)VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryWrapper.cs (1)
TransactionScope(18-18)VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryWrapper.cs (1)
TransactionScope(40-43)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (1)
DeleteFileInStorage(75-92)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs (3)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/TeamMembers/TeamMembersRepository.cs (2)
TeamMembersRepository(8-14)TeamMembersRepository(10-13)VictoryCenter/VictoryCenter.DAL/Entities/TeamMember.cs (1)
TeamMember(5-30)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMemberById.cs (2)
TeamMember(69-75)TeamMember(77-80)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (12)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (2)
Task(162-186)GetExtensionFromMimeType(188-198)VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/IntegrationTestsDatabaseSeeder.cs (1)
Task(19-28)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (2)
Task(229-248)GetExtensionFromMimeType(250-260)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs (2)
Task(146-173)GetExtensionFromMimeType(175-185)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)
GetExtensionFromMimeType(168-178)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(3-14)BlobStorageException(5-8)BlobStorageException(10-13)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (3)
BlobFileSystemException(3-18)BlobFileSystemException(5-9)BlobFileSystemException(11-15)VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (2)
GetFileUrl(6-6)DeleteFileInStorage(8-8)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
InvalidBase64FormatException(3-14)InvalidBase64FormatException(5-8)InvalidBase64FormatException(10-13)VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2)
ImageConstants(3-41)CantGetFile(37-40)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
ImageProcessingException(3-18)ImageProcessingException(5-9)ImageProcessingException(11-15)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
BlobFileNameException(3-18)BlobFileNameException(5-9)BlobFileNameException(11-15)
VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs (2)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Media/ImageRepository.cs (2)
ImageRepository(8-14)ImageRepository(10-13)VictoryCenter/VictoryCenter.DAL/Repositories/Options/QueryOptions.cs (1)
QueryOptions(6-15)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (3)
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryWrapper.cs (1)
TransactionScope(18-18)VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryWrapper.cs (1)
TransactionScope(40-43)VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Media/ImageRepository.cs (2)
ImageRepository(8-14)ImageRepository(10-13)
⏰ 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). (1)
- GitHub Check: Build and analyze
🔇 Additional comments (30)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (1)
7-7: LGTM: Targeted BlobStorage exception namespace importThe dedicated BlobStorageExceptions namespace aligns with the new error model and the catch below.
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (2)
24-24: Constructor dependency removal looks good — double-check DI and testsDropping IBlobService from the handler’s constructor aligns with the PR’s image URL approach. Please ensure DI registrations and any tests constructing this handler are updated accordingly.
60-61: Good, consistent NotFound handling for CategoryUsing ErrorMessagesConstants.NotFound for missing Category keeps responses uniform across the codebase.
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (4)
8-8: Import aligns with ImageDTO usage — LGTMThe added DTOs.Images import is appropriate now that the handler assigns ImageDTO on the result.
10-10: Exception namespace update looks correctThe BlobStorageExceptions namespace aligns with the refactor across the codebase.
43-44: Standardized NotFound message — LGTMThis matches ErrorMessagesConstants.NotFound(...) usage in other handlers.
23-28: DI coverage verified for IBlobService and BlobToUrlResolver
IBlobService remains registered in DI and the AutoMapper profile includes the BlobToUrlResolver mapping:
- ServicesConfiguration.cs (lines 300 & 306): services.AddScoped<IBlobService, BlobService>()
- ImagesProfile.cs (line 14): CreateMap<Image, ImageDTO>().ForMember(d ⇒ d.Url, o ⇒ o.MapFrom<BlobToUrlResolver>())
VictoryCenter/VictoryCenter.BLL/DTOs/Common/PaginationResult.cs (1)
3-4: Solid, minimal DTO for pagination.The simple record with Items + TotalItemsCount aligns well with the new pagination flow and LongCount usage.
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (1)
83-86: CountAsync implementation is correct and consistent with EF Core.Delegating to LongCountAsync(filter) matches the new interface and supports the pagination total count cleanly.
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (1)
23-23: Interface addition looks good and supports the new pagination flow.Introducing CountAsync on the repository aligns with the handler’s need to compute TotalItemsCount.
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersQuery.cs (1)
8-10: Return type change to PaginationResult is appropriate.This keeps the public contract aligned with the handler and tests, enabling efficient pagination without changing the query shape.
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs (6)
1-1: Added necessary import for Expression support.The
System.Linq.Expressionsimport is correctly added to support the newCountAsyncmethod mock setup that usesExpression<Func<TeamMember, bool>>.
63-67: Updated test assertions to validate paginated response structure.The test assertions have been correctly updated to work with the new paginated response structure:
- Accessing filtered results through
result.Value.Items- Validating total count through
result.Value.TotalItemsCount- Maintaining proper comparison logic for both filtered and total datasets
100-102: Consistent paginated assertions across filter tests.All filter-based tests now consistently validate the paginated response structure, ensuring both the filtered items and total count are properly asserted.
136-138: Filter by category test properly validates pagination.The category filter test correctly asserts the paginated structure while maintaining the filtering logic validation.
173-175: Combined filter test maintains proper pagination validation.The status and category combined filter test appropriately validates both the filtered results and total count in the paginated response.
276-277: Added repository mock setup for counting functionality.The
CountAsyncmock setup correctly returns the total count of the team members list, enabling the pagination total count functionality in the tests.VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs (6)
6-6: Added pagination DTO import.The import for
VictoryCenter.BLL.DTOs.Commonis correctly added to support the newPaginationResult<T>return type.
16-16: Updated handler interface to support pagination.The class declaration has been correctly updated to implement
IRequestHandler<GetTeamMembersByFiltersQuery, Result<PaginationResult<TeamMemberDto>>>, aligning with the new paginated response structure.
29-29: Method signature updated for paginated results.The
Handlemethod signature correctly returnsTask<Result<PaginationResult<TeamMemberDto>>>to support the new pagination functionality.
47-47: Added total count retrieval for pagination.The handler now correctly retrieves the total count of items matching the filter criteria using
CountAsync, which is essential for proper pagination metadata.
49-49: Proper pagination result construction.The return statement correctly constructs a
PaginationResult<TeamMemberDto>with the mapped DTOs and total count, using the collection expression syntax for the items array.
47-47: No action required on CountAsync return typeI’ve verified that:
IRepositoryBasedeclaresTask<long> CountAsync(Expression<Func<T, bool>> filter).RepositoryBaseimplements it by returning_dbContext.Set<T>().LongCountAsync(filter).- The handler correctly awaits the
Task<long>to obtain along.All definitions and usages are consistent—no changes needed here.
VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (1)
36-50: Transactional flow looks solid with TransactionScopeAsyncFlowOption.EnabledValidation precedes data access; not-found early-return is clean; and exceptions translate to a Result with consistent messages. With the SaveChanges fix above, the DB and blob operations will remain consistent.
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1)
1-18: LGTM! Well-structured exception class for blob file name errors.The implementation follows proper exception handling patterns with appropriate constructors and property initialization. The class correctly inherits from
BlobStorageExceptionand provides clear semantics for blob file name-related errors.VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (1)
1-18: LGTM! Clean exception implementation for filesystem-related blob errors.The class follows the established pattern with proper constructor chaining and exposes the filesystem path for diagnostic purposes. This will be helpful for debugging storage-related issues.
VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (1)
42-42: Good addition of timestamp tracking.Setting
CreatedAtto UTC ensures consistent timestamp handling across different timezones.VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (2)
136-150: LGTM on file creation path and error mapping.Validate-then-write with a clear ImageProcessingException on failure aligns with the new, non-encrypted flow.
152-171: LGTM on read path and exception mapping.Clean separation of not-found vs. read failure with BlobNotFoundException and ImageProcessingException.
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs (1)
119-125: LGTM: Validation failure path is asserted via FluentResults.Asserting for “required/cannot be null” aligns with FluentValidation surface.
| return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase + | ||
| ex.Message); |
There was a problem hiding this comment.
Avoid leaking database exception details to clients
Returning DbUpdateException.Message can expose internal DB details. Prefer a generic, user-safe message and log the exception server-side.
Apply:
- return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase +
- ex.Message);
+ return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase);Follow-up: If there’s a centralized logger in the handler pipeline, rely on it; otherwise consider logging ex with context here.
📝 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.
| return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase + | |
| ex.Message); | |
| return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase); |
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs
around lines 88-89, the handler returns the raw DbUpdateException.Message to the
client; replace that with a generic, user-safe failure message (e.g.
TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase) and do not append
ex.Message to the Result.Fail return value. Instead log the full exception
server-side (use the centralized pipeline logger if available, otherwise call a
local logger.LogError(ex, "Failed creating team member for {TeamMemberInfo}")
with relevant context) before returning the generic failure result.
| public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType) | ||
| { | ||
| ValidateFileName(newBlobName); | ||
| DeleteFileInStorage(previousBlobName, previousMimeType); | ||
| await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); | ||
| return newBlobName; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid data loss: save new blob before deleting the old one.
Currently, the old file is deleted first. If saving the new file fails, you’ve lost the original asset. Reverse the order to be fail-safe.
Apply this diff:
public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType)
{
ValidateFileName(newBlobName);
- DeleteFileInStorage(previousBlobName, previousMimeType);
- await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
- return newBlobName;
+ // Save first to avoid data loss, then delete the previous blob
+ await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
+ DeleteFileInStorage(previousBlobName, previousMimeType);
+ return newBlobName;
}📝 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<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType) | |
| { | |
| ValidateFileName(newBlobName); | |
| DeleteFileInStorage(previousBlobName, previousMimeType); | |
| await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); | |
| return newBlobName; | |
| } | |
| public async Task<string> UpdateFileInStorageAsync( | |
| string previousBlobName, | |
| string previousMimeType, | |
| string base64Format, | |
| string newBlobName, | |
| string mimeType) | |
| { | |
| ValidateFileName(newBlobName); | |
| // Save first to avoid data loss, then delete the previous blob | |
| await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); | |
| DeleteFileInStorage(previousBlobName, previousMimeType); | |
| return newBlobName; | |
| } |
🤖 Prompt for AI Agents
In VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs around
lines 67-73, the method deletes the previous blob before saving the new one
which risks data loss; change the flow to ValidateFileName(newBlobName), then
await SaveFileInStorageAsync(...) to persist the new blob first, and only after
a successful save call DeleteFileInStorage(previousBlobName, previousMimeType);
additionally wrap the save call in error handling so any exception prevents
deletion (and log/report the error) to ensure the original asset remains if the
new upload fails.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1)
59-77: Re-evaluate where to validate empty BlobName (creation vs retrieval)This test asserts 400 on GET when BlobName is empty by inserting an invalid record directly into the DB. It’s generally cleaner to prevent such invalid state at creation time rather than at retrieval. Pick one policy and align tests and controller accordingly.
- Policy A (preferred): Disallow creating images with empty BlobName (return 400/422 on POST). Remove or repurpose this GET test.
- Policy B: If API contract specifies 400 on GET for empty BlobName, ensure controller explicitly returns 400 with a clear error payload and document it. Add an assertion on the error message/code, not just the status.
I can update the tests and (if needed) the controller to enforce either policy—what’s your preference?
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)
118-128: Extract GetExtensionFromMimeType into a shared test helper to avoid duplicationThis helper is now present in multiple test files. Centralize it to a single utility to keep behavior consistent with production mapping.
Proposed shared helper (new file in the test project):
namespace VictoryCenter.IntegrationTests.TestUtils; public static class ImageTestHelper { public static string GetExtensionFromMimeType(string mimeType) { return mimeType.ToLowerInvariant() switch { "image/jpeg" => "jpg", "image/jpg" => "jpg", "image/png" => "png", "image/webp" => "webp", _ => "jpg" }; } }Then replace local calls with ImageTestHelper.GetExtensionFromMimeType(mimeType).
🧹 Nitpick comments (9)
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImageEntityForSeed.cs (2)
3-9: Make the seed model immutable and explicit.Given this type is only for seeding, using init-only and required members (C# 11+) avoids accidental mutation and null-forgiveness. It also documents intent.
Apply this diff:
-public class ImageEntityForSeed -{ - public int Id { get; set; } - public string BlobName { get; set; } = default!; - public string MimeType { get; set; } = default!; - public string Base64 { get; set; } = default!; -} +public sealed record ImageEntityForSeed +{ + public required int Id { get; init; } + public required string BlobName { get; init; } + public required string MimeType { get; init; } + // Note: this currently contains a data URI (e.g., "data:image/jpeg;base64,...") + public required string Base64 { get; init; } +}
8-8: Clarify representation: Base64 vs Data URI.The Base64 values you pass include a data URI prefix (e.g., "data:image/jpeg;base64,..."). Consider renaming to DataUri or documenting this explicitly to prevent misuse later.
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs (4)
13-28: Move large inline Base64 blobs to test assets for maintainability.Embedding multi-hundred-KB Base64 strings in code hurts readability, diffs, and repository size. Prefer small fixture images (e.g., 1x1 or tiny thumbnails) stored under a test assets folder and load bytes at runtime.
Example approach (outside this hunk):
- Place files under VictoryCenter.IntegrationTests/TestAssets/Images/testname1.jpg.
- Load and convert to Base64 only for seeding.
// Example helper static string ToDataUri(string path, string mime) => $"data:{mime};base64,{Convert.ToBase64String(File.ReadAllBytes(path))}";If you want, I can provide a follow-up patch to wire this into the seeder.
47-53: Optional: parallelize uploads for speed (tiny dataset, but scalable).If the storage backend allows, you can upload seeds concurrently. Keep sequential if the backend throttles or if ordering matters.
Example (outside this hunk to avoid diff conflicts with the guard above):
var uploadTasks = Images .Where(i => !string.IsNullOrWhiteSpace(i.Base64)) .Select(i => _blobService.SaveFileInStorageAsync(i.Base64, i.BlobName, i.MimeType)); await Task.WhenAll(uploadTasks);
11-11: Prefer IReadOnlyList for immutable seed data.The seed collection isn’t modified; using IReadOnlyList documents intent and prevents accidental mutation.
-private static readonly List<ImageEntityForSeed> Images = +private static readonly IReadOnlyList<ImageEntityForSeed> Images =
49-53: Strengthen the null/empty guard; no need to strip data-URI manuallyI’ve confirmed that
IBlobService.SaveFileInStorageAsyncaccepts both raw Base64 and full data-URI formats—the implementation’sConvertBase64ToByteshelper automatically splits out the prefix when a comma is present . The integration tests even seed images using"data:image/...;base64,...", while unit tests pass raw Base64, and both work correctly.The only change still worth making is tightening the guard to skip empty or whitespace-only strings:
• File:
VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs, around lines 49–53
• Update the null check to also exclude empty/whitespace strings- if (image.Base64 != null) + if (!string.IsNullOrWhiteSpace(image.Base64)) { await _blobService.SaveFileInStorageAsync(image.Base64, image.BlobName, image.MimeType); }VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json (1)
8-8: Good isolation intent; consider per-run subpath to avoid cross-test collisionsSetting ImagesSubPath to "IntegrationTest" keeps test artifacts away from real data. To harden this further (parallel runs, flaky cleanups), generate a unique subfolder per run (e.g., IntegrationTest/{GUID or timestamp}) via test-time configuration override rather than static JSON. This also lets you guard deletions safely in teardown.
Would you like me to sketch a factory override that sets a per-run ImagesSubPath in your VictoryCenterWebApplicationFactory?
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1)
35-46: Guard against null seed record to avoid intermittent NREsFirstOrDefaultAsync can return null and cause a NullReference when dereferencing image.Id. Assert non-null before use.
- Image? image = await _fixture.DbContext.Images.FirstOrDefaultAsync(); - var id = image.Id; + Image? image = await _fixture.DbContext.Images.FirstOrDefaultAsync(); + Assert.NotNull(image); + var id = image!.Id;VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)
84-116: Nice coverage for MIME variants; tighten a couple of assertionsGood parameterization across MIME types and on-disk verification. Two small improvements:
- Also assert responseContext.Url is absolute and not empty.
- Keep comments in English for consistency with the rest of the suite.
- // Перевіряємо що файл створено з правильним розширенням + // Verify that the file is created with the correct extension string expectedExtension = GetExtensionFromMimeType(mimeType); string filePath = Path.Combine(_fixture.BlobEnvironmentVariables.FullPath, $"{responseContext.BlobName}.{expectedExtension}"); Assert.True(File.Exists(filePath)); - // Перевіряємо що URL містить правильне розширення - Assert.Contains($".{expectedExtension}", responseContext.Url); + // Verify that the URL contains the correct extension and is absolute + Assert.False(string.IsNullOrWhiteSpace(responseContext.Url)); + Assert.Contains($".{expectedExtension}", responseContext.Url); + Assert.True(Uri.TryCreate(responseContext.Url, UriKind.Absolute, out _));
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/DbFixture/IntegrationTestDbFixture.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs(2 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs(3 hunks)VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImageEntityForSeed.cs(1 hunks)VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs(2 hunks)VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs
- VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs
- VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
🧰 Additional context used
🧬 Code graph analysis (2)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (3)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (5)
Fact(32-46)Fact(48-57)Fact(72-89)Task(25-28)Task(30-30)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs (3)
Fact(40-67)Fact(69-92)Fact(94-125)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5)
Task(22-40)Task(42-48)Task(67-73)Task(136-150)Task(152-171)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (6)
Task(22-40)Task(42-48)Task(67-73)Task(136-150)Task(152-171)GetExtensionFromMimeType(124-134)
⏰ 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: Build and analyze
- GitHub Check: Build and analyze
🔇 Additional comments (1)
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImageEntityForSeed.cs (1)
3-9: LGTM on scope and simplicity.A minimal, seed-only DTO is appropriate here and keeps the app code decoupled from test data shape.
| new ImageEntityForSeed | ||
| { | ||
| Id = 1, | ||
| BlobName = "testname1", | ||
| Base64 = | ||
| "data:image/jpeg;base64,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", | ||
| MimeType = "image/jpg" | ||
| }, |
There was a problem hiding this comment.
Fix MIME type mismatch (image/jpg → image/jpeg).
Your data URIs start with "data:image/jpeg;base64,...", but MimeType is set to "image/jpg". Some servers/clients rely on the exact content type; the non-standard "image/jpg" can lead to incorrect headers and rendering issues.
Apply this diff in both seed items:
- MimeType = "image/jpg"
+ MimeType = "image/jpeg"Also applies to: 21-28
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs
around lines 13-20 and 21-28, the ImageEntityForSeed entries set MimeType =
"image/jpg" while the Base64 data URIs begin with "data:image/jpeg;base64,...";
update both seed items to use the standard MIME type "image/jpeg" (replace
"image/jpg" with "image/jpeg") so the MIME matches the data URI and avoids
client/server rendering issues.
| var entities = Images.Select(i => new Image | ||
| { | ||
| Id = i.Id, | ||
| BlobName = i.BlobName, | ||
| Base64 = i.Base64, | ||
| MimeType = i.MimeType | ||
| }).ToList(); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Explicit Id assignment may conflict with DB identity strategy—please verify.
If Image.Id is configured as ValueGeneratedOnAdd/Identity, setting Id explicitly can fail on some providers unless identity insert is enabled. It’s fine if the model is configured for explicit keys in tests.
Run:
If Identity is enforced, consider omitting Id here and mapping by BlobName, or temporarily enabling identity insert in the test setup.
🏁 Script executed:
#!/bin/bash
# Inspect Image entity configuration and annotations
rg -nP --type=cs -C3 $'class\\s+Image\\b'
rg -nP --type=cs -C3 'builder\\.(Entity<\\s*Image\\s*>|Property\\(|HasKey\\()'
rg -nP --type=cs -C3 'DatabaseGenerated|ValueGenerated|ValueGeneratedOn(Add|Never)'Length of output: 3036
Address explicit Id assignment in the Image seeder
I noticed that in VictoryCenter.DAL/Data/EntityTypeConfigurations/ImageConfig.cs, the Id property is configured with .ValueGeneratedOnAdd(), so the database will generate its value on insert. In your integration‐test seeder you’re explicitly setting Id on each Image instance, which will cause EF Core to include the Id column in the INSERT and fail under a SQL identity strategy unless IDENTITY_INSERT is enabled.
To resolve this, choose one of the following approaches:
• Remove explicit Id assignment in the seeder
– Let the database generate the key. After calling SaveChanges(), EF Core will populate each Image.Id automatically.
– Update your test assertions to refer to another stable property (e.g. BlobName) or read back the generated Id value from the context.
• Enable IDENTITY_INSERT around your seed operation
– Open a connection and toggle IDENTITY_INSERT on the target table, then seed and toggle it off. For example:
csharp await using var conn = context.Database.GetDbConnection(); await conn.OpenAsync(); await context.Database.ExecuteSqlRawAsync("SET IDENTITY_INSERT media.images ON"); context.Images.AddRange(entities); await context.SaveChangesAsync(); await context.Database.ExecuteSqlRawAsync("SET IDENTITY_INSERT media.images OFF");
– Wrap the above in a transaction to ensure cleanup on failures.
Please apply one of these fixes so that your integration tests can seed Image records reliably under the existing identity‐generation configuration.
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs
around lines 55-60 you are explicitly assigning Ids on Image instances even
though Image.Id is configured with .ValueGeneratedOnAdd(); remove the explicit
Id assignment and let the database generate keys (call
SaveChanges/SaveChangesAsync so EF populates Image.Id) and update tests to
reference BlobName or the generated Id read from the context, or alternatively
implement IDENTITY_INSERT around the seed operation (open connection, SET
IDENTITY_INSERT ON, add entities, SaveChanges, SET IDENTITY_INSERT OFF) wrapped
in a transaction to ensure cleanup on failure—pick one of these two approaches
and apply it.
There was a problem hiding this comment.
Add comments that explain what every type of exceptions is responsible for.
| return $"File not found: {filePath}"; | ||
| } | ||
|
|
||
| public static string CantGetFile(string name) |
There was a problem hiding this comment.
Give this method a better name.
| public static readonly string FailedToReadOrDecryptFile = "Failed to read or decrypt file."; | ||
| public static readonly string UnexpectedBlobReadError = "Unexpected error during file retrieval."; | ||
| public static readonly string ImageDataNotAvailable = "Image data not available"; | ||
| public static readonly string FailedToSaveImage = "Failed to save the image."; |
There was a problem hiding this comment.
Extract this to a common constant. You can take a look at our pr: ita-social-projects/VictoryCenter-Client#52
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (1)
118-122: Fix assertion path to include Images subdirectory.Files are saved under RootPath/ImagesSubPath; the current assertion points to RootPath only, so it will falsely fail.
- var filePath = Path.Combine(_tempDir, $"{_fileName}.png"); + var filePath = Path.Combine(_tempDir, _subDir, $"{_fileName}.png");
♻️ Duplicate comments (4)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs (1)
5-6: Make both properties init-only (aligns with prior feedback).These are configuration values set at startup and never mutated at runtime. Using init enforces immutability and matches ImagesSubPath. This also addresses the earlier “Why aren't those init?” comment.
- public required string RootPath { get; set; } - public required string ImagesSubPath { get; init; } + public required string RootPath { get; init; } + public required string ImagesSubPath { get; init; }VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (3)
10-11: Add XML docs listing thrown exceptions per method.Prior feedback requested documenting exception types. Adding tags will help API consumers handle failures properly.
Example:
/// <summary>Saves an image to blob storage.</summary> /// <exception cref="BlobFileNameException">Invalid blob name.</exception> /// <exception cref="InvalidBase64FormatException">Base64 invalid.</exception> /// <exception cref="BlobFileSystemException">I/O or filesystem issues.</exception> public Task<string> SaveFileInStorageAsync(string base64, string name, string mimeType) { ... }
15-20: Guard directory creation and use FullPath consistently.Directory creation can fail (permissions/disk). Wrap it and prefer _blobEnv.FullPath for consistency. This echoes the earlier suggestion.
- public BlobService(IOptions<BlobEnvironmentVariables> environment, IHttpContextAccessor httpContextAccessor) - { - _blobEnv = environment.Value; - _httpContextAccessor = httpContextAccessor; - Directory.CreateDirectory(Path.Combine(_blobEnv.RootPath, _blobEnv.ImagesSubPath)); - } + public BlobService(IOptions<BlobEnvironmentVariables> environment, IHttpContextAccessor httpContextAccessor) + { + _blobEnv = environment.Value; + _httpContextAccessor = httpContextAccessor; + try + { + Directory.CreateDirectory(_blobEnv.FullPath); + } + catch (Exception ex) + { + throw new BlobFileSystemException(_blobEnv.FullPath, "Failed to create blob storage directory.", ex); + } + }
74-80: Save-first, then delete to avoid data loss.Current flow deletes the old blob before saving the new one. If the save fails, the asset is lost. Reverse the order.
public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType) { ValidateFileName(newBlobName); - DeleteFileInStorage(previousBlobName, previousMimeType); - await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); + await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); + DeleteFileInStorage(previousBlobName, previousMimeType); return newBlobName; }Optional: if you want transactional behavior, save to a temp name, then swap and delete on success.
🧹 Nitpick comments (9)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs (1)
8-8: Normalize FullPath to avoid surprises with relative segments and mixed separators.Path.GetFullPath() ensures the combined path is canonical (e.g., trims “.”/“..”, normalizes separators). It’s a low-risk hardening step.
- public string FullPath => Path.Combine(RootPath, ImagesSubPath); + public string FullPath => Path.GetFullPath(Path.Combine( + RootPath, + ImagesSubPath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));Follow-up (optional): If you want to guarantee ImagesSubPath does not escape RootPath, validate that FullPath starts with the normalized RootPath; otherwise throw during app startup.
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (4)
31-33: Optional: avoid redundant CreateDirectory at write-time.You already ensure the directory exists in the constructor; this extra call is defensive but redundant. Either remove it, or keep it with a try-wrap for symmetry.
108-124: Preserve the inner exception when base64 decoding fails.Re-throwing without the inner exception drops valuable diagnostics.
- catch (Exception ex) when (ex is not InvalidBase64FormatException) - { - throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64); - } + catch (Exception ex) when (ex is not InvalidBase64FormatException) + { + throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64, ex); + }
131-141: Expand/validate supported MIME types (or fail fast on unknown).Defaulting to “jpg” can hide integration errors. Consider whitelisting and throwing for unsupported types or at least extending the map.
private string GetExtensionFromMimeType(string mimeType) { return mimeType.ToLower() switch { "image/jpeg" => "jpg", "image/jpg" => "jpg", "image/png" => "png", "image/webp" => "webp", - _ => "jpg" + "image/gif" => "gif", + "image/svg+xml" => "svg", + "image/bmp" => "bmp", + "image/tiff" => "tiff", + _ => throw new ArgumentException(ImageConstants.MimeTypeValidationError(new[] + { + "image/jpeg","image/jpg","image/png","image/webp","image/gif","image/svg+xml","image/bmp","image/tiff" + }), nameof(mimeType)) }; }If changing behavior is risky, keep the default but log a warning; add unit tests for each supported type.
10-20: Decouple BLL from HttpContext to improve testability and layering.Injecting IHttpContextAccessor in BLL creates a Web dependency. Consider introducing an IUrlProvider abstraction or returning a relative path from BLL and composing the absolute URL in the Web layer.
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (4)
71-87: Rename test to reflect URL semantics (not Base64).The name still references Base64. Update for clarity.
- public async Task GetFileUrl_ShouldReturnOriginalBase64() + public async Task GetFileUrl_ShouldBuildAbsoluteUrl()
45-48: Nit: “encryptedContent” is a leftover from previous implementation.Rename to “savedContent” to avoid confusion.
- var encryptedContent = File.ReadAllBytes(filePath); + var savedContent = File.ReadAllBytes(filePath); - var originalContent = Convert.FromBase64String(_base64); - Assert.Equal(originalContent, encryptedContent); + var originalContent = Convert.FromBase64String(_base64); + Assert.Equal(originalContent, savedContent);
89-101: Add coverage for PathBase and URL-encoding.Given the updated GetFileUrl, add tests for:
- PathBase = “/api/v1”
- blob names with spaces or non-ASCII (“team logo 1”, “фото”)
I can draft these tests if you’d like.
103-113: Consider a regression test for update flow (save-first semantics).Once UpdateFileInStorageAsync is refactored, add a test that forces the save to fail and ensures the old file remains intact.
Happy to provide a test that uses a mocked file system or injected file writer to simulate a write failure.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageToUrlResolver.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs(4 hunks)VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs(1 hunks)VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageToUrlResolver.cs
- VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
🧰 Additional context used
🧬 Code graph analysis (2)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (7)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(3-14)BlobStorageException(5-8)BlobStorageException(10-13)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (3)
BlobFileSystemException(3-18)BlobFileSystemException(5-9)BlobFileSystemException(11-15)VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (2)
GetFileUrl(6-6)DeleteFileInStorage(8-8)VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2)
ImageConstants(3-40)WrongFileName(36-39)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
ImageProcessingException(3-18)ImageProcessingException(5-9)ImageProcessingException(11-15)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
InvalidBase64FormatException(3-14)InvalidBase64FormatException(5-8)InvalidBase64FormatException(10-13)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
BlobFileNameException(3-18)BlobFileNameException(5-9)BlobFileNameException(11-15)
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (3)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (8)
BlobService(10-189)BlobService(15-20)Task(22-40)Task(42-48)Task(74-80)Task(143-157)Task(159-178)GetFileUrl(50-72)VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (1)
GetFileUrl(6-6)VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (1)
ImageConstants(3-40)
⏰ 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: Build and analyze
- GitHub Check: Build and analyze
🔇 Additional comments (4)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (1)
143-157: Write-path exceptions mapped correctly. LGTM.VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (3)
131-141: Base64 failure path well-covered. LGTM.
152-165: Read failure path under lock is solid. LGTM.
167-178: Robust cleanup. LGTM.
| public string GetFileUrl(string name, string mimeType) | ||
| { | ||
| ValidateFileName(name); | ||
|
|
||
| try | ||
| { | ||
| using var stream = await FindFileInStorageAsMemoryStreamAsync(name, mimeType); | ||
| return Convert.ToBase64String(stream.ToArray()); | ||
| var extension = GetExtensionFromMimeType(mimeType); | ||
| var fileName = $"{name}.{extension}"; | ||
| HttpRequest? request = _httpContextAccessor.HttpContext?.Request; | ||
|
|
||
| if (request == null) | ||
| { | ||
| throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.HttpContextIsNotAvailable); | ||
| } | ||
|
|
||
| var baseUrl = $"{request.Scheme}://{request.Host}"; | ||
| return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}"; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Harden URL construction: respect PathBase, avoid double slashes, and URL-encode file names.
- Include Request.PathBase for apps mounted under a sub-path.
- Trim separators on ImagesSubPath to prevent “//”.
- URL-encode file names to handle spaces and non-ASCII safely.
- public string GetFileUrl(string name, string mimeType)
+ public string GetFileUrl(string name, string mimeType)
{
ValidateFileName(name);
try
{
var extension = GetExtensionFromMimeType(mimeType);
- var fileName = $"{name}.{extension}";
+ var fileName = $"{name}.{extension}";
+ var encodedFileName = Uri.EscapeDataString(fileName);
HttpRequest? request = _httpContextAccessor.HttpContext?.Request;
if (request == null)
{
throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.HttpContextIsNotAvailable);
}
- var baseUrl = $"{request.Scheme}://{request.Host}";
- return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}";
+ var baseUrl = $"{request.Scheme}://{request.Host}";
+ var pathBase = request.PathBase.HasValue ? request.PathBase.Value.TrimEnd('/') : string.Empty;
+ var imagesPath = _blobEnv.ImagesSubPath.Trim('/', '\\');
+ return $"{baseUrl}{pathBase}/{imagesPath}/{encodedFileName}";
}
catch (Exception ex) when (ex is not BlobStorageException)
{
- throw new ImageProcessingException(_blobEnv.FullPath, ex.Message, ex);
+ throw new ImageProcessingException(fileName, ex.Message, ex);
}
}Note: If you prefer framework help, inject LinkGenerator or use PathString to combine segments safely.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs around
lines 50 to 67, the URL construction ignores Request.PathBase, can produce
double slashes from ImagesSubPath, and doesn't URL-encode file names; update the
method to include request.PathBase (when present), trim leading/trailing slashes
from _blobEnv.ImagesSubPath, URL-encode the fileName (or the name before
appending extension), and build the final path by combining PathBase,
ImagesSubPath and fileName using safe join semantics
(PathString/Uri/LinkGenerator or manual trim + single '/' concatenation) so you
never emit "//" and non-ASCII/space characters are percent-encoded.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (5)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (1)
36-39: Rename WrongFileName to reflect its semantics and align wording with FileNotFoundThe method name implies an invalid filename, but the message describes a retrieval failure. Consider renaming to match behavior and keep phrasing consistent with FileNotFound.
Suggested change:
- public static string WrongFileName(string name) - { - return $"An error occurred while retrieving the file {name}."; - } + public static string FailedToRetrieveFile(string fileName) + { + return $"Failed to retrieve file: {fileName}."; + }Follow-up: update call sites accordingly. Using “fileName” also aligns parameter naming with FileNotFound’s “filePath”.
Find call sites to safely apply the rename:
#!/bin/bash rg -nP -C2 '\bImageConstants\.WrongFileName\s*\('VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (4)
15-27: Good catch: constructor now hardens directory creation with explicit error handlingWrapping CreateDirectory in try/catch and surfacing BlobFileSystemException with a domain-specific message aligns with previous feedback and avoids silent failures.
10-11: Add XML docs for the service and public methods with declared exception contractsPlease add summary and exception tags for all public methods (e.g., SaveFileInStorageAsync, GetFileUrl, UpdateFileInStorageAsync, DeleteFileInStorage). This was requested earlier and will help consumers understand failure modes (BlobFileSystemException, ImageProcessingException, BlobNotFoundException, InvalidBase64FormatException, BlobFileNameException).
81-86: Avoid data loss in UpdateFileInStorageAsync: save new file before deleting the old oneDeleting first risks irrecoverable loss if the subsequent save fails. Swap the order; delete only after a successful save. Optionally, best-effort delete failures can be logged but should not fail the whole operation after a successful save.
public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType) { ValidateFileName(newBlobName); - DeleteFileInStorage(previousBlobName, previousMimeType); - await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); - return newBlobName; + // 1) Save new first + await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); + // 2) Then delete old (best-effort) + try + { + DeleteFileInStorage(previousBlobName, previousMimeType); + } + catch (BlobStorageException) + { + // Consider logging; returning success keeps the new asset available + // Re-throw if your domain requires strict cleanup + } + return newBlobName; }
57-79: Harden URL generation: respect PathBase, URL-encode the file name, and fix exception context
- Respect Request.PathBase for apps hosted under a sub-path.
- Trim separators on ImagesSubPath to avoid double slashes.
- URL-encode file names so spaces/non-ASCII are safe.
- In the catch, pass the file identifier (not the storage directory) to ImageProcessingException.
This prevents broken links and clarifies error telemetry.
public string GetFileUrl(string name, string mimeType) { ValidateFileName(name); try { var extension = GetExtensionFromMimeType(mimeType); - var fileName = $"{name}.{extension}"; + var fileName = $"{name}.{extension}"; + var encodedFileName = Uri.EscapeDataString(fileName); HttpRequest? request = _httpContextAccessor.HttpContext?.Request; if (request == null) { throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.HttpContextIsNotAvailable); } - var baseUrl = $"{request.Scheme}://{request.Host}"; - return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}"; + var baseUrl = $"{request.Scheme}://{request.Host}"; + var pathBase = request.PathBase.HasValue ? request.PathBase.Value.TrimEnd('/') : string.Empty; + var imagesPath = _blobEnv.ImagesSubPath.Trim('/', '\\'); + return $"{baseUrl}{pathBase}/{imagesPath}/{encodedFileName}"; } catch (Exception ex) when (ex is not BlobStorageException) { - throw new ImageProcessingException(_blobEnv.FullPath, ex.Message, ex); + // Use the logical file identifier rather than the directory path + var fileId = $"{name}.{GetExtensionFromMimeType(mimeType)}"; + throw new ImageProcessingException(fileId, ex.Message, ex); } }
🧹 Nitpick comments (6)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (1)
16-19: Clarify and Standardize ImageConstants NamingTo improve clarity around the different “save image” errors and ensure consistent naming:
Distinguish scopes of save-image constants
•FailToSaveImageInStorage(used inBlobFileSystemExceptionand unit tests) denotes failures writing to blob/file storage.
•FailedToSaveImage(used inImageProcessingExceptioninBlobService.cs) covers general image-processing save failures.
If you’d like more explicit names, consider renaming to something like:
–FailedToSaveImageToStorage
–FailedToSaveImageAfterProcessingEnforce the “FailedTo*” pattern for directory-creation errors
Current constant breaks pattern:- public static readonly string FailToCreateDirectory = "Failed to create blob storage directory"; + public static readonly string FailedToCreateDirectory = "Failed to create blob storage directory";Update all call sites in
BlobService.cs(line 25) and any tests. To preserve backward compatibility, you could mark the oldFailToCreateDirectoryas[Obsolete]and introduce the newFailedToCreateDirectoryin parallel for one release cycle.Key call sites to update:
VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
– Line 25:throw new BlobFileSystemException(..., ImageConstants.FailToCreateDirectory, ex);VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
– Line 162:throw new ImageProcessingException(..., ImageConstants.FailedToSaveImage, ex);VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs
– Lines 169 & 184: referencesImageConstants.FailToSaveImageInStorageVictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5)
33-47: Normalize error mapping in SaveFileInStorageAsync to avoid leaking low-level exception messagesRight now unexpected exceptions bubble via ex.Message. Prefer a consistent, user-safe message (and keep the inner exception for diagnostics).
Apply:
- catch (Exception ex) when (ex is not BlobStorageException) - { - throw new BlobFileSystemException(_blobEnv.FullPath, ex.Message, ex); - } + catch (Exception ex) when (ex is not BlobStorageException) + { + // Keep inner exception for logs; present a stable domain message outward + throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.FailToSaveImageInStorage, ex); + }
49-55: Return a read-only MemoryStream to prevent accidental mutationCreating the stream as non-writable communicates intent and avoids accidental writes in consumers.
- return new MemoryStream(decodedBytes); + return new MemoryStream(decodedBytes, writable: false);
108-136: Preserve inner exception and avoid extra allocations when parsing Base64
- Use Split with a max count to avoid unnecessary array allocations.
- Preserve the inner exception when rethrowing to aid diagnostics (you already have an overload).
- if (base64.Contains(',')) - { - base64 = base64.Split(',')[1]; - } + if (base64.Contains(',')) + { + base64 = base64.Split(',', 2)[1]; + } var byteCount = base64.Length * 3 / 4; var buffer = ArrayPool<byte>.Shared.Rent(byteCount); try { if (!Convert.TryFromBase64String(base64, buffer, out var bytesWritten)) { throw new InvalidBase64FormatException(ImageConstants.InvalidBase64String); } var result = new byte[bytesWritten]; Array.Copy(buffer, result, bytesWritten); return result; } - catch (Exception ex) when (ex is not InvalidBase64FormatException) - { - throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64); - } + catch (Exception ex) when (ex is not InvalidBase64FormatException) + { + throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64, ex); + } finally { ArrayPool<byte>.Shared.Return(buffer); }
187-195: Strengthen file name validation by explicitly rejecting path separatorsOn Linux, backslash is not an invalid file name char; explicitly reject both directory separators to be platform-agnostic and prevent path injection.
- if (string.IsNullOrWhiteSpace(name) - || name.Contains("..") - || Path.GetInvalidFileNameChars().Any(name.Contains)) + if (string.IsNullOrWhiteSpace(name) + || name.Contains("..") + || name.Contains(Path.DirectorySeparatorChar) + || name.Contains(Path.AltDirectorySeparatorChar) + || Path.GetInvalidFileNameChars().Any(c => name.Contains(c))) { throw new BlobFileNameException(name, ImageConstants.WrongFileName(name)); }
57-74: Optional: Use LinkGenerator/PathString to construct URLs and support non-HTTP contextsIf HttpContext can be absent (background jobs, messaging handlers), consider:
- Injecting LinkGenerator to build paths safely (handles PathBase).
- Falling back to a configured BaseUrl when HttpContext is null.
This keeps URL generation reliable beyond web-request pipelines.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs(2 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (7)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (3)
BlobFileSystemException(3-18)BlobFileSystemException(5-9)BlobFileSystemException(11-15)VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2)
ImageConstants(3-40)WrongFileName(36-39)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(3-14)BlobStorageException(5-8)BlobStorageException(10-13)VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (2)
GetFileUrl(6-6)DeleteFileInStorage(8-8)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
ImageProcessingException(3-18)ImageProcessingException(5-9)ImageProcessingException(11-15)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
InvalidBase64FormatException(3-14)InvalidBase64FormatException(5-8)InvalidBase64FormatException(10-13)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
BlobFileNameException(3-18)BlobFileNameException(5-9)BlobFileNameException(11-15)
⏰ 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: Build and analyze
- GitHub Check: Build and analyze
🔇 Additional comments (3)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (3)
150-164: CreateFileAsync: solid error mapping and contextValidating the name, composing a deterministic path, and mapping failures to ImageProcessingException with the file identifier is spot on.
166-185: GetFileAsync: good use of BlobNotFoundException and contextual error mappingClear, minimal, and precise. Nice.
138-147: No action needed—extension derivation is consistentI reviewed every call site of GetFileUrl and confirmed that it always receives the same mimeType value that was used when the blob was saved or updated.
- Create/Update handlers persist the DTO’s MimeType into Image.MimeType and immediately use that exact value when saving the file (via GetExtensionFromMimeType) and later when resolving URLs.
- Validators restrict MimeType to the allowed set (
image/jpeg,image/jpg,image/png,image/webp), so the derived extension will always match one of the switch cases.- Unit and integration tests cover all supported MIME types—including mixed-case and “jpg” vs. “jpeg”—and assert the correct file extension is both written to disk and returned in the URL.
Since all saved blobs carry their own validated MIME string and every URL lookup derives its extension from that same string, there is no upstream mismatch risk in the current code.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (1)
115-122: Fix: asserting deletion at the wrong path (false-positive risk)The saved file lives under the Images subdir; this assertion checks the root temp dir instead and could pass even if deletion fails.
- var filePath = Path.Combine(_tempDir, $"{_fileName}.png"); + var filePath = Path.Combine(Path.Combine(_tempDir, _subDir), $"{_fileName}.png"); Assert.False(File.Exists(filePath));
♻️ Duplicate comments (1)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (1)
160-167: Avoid data loss when updating: save new first, delete old after successful saveDeleting the old blob before saving the new one risks permanent loss if the save fails mid-flight.
public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType) { ValidateFileName(newBlobName); ValidateFileName(previousBlobName); - DeleteFileInStorage(previousBlobName, previousMimeType); - await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); + await SaveFileInStorageAsync(base64Format, newBlobName, mimeType); + DeleteFileInStorage(previousBlobName, previousMimeType); return newBlobName; }Optionally log delete failures and continue to avoid blocking the update if cleanup fails.
🧹 Nitpick comments (9)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (2)
5-8: Doc comment punctuation/flow nit: collapse into one sentenceCurrently renders as “…HttpContext/>. during…”. Merge the two lines for cleaner XML docs.
/// <summary> -/// Represents errors that occur when working with <see cref="HttpContext"/>. -/// during blob storage operations (e.g., when building URLs). +/// Represents errors that occur when working with <see cref="HttpContext"/> during blob storage operations (e.g., when building URLs). /// </summary>
9-20: Consider sealing and adding serialization constructorException types are typically sealed and (optionally) serializable for completeness.
+using System.Runtime.Serialization; @@ -public class BlobHttpContextException : BlobStorageException +[Serializable] +public sealed class BlobHttpContextException : BlobStorageException { public BlobHttpContextException(string message) : base(message) { } public BlobHttpContextException(string message, Exception innerException) : base(message, innerException) { } + + protected BlobHttpContextException(SerializationInfo info, StreamingContext context) + : base(info.GetString(nameof(info)), new Exception()) // adapt if base adds serialization + { + } }If your solution doesn’t use binary serialization, you can skip the constructor and just mark the class sealed.
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (6)
76-91: XML docs: stray “/// ///”There’s a duplicated XML doc prefix on Line 85 that hurts IDE rendering.
- /// /// <exception cref="ImageProcessingException"> + /// <exception cref="ImageProcessingException">
114-136: Harden URL construction: respect PathBase, trim slashes, and URL-encode file namesWithout PathBase, apps hosted under a sub-path will generate broken links. Not encoding file names risks invalid URLs for spaces/non-ASCII. Trimming avoids “//”.
Please add tests covering PathBase and encoded names (see unit-test suggestion below).
public string GetFileUrl(string name, string mimeType) { ValidateFileName(name); try { var extension = GetExtensionFromMimeType(mimeType); - var fileName = $"{name}.{extension}"; + var fileName = $"{name}.{extension}"; + var encodedFileName = Uri.EscapeDataString(fileName); HttpRequest? request = _httpContextAccessor.HttpContext?.Request; if (request == null) { throw new BlobHttpContextException(ImageConstants.HttpContextIsNotAvailable); } - var baseUrl = $"{request.Scheme}://{request.Host}"; - return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}"; + var baseUrl = $"{request.Scheme}://{request.Host}"; + var pathBase = request.PathBase.HasValue ? request.PathBase.Value.TrimEnd('/') : string.Empty; + var imagesPath = _blobEnv.ImagesSubPath.Trim('/', '\\'); + return $"{baseUrl}{pathBase}/{imagesPath}/{encodedFileName}"; } catch (Exception ex) when (ex is not BlobStorageException) { throw new BlobFileSystemException(name, ex.Message, ex); } }
184-186: Remove unused variable and inline computationfullName is only used once to compute filePath; inline to reduce noise.
- var fullName = name + "." + GetExtensionFromMimeType(mimeType); - var filePath = Path.Combine(_blobEnv.FullPath, fullName); + var filePath = Path.Combine(_blobEnv.FullPath, $"{name}.{GetExtensionFromMimeType(mimeType)}");
219-222: Preserve inner exception for easier diagnosticsRe-throwing without inner exception hides the original cause.
- catch (Exception ex) when (ex is not InvalidBase64FormatException) - { - throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64); - } + catch (Exception ex) when (ex is not InvalidBase64FormatException) + { + throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64, ex); + }
231-238: Broaden MIME type support (gif, svg, avif, heic)Fallback to jpg may mislabel content and break consumers. Recognize common modern formats.
private string GetExtensionFromMimeType(string mimeType) { return mimeType.ToLower() switch { "image/jpeg" => "jpg", "image/jpg" => "jpg", "image/png" => "png", "image/webp" => "webp", + "image/gif" => "gif", + "image/svg+xml" => "svg", + "image/avif" => "avif", + "image/heic" => "heic", _ => "jpg" }; }
278-286: Reduce over-restriction: “..” check is unnecessary once path separators are blockedSince path separators are disallowed, “..” cannot traverse and may be valid in legitimate names (e.g., “v1..backup”). Consider removing it.
- if (string.IsNullOrWhiteSpace(name) - || name.Contains("..") - || Path.GetInvalidFileNameChars().Any(name.Contains)) + if (string.IsNullOrWhiteSpace(name) + || Path.GetInvalidFileNameChars().Any(name.Contains)) { throw new BlobFileNameException(name, ImageConstants.WrongFileName(name)); }VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (1)
71-87: Rename for clarity and add PathBase assertionMethod name still says “Base64” though we now return URLs. Also, consider asserting PathBase handling once BlobService is updated.
- public async Task GetFileUrl_ShouldReturnOriginalBase64() + public async Task GetFileUrl_ShouldReturnAbsoluteUrl() @@ - Assert.Equal("https://example.com/Images/image123.png", result); + Assert.Equal("https://example.com/Images/image123.png", result);Additionally, add a test where request.PathBase = "/api" and ensure the URL is “https://example.com/api/Images/image123.png” after the BlobService change.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs(5 hunks)VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs
🧰 Additional context used
🧬 Code graph analysis (3)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(6-17)BlobStorageException(8-11)BlobStorageException(13-16)
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (7)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (8)
BlobService(10-287)BlobService(24-36)Task(57-74)Task(91-97)Task(160-167)Task(241-255)Task(257-276)GetFileUrl(114-136)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
BlobFileNameException(7-22)BlobFileNameException(9-13)BlobFileNameException(15-19)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (3)
BlobHttpContextException(9-20)BlobHttpContextException(11-14)BlobHttpContextException(16-19)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
InvalidBase64FormatException(7-18)InvalidBase64FormatException(9-12)InvalidBase64FormatException(14-17)VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (1)
ImageConstants(3-40)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (2)
BlobNotFoundException(7-16)BlobNotFoundException(9-13)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
ImageProcessingException(7-22)ImageProcessingException(9-13)ImageProcessingException(15-19)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (8)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (3)
BlobFileSystemException(7-22)BlobFileSystemException(9-13)BlobFileSystemException(15-19)VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2)
ImageConstants(3-40)WrongFileName(36-39)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(6-17)BlobStorageException(8-11)BlobStorageException(13-16)VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (2)
GetFileUrl(6-6)DeleteFileInStorage(8-8)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (3)
BlobHttpContextException(9-20)BlobHttpContextException(11-14)BlobHttpContextException(16-19)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
ImageProcessingException(7-22)ImageProcessingException(9-13)ImageProcessingException(15-19)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
InvalidBase64FormatException(7-18)InvalidBase64FormatException(9-12)InvalidBase64FormatException(14-17)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
BlobFileNameException(7-22)BlobFileNameException(9-13)BlobFileNameException(15-19)
⏰ 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: Build and analyze
- GitHub Check: Build and analyze
🔇 Additional comments (10)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (2)
12-14: DI choices look goodStoring BlobEnvironmentVariables and IHttpContextAccessor as readonly fields is appropriate for this service’s responsibilities.
15-36: Constructor hardening is solidCreating the directory in a guarded block and mapping failures to BlobFileSystemException with a meaningful path/message is exactly what we want here.
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (8)
38-48: Test covers content integrity—niceVerifies persisted bytes match decoded Base64 and asserts expected file path under subdir. Good guardrail for regressions.
50-60: Good negative-path coverage on invalid namesAsserting message contains the invalid name improves diagnosability of failures.
62-69: Stream retrieval test is clear and robustRoundtrip check via MemoryStream validates read path behavior. LGTM.
89-101: Good guard: null HttpRequest → BlobHttpContextExceptionEnsures callers don’t silently get malformed URLs. Nice.
131-141: Invalid Base64 path covered wellAsserting specific message aligns with ImageConstants and helps catch regressions.
142-150: Not-found read path test looks goodConfirms BlobNotFoundException surfaces with informative message.
151-165: Robust “file locked” scenarioGreat to see a realistic IO exception path validated with the expected ImageProcessingException wrapper.
167-179: Cleanup is resilientNormalizing attributes before recursive delete avoids stubborn temp files on Windows. Nicely done.
| namespace VictoryCenter.BLL.Exceptions.BlobStorageExceptions; | ||
|
|
||
| /// <summary> | ||
| /// Represents errors that occur when a requested file is not found. |
There was a problem hiding this comment.
What about punctuation? It doesn't look like an end of a sentence.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1)
3-7: Good domain-specific exception with clear XML summary — thanks for adding itThe class purpose is clear and aligns with the BlobStorageException hierarchy. This directly addresses the earlier request to document what each exception is responsible for.
🧹 Nitpick comments (5)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (5)
6-6: Seal the exception to prevent unintended inheritanceException types are rarely intended as base classes. Sealing avoids accidental subclassing and keeps the hierarchy simple.
-public class BlobFileNameException : BlobStorageException +public sealed class BlobFileNameException : BlobStorageException
8-12: Add the file name to Exception.Data for easier telemetry correlationAttaching the contextual key to Exception.Data helps centralized logging/telemetry without forcing string parsing of the message.
public BlobFileNameException(string name, string message) : base(message) { Name = name; + Data["BlobFileName"] = name; } public BlobFileNameException(string name, string message, Exception innerException) : base(message, innerException) { Name = name; + Data["BlobFileName"] = name; }If you apply the earlier rename, replace
namewithfileName.Also applies to: 14-18
8-12: Document the constructors and property for IntelliSense completenessMinor, but helpful for consumers. If you decide not to rename, keep the tags in sync with the current identifiers.
+ /// <summary> + /// Initializes a new instance of the <see cref="BlobFileNameException"/> class. + /// </summary> + /// <param name="name">The invalid blob file name associated with the error.</param> + /// <param name="message">The error message that explains the reason for the exception.</param> public BlobFileNameException(string name, string message) : base(message) { Name = name; } + /// <summary> + /// Initializes a new instance of the <see cref="BlobFileNameException"/> class with an inner exception. + /// </summary> + /// <param name="name">The invalid blob file name associated with the error.</param> + /// <param name="message">The error message that explains the reason for the exception.</param> + /// <param name="innerException">The exception that is the cause of the current exception.</param> public BlobFileNameException(string name, string message, Exception innerException) : base(message, innerException) { Name = name; } + /// <summary> + /// Gets the blob file name related to this exception. + /// </summary> public string Name { get; }Also applies to: 14-18, 20-20
8-18: Sanitize logged file names if they can contain full paths or PIIIf exception messages or the Name/FileName value are logged, ensure they do not leak absolute paths or user-identifying segments. Prefer storing only the file name (no directory) or a hashed value if necessary.
Happy to help add a small helper (e.g., IPathRedactor.Redact) and use it at throw sites if needed.
8-12: Rename exception property and parameter for clarityThe
BlobFileNameExceptionAPI currently exposes a genericNameproperty and constructor parameter. Renaming these toFileNamewill improve readability at call sites and in logs, and align with any other blob-related exceptions that useFileNameorBlobName. Since this is a public API change, consider providing a deprecated alias for one release cycle to avoid breaking existing consumers.Locations to update:
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs
• Rename ctor parameters fromstring nametostring fileNamein both overloads
• Change assignments fromName = name;toFileName = fileName;
• Rename the auto-property frompublic string Name { get; }topublic string FileName { get; }
• (Optional but recommended) Add a[Obsolete("Use FileName instead")] public string Name => FileName;aliasExample diff in BlobFileNameException.cs:
- public BlobFileNameException(string name, string message) + public BlobFileNameException(string fileName, string message) : base(message) { - Name = name; + FileName = fileName; } - public BlobFileNameException(string name, string message, Exception innerException) + public BlobFileNameException(string fileName, string message, Exception innerException) : base(message, innerException) { - Name = name; + FileName = fileName; } - public string Name { get; } + public string FileName { get; } + [Obsolete("Use FileName instead")] + public string Name => FileName;
- No call-site changes are required in
BlobService.cs, as the constructor invocation still passes the localnamevariable.- After renaming, run a quick search to confirm no other code or tests reference the old
Namemember.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs
- VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs
- VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
🧰 Additional context used
🧬 Code graph analysis (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
BlobStorageException(6-17)BlobStorageException(8-11)BlobStorageException(13-16)
⏰ 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: Build and analyze
- GitHub Check: Build and analyze
|
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetProgramByIdTests.cs (1)
1-12: Ensure xUnit namespace is imported
Tests use the[Fact]attribute but there’s nousing Xunit;(nor a global using); addusing Xunit;at the top of VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetProgramByIdTests.cs or declare it in a shared Usings.cs to resolve the attribute.
🧹 Nitpick comments (20)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/ProgramCategories/GetProgramCategoriesTests.cs (1)
87-89: Mocks updated to new URL-based contract — looks good.Switch to GetFileUrl with a synchronous Returns is correct for the new interface.
To reduce duplication across tests, consider a shared test constant for the URL.
- .Returns("https://localhost:5000/supersecretimage.png"); + .Returns(TestImageData.ImageUrl);VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/CreateProgramTests.cs (1)
130-132: Correct API usage; but this setup appears unused in this test.AutoMapper mapping is stubbed to return a prebuilt DTO, so the blob service isn’t exercised. Either remove the setup or assert it’s not called to keep the test intention clear.
Minimal options:
- _blobServiceMock - .Setup(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>())) - .Returns("https://localhost:5000/supersecretimage.png"); + // Not needed: mapping is stubbed and does not invoke blob service in this test.Or verify zero usage:
Result<ProgramDto> result = await handler.Handle(new CreateProgramCommand(_createProgramDto), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(result.Value.Name, _programEntity.Name); + _blobServiceMock.VerifyNoOtherCalls();VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetPublishedProgramsTests.cs (1)
96-98: Aligned with GetFileUrl — OK.Same note as other tests: if mapping is stubbed, this setup likely doesn’t run. Consider removing or centralizing the URL constant.
VictoryCenter/VictoryCenter.BLL/Commands/Programs/Create/CreateProgramHandler.cs (2)
21-26: Constructor injects IBlobService but never uses it.This introduces dead code and can confuse readers. Two paths:
- Short-term (no breaking changes): explicitly discard the dependency to document intent.
- Better (follow-up): remove the parameter and update DI/tests.
Apply the short-term fix:
public CreateProgramHandler(IMapper mapper, IRepositoryWrapper repositoryWrapper, IValidator<CreateProgramCommand> validator, IBlobService blobService) { _mapper = mapper; _repositoryWrapper = repositoryWrapper; _validator = validator; + _ = blobService; // intentionally unused; AutoMapper resolvers obtain it via DI }
70-73: Catching BlobStorageException is fine; consider narrowing scope.If only the mapping step can throw blob-related errors, wrap just the mapping in the catch to avoid masking unrelated faults.
- if (await _repositoryWrapper.SaveChangesAsync() > 0) - { - return Result.Ok(_mapper.Map<ProgramDto>(entity)); - } + if (await _repositoryWrapper.SaveChangesAsync() > 0) + { + try + { + return Result.Ok(_mapper.Map<ProgramDto>(entity)); + } + catch (BlobStorageException) + { + return Result.Fail<ProgramDto>(ProgramConstants.FailedRetrievingProgramPhoto); + } + } - } - catch (BlobStorageException) - { - return Result.Fail<ProgramDto>(ProgramConstants.FailedRetrievingProgramPhoto); - } + }VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetPrograms.cs (1)
191-193: Good switch to GetFileUrl; minor test hygiene.As with the other tests, if AutoMapper is stubbed, this setup doesn’t execute. Remove it or verify interactions. Also consider a shared TestImageUrl constant.
- _blobService - .Setup(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>())) - .Returns("https://localhost:5000/supersecretimage.png"); + // Not required here: mapping is stubbed.VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetProgramByIdTests.cs (4)
94-95: Good switch to URL-based blob retrieval; add stronger assertions and invocation verificationThe mock aligns with the new IBlobService API. To actually exercise the new flow, assert the returned URL on the DTO and verify the blob service was called. Also, prefer a neutral domain in tests.
Apply within this hunk:
- .Returns("https://localhost:5000/supersecretimage.png"); + .Returns("https://example.com/image.png");And add the following (outside this hunk) in the success test, after asserting Status and before the existing verifications:
+ Assert.NotNull(result.Value.Image); + Assert.Equal("https://example.com/image.png", result.Value.Image.Url); + _mockBlobService.Verify(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
45-60: Validate image URL on success and verify GetFileUrl is invoked onceWithout checking Image.Url and the invocation count, we could regress without noticing.
Apply:
Assert.Equal(_programDto.Status, result.Value.Status); + Assert.NotNull(result.Value.Image); + Assert.Equal("https://example.com/image.png", result.Value.Image.Url); _mockRepositoryWrapper.Verify(x => x.ProgramsRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<DAL.Entities.Program>>()), Times.Once); - _mapperMock.Verify(x => x.Map<ProgramDto>(It.IsAny<DAL.Entities.Program>()), Times.Once); + _mapperMock.Verify(x => x.Map<ProgramDto>(It.IsAny<DAL.Entities.Program>()), Times.Once); + _mockBlobService.Verify(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
62-71: Ensure blob service is not called on not-found pathThis tightens the negative-path behavior for the new URL logic.
Apply:
Assert.False(result.IsSuccess); Assert.Equal(ErrorMessagesConstants.NotFound(_programEntity.Id, typeof(Program)), result.Errors[0].Message); + _mockBlobService.Verify(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
70-70: Avoid potential type ambiguity in error assertionBe explicit about the entity type to future-proof the message against any other Program types in scope.
Apply:
- Assert.Equal(ErrorMessagesConstants.NotFound(_programEntity.Id, typeof(Program)), result.Errors[0].Message); + Assert.Equal(ErrorMessagesConstants.NotFound(_programEntity.Id, typeof(VictoryCenter.DAL.Entities.Program)), result.Errors[0].Message);VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (4)
262-264: Avoid reusing a disposed TransactionScope in mocksMoq .Returns(value) reuses the same instance per call setup. Future refactors that call BeginTransaction() multiple times in one handler execution could hit a disposed scope. Prefer a factory delegate to return a fresh scope each time.
- _mockRepositoryWrapper.Setup(x => x.BeginTransaction()) - .Returns(new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)); + _mockRepositoryWrapper.Setup(x => x.BeginTransaction()) + .Returns(() => new TransactionScope(TransactionScopeAsyncFlowOption.Enabled));
82-106: Minor: InlineData values read like names but represent descriptionsYour theory data ("Valid Name", "Updated Name", "A") is used as Description. Consider renaming the strings to avoid confusion in future maintenance.
Example:
- [InlineData("Valid Name")] - [InlineData("Updated Name")] - [InlineData("A")] + [InlineData("Valid description")] + [InlineData("Another description")] + [InlineData("A")]
151-156: Avoid mutating shared fixture fields in testsMutating _testUpdatedTeamMember.FullName inside a test can create subtle coupling. Prefer a local clone to keep tests self-contained.
- _testUpdatedTeamMember.FullName = testName!; + var localTeamMember = _testUpdatedTeamMember with { FullName = testName! }; + _mockMapper.Reset(); + _mockMapper.Setup(m => m.Map<UpdateTeamMemberDto, TeamMember>(It.IsAny<UpdateTeamMemberDto>())) + .Returns(localTeamMember);
238-268: Add coverage for ImageId path and category change priorityTwo critical branches in the handler aren’t exercised here:
- When ImageId is provided, the handler fetches Image and maps it.
- When CategoryId changes, Priority should become max+1.
Consider adding targeted tests for both.
Sample test sketch (add as separate facts):
[Fact] public async Task Handle_WhenImageIdProvided_ShouldLoadImageAndMapUrl() { var existing = _testExistingTeamMember; var toUpdate = _testUpdatedTeamMember with { ImageId = 42 }; var image = new Image { Id = 42, BlobName = "img.png", MimeType = "image/png" }; _mockRepositoryWrapper.Setup(r => r.TeamMembersRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<TeamMember>>())) .ReturnsAsync(existing); _mockRepositoryWrapper.Setup(r => r.ImageRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<Image>>())) .ReturnsAsync(image); _mockRepositoryWrapper.Setup(r => r.SaveChangesAsync()).ReturnsAsync(1); _mockRepositoryWrapper.Setup(r => r.BeginTransaction()) .Returns(() => new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)); _mockMapper.Setup(m => m.Map<UpdateTeamMemberDto, TeamMember>(It.IsAny<UpdateTeamMemberDto>())).Returns(toUpdate); _mockMapper.Setup(m => m.Map<TeamMember, TeamMemberDto>(It.IsAny<TeamMember>())) .Returns(new TeamMemberDto { Id = toUpdate.Id, Image = new ImageDto { Url = "http://example/img.png" } }); var handler = new UpdateTeamMemberHandler(_mockMapper.Object, _mockRepositoryWrapper.Object, _validator); var result = await handler.Handle(new UpdateTeamMemberCommand(new UpdateTeamMemberDto { FullName = "X", CategoryId = existing.CategoryId }, existing.Id), CancellationToken.None); Assert.True(result.IsSuccess); Assert.NotNull(result.Value.Image); Assert.False(string.IsNullOrWhiteSpace(result.Value.Image.Url)); } [Fact] public async Task Handle_WhenCategoryChanges_ShouldReassignPriorityToMaxPlusOne() { var existing = _testExistingTeamMember; var toUpdate = _testUpdatedTeamMember with { CategoryId = 2 }; _mockRepositoryWrapper.Setup(r => r.TeamMembersRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<TeamMember>>())) .ReturnsAsync(existing); _mockRepositoryWrapper.Setup(r => r.TeamMembersRepository.MaxAsync(It.IsAny<Expression<Func<TeamMember, int?>>>(), It.IsAny<Expression<Func<TeamMember, bool>>>())) .ReturnsAsync(5); _mockRepositoryWrapper.Setup(r => r.CategoriesRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<Category>>())) .ReturnsAsync(new Category { Id = 2, Name = "New" }); _mockRepositoryWrapper.Setup(r => r.SaveChangesAsync()).ReturnsAsync(1); _mockRepositoryWrapper.Setup(r => r.BeginTransaction()) .Returns(() => new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)); _mockMapper.Setup(m => m.Map<UpdateTeamMemberDto, TeamMember>(It.IsAny<UpdateTeamMemberDto>())).Returns(toUpdate); _mockMapper.Setup(m => m.Map<TeamMember, TeamMemberDto>(It.IsAny<TeamMember>())) .Returns(new TeamMemberDto { Id = toUpdate.Id, CategoryId = 2, Priority = 6 }); var handler = new UpdateTeamMemberHandler(_mockMapper.Object, _mockRepositoryWrapper.Object, _validator); var result = await handler.Handle(new UpdateTeamMemberCommand(new UpdateTeamMemberDto { FullName = "X", CategoryId = 2 }, existing.Id), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(6, result.Value.Priority); Assert.Equal(2, result.Value.CategoryId); }VictoryCenter/VictoryCenter.BLL/Commands/Programs/Update/UpdateProgramHandler.cs (2)
22-27: Remove unused IBlobService dependency from the constructor.The handler no longer uses IBlobService directly. Keeping it increases coupling and complicates DI/tests.
Apply:
- public UpdateProgramHandler(IMapper mapper, IRepositoryWrapper repositoryWrapper, IValidator<UpdateProgramCommand> validator, IBlobService blobService) + public UpdateProgramHandler(IMapper mapper, IRepositoryWrapper repositoryWrapper, IValidator<UpdateProgramCommand> validator) { _mapper = mapper; _repositoryWrapper = repositoryWrapper; _validator = validator; }
90-93: Clarify why BlobStorageException is handled here.No direct blob calls in this handler; the exception likely originates from mapping (e.g., URL resolvers). Add a brief comment to prevent future removal.
Apply:
+ // Mapping to ProgramDto may resolve image URLs via blob storage (e.g., AutoMapper resolver), so handle BlobStorageException. catch (BlobStorageException) { return Result.Fail<ProgramDto>(ProgramConstants.FailedToUpdateProgram); }VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/UpdateProgramTests.cs (4)
141-143: Remove unused blob service setup.IMapper is fully mocked, and the handler doesn’t use IBlobService; this setup never executes.
Apply:
- private void SetUpBlobService() - { - _blobServiceMock - .Setup(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>())) - .Returns("https://localhost:5000/supersecretimage.png"); - } + // Blob service is not used in these unit tests because IMapper is mocked.And stop calling it:
- SetUpAutomapper(); - SetUpBlobService(); - SetUpRepositoryWrapper(saveResult, programEntity); + SetUpAutomapper(); + SetUpRepositoryWrapper(saveResult, programEntity);
86-88: Verify repository interactions on success.Assert that Update and SaveChanges were called once.
Apply:
Assert.True(result.IsSuccess); Assert.Equal(result.Value.Name, _updateProgramDto.Name); + _repositoryWrapperMock.Verify(r => r.ProgramsRepository.Update(It.IsAny<DAL.Entities.Program>()), Times.Once); + _repositoryWrapperMock.Verify(r => r.SaveChangesAsync(), Times.Once);
100-102: Guard against side effects on validation failure.Ensure no save attempt was made when validation fails.
Apply:
Assert.False(result.IsSuccess); Assert.Contains("Validation failed", result.Errors[0].Message); + _repositoryWrapperMock.Verify(r => r.SaveChangesAsync(), Times.Never);
117-122: Strengthen NotFound test with interaction checks.Confirm Update wasn’t called when entity is missing.
Apply:
Assert.False(result.IsSuccess); Assert.Equal(ErrorMessagesConstants.NotFound(1, typeof(DAL.Entities.Program)), result.Errors[0].Message); + _repositoryWrapperMock.Verify(r => r.ProgramsRepository.Update(It.IsAny<DAL.Entities.Program>()), Times.Never); + _repositoryWrapperMock.Verify(r => r.SaveChangesAsync(), Times.Never);
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (15)
VictoryCenter/VictoryCenter.BLL/Commands/Programs/Create/CreateProgramHandler.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Commands/Programs/Update/UpdateProgramHandler.cs(1 hunks)VictoryCenter/VictoryCenter.BLL/Queries/ProgramCategories/GetProgramCategoriesHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetByFilters/GetProgramsByFiltersHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetById/GetProgramByIdHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetPublished/GetPublishedProgramsHandler.cs(0 hunks)VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs(3 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/ProgramCategories/GetProgramCategoriesTests.cs(1 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/CreateProgramTests.cs(1 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetProgramByIdTests.cs(1 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetPrograms.cs(1 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetPublishedProgramsTests.cs(1 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/UpdateProgramTests.cs(1 hunks)VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs(5 hunks)VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs(1 hunks)
💤 Files with no reviewable changes (4)
- VictoryCenter/VictoryCenter.BLL/Queries/ProgramCategories/GetProgramCategoriesHandler.cs
- VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetByFilters/GetProgramsByFiltersHandler.cs
- VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetById/GetProgramByIdHandler.cs
- VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetPublished/GetPublishedProgramsHandler.cs
🚧 Files skipped from review as they are similar to previous changes (2)
- VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs
- VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-20T18:22:51.823Z
Learnt from: Oleh-Bashtovyi
PR: ita-social-projects/VictoryCenter-Back#179
File: VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Reorder/ReorderTeamMembersHandler.cs:27-28
Timestamp: 2025-06-20T18:22:51.823Z
Learning: In the VictoryCenter codebase, FluentValidation is used for input validation in MediatR handlers. The handlers call ValidateAndThrowAsync() early in the Handle method, and validation exceptions are caught and converted to Result.Fail responses. This means validation logic should be kept in the validator classes rather than duplicated in the handlers.
Applied to files:
VictoryCenter/VictoryCenter.BLL/Commands/Programs/Create/CreateProgramHandler.cs
🧬 Code graph analysis (1)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (1)
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (2)
UpdateTeamMemberHandler(15-107)UpdateTeamMemberHandler(21-29)
⏰ 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: Build and analyze
- GitHub Check: Build and analyze
🔇 Additional comments (8)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (5)
157-157: Constructor update repeated — looks goodThis aligns with the refactor and keeps tests consistent.
184-184: Constructor update repeated — looks goodConsistent with earlier changes.
205-205: Constructor update repeated — looks goodNo issues spotted.
223-223: Constructor update repeated — looks goodAll handler constructions in this file now use the new signature.
124-124: Constructor update verified — LGTM
Instantiation matches the new 3-arg handler signature; no lingering 4-arg usages or IBlobService references remain in UpdateTeamMemberTests.VictoryCenter/VictoryCenter.BLL/Commands/Programs/Update/UpdateProgramHandler.cs (3)
12-12: Correct namespace import.Switching to the new BlobStorageExceptions namespace is consistent with the refactor.
58-67: Align ImageId null-handling and navigation property consistency.If the DTO sets ImageId to null, the navigation property remains unchanged. Also, fetch is conditioned on programToUpdate.ImageId whereas the lookup uses request.updateProgramDto.ImageId. Prefer checking the DTO and explicitly clearing the nav prop; if a non-null ImageId is provided but not found, fail early.
Apply:
- if (programToUpdate.ImageId != null) + if (request.updateProgramDto.ImageId.HasValue) { Image? newImage = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(new QueryOptions<Image> { Filter = image => image.Id == request.updateProgramDto.ImageId, AsNoTracking = false }); - programToUpdate.Image = newImage; + if (newImage is null) + { + return Result.Fail<ProgramDto>(ErrorMessagesConstants.NotFound(request.updateProgramDto.ImageId!.Value, typeof(Image))); + } + programToUpdate.Image = newImage; } + else + { + // DTO explicitly clears the image + programToUpdate.Image = null; + }
49-55: Validate category IDs exist to avoid partial updates.Today, missing IDs silently drop from the assignment. Consider verifying the requested IDs against fetched IDs and failing fast if any are missing.
If helpful, I can propose a small helper to compute and report missing IDs.
Also applies to: 71-74



dev
JIRA
Code reviewers
Second Level Review
Summary of issue
Research different approaches to image loading (e.g., caching, thumbnails, direct URLs) and speed up loading on the visitor team page.
Summary of change
Change file processing logic: use URLs instead of base64 data
Testing approach
unit/integration tests
CHECK LIST
Summary by CodeRabbit
New Features
Bug Fixes
Chores
Tests