refactor: Restructure Application layer with vertical slices and AutoMapper
Vertical slice architecture:
- Move Features/Documents/{UseCase}/ to {UseCase}/Queries/
- Query + Handler in SAME file (co-located)
- Validator in separate file (single responsibility)
New structure:
- ValidatePdf/Queries/ValidatePdfQuery.cs (Query + Handler)
- ValidatePdf/Queries/ValidatePdfQueryValidator.cs
- SwissQrCode/Queries/ExtractSwissQrCodeQuery.cs (Query + Handler)
- SwissQrCode/Queries/ExtractSwissQrCodeQueryValidator.cs
AutoMapper integration:
- Add Common/Mapping/MappingProfile.cs
- Map PdfMetadata -> PdfValidationResult (domain -> DTO)
- Map SwissQrCodeData -> SwissQrCodeExtractionResult (domain -> DTO)
- Controllers now thin: pass request to MediatR, AutoMapper handles mapping
DTO improvements:
- Rename: ValidatePdfResponse -> PdfValidationResult (business-friendly)
- Rename: ExtractSwissQrCodeResponse -> SwissQrCodeExtractionResult
- Support BOTH byte[] and Base64Pdf string (XOR validation)
- Use modern C# 12 collection expressions
Code quality:
- Use PascalCase for primary constructor parameters
- Fix LoggingBehavior logging format
Deleted old structure:
- Features/Documents/ValidatePdf/ (old horizontal structure)
- Features/Documents/ExtractSwissQrCode/ (old horizontal structure)
- Common/DTOs/{Request|Response} (replaced with {Result})
Result: Vertical slices, AutoMapper v16.2.0, thin controllers
This commit is contained in:
@@ -8,16 +8,9 @@ namespace DocumentOperator.Application.Common.Behaviors;
|
|||||||
/// MediatR Pipeline Behavior that logs requests and tracks performance
|
/// MediatR Pipeline Behavior that logs requests and tracks performance
|
||||||
/// Executes AFTER ValidationBehavior, BEFORE Handler
|
/// Executes AFTER ValidationBehavior, BEFORE Handler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> Logger) : IPipelineBehavior<TRequest, TResponse>
|
||||||
where TRequest : IRequest<TResponse>
|
where TRequest : IRequest<TResponse>
|
||||||
{
|
{
|
||||||
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
|
|
||||||
|
|
||||||
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TResponse> Handle(
|
public async Task<TResponse> Handle(
|
||||||
TRequest request,
|
TRequest request,
|
||||||
RequestHandlerDelegate<TResponse> next,
|
RequestHandlerDelegate<TResponse> next,
|
||||||
@@ -25,21 +18,18 @@ public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest,
|
|||||||
{
|
{
|
||||||
var requestName = typeof(TRequest).Name;
|
var requestName = typeof(TRequest).Name;
|
||||||
|
|
||||||
// Request Start
|
|
||||||
_logger.LogInformation("Handling {RequestName}: {@Request}", requestName, request);
|
|
||||||
|
|
||||||
// Performance Tracking
|
// Performance Tracking
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Handler ausführen
|
// Handler ausführen
|
||||||
var response = await next();
|
var response = await next(cancellationToken);
|
||||||
|
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|
||||||
// Request Success
|
// Request Success
|
||||||
_logger.LogInformation(
|
Logger.LogInformation(
|
||||||
"Handled {RequestName} in {ElapsedMs}ms",
|
"Handled {RequestName} in {ElapsedMs}ms",
|
||||||
requestName,
|
requestName,
|
||||||
stopwatch.ElapsedMilliseconds
|
stopwatch.ElapsedMilliseconds
|
||||||
@@ -52,7 +42,7 @@ public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest,
|
|||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|
||||||
// Request Failed
|
// Request Failed
|
||||||
_logger.LogError(
|
Logger.LogError(
|
||||||
ex,
|
ex,
|
||||||
"Error handling {RequestName} after {ElapsedMs}ms: {ErrorMessage}",
|
"Error handling {RequestName} after {ElapsedMs}ms: {ErrorMessage}",
|
||||||
requestName,
|
requestName,
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace DocumentOperator.Application.Common.DTOs;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request to extract Swiss QR Code from a PDF document.
|
|
||||||
/// The QR code must be located on the last page of the document.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="References">Array of reference strings to pass through in the response</param>
|
|
||||||
/// <param name="Base64Pdf">PDF document encoded as Base64 string</param>
|
|
||||||
/// <example>
|
|
||||||
/// {
|
|
||||||
/// "references": ["REF-001", "REF-002"],
|
|
||||||
/// "base64Pdf": "JVBERi0xLjQK..."
|
|
||||||
/// }
|
|
||||||
/// </example>
|
|
||||||
public record ExtractSwissQrCodeRequest(
|
|
||||||
IReadOnlyList<string> References,
|
|
||||||
string Base64Pdf
|
|
||||||
);
|
|
||||||
@@ -4,12 +4,12 @@ namespace DocumentOperator.Application.Common.DTOs;
|
|||||||
/// Response mit PDF-Metadaten
|
/// Response mit PDF-Metadaten
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="PageCount">Anzahl der Seiten</param>
|
/// <param name="PageCount">Anzahl der Seiten</param>
|
||||||
/// <param name="FileSizeBytes">Dateigröße in Bytes</param>
|
/// <param name="FileSizeBytes">Dateigröße in Bytes</param>
|
||||||
/// <param name="FileSizeMB">Dateigröße in MB (gerundet auf 2 Dezimalstellen)</param>
|
/// <param name="FileSizeMB">Dateigröße in MB (gerundet auf 2 Dezimalstellen)</param>
|
||||||
/// <param name="PdfVersion">PDF-Version (z.B. "1.4")</param>
|
/// <param name="PdfVersion">PDF-Version (z.B. "1.4")</param>
|
||||||
/// <param name="HasAttachments">Hat das PDF Anhänge?</param>
|
/// <param name="HasAttachments">Hat das PDF Anhänge?</param>
|
||||||
/// <param name="AttachmentCount">Anzahl der Anhänge</param>
|
/// <param name="AttachmentCount">Anzahl der Anhänge</param>
|
||||||
public record ValidatePdfResponse(
|
public record PdfValidationResult(
|
||||||
int PageCount,
|
int PageCount,
|
||||||
long FileSizeBytes,
|
long FileSizeBytes,
|
||||||
double FileSizeMB,
|
double FileSizeMB,
|
||||||
@@ -29,7 +29,7 @@ namespace DocumentOperator.Application.Common.DTOs;
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// </example>
|
/// </example>
|
||||||
public record ExtractSwissQrCodeResponse(
|
public record SwissQrCodeExtractionResult(
|
||||||
IReadOnlyList<string> References,
|
IReadOnlyList<string> References,
|
||||||
SwissQrCodeDataDto QrCodeData
|
SwissQrCodeDataDto QrCodeData
|
||||||
);
|
);
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace DocumentOperator.Application.Common.DTOs;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request für PDF-Validierung
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Base64Pdf">Base64-encodiertes PDF-Dokument</param>
|
|
||||||
public record ValidatePdfRequest(string Base64Pdf);
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DocumentOperator.Application.Common.DTOs;
|
||||||
|
using DocumentOperator.Domain.Models.ValueObjects;
|
||||||
|
using DocumentOperator.Domain.ValueObjects;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Application.Common.Mapping;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AutoMapper profile for mapping domain entities to DTOs
|
||||||
|
/// </summary>
|
||||||
|
public class MappingProfile : Profile
|
||||||
|
{
|
||||||
|
public MappingProfile()
|
||||||
|
{
|
||||||
|
// PdfMetadata -> PdfValidationResult
|
||||||
|
CreateMap<PdfMetadata, PdfValidationResult>();
|
||||||
|
|
||||||
|
// SwissQrCodeData -> SwissQrCodeDataDto
|
||||||
|
CreateMap<SwissQrCodeData, SwissQrCodeDataDto>();
|
||||||
|
|
||||||
|
// AddressData -> AddressDataDto
|
||||||
|
CreateMap<AddressData, AddressDataDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using DocumentOperator.Application.Common.Interfaces;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace DocumentOperator.Application.Features.Documents.ExtractSwissQrCode;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles extraction of Swiss QR Code from PDF documents.
|
|
||||||
/// Uses ISwissQrCodeProcessor to extract QR code from the last page and parse it.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ExtractSwissQrCodeHandler : IRequestHandler<ExtractSwissQrCodeQuery, ExtractSwissQrCodeResult>
|
|
||||||
{
|
|
||||||
private readonly ISwissQrCodeProcessor _qrCodeProcessor;
|
|
||||||
|
|
||||||
public ExtractSwissQrCodeHandler(ISwissQrCodeProcessor qrCodeProcessor)
|
|
||||||
{
|
|
||||||
_qrCodeProcessor = qrCodeProcessor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ExtractSwissQrCodeResult> Handle(ExtractSwissQrCodeQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Convert Base64 string to byte array
|
|
||||||
byte[] pdfBytes = request.PdfContent.ToByteArray();
|
|
||||||
|
|
||||||
// Extract and parse Swiss QR Code from last page
|
|
||||||
var qrCodeData = await _qrCodeProcessor.ExtractSwissQrCodeAsync(pdfBytes, cancellationToken);
|
|
||||||
|
|
||||||
// Return references (passed through) + QR code data
|
|
||||||
return new ExtractSwissQrCodeResult(
|
|
||||||
References: request.References,
|
|
||||||
QrCodeData: qrCodeData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using DocumentOperator.Domain.Models.ValueObjects;
|
|
||||||
using DocumentOperator.Domain.ValueObjects;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace DocumentOperator.Application.Features.Documents.ExtractSwissQrCode;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Query to extract Swiss QR Code data from a PDF document.
|
|
||||||
/// Returns references (passed through) and parsed QR code data.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="References">Array of reference strings to pass through in the response</param>
|
|
||||||
/// <param name="PdfContent">PDF document content as Base64 string</param>
|
|
||||||
public record ExtractSwissQrCodeQuery(
|
|
||||||
IReadOnlyList<string> References,
|
|
||||||
Base64String PdfContent
|
|
||||||
) : IRequest<ExtractSwissQrCodeResult>;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Result containing passed-through references and extracted Swiss QR Code data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="References">Reference strings passed through from request</param>
|
|
||||||
/// <param name="QrCodeData">Parsed Swiss QR Code data from the last page of the PDF</param>
|
|
||||||
public record ExtractSwissQrCodeResult(
|
|
||||||
IReadOnlyList<string> References,
|
|
||||||
SwissQrCodeData QrCodeData
|
|
||||||
);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace DocumentOperator.Application.Features.Documents.ExtractSwissQrCode;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates ExtractSwissQrCodeQuery before handler execution.
|
|
||||||
/// Ensures references array and PDF content are provided.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ExtractSwissQrCodeValidator : AbstractValidator<ExtractSwissQrCodeQuery>
|
|
||||||
{
|
|
||||||
public ExtractSwissQrCodeValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.References)
|
|
||||||
.NotNull()
|
|
||||||
.WithMessage("References array is required.");
|
|
||||||
|
|
||||||
RuleFor(x => x.PdfContent)
|
|
||||||
.NotNull()
|
|
||||||
.WithMessage("PDF content is required.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using DocumentOperator.Application.Common.Interfaces;
|
|
||||||
using DocumentOperator.Domain.Models.ValueObjects;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace DocumentOperator.Application.Features.Documents.ValidatePdf;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handler for ValidatePdfQuery
|
|
||||||
/// Orchestrates PDF validation using IPdfProcessor
|
|
||||||
/// </summary>
|
|
||||||
public class ValidatePdfHandler : IRequestHandler<ValidatePdfQuery, PdfMetadata>
|
|
||||||
{
|
|
||||||
private readonly IPdfProcessor _pdfProcessor;
|
|
||||||
|
|
||||||
public ValidatePdfHandler(IPdfProcessor pdfProcessor)
|
|
||||||
{
|
|
||||||
_pdfProcessor = pdfProcessor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates PDF and returns metadata
|
|
||||||
/// </summary>
|
|
||||||
public async Task<PdfMetadata> Handle(ValidatePdfQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Value Object ? Byte Array
|
|
||||||
byte[] pdfBytes = request.PdfContent.ToByteArray();
|
|
||||||
|
|
||||||
// DevExpress Service aufrufen (kann PdfProcessingException werfen)
|
|
||||||
var metadata = await _pdfProcessor.ValidateAsync(pdfBytes);
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using DocumentOperator.Domain.Models.ValueObjects;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace DocumentOperator.Application.Features.Documents.ValidatePdf;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Query to validate a PDF document and return metadata
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="PdfContent">PDF content as Base64 string (validated by Value Object)</param>
|
|
||||||
public record ValidatePdfQuery(Base64String PdfContent) : IRequest<PdfMetadata>;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace DocumentOperator.Application.Features.Documents.ValidatePdf;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validator for ValidatePdfQuery
|
|
||||||
/// Validates that PdfContent is not null (Base64String already validates format in its constructor)
|
|
||||||
/// </summary>
|
|
||||||
public class ValidatePdfValidator : AbstractValidator<ValidatePdfQuery>
|
|
||||||
{
|
|
||||||
public ValidatePdfValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.PdfContent)
|
|
||||||
.NotNull()
|
|
||||||
.WithMessage("PDF content is required");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DocumentOperator.Application.Common.DTOs;
|
||||||
|
using DocumentOperator.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Application.SwissQrCode.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query for extracting Swiss QR Code from PDF (supports both byte array and Base64 input)
|
||||||
|
/// </summary>
|
||||||
|
public record ExtractSwissQrCodeQuery : IRequest<SwissQrCodeExtractionResult>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF as byte array (direct upload)
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? PdfBytes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF as Base64 string (API clients)
|
||||||
|
/// </summary>
|
||||||
|
public string? Base64Pdf { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional reference strings (passed through to response for external tracking)
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string>? References { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for ExtractSwissQrCodeQuery
|
||||||
|
/// Orchestrates Swiss QR Code extraction using ISwissQrCodeProcessor and AutoMapper
|
||||||
|
/// </summary>
|
||||||
|
public class ExtractSwissQrCodeQueryHandler(ISwissQrCodeProcessor qrCodeProcessor, IMapper mapper)
|
||||||
|
: IRequestHandler<ExtractSwissQrCodeQuery, SwissQrCodeExtractionResult>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts and parses Swiss QR Code from the last page of the PDF
|
||||||
|
/// </summary>
|
||||||
|
public async Task<SwissQrCodeExtractionResult> Handle(ExtractSwissQrCodeQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Use byte[] if available, otherwise convert Base64
|
||||||
|
byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!);
|
||||||
|
|
||||||
|
// Extract and parse Swiss QR Code from last page (can throw PdfProcessingException or QrCodeNotFoundException)
|
||||||
|
var qrCodeData = await qrCodeProcessor.ExtractSwissQrCodeAsync(pdfBytes, cancellationToken);
|
||||||
|
|
||||||
|
// Map domain value object to DTO using AutoMapper
|
||||||
|
var qrCodeDto = mapper.Map<SwissQrCodeDataDto>(qrCodeData);
|
||||||
|
|
||||||
|
// Return references (passed through) + QR code data
|
||||||
|
return new SwissQrCodeExtractionResult(
|
||||||
|
References: request.References ?? [],
|
||||||
|
QrCodeData: qrCodeDto
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using DocumentOperator.Application.SwissQrCode.Queries;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Application.SwissQrCode.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates ExtractSwissQrCodeQuery before handler execution.
|
||||||
|
/// Ensures exactly ONE input method is provided (either PdfBytes OR Base64Pdf, not both, not none).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExtractSwissQrCodeQueryValidator : AbstractValidator<ExtractSwissQrCodeQuery>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DocumentOperator.Application.Common.DTOs;
|
||||||
|
using DocumentOperator.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Application.ValidatePdf.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query for PDF validation (supports both byte array and Base64 input)
|
||||||
|
/// </summary>
|
||||||
|
public record ValidatePdfQuery : IRequest<PdfValidationResult>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF as byte array (direct upload)
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? PdfBytes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF as Base64 string (API clients)
|
||||||
|
/// </summary>
|
||||||
|
public string? Base64Pdf { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for ValidatePdfQuery
|
||||||
|
/// Orchestrates PDF validation using IPdfProcessor and AutoMapper
|
||||||
|
/// </summary>
|
||||||
|
public class ValidatePdfQueryHandler(IPdfProcessor PdfProcessor, IMapper Mapper)
|
||||||
|
: IRequestHandler<ValidatePdfQuery, PdfValidationResult>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates PDF and returns metadata
|
||||||
|
/// </summary>
|
||||||
|
public async Task<PdfValidationResult> Handle(ValidatePdfQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Use byte[] if available, otherwise convert Base64
|
||||||
|
byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!);
|
||||||
|
|
||||||
|
// Call DevExpress service (can throw PdfProcessingException)
|
||||||
|
var metadata = await PdfProcessor.ValidateAsync(pdfBytes);
|
||||||
|
|
||||||
|
// Map domain entity to DTO using AutoMapper
|
||||||
|
return Mapper.Map<PdfValidationResult>(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using DocumentOperator.Application.ValidatePdf.Queries;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Application.ValidatePdf.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validator for ValidatePdfQuery
|
||||||
|
/// Ensures exactly ONE input method is provided (either PdfBytes OR Base64Pdf, not both, not none)
|
||||||
|
/// </summary>
|
||||||
|
public class ValidatePdfQueryValidator : AbstractValidator<ValidatePdfQuery>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user