Add WorkflowsClient for PDF validation and QR extraction
Introduced a new `WorkflowsClient` to orchestrate multi-step operations, combining `IPdfValidationClient` and `ISwissQrCodeClient`. Added methods to validate PDFs and extract Swiss QR Codes in a single call, with overloads for byte arrays, streams, and file paths. Registered `WorkflowsClient` as a scoped service in `ServiceCollectionExtensions`. Updated `PdfValidationResult` with an `IsValid` property to simplify PDF validity checks. Added `IWorkflowsClient` interface to define the contract for the new client, with detailed XML documentation for its methods.
This commit is contained in:
@@ -17,4 +17,7 @@ public record PdfValidationResult(
|
|||||||
string PdfVersion,
|
string PdfVersion,
|
||||||
bool HasAttachments,
|
bool HasAttachments,
|
||||||
int AttachmentCount,
|
int AttachmentCount,
|
||||||
bool IsEncrypted);
|
bool IsEncrypted)
|
||||||
|
{
|
||||||
|
public bool IsValid => !IsEncrypted && PageCount > 0 && FileSizeBytes > 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,5 +89,7 @@ public static class Client
|
|||||||
public static ISwissQrCodeClient SwissQrCode => GetRequiredServiceOfScope<ISwissQrCodeClient>();
|
public static ISwissQrCodeClient SwissQrCode => GetRequiredServiceOfScope<ISwissQrCodeClient>();
|
||||||
|
|
||||||
public static IZugferdClient Zugferd => GetRequiredServiceOfScope<IZugferdClient>();
|
public static IZugferdClient Zugferd => GetRequiredServiceOfScope<IZugferdClient>();
|
||||||
|
|
||||||
|
public static IWorkflowsClient Workflows => GetRequiredServiceOfScope<IWorkflowsClient>();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
84
DocumentService.Client/Clients/WorkflowsClient.cs
Normal file
84
DocumentService.Client/Clients/WorkflowsClient.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using DocumentService.Client.Interfaces;
|
||||||
|
|
||||||
|
namespace DocumentService.Client.Clients;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orchestrated multi-step workflows that compose multiple DocumentService clients
|
||||||
|
/// into single, higher-level operations.
|
||||||
|
/// </summary>
|
||||||
|
public class WorkflowsClient(IPdfValidationClient validation, ISwissQrCodeClient swissQrCode) : IWorkflowsClient
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeValidatedAsync(
|
||||||
|
byte[] pdfBytes,
|
||||||
|
bool raw = false,
|
||||||
|
bool throwIfInvalid = true,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var validationResult = await validation.ValidatePdfAsync(pdfBytes, cancellationToken);
|
||||||
|
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
return throwIfInvalid
|
||||||
|
? throw new InvalidOperationException(
|
||||||
|
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
|
||||||
|
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
|
||||||
|
: (SwissQrCodeExtractionResult?)null;
|
||||||
|
|
||||||
|
return await swissQrCode.ExtractSwissQrCodeAsync(pdfBytes, raw, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeValidatedAsync(
|
||||||
|
Stream pdfStream,
|
||||||
|
bool raw = false,
|
||||||
|
bool throwIfInvalid = true,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var validationResult = await validation.ValidatePdfAsync(pdfStream, cancellationToken);
|
||||||
|
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
return throwIfInvalid
|
||||||
|
? throw new InvalidOperationException(
|
||||||
|
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
|
||||||
|
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
|
||||||
|
: (SwissQrCodeExtractionResult?)null;
|
||||||
|
|
||||||
|
// Stream was consumed by validation – reset if possible, otherwise re-open is caller's responsibility.
|
||||||
|
// Since callers typically pass a FileStream or MemoryStream, Position reset covers the common case.
|
||||||
|
if (pdfStream.CanSeek)
|
||||||
|
pdfStream.Position = 0;
|
||||||
|
|
||||||
|
return await swissQrCode.ExtractSwissQrCodeAsync(pdfStream, raw, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeValidatedAsync(
|
||||||
|
string filePath,
|
||||||
|
bool raw = false,
|
||||||
|
bool throwIfInvalid = true,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
#if NETFRAMEWORK
|
||||||
|
using var stream = File.OpenRead(filePath);
|
||||||
|
var validationResult = await validation.ValidatePdfAsync(stream, cancellationToken);
|
||||||
|
#else
|
||||||
|
var pdfBytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
|
||||||
|
var validationResult = await validation.ValidatePdfAsync(pdfBytes, cancellationToken);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
return throwIfInvalid
|
||||||
|
? throw new InvalidOperationException(
|
||||||
|
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
|
||||||
|
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
|
||||||
|
: (SwissQrCodeExtractionResult?)null;
|
||||||
|
|
||||||
|
#if NETFRAMEWORK
|
||||||
|
using var extractStream = File.OpenRead(filePath);
|
||||||
|
return await swissQrCode.ExtractSwissQrCodeAsync(extractStream, raw, cancellationToken);
|
||||||
|
#else
|
||||||
|
return await swissQrCode.ExtractSwissQrCodeAsync(pdfBytes, raw, cancellationToken);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,9 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddHttpClient<IPdfConversionClient, PdfConversionClient>(ConfigureClient)
|
services.AddHttpClient<IPdfConversionClient, PdfConversionClient>(ConfigureClient)
|
||||||
.ConfigurePrimaryHttpMessageHandler(configureHandler);
|
.ConfigurePrimaryHttpMessageHandler(configureHandler);
|
||||||
|
|
||||||
|
// WorkflowsClient composes existing clients – no HttpClient needed.
|
||||||
|
services.AddScoped<IWorkflowsClient, WorkflowsClient>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
DocumentService.Client/Interfaces/IWorkflowsClient.cs
Normal file
59
DocumentService.Client/Interfaces/IWorkflowsClient.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
|
||||||
|
namespace DocumentService.Client.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orchestrated multi-step workflows that combine multiple DocumentService operations into
|
||||||
|
/// a single, higher-level call. Prefer these over manually chaining individual clients.
|
||||||
|
/// </summary>
|
||||||
|
public interface IWorkflowsClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the PDF and extracts its Swiss QR Code in a single call.
|
||||||
|
/// Validation runs first; extraction is only attempted on valid documents.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pdfBytes">PDF file as byte array.</param>
|
||||||
|
/// <param name="raw">
|
||||||
|
/// <c>false</c> (default) – returns a parsed <see cref="SwissQrCodeExtractionResult.Bill"/> object.<br/>
|
||||||
|
/// <c>true</c> – returns raw QR text lines without parsing.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="throwIfInvalid">
|
||||||
|
/// <c>true</c> (default) – throws <see cref="InvalidOperationException"/> when the PDF fails validation.<br/>
|
||||||
|
/// <c>false</c> – returns <c>null</c> instead, allowing the caller to handle the case without a try/catch.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Swiss QR Code extraction result, or <c>null</c> if the document is invalid and
|
||||||
|
/// <paramref name="throwIfInvalid"/> is <c>false</c>.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// Thrown when the PDF fails validation and <paramref name="throwIfInvalid"/> is <c>true</c>.
|
||||||
|
/// </exception>
|
||||||
|
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeValidatedAsync(
|
||||||
|
byte[] pdfBytes,
|
||||||
|
bool raw = false,
|
||||||
|
bool throwIfInvalid = true,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ExtractSwissQrCodeValidatedAsync(byte[], bool, bool, CancellationToken)"/>
|
||||||
|
/// <param name="pdfStream">PDF file as a stream.</param>
|
||||||
|
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeValidatedAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
|
||||||
|
/// <param name="throwIfInvalid"><inheritdoc cref="ExtractSwissQrCodeValidatedAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='throwIfInvalid']"/></param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeValidatedAsync(
|
||||||
|
Stream pdfStream,
|
||||||
|
bool raw = false,
|
||||||
|
bool throwIfInvalid = true,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ExtractSwissQrCodeValidatedAsync(byte[], bool, bool, CancellationToken)"/>
|
||||||
|
/// <param name="filePath">Absolute or relative path to the PDF file.</param>
|
||||||
|
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeValidatedAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
|
||||||
|
/// <param name="throwIfInvalid"><inheritdoc cref="ExtractSwissQrCodeValidatedAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='throwIfInvalid']"/></param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeValidatedAsync(
|
||||||
|
string filePath,
|
||||||
|
bool raw = false,
|
||||||
|
bool throwIfInvalid = true,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user