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:
2026-07-07 19:01:05 +02:00
parent b460d8df39
commit d6e3a5fda1
16 changed files with 253 additions and 185 deletions

View File

@@ -8,16 +8,9 @@ namespace DocumentOperator.Application.Common.Behaviors;
/// MediatR Pipeline Behavior that logs requests and tracks performance
/// Executes AFTER ValidationBehavior, BEFORE Handler
/// </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>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
@@ -25,21 +18,18 @@ public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest,
{
var requestName = typeof(TRequest).Name;
// Request Start
_logger.LogInformation("Handling {RequestName}: {@Request}", requestName, request);
// Performance Tracking
var stopwatch = Stopwatch.StartNew();
try
{
// Handler ausführen
var response = await next();
var response = await next(cancellationToken);
stopwatch.Stop();
// Request Success
_logger.LogInformation(
Logger.LogInformation(
"Handled {RequestName} in {ElapsedMs}ms",
requestName,
stopwatch.ElapsedMilliseconds
@@ -52,7 +42,7 @@ public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest,
stopwatch.Stop();
// Request Failed
_logger.LogError(
Logger.LogError(
ex,
"Error handling {RequestName} after {ElapsedMs}ms: {ErrorMessage}",
requestName,

View File

@@ -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
);

View File

@@ -4,12 +4,12 @@ namespace DocumentOperator.Application.Common.DTOs;
/// Response mit PDF-Metadaten
/// </summary>
/// <param name="PageCount">Anzahl der Seiten</param>
/// <param name="FileSizeBytes">Dateigröße in Bytes</param>
/// <param name="FileSizeMB">Dateigröße in MB (gerundet auf 2 Dezimalstellen)</param>
/// <param name="FileSizeBytes">Dateigröße in Bytes</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="HasAttachments">Hat das PDF Anhänge?</param>
/// <param name="AttachmentCount">Anzahl der Anhänge</param>
public record ValidatePdfResponse(
/// <param name="HasAttachments">Hat das PDF Anhänge?</param>
/// <param name="AttachmentCount">Anzahl der Anhänge</param>
public record PdfValidationResult(
int PageCount,
long FileSizeBytes,
double FileSizeMB,

View File

@@ -29,7 +29,7 @@ namespace DocumentOperator.Application.Common.DTOs;
/// }
/// }
/// </example>
public record ExtractSwissQrCodeResponse(
public record SwissQrCodeExtractionResult(
IReadOnlyList<string> References,
SwissQrCodeDataDto QrCodeData
);

View File

@@ -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);

View File

@@ -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>();
}
}