From c93488c29f45057eb8de0fa782b852fb57e32e4a Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 20 Jul 2026 16:31:14 +0200 Subject: [PATCH] refactor(application): migrate all queries to Stream-based API - Replace byte[] and Base64String with required Stream PdfStream - Simplify validators: remove XOR/Base64 validation, only check NotNull - Affected queries: ValidatePdfQuery, ValidatePdfAQuery, CheckPdfAttachmentsQuery, ExtractSwissQrCodeQuery - Memory efficiency: direct stream usage, no intermediate byte[] copies --- .../Queries/CheckPdfAttachmentsQuery.cs | 21 ++------ .../CheckPdfAttachmentsQueryValidator.cs | 36 ++----------- .../Queries/ExtractSwissQrCodeQuery.cs | 18 ++----- .../ExtractSwissQrCodeQueryValidator.cs | 50 ++----------------- .../ValidatePdf/Queries/ValidatePdfQuery.cs | 21 ++------ .../Queries/ValidatePdfQueryValidator.cs | 50 ++----------------- .../ValidatePdfA/Queries/ValidatePdfAQuery.cs | 21 ++------ .../Validators/ValidatePdfAQueryValidator.cs | 40 ++------------- 8 files changed, 40 insertions(+), 217 deletions(-) diff --git a/DocumentOperator.Application/CheckPdfAttachments/Queries/CheckPdfAttachmentsQuery.cs b/DocumentOperator.Application/CheckPdfAttachments/Queries/CheckPdfAttachmentsQuery.cs index f8e5480..6cefd9b 100644 --- a/DocumentOperator.Application/CheckPdfAttachments/Queries/CheckPdfAttachmentsQuery.cs +++ b/DocumentOperator.Application/CheckPdfAttachments/Queries/CheckPdfAttachmentsQuery.cs @@ -6,19 +6,14 @@ using MediatR; namespace DocumentOperator.Application.CheckPdfAttachments.Queries; /// -/// Query for checking PDF attachments (supports both byte array and Base64 input) +/// Query for checking PDF attachments (Stream-based) /// public record CheckPdfAttachmentsQuery : IRequest { /// - /// PDF as byte array (direct upload via multipart/form-data) + /// PDF as stream (caller is responsible for disposal) /// - public byte[]? PdfBytes { get; init; } - - /// - /// PDF as Base64 string (for API clients using application/json) - /// - public string? Base64Pdf { get; init; } + public required Stream PdfStream { get; init; } } /// @@ -33,14 +28,8 @@ public class CheckPdfAttachmentsQueryHandler(IPdfProcessor PdfProcessor, IMapper /// public async Task Handle(CheckPdfAttachmentsQuery request, CancellationToken cancellationToken) { - // Use byte[] if available, otherwise convert Base64 - byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!); - - // Convert to stream for IPdfProcessor - using var pdfStream = new MemoryStream(pdfBytes); - - // Call DevExpress service (exceptions propagate naturally) - var attachmentInfo = await PdfProcessor.CheckAttachmentsAsync(pdfStream); + // Call DevExpress service directly with stream (exceptions propagate naturally) + var attachmentInfo = await PdfProcessor.CheckAttachmentsAsync(request.PdfStream); // Map DTO to response DTO using AutoMapper return Mapper.Map(attachmentInfo); diff --git a/DocumentOperator.Application/CheckPdfAttachments/Queries/CheckPdfAttachmentsQueryValidator.cs b/DocumentOperator.Application/CheckPdfAttachments/Queries/CheckPdfAttachmentsQueryValidator.cs index 78aa602..a075f76 100644 --- a/DocumentOperator.Application/CheckPdfAttachments/Queries/CheckPdfAttachmentsQueryValidator.cs +++ b/DocumentOperator.Application/CheckPdfAttachments/Queries/CheckPdfAttachmentsQueryValidator.cs @@ -4,41 +4,15 @@ namespace DocumentOperator.Application.CheckPdfAttachments.Queries; /// /// Validator for CheckPdfAttachmentsQuery -/// Ensures exactly one input type (PdfBytes OR Base64Pdf) is provided +/// Ensures PdfStream is not null /// public class CheckPdfAttachmentsQueryValidator : AbstractValidator { public CheckPdfAttachmentsQueryValidator() { - // Rule 1: Exactly ONE input must be provided (XOR logic) - RuleFor(x => x) - .Must(x => (x.PdfBytes != null && x.PdfBytes.Length > 0) ^ - (!string.IsNullOrWhiteSpace(x.Base64Pdf))) - .WithMessage("Either PdfBytes or Base64Pdf must be provided, but not both"); - - // Rule 2: Base64 format validation (if provided) - RuleFor(x => x.Base64Pdf) - .Must(BeValidBase64) - .When(x => !string.IsNullOrWhiteSpace(x.Base64Pdf)) - .WithMessage("Base64Pdf must be a valid Base64 string"); - } - - /// - /// Validates if a string is a valid Base64 format - /// - private bool BeValidBase64(string? base64) - { - if (string.IsNullOrWhiteSpace(base64)) - return true; // Skip validation if null/empty (handled by Rule 1) - - try - { - Convert.FromBase64String(base64); - return true; - } - catch (FormatException) - { - return false; - } + // Rule: PdfStream must be provided and non-empty + RuleFor(x => x.PdfStream) + .NotNull() + .WithMessage("PdfStream is required"); } } diff --git a/DocumentOperator.Application/SwissQrCode/Queries/ExtractSwissQrCodeQuery.cs b/DocumentOperator.Application/SwissQrCode/Queries/ExtractSwissQrCodeQuery.cs index 8c093fa..b0992d6 100644 --- a/DocumentOperator.Application/SwissQrCode/Queries/ExtractSwissQrCodeQuery.cs +++ b/DocumentOperator.Application/SwissQrCode/Queries/ExtractSwissQrCodeQuery.cs @@ -6,19 +6,14 @@ using MediatR; namespace DocumentOperator.Application.SwissQrCode.Queries; /// -/// Query for extracting Swiss QR Code from PDF (supports both byte array and Base64 input) +/// Query for extracting Swiss QR Code from PDF (Stream-based) /// public record ExtractSwissQrCodeQuery : IRequest { /// - /// PDF as byte array (direct upload) + /// PDF as stream (caller is responsible for disposal) /// - public byte[]? PdfBytes { get; init; } - - /// - /// PDF as Base64 string (API clients) - /// - public string? Base64Pdf { get; init; } + public required Stream PdfStream { get; init; } } /// @@ -34,11 +29,8 @@ public class ExtractSwissQrCodeQueryHandler(ISwissQrCodeProcessor qrCodeProcesso /// public async Task Handle(ExtractSwissQrCodeQuery request, CancellationToken cancellationToken) { - // Use byte[] if available, otherwise convert Base64 - byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!); - - // Extract: returns (Bill, RawLines) - var (bill, rawLines) = await qrCodeProcessor.ExtractSwissQrCodeAsync(pdfBytes, pageNumbers: null, cancellationToken); + // Extract: returns (Bill, RawLines) - pass stream directly + var (bill, rawLines) = await qrCodeProcessor.ExtractSwissQrCodeAsync(request.PdfStream, pageNumbers: null, cancellationToken); // Map Codecrete Bill to DTO using AutoMapper var billDto = mapper.Map(bill); diff --git a/DocumentOperator.Application/SwissQrCode/Queries/ExtractSwissQrCodeQueryValidator.cs b/DocumentOperator.Application/SwissQrCode/Queries/ExtractSwissQrCodeQueryValidator.cs index 5e99612..f7246ee 100644 --- a/DocumentOperator.Application/SwissQrCode/Queries/ExtractSwissQrCodeQueryValidator.cs +++ b/DocumentOperator.Application/SwissQrCode/Queries/ExtractSwissQrCodeQueryValidator.cs @@ -5,55 +5,15 @@ namespace DocumentOperator.Application.SwissQrCode.Queries; /// /// Validates ExtractSwissQrCodeQuery before handler execution. -/// Ensures exactly ONE input method is provided (either PdfBytes OR Base64Pdf, not both, not none). +/// Ensures PdfStream is not null. /// public sealed class ExtractSwissQrCodeQueryValidator : AbstractValidator { public ExtractSwissQrCodeQueryValidator() { - RuleFor(x => x) - .Must(HasExactlyOneInput) - .WithMessage("Either PdfBytes or Base64Pdf must be provided, but not both"); - - // Validate Base64 format if provided - When(x => !string.IsNullOrWhiteSpace(x.Base64Pdf), () => - { - RuleFor(x => x.Base64Pdf) - .Must(BeValidBase64) - .WithMessage("Invalid Base64 format"); - }); - - // Validate byte array if provided - When(x => x.PdfBytes != null, () => - { - RuleFor(x => x.PdfBytes) - .NotEmpty() - .WithMessage("PdfBytes cannot be empty"); - }); - } - - private static bool HasExactlyOneInput(ExtractSwissQrCodeQuery request) - { - var hasPdfBytes = request.PdfBytes != null && request.PdfBytes.Length > 0; - var hasBase64 = !string.IsNullOrWhiteSpace(request.Base64Pdf); - - // XOR: exactly one must be true - return hasPdfBytes ^ hasBase64; - } - - private static bool BeValidBase64(string? base64) - { - if (string.IsNullOrWhiteSpace(base64)) - return false; - - try - { - Convert.FromBase64String(base64); - return true; - } - catch (FormatException) - { - return false; - } + // Rule: PdfStream must be provided + RuleFor(x => x.PdfStream) + .NotNull() + .WithMessage("PdfStream is required"); } } diff --git a/DocumentOperator.Application/ValidatePdf/Queries/ValidatePdfQuery.cs b/DocumentOperator.Application/ValidatePdf/Queries/ValidatePdfQuery.cs index 0f08a5c..453ebac 100644 --- a/DocumentOperator.Application/ValidatePdf/Queries/ValidatePdfQuery.cs +++ b/DocumentOperator.Application/ValidatePdf/Queries/ValidatePdfQuery.cs @@ -6,19 +6,14 @@ using MediatR; namespace DocumentOperator.Application.ValidatePdf.Queries; /// -/// Query for PDF validation (supports both byte array and Base64 input) +/// Query for PDF validation (Stream-based) /// public record ValidatePdfQuery : IRequest { /// - /// PDF as byte array (direct upload) + /// PDF as stream (caller is responsible for disposal) /// - public byte[]? PdfBytes { get; init; } - - /// - /// PDF as Base64 string (API clients) - /// - public string? Base64Pdf { get; init; } + public required Stream PdfStream { get; init; } } /// @@ -33,14 +28,8 @@ public class ValidatePdfQueryHandler(IPdfProcessor PdfProcessor, IMapper Mapper) /// public async Task Handle(ValidatePdfQuery request, CancellationToken cancellationToken) { - // Use byte[] if available, otherwise convert Base64 - byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!); - - // Convert to stream for IPdfProcessor (using MemoryStream) - using var pdfStream = new MemoryStream(pdfBytes); - - // Call DevExpress service (exceptions propagate naturally) - var metadata = await PdfProcessor.ValidateAsync(pdfStream); + // Call DevExpress service directly with stream (exceptions propagate naturally) + var metadata = await PdfProcessor.ValidateAsync(request.PdfStream); // Map DTO to response DTO using AutoMapper return Mapper.Map(metadata); diff --git a/DocumentOperator.Application/ValidatePdf/Queries/ValidatePdfQueryValidator.cs b/DocumentOperator.Application/ValidatePdf/Queries/ValidatePdfQueryValidator.cs index 1e9bb5b..4a1dfa8 100644 --- a/DocumentOperator.Application/ValidatePdf/Queries/ValidatePdfQueryValidator.cs +++ b/DocumentOperator.Application/ValidatePdf/Queries/ValidatePdfQueryValidator.cs @@ -5,55 +5,15 @@ namespace DocumentOperator.Application.ValidatePdf.Queries; /// /// Validator for ValidatePdfQuery -/// Ensures exactly ONE input method is provided (either PdfBytes OR Base64Pdf, not both, not none) +/// Ensures PdfStream is not null /// public class ValidatePdfQueryValidator : AbstractValidator { public ValidatePdfQueryValidator() { - RuleFor(x => x) - .Must(HasExactlyOneInput) - .WithMessage("Either PdfBytes or Base64Pdf must be provided, but not both"); - - // Validate Base64 format if provided - When(x => !string.IsNullOrWhiteSpace(x.Base64Pdf), () => - { - RuleFor(x => x.Base64Pdf) - .Must(BeValidBase64) - .WithMessage("Invalid Base64 format"); - }); - - // Validate byte array if provided - When(x => x.PdfBytes != null, () => - { - RuleFor(x => x.PdfBytes) - .NotEmpty() - .WithMessage("PdfBytes cannot be empty"); - }); - } - - private static bool HasExactlyOneInput(ValidatePdfQuery request) - { - var hasPdfBytes = request.PdfBytes != null && request.PdfBytes.Length > 0; - var hasBase64 = !string.IsNullOrWhiteSpace(request.Base64Pdf); - - // XOR: exactly one must be true - return hasPdfBytes ^ hasBase64; - } - - private static bool BeValidBase64(string? base64) - { - if (string.IsNullOrWhiteSpace(base64)) - return false; - - try - { - Convert.FromBase64String(base64); - return true; - } - catch (FormatException) - { - return false; - } + // Rule: PdfStream must be provided + RuleFor(x => x.PdfStream) + .NotNull() + .WithMessage("PdfStream is required"); } } diff --git a/DocumentOperator.Application/ValidatePdfA/Queries/ValidatePdfAQuery.cs b/DocumentOperator.Application/ValidatePdfA/Queries/ValidatePdfAQuery.cs index 64cdcca..6a23273 100644 --- a/DocumentOperator.Application/ValidatePdfA/Queries/ValidatePdfAQuery.cs +++ b/DocumentOperator.Application/ValidatePdfA/Queries/ValidatePdfAQuery.cs @@ -6,19 +6,14 @@ using MediatR; namespace DocumentOperator.Application.ValidatePdfA.Queries; /// -/// Query for PDF/A validation (supports both byte array and Base64 input) +/// Query for PDF/A validation (Stream-based) /// public record ValidatePdfAQuery : IRequest { /// - /// PDF as byte array (direct upload) + /// PDF as stream (caller is responsible for disposal) /// - public byte[]? PdfBytes { get; init; } - - /// - /// PDF as Base64 string (API clients) - /// - public string? Base64Pdf { get; init; } + public required Stream PdfStream { get; init; } } /// @@ -33,14 +28,8 @@ public class ValidatePdfAQueryHandler(IPdfProcessor PdfProcessor, IMapper Mapper /// public async Task Handle(ValidatePdfAQuery request, CancellationToken cancellationToken) { - // Use byte[] if available, otherwise convert Base64 - byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!); - - // Convert to stream for IPdfProcessor - using var pdfStream = new MemoryStream(pdfBytes); - - // Call DevExpress service (exceptions propagate naturally) - var metadata = await PdfProcessor.ValidatePdfAAsync(pdfStream); + // Call DevExpress service directly with stream (exceptions propagate naturally) + var metadata = await PdfProcessor.ValidatePdfAAsync(request.PdfStream); // Map DTO to response DTO using AutoMapper return Mapper.Map(metadata); diff --git a/DocumentOperator.Application/ValidatePdfA/Validators/ValidatePdfAQueryValidator.cs b/DocumentOperator.Application/ValidatePdfA/Validators/ValidatePdfAQueryValidator.cs index 8ee807f..67277ed 100644 --- a/DocumentOperator.Application/ValidatePdfA/Validators/ValidatePdfAQueryValidator.cs +++ b/DocumentOperator.Application/ValidatePdfA/Validators/ValidatePdfAQueryValidator.cs @@ -4,45 +4,15 @@ namespace DocumentOperator.Application.ValidatePdfA.Validators; /// /// Validator for ValidatePdfAQuery -/// Ensures exactly ONE input format is provided (PdfBytes XOR Base64Pdf) +/// Ensures PdfStream is not null /// public class ValidatePdfAQueryValidator : AbstractValidator { public ValidatePdfAQueryValidator() { - RuleFor(x => x) - .Must(x => (x.PdfBytes != null && x.PdfBytes.Length > 0) ^ - !string.IsNullOrWhiteSpace(x.Base64Pdf)) - .WithMessage("Either PdfBytes or Base64Pdf must be provided, but not both"); - - When(x => !string.IsNullOrWhiteSpace(x.Base64Pdf), () => - { - RuleFor(x => x.Base64Pdf!) - .Must(BeValidBase64) - .WithMessage("Base64Pdf must be a valid Base64 string"); - }); - - When(x => x.PdfBytes != null, () => - { - RuleFor(x => x.PdfBytes!) - .Must(bytes => bytes.Length > 0) - .WithMessage("PdfBytes cannot be empty"); - }); - } - - private static bool BeValidBase64(string base64) - { - if (string.IsNullOrWhiteSpace(base64)) - return false; - - try - { - Convert.FromBase64String(base64); - return true; - } - catch (FormatException) - { - return false; - } + // Rule: PdfStream must be provided + RuleFor(x => x.PdfStream) + .NotNull() + .WithMessage("PdfStream is required"); } }