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,
|
||||
bool HasAttachments,
|
||||
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 IZugferdClient Zugferd => GetRequiredServiceOfScope<IZugferdClient>();
|
||||
|
||||
public static IWorkflowsClient Workflows => GetRequiredServiceOfScope<IWorkflowsClient>();
|
||||
#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)
|
||||
.ConfigurePrimaryHttpMessageHandler(configureHandler);
|
||||
|
||||
// WorkflowsClient composes existing clients – no HttpClient needed.
|
||||
services.AddScoped<IWorkflowsClient, WorkflowsClient>();
|
||||
|
||||
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