diff --git a/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs b/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs index 4291526..3c06130 100644 --- a/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs +++ b/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs @@ -5,6 +5,8 @@ using CMSMicroservice.Protobuf.Protos.UserAddress; using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserRole; using CMSMicroservice.Protobuf.Protos.Products; +using CMSMicroservice.Protobuf.Protos.ProductImages; +using CMSMicroservice.Protobuf.Protos.ProductGallerys; using FMSMicroservice.Protobuf.Protos.FileInfo; namespace BackOffice.BFF.Application.Common.Interfaces; @@ -17,6 +19,8 @@ public interface IApplicationContractContext #region CMS PackageContract.PackageContractClient Packages { get; } ProductsContract.ProductsContractClient Products { get; } + ProductImagesContract.ProductImagesContractClient ProductImages { get; } + ProductGallerysContract.ProductGallerysContractClient ProductGallerys { get; } RoleContract.RoleContractClient Roles { get; } UserAddressContract.UserAddressContractClient UserAddress { get; } UserContract.UserContractClient Users { get; } diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommand.cs b/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommand.cs new file mode 100644 index 0000000..6c4f0af --- /dev/null +++ b/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommand.cs @@ -0,0 +1,16 @@ +namespace BackOffice.BFF.Application.ProductsCQ.Commands.AddProductImage; + +public record AddProductImageCommand : IRequest +{ + public long ProductId { get; init; } + public string Title { get; init; } + public ImageFileModel ImageFile { get; init; } +} + +public class ImageFileModel +{ + public byte[] File { get; set; } + public string FileName { get; set; } + public string Mime { get; set; } +} + diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommandHandler.cs b/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommandHandler.cs new file mode 100644 index 0000000..15a8ce6 --- /dev/null +++ b/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommandHandler.cs @@ -0,0 +1,95 @@ +using System.IO; +using BackOffice.BFF.Application.Common.Interfaces; +using CMSMicroservice.Protobuf.Protos.ProductImages; +using CMSMicroservice.Protobuf.Protos.ProductGallerys; +using Google.Protobuf; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; + +namespace BackOffice.BFF.Application.ProductsCQ.Commands.AddProductImage; + +public class AddProductImageCommandHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public AddProductImageCommandHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(AddProductImageCommand request, CancellationToken cancellationToken) + { + if (request.ImageFile == null || request.ImageFile.File == null || request.ImageFile.File.Length == 0) + { + throw new ArgumentException("Image file is required."); + } + + var optimizedMainImage = OptimizeImage(request.ImageFile.File, 1200, 1200, 80); + var thumbnailImage = OptimizeImage(request.ImageFile.File, 300, 300, 75); + + var mainFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new() + { + Directory = "Images/Products/Gallery", + IsBase64 = false, + MIME = request.ImageFile.Mime, + FileName = request.ImageFile.FileName, + File = ByteString.CopyFrom(optimizedMainImage) + }, cancellationToken: cancellationToken); + + var thumbFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new() + { + Directory = "Images/Products/Gallery/Thumbnail", + IsBase64 = false, + MIME = request.ImageFile.Mime, + FileName = request.ImageFile.FileName, + File = ByteString.CopyFrom(thumbnailImage) + }, cancellationToken: cancellationToken); + + if (mainFileInfo == null || string.IsNullOrWhiteSpace(mainFileInfo.File) || + thumbFileInfo == null || string.IsNullOrWhiteSpace(thumbFileInfo.File)) + { + throw new InvalidOperationException("Error while uploading gallery image."); + } + + var createImageResponse = await _context.ProductImages.CreateNewProductImagesAsync(new CreateNewProductImagesRequest + { + Title = request.Title ?? string.Empty, + ImagePath = mainFileInfo.File, + ImageThumbnailPath = thumbFileInfo.File + }, cancellationToken: cancellationToken); + + var createGalleryResponse = await _context.ProductGallerys.CreateNewProductGallerysAsync(new CreateNewProductGallerysRequest + { + ProductId = request.ProductId, + ProductImageId = createImageResponse.Id + }, cancellationToken: cancellationToken); + + return new AddProductImageResponseDto + { + ProductGalleryId = createGalleryResponse.Id, + ProductImageId = createImageResponse.Id, + Title = request.Title ?? string.Empty, + ImagePath = mainFileInfo.File, + ImageThumbnailPath = thumbFileInfo.File + }; + } + + private static byte[] OptimizeImage(byte[] original, int maxWidth, int maxHeight, int quality) + { + using var image = Image.Load(original); + + var ratio = Math.Min(maxWidth / (float)image.Width, maxHeight / (float)image.Height); + if (ratio < 1f) + { + var width = (int)(image.Width * ratio); + var height = (int)(image.Height * ratio); + image.Mutate(x => x.Resize(width, height)); + } + + using var ms = new MemoryStream(); + image.Save(ms, new JpegEncoder { Quality = quality }); + return ms.ToArray(); + } +} + diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageResponseDto.cs b/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageResponseDto.cs new file mode 100644 index 0000000..e1166ea --- /dev/null +++ b/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageResponseDto.cs @@ -0,0 +1,11 @@ +namespace BackOffice.BFF.Application.ProductsCQ.Commands.AddProductImage; + +public class AddProductImageResponseDto +{ + public long ProductGalleryId { get; set; } + public long ProductImageId { get; set; } + public string Title { get; set; } + public string ImagePath { get; set; } + public string ImageThumbnailPath { get; set; } +} + diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Commands/RemoveProductImage/RemoveProductImageCommand.cs b/src/BackOffice.BFF.Application/ProductsCQ/Commands/RemoveProductImage/RemoveProductImageCommand.cs new file mode 100644 index 0000000..8769094 --- /dev/null +++ b/src/BackOffice.BFF.Application/ProductsCQ/Commands/RemoveProductImage/RemoveProductImageCommand.cs @@ -0,0 +1,7 @@ +namespace BackOffice.BFF.Application.ProductsCQ.Commands.RemoveProductImage; + +public record RemoveProductImageCommand : IRequest +{ + public long ProductGalleryId { get; init; } +} + diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Commands/RemoveProductImage/RemoveProductImageCommandHandler.cs b/src/BackOffice.BFF.Application/ProductsCQ/Commands/RemoveProductImage/RemoveProductImageCommandHandler.cs new file mode 100644 index 0000000..98afd08 --- /dev/null +++ b/src/BackOffice.BFF.Application/ProductsCQ/Commands/RemoveProductImage/RemoveProductImageCommandHandler.cs @@ -0,0 +1,25 @@ +using BackOffice.BFF.Application.Common.Interfaces; +using CMSMicroservice.Protobuf.Protos.ProductGallerys; + +namespace BackOffice.BFF.Application.ProductsCQ.Commands.RemoveProductImage; + +public class RemoveProductImageCommandHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public RemoveProductImageCommandHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(RemoveProductImageCommand request, CancellationToken cancellationToken) + { + await _context.ProductGallerys.DeleteProductGallerysAsync(new DeleteProductGallerysRequest + { + Id = request.ProductGalleryId + }, cancellationToken: cancellationToken); + + return Unit.Value; + } +} + diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryQuery.cs b/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryQuery.cs new file mode 100644 index 0000000..ed603e4 --- /dev/null +++ b/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryQuery.cs @@ -0,0 +1,7 @@ +namespace BackOffice.BFF.Application.ProductsCQ.Queries.GetProductGallery; + +public record GetProductGalleryQuery : IRequest +{ + public long ProductId { get; init; } +} + diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryQueryHandler.cs b/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryQueryHandler.cs new file mode 100644 index 0000000..d7acf6e --- /dev/null +++ b/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryQueryHandler.cs @@ -0,0 +1,55 @@ +using BackOffice.BFF.Application.Common.Interfaces; +using CMSMicroservice.Protobuf.Protos.ProductGallerys; +using CMSMicroservice.Protobuf.Protos.ProductImages; + +namespace BackOffice.BFF.Application.ProductsCQ.Queries.GetProductGallery; + +public class GetProductGalleryQueryHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public GetProductGalleryQueryHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(GetProductGalleryQuery request, CancellationToken cancellationToken) + { + var galleryRequest = new GetAllProductGallerysByFilterRequest + { + Filter = new GetAllProductGallerysByFilterFilter() + }; + + var galleryResponse = await _context.ProductGallerys.GetAllProductGallerysByFilterAsync(galleryRequest, cancellationToken: cancellationToken); + + // Filter by product id on client side because generated type may not support assigning Int64Value directly + var filteredModels = galleryResponse?.Models?.Where(x => x.ProductId == request.ProductId).ToList(); + + var result = new GetProductGalleryResponseDto(); + + if (filteredModels == null || filteredModels.Count == 0) + return result; + + foreach (var item in filteredModels) + { + var image = await _context.ProductImages.GetProductImagesAsync(new GetProductImagesRequest + { + Id = item.ProductImageId + }, cancellationToken: cancellationToken); + + if (image == null) + continue; + + result.Items.Add(new ProductGalleryItemDto + { + ProductGalleryId = item.Id, + ProductImageId = item.ProductImageId, + Title = image.Title, + ImagePath = image.ImagePath, + ImageThumbnailPath = image.ImageThumbnailPath + }); + } + + return result; + } +} diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryResponseDto.cs b/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryResponseDto.cs new file mode 100644 index 0000000..f386f46 --- /dev/null +++ b/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductGallery/GetProductGalleryResponseDto.cs @@ -0,0 +1,16 @@ +namespace BackOffice.BFF.Application.ProductsCQ.Queries.GetProductGallery; + +public class GetProductGalleryResponseDto +{ + public List Items { get; set; } = new(); +} + +public class ProductGalleryItemDto +{ + public long ProductGalleryId { get; set; } + public long ProductImageId { get; set; } + public string Title { get; set; } + public string ImagePath { get; set; } + public string ImageThumbnailPath { get; set; } +} + diff --git a/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs b/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs index 0db83fa..f0fa8ce 100644 --- a/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs +++ b/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs @@ -6,6 +6,8 @@ using CMSMicroservice.Protobuf.Protos.UserAddress; using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserRole; using CMSMicroservice.Protobuf.Protos.Products; +using CMSMicroservice.Protobuf.Protos.ProductImages; +using CMSMicroservice.Protobuf.Protos.ProductGallerys; using FMSMicroservice.Protobuf.Protos.FileInfo; using Microsoft.Extensions.DependencyInjection; @@ -41,6 +43,8 @@ public class ApplicationContractContext : IApplicationContractContext #region CMS public PackageContract.PackageContractClient Packages => GetService(); public ProductsContract.ProductsContractClient Products => GetService(); + public ProductImagesContract.ProductImagesContractClient ProductImages => GetService(); + public ProductGallerysContract.ProductGallerysContractClient ProductGallerys => GetService(); public RoleContract.RoleContractClient Roles => GetService(); public UserAddressContract.UserAddressContractClient UserAddress => GetService(); public UserContract.UserContractClient Users => GetService(); diff --git a/src/BackOffice.BFF.WebApi/Services/ProductsService.cs b/src/BackOffice.BFF.WebApi/Services/ProductsService.cs index fcd1edf..fb6b677 100644 --- a/src/BackOffice.BFF.WebApi/Services/ProductsService.cs +++ b/src/BackOffice.BFF.WebApi/Services/ProductsService.cs @@ -5,6 +5,9 @@ using BackOffice.BFF.Application.ProductsCQ.Commands.UpdateProducts; using BackOffice.BFF.Application.ProductsCQ.Commands.DeleteProducts; using BackOffice.BFF.Application.ProductsCQ.Queries.GetProducts; using BackOffice.BFF.Application.ProductsCQ.Queries.GetAllProductsByFilter; +using BackOffice.BFF.Application.ProductsCQ.Commands.AddProductImage; +using BackOffice.BFF.Application.ProductsCQ.Queries.GetProductGallery; +using BackOffice.BFF.Application.ProductsCQ.Commands.RemoveProductImage; namespace BackOffice.BFF.WebApi.Services; @@ -41,5 +44,19 @@ public class ProductsService : ProductsContract.ProductsContractBase { return await _dispatchRequestToCQRS.Handle(request, context); } -} + public override async Task AddProductImage(AddProductImageRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } + + public override async Task GetProductGallery(GetProductGalleryRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } + + public override async Task RemoveProductImage(RemoveProductImageRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } +} diff --git a/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj b/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj index 6e15dd0..86395ca 100644 --- a/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj +++ b/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj @@ -4,7 +4,7 @@ net9.0 enable enable - 0.0.1 + 0.0.2 None False False diff --git a/src/Protobufs/BackOffice.BFF.Products.Protobuf/Protos/products.proto b/src/Protobufs/BackOffice.BFF.Products.Protobuf/Protos/products.proto index 09bcc2d..c58d9b6 100644 --- a/src/Protobufs/BackOffice.BFF.Products.Protobuf/Protos/products.proto +++ b/src/Protobufs/BackOffice.BFF.Products.Protobuf/Protos/products.proto @@ -40,6 +40,26 @@ service ProductsContract get: "/GetAllProductsByFilter" }; }; + + rpc AddProductImage(AddProductImageRequest) returns (AddProductImageResponse){ + option (google.api.http) = { + post: "/AddProductImage" + body: "*" + }; + }; + + rpc GetProductGallery(GetProductGalleryRequest) returns (GetProductGalleryResponse){ + option (google.api.http) = { + get: "/GetProductGallery" + }; + }; + + rpc RemoveProductImage(RemoveProductImageRequest) returns (google.protobuf.Empty){ + option (google.api.http) = { + delete: "/RemoveProductImage" + body: "*" + }; + }; } message ImageFileModel @@ -174,3 +194,42 @@ message DecimalValue sfixed32 nanos = 2; } +message AddProductImageRequest +{ + int64 product_id = 1; + string title = 2; + ImageFileModel image_file = 3; +} + +message AddProductImageResponse +{ + int64 product_gallery_id = 1; + int64 product_image_id = 2; + string title = 3; + string image_path = 4; + string image_thumbnail_path = 5; +} + +message GetProductGalleryRequest +{ + int64 product_id = 1; +} + +message ProductGalleryItem +{ + int64 product_gallery_id = 1; + int64 product_image_id = 2; + string title = 3; + string image_path = 4; + string image_thumbnail_path = 5; +} + +message GetProductGalleryResponse +{ + repeated ProductGalleryItem items = 1; +} + +message RemoveProductImageRequest +{ + int64 product_gallery_id = 1; +}