Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,4 @@ dist
*idea/
*bin/
*obj/
*StaticFile/
*wwwroot/
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using AutoMapper;
using System.Transactions;
using AutoMapper;
using FluentResults;
using FluentValidation;
using MediatR;
using VictoryCenter.BLL.Constants;
using VictoryCenter.BLL.DTOs.Images;
using VictoryCenter.BLL.Exceptions;
using VictoryCenter.BLL.Exceptions.BlobStorageExceptions;
using VictoryCenter.BLL.Interfaces.BlobStorage;
using VictoryCenter.DAL.Entities;
using VictoryCenter.DAL.Repositories.Interfaces.Base;
Expand Down Expand Up @@ -34,7 +35,7 @@ public async Task<Result<ImageDTO>> Handle(CreateImageCommand request, Cancellat

var fileName = Guid.NewGuid().ToString().Replace("-", "");

using var transaction = _repositoryWrapper.BeginTransaction();
using TransactionScope transaction = _repositoryWrapper.BeginTransaction();

Image image = _mapper.Map<Image>(request.CreateImageDto);
image.BlobName = fileName;
Expand All @@ -49,8 +50,7 @@ public async Task<Result<ImageDTO>> Handle(CreateImageCommand request, Cancellat

await _blobService.SaveFileInStorageAsync(request.CreateImageDto.Base64, fileName, request.CreateImageDto.MimeType);

createdImage.Base64 = await _blobService.FindFileInStorageAsBase64Async(createdImage.BlobName, createdImage.MimeType);
var response = _mapper.Map<ImageDTO>(createdImage);
ImageDTO? response = _mapper.Map<ImageDTO>(createdImage);

transaction.Complete();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
using System.Transactions;
using FluentResults;
using MediatR;
using VictoryCenter.BLL.Constants;
using VictoryCenter.BLL.Exceptions.BlobStorageExceptions;
using VictoryCenter.BLL.Interfaces.BlobStorage;
using VictoryCenter.DAL.Entities;
using VictoryCenter.DAL.Repositories.Interfaces.Base;
using VictoryCenter.DAL.Repositories.Options;
using VictoryCenter.BLL.Interfaces.BlobStorage;
using VictoryCenter.BLL.Constants;
using VictoryCenter.BLL.Exceptions;

namespace VictoryCenter.BLL.Commands.Images.Delete;

public class DeleteImageHandler : IRequestHandler<DeleteImageCommand, Result<long>>
{
private readonly IRepositoryWrapper _repositoryWrapper;
private readonly IBlobService _blobService;
private readonly IRepositoryWrapper _repositoryWrapper;

public DeleteImageHandler(IRepositoryWrapper repositoryWrapper, IBlobService blobService)
{
Expand All @@ -24,17 +25,18 @@ public async Task<Result<long>> Handle(DeleteImageCommand request, CancellationT
{
try
{
var entityToDelete = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(new QueryOptions<Image>
{
Filter = entity => entity.Id == request.Id,
});
Image? entityToDelete = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(
new QueryOptions<Image>
{
Filter = entity => entity.Id == request.Id
});

if (entityToDelete is null)
{
return Result.Fail<long>(ErrorMessagesConstants.NotFound(request.Id, typeof(Image)));
}

using var transaction = _repositoryWrapper.BeginTransaction();
using TransactionScope transaction = _repositoryWrapper.BeginTransaction();

_repositoryWrapper.ImageRepository.Delete(entityToDelete);

Expand All @@ -45,7 +47,7 @@ public async Task<Result<long>> Handle(DeleteImageCommand request, CancellationT

if (!string.IsNullOrEmpty(entityToDelete.BlobName))
{
_blobService.DeleteFileInStorage(entityToDelete.BlobName, entityToDelete.MimeType);
_blobService.DeleteFileInStorage(entityToDelete.BlobName, entityToDelete.MimeType);
}

transaction.Complete();
Expand All @@ -54,7 +56,7 @@ public async Task<Result<long>> Handle(DeleteImageCommand request, CancellationT
}
catch (BlobStorageException e)
{
return Result.Fail<long>(ErrorMessagesConstants.BlobStorageError(e.Message) );
return Result.Fail<long>(ErrorMessagesConstants.BlobStorageError(e.Message));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
using System.Transactions;
using AutoMapper;
using FluentResults;
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using VictoryCenter.BLL.Constants;
using VictoryCenter.BLL.DTOs.Images;
using VictoryCenter.BLL.Exceptions.BlobStorageExceptions;
using VictoryCenter.BLL.Interfaces.BlobStorage;
using VictoryCenter.DAL.Entities;
using VictoryCenter.DAL.Repositories.Interfaces.Base;
using VictoryCenter.DAL.Repositories.Options;
using VictoryCenter.BLL.Constants;
using VictoryCenter.BLL.Exceptions;

namespace VictoryCenter.BLL.Commands.Images.Update;

Expand Down Expand Up @@ -47,11 +49,12 @@ public async Task<Result<ImageDTO>> Handle(UpdateImageCommand request, Cancellat
return Result.Fail<ImageDTO>(ErrorMessagesConstants.NotFound(request.Id, typeof(Image)));
}

using var transaction = _repositoryWrapper.BeginTransaction();
using TransactionScope transaction = _repositoryWrapper.BeginTransaction();

var previousType = imageEntity.MimeType;
imageEntity.MimeType = request.UpdateImageDto.MimeType!;

var result = _repositoryWrapper.ImageRepository.Update(imageEntity);
EntityEntry<Image> result = _repositoryWrapper.ImageRepository.Update(imageEntity);

if (await _repositoryWrapper.SaveChangesAsync() <= 0)
{
Expand All @@ -60,15 +63,14 @@ public async Task<Result<ImageDTO>> Handle(UpdateImageCommand request, Cancellat

var updatedBlobName = await _blobService.UpdateFileInStorageAsync(
imageEntity.BlobName,
imageEntity.MimeType,
previousType,
request.UpdateImageDto.Base64!,
imageEntity.BlobName,
request.UpdateImageDto.MimeType!);

imageEntity.BlobName = updatedBlobName;

ImageDTO resultDto = _mapper.Map<Image, ImageDTO>(imageEntity);
resultDto.Base64 = await _blobService.FindFileInStorageAsBase64Async(resultDto.BlobName, resultDto.MimeType);

transaction.Complete();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using VictoryCenter.BLL.DTOs.Images;
using VictoryCenter.BLL.Constants;
using VictoryCenter.BLL.DTOs.Images;
using VictoryCenter.BLL.DTOs.TeamMembers;
using VictoryCenter.BLL.Exceptions;
using VictoryCenter.BLL.Interfaces.BlobStorage;
using VictoryCenter.BLL.Exceptions.BlobStorageExceptions;
using VictoryCenter.DAL.Entities;
using VictoryCenter.DAL.Repositories.Interfaces.Base;
using VictoryCenter.DAL.Repositories.Options;
Expand All @@ -20,30 +19,29 @@ public class CreateTeamMemberHandler : IRequestHandler<CreateTeamMemberCommand,
private readonly IMapper _mapper;
private readonly IRepositoryWrapper _repositoryWrapper;
private readonly IValidator<CreateTeamMemberCommand> _validator;
private readonly IBlobService _blobService;

public CreateTeamMemberHandler(IRepositoryWrapper repositoryWrapper, IMapper mapper, IValidator<CreateTeamMemberCommand> validator, IBlobService blobService)
public CreateTeamMemberHandler(IRepositoryWrapper repositoryWrapper, IMapper mapper, IValidator<CreateTeamMemberCommand> validator)
{
_repositoryWrapper = repositoryWrapper;
_mapper = mapper;
_validator = validator;
_blobService = blobService;
}

public async Task<Result<TeamMemberDto>> Handle(CreateTeamMemberCommand request, CancellationToken cancellationToken)
{
try
{
await _validator.ValidateAndThrowAsync(request, cancellationToken);
var category = await _repositoryWrapper.CategoriesRepository.GetFirstOrDefaultAsync(
new QueryOptions<Category>()
Category? category = await _repositoryWrapper.CategoriesRepository.GetFirstOrDefaultAsync(
new QueryOptions<Category>
{
Filter = c => c.Id == request.createTeamMemberDto.CategoryId
});

if (category == null)
{
return Result.Fail<TeamMemberDto>(ErrorMessagesConstants.NotFound(request.createTeamMemberDto.CategoryId, typeof(Category)));
return Result.Fail<TeamMemberDto>(
ErrorMessagesConstants.NotFound(request.createTeamMemberDto.CategoryId, typeof(Category)));
}

TeamMember? entity = _mapper.Map<TeamMember>(request.createTeamMemberDto);
Expand All @@ -65,14 +63,10 @@ public async Task<Result<TeamMemberDto>> Handle(CreateTeamMemberCommand request,
if (entity.ImageId != null)
{
Image? imageResult = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(
new QueryOptions<Image>()
new QueryOptions<Image>
{
Filter = i => i.Id == entity.ImageId
});
if (imageResult is not null)
{
imageResult.Base64 = await _blobService.FindFileInStorageAsBase64Async(imageResult.BlobName, imageResult.MimeType);
}

result.Image = _mapper.Map<ImageDTO>(imageResult);
}
Expand All @@ -87,11 +81,12 @@ public async Task<Result<TeamMemberDto>> Handle(CreateTeamMemberCommand request,
}
catch (BlobStorageException e)
{
return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}" );
return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}");
}
catch (DbUpdateException ex)
{
return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase + ex.Message);
return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase +
ex.Message);
Comment on lines +88 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
using FluentResults;
using FluentValidation;
using MediatR;
using VictoryCenter.BLL.DTOs.Images;
using VictoryCenter.BLL.Constants;
using VictoryCenter.BLL.DTOs.TeamMembers;
using VictoryCenter.BLL.Exceptions;
using VictoryCenter.BLL.Interfaces.BlobStorage;
using VictoryCenter.BLL.Exceptions.BlobStorageExceptions;
using VictoryCenter.DAL.Entities;
using VictoryCenter.DAL.Repositories.Interfaces.Base;
using VictoryCenter.DAL.Repositories.Options;
Expand All @@ -19,18 +17,15 @@ public class UpdateTeamMemberHandler : IRequestHandler<UpdateTeamMemberCommand,
private readonly IMapper _mapper;
private readonly IRepositoryWrapper _repositoryWrapper;
private readonly IValidator<UpdateTeamMemberCommand> _validator;
private readonly IBlobService _blobService;

public UpdateTeamMemberHandler(
IMapper mapper,
IRepositoryWrapper repositoryWrapper,
IValidator<UpdateTeamMemberCommand> validator,
IBlobService blobService)
IValidator<UpdateTeamMemberCommand> validator)
{
_mapper = mapper;
_repositoryWrapper = repositoryWrapper;
_validator = validator;
_blobService = blobService;
}

public async Task<Result<TeamMemberDto>> Handle(UpdateTeamMemberCommand request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -62,7 +57,8 @@ await _repositoryWrapper.TeamMembersRepository.GetFirstOrDefaultAsync(new QueryO
});
if (category is null)
{
return Result.Fail<TeamMemberDto>(ErrorMessagesConstants.NotFound(request.UpdateTeamMemberDto.CategoryId, typeof(Category)));
return Result.Fail<TeamMemberDto>(
ErrorMessagesConstants.NotFound(request.UpdateTeamMemberDto.CategoryId, typeof(Category)));
}

if (entityToUpdate.CategoryId == teamMemberEntity.CategoryId)
Expand All @@ -81,26 +77,18 @@ await _repositoryWrapper.TeamMembersRepository.GetFirstOrDefaultAsync(new QueryO

if (await _repositoryWrapper.SaveChangesAsync() > 0)
{
var resultDto = _mapper.Map<TeamMember, TeamMemberDto>(entityToUpdate);
if (entityToUpdate.ImageId != null)
{
Image? image = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(
new QueryOptions<Image>()
new QueryOptions<Image>
{
Filter = i => i.Id == entityToUpdate.ImageId
});
if (image != null)
{
var imageDto = _mapper.Map<ImageDTO>(image);
imageDto.Base64 = await _blobService.FindFileInStorageAsBase64Async(image.BlobName, image.MimeType);
resultDto.Image = imageDto;
}
else
{
return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedRetrievingMemberPhoto);
}
entityToUpdate.Image = image;
}

TeamMemberDto? resultDto = _mapper.Map<TeamMember, TeamMemberDto>(entityToUpdate);

scope.Complete();
return Result.Ok(resultDto);
}
Expand All @@ -109,7 +97,7 @@ await _repositoryWrapper.TeamMembersRepository.GetFirstOrDefaultAsync(new QueryO
}
catch (BlobStorageException e)
{
return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}" );
return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}");
}
catch (ValidationException vex)
{
Expand Down
14 changes: 8 additions & 6 deletions VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,12 @@ public static class ImageConstants
public static readonly string FailToUpdateImage = "Failed to update image";
public static readonly string InvalidBase64String = "Invalid Base64 string.";
public static readonly string FailedToConvertBase64 = "Failed to convert Base64";
public static readonly string InvalidIVLength = "Invalid IV length";
public static readonly string ImageNotFoundGeneric = "Image not found";
public static readonly string ImageBlobNameIsNull = "Image blob name is null";
public static readonly string EncryptionFailed = "Encryption failed.";
public static readonly string DecryptionFailed = "Decryption failed.";
public static readonly string FailedToWriteEncryptedFile = "Failed to write encrypted file.";
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.";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Extract this to a common constant. You can take a look at our pr: ita-social-projects/VictoryCenter-Client#52

public static readonly string FailedToReadImage = "Failed to retrieve the image.";
public static readonly string HttpContextIsNotAvailable = "HttpContext is not available.";

public static string FieldIsRequired(string name)
{
Expand All @@ -35,4 +32,9 @@ public static string FileNotFound(string filePath)
{
return $"File not found: {filePath}";
}

public static string WrongFileName(string name)
{
return $"An error occurred while retrieving the file {name}.";
}
}
2 changes: 1 addition & 1 deletion VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ public record ImageDTO
{
public long Id { get; init; }
public string BlobName { get; init; } = null!;
public string Base64 { get; set; } = null!;
public string Url { get; init; } = null!;
public string MimeType { get; init; } = null!;
public DateTime CreatedAt { get; init; }
}
Loading
Loading