Compare commits

...

16 Commits

Author SHA1 Message Date
d1fdbb494b Update README, remove STREAM_API_DESIGN, adjust solution
Enhanced README with German translation and examples.

- Added German translation to README alongside English content.
- Expanded installation, configuration, and usage examples.
- Documented static `Client` entry point and error handling.
- Removed STREAM_API_DESIGN.md as it is no longer relevant.
- Changed solution configuration to build in Release mode by default.
2026-08-31 02:29:18 +02:00
14645514d9 Update project version to 1.1.0
Updated versioning details in the `.csproj` file:
- Incremented `Version` to 1.1.0.
- Updated `FileVersion`, `AssemblyVersion`, and `InformationalVersion` to reflect the new version.
This update likely includes new features or enhancements while maintaining backward compatibility.
2026-08-31 02:29:01 +02:00
2c98558131 Refactor DTOs and namespaces for maintainability
Consolidated DTOs under `DocumentService.Application.Common.DTOs.Requests`
to centralize and align them with the application layer. Introduced
`PdfBase64RequestBase` to encapsulate shared properties, reducing
redundancy across request DTOs. Updated controllers, clients, and tests
to use the new DTO structure. Removed redundant DTOs and cleaned up
unused namespaces and comments. Improved documentation and ensured
consistent naming conventions across the codebase.
2026-08-31 01:19:52 +02:00
af0fca198a Remove AddAttachmentsRequest record from PdfAttachmentRequests
The `AddAttachmentsRequest` record has been removed. This record was previously used as a request DTO for adding attachments to a Base64-encoded PDF. It included a `Base64Pdf` property for the PDF document and an `Attachments` property for the list of attachments to embed.

The `ExtractPdfAttachmentsRequest` and `AttachmentRequestDto` records remain unchanged.
2026-08-31 00:56:27 +02:00
75bea9ef29 Refactor DTOs and tests; add framework-specific handling
Refactored the codebase to use shared DTOs from `DocumentService.Application.Common.DTOs`, removing inline DTO definitions in client interfaces and tests. Introduced conditional compilation to handle framework-specific differences in DTOs and handlers.

Updated test cases to align with the new DTO structures, including `SwissQrCodeExtractionResult`, `PdfValidationResult`, and `ZugferdExtractionResult`. Adjusted test data to match updated DTO properties.

Cleaned up solution and project files, ensuring proper references and organization. Removed unused files and fixed project references in `DocumentService.Tests.csproj`. Improved maintainability and consistency across the codebase.
2026-08-31 00:54:38 +02:00
108e522316 Add multi-targeting and refactor project structure
Enabled multi-targeting in `DocumentService.Application` to support `net462`, `net480`, and `net8.0`. Updated dependencies with framework-specific versions and added conditional configurations for compatibility.

Refactored `DocumentService.Client` by removing redundant enums and unused code. Adjusted namespaces and project references to streamline the codebase and improve maintainability.

These changes modernize the project, enhance compatibility, and align with modular design principles.
2026-08-31 00:53:34 +02:00
95d67567f0 Support multiple frameworks and set C# language version
Updated the project to target multiple frameworks (`net462`,
`net480`, and `net8.0`) by replacing `<TargetFramework>` with
`<TargetFrameworks>`. Added `<LangVersion>` property set to
`latest` to enable the use of the latest C# language features.
Adjusted `<Nullable>` property formatting for consistency.
2026-08-30 22:20:12 +02:00
207c778f3a Refactor: Rename DocumentOperator to DocumentService
Updated project and solution files to rename and restructure
`DocumentOperator` to `DocumentService`. Modified project
references in `DocumentService.API.csproj`, `DocumentService.Application.csproj`,
and `DocumentService.Infrastructure.csproj` to reflect the new
naming convention. Updated `DocumentService.sln` to remove
old project references, add new ones, and adjust configuration
and nested project mappings accordingly. This change aligns
the project structure with the new naming convention.
2026-08-30 21:33:25 +02:00
050cb55bf9 test: strengthen PdfValidationClient tests (IsEncrypted + full field assertions)
- Add IsEncrypted=true deserialization test (EncryptedPdf_DeserializesIsEncryptedTrue)
- Add HasAttachments/AttachmentCount assertions on Bytes overload
- Add 400/500 error tests for both byte[] overloads of ValidatePdfAAsync
- Strengthen ValidatePdfAAsync assertions (Errors count, Warnings count)
- Extract BuildError() helper to reduce boilerplate
- Total: 8 -> 11 tests
2026-08-28 11:46:37 +02:00
7559028164 test: add FluentValidation unit tests for all command/query validators
ValidatorTests.cs covers 21 scenarios across 4 validators:

MergePdfsCommandValidator (3):
- TwoOrMoreStreams passes, SingleStream fails, NullStreams fails

AddAnnotationValidator (7):
- Valid circle passes, FreeText without content fails,
  TextMarkup without style fails, invalid/valid color hex,
  zero page number fails, invalid rectangle (X2<X1) fails

AddStampValidator (6):
- Valid text stamp passes, missing Text/ImageBytes/PredefinedType each fail,
  opacity > 1.0 fails, invalid color hex fails

ConvertToPdfACommandValidator (4):
- All valid levels pass, invalid level fails, empty level fails, null stream fails
2026-08-28 11:46:28 +02:00
fbaefe1fd2 test: add Application handler tests for all missing features
New test files (4 tests each — 2 success + 2 failure):
- MergePdfsCommandHandlerTests: TwoStreams, WithPageRanges, BadRequest, GenericException
- AddAnnotationHandlerTests: TextMarkup, FreeText, BadRequest (page OOB), GenericException
- AddStampHandlerTests: TextStamp, PredefinedStamp, BadRequest (opacity), GenericException
- ExtractPdfAttachmentsHandlerTests: ReturnsZip, NotFoundException, BadRequest, GenericException
- ConvertPdfAHandlerTests (ConvertTo + ConvertFrom):
  ConvertTo: ValidLevel, NotImplemented, BadRequest
  ConvertFrom: ValidPdfA, BadRequest, GenericException
2026-08-28 11:46:18 +02:00
9040aed1da test: rewrite existing Application handler tests (no mapper mock, full assertions)
ValidatePdfHandlerTests:
- Remove IMapper mock (handler no longer uses mapper)
- Add: EncryptedPdf_ReturnsIsEncryptedTrue
- Add: PdfWithAttachments_ReturnsAttachmentData
- Add: ProcessorThrowsGenericException_PropagatesException

ValidatePdfAQueryHandlerTests:
- Remove IMapper mock
- Strengthen assertions (Errors count, Warnings content)
- Add: ProcessorThrowsGenericException_PropagatesException

CheckPdfAttachmentsQueryHandlerTests:
- Remove duplicate Handle_WithBase64Pdf test (identical to WithPdfBytes)
- Add: ProcessorThrowsBadRequest_PropagatesException
- Add: ProcessorThrowsGenericException_PropagatesException
- Strengthen: assert individual attachment fields (FileName, MimeType, Size)
2026-08-28 11:46:09 +02:00
9a9d775fec test: strengthen DevExpressPdfProcessor tests (IsEncrypted + DetectEncryption)
- Rename ValidateAsync_ValidPdf_ReturnsPdfMetadata to ReturnsPdfValidationResult
  and add FileSizeMB assertion
- Add Encryption Detection region with 4 tests:
  - NormalPdf_IsEncryptedFalse (real PDF via ValidateAsync)
  - ValidatePdfAAsync_NormalPdf_EncryptedFalse (real PDF via ValidatePdfAAsync)
  - DetectEncryption_TrailerWithEncryptEntry_ReturnsTrue (reflection, byte scan)
  - DetectEncryption_NoEncryptEntry_ReturnsFalse (reflection, byte scan)
  - DetectEncryptionKeyword_NotStandaloneToken_ReturnsFalse (token boundary check)
- Add IsEncrypted.Should().BeFalse() assertion to attachment count test
2026-08-28 11:45:59 +02:00
2630860e7e test: add encryptedPdf.pdf test fixture with /Encrypt in trailer
Minimal hand-crafted PDF containing a Standard encryption dictionary
referenced from the trailer (/Encrypt 4 0 R). Used by DetectEncryption
unit tests to verify byte-level encryption detection.

Registered as EmbeddedResource in DocumentService.Tests.csproj.
2026-08-28 11:45:50 +02:00
7b9318e75c fix: replace ASCII-based DetectEncryption with byte-level trailer scan
Old implementation used Encoding.ASCII.GetString() which:
- Corrupts binary PDF bytes 0x80-0xFF -> '?' (false negatives)
- Matches '/Encrypt' anywhere in document content (false positives)
- Matches '/Encrypted', '/EncryptionKey' etc. (token boundary not checked)

New implementation uses ReadOnlySpan<byte> with three strategies:
1. Search last 2KB (trailer region) - fast path, covers standard PDFs
2. Search first 2KB (linearized PDFs have duplicate trailer at start)
3. Full-file fallback for PDF 1.5+ compressed xref streams, with token
   boundary check (next byte must be space/newline/tab/'<'/'/'/'[')
   to avoid matching '/Encrypted' or '/EncryptionKey'

Also: DevExpressPdfProcessor.ValidateAsync now returns PdfValidationResult
directly (no more PdfMetadata wrapper), includes IsEncrypted field.
2026-08-28 11:45:43 +02:00
ef78fb5cdd refactor: remove PdfMetadata/PdfAMetadata, use result DTOs directly
PdfMetadata and PdfAMetadata were redundant intermediate classes that
mirrored PdfValidationResult and PdfAValidationResult exactly.

Changes:
- Delete PdfMetadata.cs and PdfAMetadata.cs
- IPdfProcessor.ValidateAsync now returns PdfValidationResult directly
- IPdfProcessor.ValidatePdfAAsync now returns PdfAValidationResult directly
- ValidatePdfQueryHandler: remove IMapper dependency, return result directly
- ValidatePdfAQueryHandler: remove IMapper dependency, return result directly
- MappingProfile: remove the two now-obsolete CreateMap calls
- PdfValidationResult: add IsEncrypted parameter (was missing)
2026-08-28 11:45:33 +02:00
144 changed files with 2855 additions and 1975 deletions

View File

@@ -1,34 +0,0 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// PDF/A validation metadata including conformance level and validation errors/warnings
/// </summary>
public sealed class PdfAMetadata(
bool isValid,
string pdfVersion,
int pageCount,
long fileSizeBytes,
bool encrypted,
string? pdfaVersion,
bool pdfaCompliant,
IReadOnlyList<string> errors,
IReadOnlyList<string> warnings)
{
public bool IsValid { get; } = isValid;
public string PdfVersion { get; } = pdfVersion;
public int PageCount { get; } = pageCount;
public long FileSizeBytes { get; } = fileSizeBytes;
public bool Encrypted { get; } = encrypted;
public string? PdfAVersion { get; } = pdfaVersion;
public bool PdfACompliant { get; } = pdfaCompliant;
public IReadOnlyList<string> Errors { get; } = errors;
public IReadOnlyList<string> Warnings { get; } = warnings;
// Computed property
public double FileSizeMB => FileSizeBytes / 1024.0 / 1024.0;
public override string ToString()
{
return $"PDF/A: {PdfAVersion ?? "None"}, {PageCount} pages, {FileSizeMB:F2} MB, Compliant: {PdfACompliant}";
}
}

View File

@@ -1,23 +0,0 @@
namespace DocumentService.Application.Common.DTOs;
public sealed class PdfMetadata(
int pageCount,
long fileSizeBytes,
string pdfVersion,
bool hasAttachments,
int attachmentCount)
{
public int PageCount { get; } = pageCount;
public long FileSizeBytes { get; } = fileSizeBytes;
public string PdfVersion { get; } = pdfVersion;
public bool HasAttachments { get; } = hasAttachments;
public int AttachmentCount { get; } = attachmentCount;
// Computed Property (berechnet aus FileSizeBytes)
public double FileSizeMB => FileSizeBytes / 1024.0 / 1024.0;
public override string ToString()
{
return $"PDF: {PageCount} pages, {FileSizeMB:F2} MB, Version {PdfVersion}, Attachments: {AttachmentCount}";
}
}

View File

@@ -1,36 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="DependencyInjection\**" />
<Compile Remove="Features\**" />
<EmbeddedResource Remove="DependencyInjection\**" />
<EmbeddedResource Remove="Features\**" />
<None Remove="DependencyInjection\**" />
<None Remove="Features\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.2.0" />
<PackageReference Include="Codecrete.SwissQRBill.Generator" Version="3.4.0" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentOperator.Domain\DocumentService.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Common\Mappings\" />
</ItemGroup>
</Project>

View File

@@ -1,91 +0,0 @@
using AutoMapper;
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.Common.Interfaces;
using DocumentService.Application.ValidatePdf.Queries;
using DocumentService.Domain.Common.Exceptions;
using FluentAssertions;
using Moq;
using Xunit;
namespace DocumentService.Tests.Unit.Application.Features.ValidatePdf;
public class ValidatePdfHandlerTests
{
private readonly Mock<IPdfProcessor> _mockPdfProcessor;
private readonly Mock<IMapper> _mockMapper;
private readonly ValidatePdfQueryHandler _handler;
public ValidatePdfHandlerTests()
{
_mockPdfProcessor = new Mock<IPdfProcessor>();
_mockMapper = new Mock<IMapper>();
_handler = new ValidatePdfQueryHandler(_mockPdfProcessor.Object, _mockMapper.Object);
}
[Fact]
public async Task Handle_ValidPdf_ReturnsPdfMetadata()
{
// Arrange
var pdfBytes = "%PDF"u8.ToArray(); // "%PDF"
var query = new ValidatePdfQuery { PdfStream = new MemoryStream(pdfBytes) };
var domainMetadata = new PdfMetadata(
pageCount: 5,
fileSizeBytes: 1024,
pdfVersion: "1.4",
hasAttachments: false,
attachmentCount: 0
);
var expectedDto = new PdfValidationResult(
PageCount: 5,
FileSizeBytes: 1024,
FileSizeMB: 0.00,
PdfVersion: "1.4",
HasAttachments: false,
AttachmentCount: 0
);
_mockPdfProcessor
.Setup(x => x.ValidateAsync(It.IsAny<Stream>()))
.ReturnsAsync(domainMetadata);
_mockMapper
.Setup(x => x.Map<PdfValidationResult>(domainMetadata))
.Returns(expectedDto);
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.PageCount.Should().Be(5);
result.FileSizeBytes.Should().Be(1024);
result.PdfVersion.Should().Be("1.4");
result.HasAttachments.Should().BeFalse();
result.AttachmentCount.Should().Be(0);
_mockPdfProcessor.Verify(x => x.ValidateAsync(It.IsAny<Stream>()), Times.Once);
_mockMapper.Verify(x => x.Map<PdfValidationResult>(domainMetadata), Times.Once);
}
[Fact]
public async Task Handle_PdfProcessorThrowsException_PropagatesException()
{
// Arrange
var pdfBytes = "%PDF"u8.ToArray(); // "%PDF"
var query = new ValidatePdfQuery { PdfStream = new MemoryStream(pdfBytes) };
_mockPdfProcessor
.Setup(x => x.ValidateAsync(It.IsAny<Stream>()))
.ThrowsAsync(new BadRequestException("Invalid PDF format"));
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => _handler.Handle(query, CancellationToken.None)
);
exception.Message.Should().Be("Invalid PDF format");
_mockPdfProcessor.Verify(x => x.ValidateAsync(It.IsAny<Stream>()), Times.Once);
}
}

View File

@@ -1,214 +0,0 @@
using AutoMapper;
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.Common.Interfaces;
using DocumentService.Application.ValidatePdfA.Queries;
using DocumentService.Domain.Common.Exceptions;
using FluentAssertions;
using Moq;
using Xunit;
namespace DocumentService.Tests.Unit.Application.Features.ValidatePdfA;
public class ValidatePdfAQueryHandlerTests
{
private readonly Mock<IPdfProcessor> _mockPdfProcessor;
private readonly Mock<IMapper> _mockMapper;
private readonly ValidatePdfAQueryHandler _handler;
public ValidatePdfAQueryHandlerTests()
{
_mockPdfProcessor = new Mock<IPdfProcessor>();
_mockMapper = new Mock<IMapper>();
_handler = new ValidatePdfAQueryHandler(_mockPdfProcessor.Object, _mockMapper.Object);
}
[Fact]
public async Task Handle_ValidPdfA_ReturnsPdfAMetadata()
{
// Arrange
var pdfBytes = "%PDF"u8.ToArray(); // "%PDF"
var query = new ValidatePdfAQuery { PdfStream = new MemoryStream(pdfBytes) };
var domainMetadata = new PdfAMetadata(
isValid: true,
pdfVersion: "1.7",
pageCount: 3,
fileSizeBytes: 2048,
encrypted: false,
pdfaVersion: "PDF/A-3b",
pdfaCompliant: true,
errors: [],
warnings: []
);
var expectedDto = new PdfAValidationResult
{
IsValid = true,
PdfVersion = "1.7",
PageCount = 3,
FileSize = 2048,
Encrypted = false,
PdfAVersion = "PDF/A-3b",
PdfACompliant = true,
Errors = new List<string>(),
Warnings = new List<string>()
};
_mockPdfProcessor
.Setup(x => x.ValidatePdfAAsync(It.IsAny<Stream>()))
.ReturnsAsync(domainMetadata);
_mockMapper
.Setup(x => x.Map<PdfAValidationResult>(domainMetadata))
.Returns(expectedDto);
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.IsValid.Should().BeTrue();
result.PageCount.Should().Be(3);
result.PdfVersion.Should().Be("1.7");
result.PdfAVersion.Should().Be("PDF/A-3b");
result.PdfACompliant.Should().BeTrue();
result.Encrypted.Should().BeFalse();
result.Errors.Should().BeEmpty();
result.Warnings.Should().BeEmpty();
_mockPdfProcessor.Verify(x => x.ValidatePdfAAsync(It.IsAny<Stream>()), Times.Once);
_mockMapper.Verify(x => x.Map<PdfAValidationResult>(domainMetadata), Times.Once);
}
[Fact]
public async Task Handle_NonCompliantPdfA_ReturnsErrorsAndWarnings()
{
// Arrange
var pdfBytes = new byte[] { 0x25, 0x50, 0x44, 0x46 }; // "%PDF"
var query = new ValidatePdfAQuery { PdfStream = new MemoryStream(pdfBytes) };
var errors = new List<string> { "Missing XMP metadata", "Invalid color space" };
var warnings = new List<string> { "Embedded font not subset" };
var domainMetadata = new PdfAMetadata(
isValid: false,
pdfVersion: "1.4",
pageCount: 2,
fileSizeBytes: 1024,
encrypted: false,
pdfaVersion: null,
pdfaCompliant: false,
errors: errors,
warnings: warnings
);
var expectedDto = new PdfAValidationResult
{
IsValid = false,
PdfVersion = "1.4",
PageCount = 2,
FileSize = 1024,
Encrypted = false,
PdfAVersion = null,
PdfACompliant = false,
Errors = errors,
Warnings = warnings
};
_mockPdfProcessor
.Setup(x => x.ValidatePdfAAsync(It.IsAny<Stream>()))
.ReturnsAsync(domainMetadata);
_mockMapper
.Setup(x => x.Map<PdfAValidationResult>(domainMetadata))
.Returns(expectedDto);
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.IsValid.Should().BeFalse();
result.PdfACompliant.Should().BeFalse();
result.PdfAVersion.Should().BeNull();
result.Errors.Should().HaveCount(2);
result.Errors.Should().Contain("Missing XMP metadata");
result.Warnings.Should().HaveCount(1);
_mockPdfProcessor.Verify(x => x.ValidatePdfAAsync(It.IsAny<Stream>()), Times.Once);
}
[Fact]
public async Task Handle_EncryptedPdf_ReturnsEncryptedFlag()
{
// Arrange
var pdfBytes = new byte[] { 0x25, 0x50, 0x44, 0x46 }; // "%PDF"
var query = new ValidatePdfAQuery { PdfStream = new MemoryStream(pdfBytes) };
var domainMetadata = new PdfAMetadata(
isValid: true,
pdfVersion: "1.7",
pageCount: 1,
fileSizeBytes: 512,
encrypted: true,
pdfaVersion: null,
pdfaCompliant: false,
errors: new List<string> { "Encrypted PDFs cannot be PDF/A compliant" },
warnings: new List<string>()
);
var expectedDto = new PdfAValidationResult
{
IsValid = true,
PdfVersion = "1.7",
PageCount = 1,
FileSize = 512,
Encrypted = true,
PdfAVersion = null,
PdfACompliant = false,
Errors = new List<string> { "Encrypted PDFs cannot be PDF/A compliant" },
Warnings = new List<string>()
};
_mockPdfProcessor
.Setup(x => x.ValidatePdfAAsync(It.IsAny<Stream>()))
.ReturnsAsync(domainMetadata);
_mockMapper
.Setup(x => x.Map<PdfAValidationResult>(domainMetadata))
.Returns(expectedDto);
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Encrypted.Should().BeTrue();
result.PdfACompliant.Should().BeFalse();
result.Errors.Should().Contain("Encrypted PDFs cannot be PDF/A compliant");
_mockPdfProcessor.Verify(x => x.ValidatePdfAAsync(It.IsAny<Stream>()), Times.Once);
}
[Fact]
public async Task Handle_PdfProcessorThrowsException_PropagatesException()
{
// Arrange
var pdfBytes = new byte[] { 0x25, 0x50, 0x44, 0x46 }; // "%PDF"
var query = new ValidatePdfAQuery { PdfStream = new MemoryStream(pdfBytes) };
_mockPdfProcessor
.Setup(x => x.ValidatePdfAAsync(It.IsAny<Stream>()))
.ThrowsAsync(new BadRequestException("Invalid PDF format"));
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => _handler.Handle(query, CancellationToken.None)
);
exception.Message.Should().Be("Invalid PDF format");
_mockPdfProcessor.Verify(x => x.ValidatePdfAAsync(It.IsAny<Stream>()), Times.Once);
}
}

View File

@@ -1,168 +0,0 @@
using DocumentService.Client.Clients;
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Requests;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using System.Net;
using System.Net.Http;
using System.Text.Json;
namespace DocumentService.Tests.Unit.Client;
/// <summary>
/// Unit tests for <see cref="PdfValidationClient"/>.
/// All tests use a fake <see cref="MockHttpMessageHandler"/> — no real HTTP calls are made.
/// </summary>
public class PdfValidationClientTests
{
// ?? helpers ?????????????????????????????????????????????????????????????
private static (PdfValidationClient client, MockHttpMessageHandler handler) Build<T>(T responseBody)
{
var handler = MockHttpMessageHandler.ReturningJson(responseBody);
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
return (client, handler);
}
private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray();
// ?? ValidatePdfAsync (Stream) ????????????????????????????????????????????
[Fact]
public async Task ValidatePdfAsync_Stream_SendsMultipartPost()
{
// Arrange
var expected = new PdfValidationResult { PageCount = 3, PdfVersion = "1.7", FileSizeBytes = 2048 };
var (client, handler) = Build(expected);
// Act
var result = await client.ValidatePdfAsync(new MemoryStream(FakePdfBytes()));
// Assert
result.PageCount.Should().Be(3);
result.PdfVersion.Should().Be("1.7");
handler.LastRequest!.Method.Should().Be(HttpMethod.Post);
handler.LastRequest.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate");
handler.LastRequest.Content.Should().BeOfType<MultipartFormDataContent>();
}
[Fact]
public async Task ValidatePdfAsync_Stream_ThrowsWhenApiReturnsNull()
{
// Arrange — API returns JSON null
var handler = MockHttpMessageHandler.ReturningJson<PdfValidationResult?>(null);
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
// Act & Assert
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
.Should().ThrowAsync<InvalidOperationException>();
}
// ?? ValidatePdfAsync (byte[]) ????????????????????????????????????????????
[Fact]
public async Task ValidatePdfAsync_Bytes_SendsJsonWithBase64()
{
// Arrange
var expected = new PdfValidationResult { PageCount = 1, IsEncrypted = false };
var (client, handler) = Build(expected);
// Act
var result = await client.ValidatePdfAsync(FakePdfBytes());
// Assert
result.PageCount.Should().Be(1);
handler.LastRequest!.Content.Should().NotBeNull();
handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json");
var body = await handler.LastRequest.Content!.ReadAsStringAsync();
var doc = JsonDocument.Parse(body);
doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty();
}
// ?? ValidatePdfAAsync (Stream) ???????????????????????????????????????????
[Fact]
public async Task ValidatePdfAAsync_Stream_SendsMultipartPost()
{
// Arrange
var expected = new PdfAValidationResult { IsValid = true, PdfAVersion = "PDF/A-3b", PageCount = 2 };
var (client, handler) = Build(expected);
// Act
var result = await client.ValidatePdfAAsync(new MemoryStream(FakePdfBytes()));
// Assert
result.IsValid.Should().BeTrue();
result.PdfAVersion.Should().Be("PDF/A-3b");
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate-pdfa");
handler.LastRequest.Content.Should().BeOfType<MultipartFormDataContent>();
}
[Fact]
public async Task ValidatePdfAAsync_Stream_WithErrors_ReturnsErrors()
{
// Arrange
var expected = new PdfAValidationResult
{
IsValid = false,
Errors = new List<string> { "Missing embedded font", "Encryption not allowed" }
};
var (client, _) = Build(expected);
// Act
var result = await client.ValidatePdfAAsync(new MemoryStream(FakePdfBytes()));
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().HaveCount(2).And.Contain("Missing embedded font");
}
// ?? ValidatePdfAAsync (byte[]) ???????????????????????????????????????????
[Fact]
public async Task ValidatePdfAAsync_Bytes_SendsJson()
{
// Arrange
var expected = new PdfAValidationResult { IsValid = true };
var (client, handler) = Build(expected);
// Act
await client.ValidatePdfAAsync(FakePdfBytes());
// Assert
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate-pdfa");
handler.LastRequest.Content.Should().NotBeNull();
handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json");
}
// ?? HTTP error propagation ???????????????????????????????????????????????
[Fact]
public async Task ValidatePdfAsync_WhenApiReturns400_ThrowsHttpRequestException()
{
// Arrange
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.BadRequest);
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
// Act & Assert
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
.Should().ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task ValidatePdfAsync_WhenApiReturns500_ThrowsHttpRequestException()
{
// Arrange
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError);
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
// Act & Assert
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
.Should().ThrowAsync<HttpRequestException>();
}
}

View File

@@ -2,7 +2,7 @@ using DocumentService.Application.AddAttachments;
using DocumentService.Application.CheckPdfAttachments.Queries;
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.ExtractPdfAttachments;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using DocumentService.Domain.Common.Exceptions;
using MediatR;
using Microsoft.AspNetCore.Http;
@@ -290,67 +290,3 @@ public class PdfAttachmentController(IMediator mediator) : ControllerBase
}
}
/// <summary>
/// Request DTO for Base64-encoded PDF attachment check
/// </summary>
public record CheckPdfAttachmentsRequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public string Base64Pdf { get; init; } = string.Empty;
}
/// <summary>
/// Request DTO for Base64-encoded PDF attachment extraction
/// </summary>
public record ExtractPdfAttachmentsRequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}
/// <summary>
/// Request DTO for Base64-encoded PDF with attachments to add
/// </summary>
public record AddAttachmentsRequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// List of attachments to embed
/// </summary>
public required List<AttachmentRequestDto> Attachments { get; init; }
}
/// <summary>
/// DTO for attachment file in request
/// </summary>
public record AttachmentRequestDto
{
/// <summary>
/// File name (e.g., "invoice.xml", "document.pdf")
/// </summary>
/// <example>factur-x.xml</example>
public required string FileName { get; init; }
/// <summary>
/// File content encoded as Base64 string
/// </summary>
/// <example>PD94bWwgdmVyc2lvbj0iMS4wIj8+...</example>
public required string Base64Content { get; init; }
/// <summary>
/// MIME type (optional, e.g., "application/xml")
/// </summary>
/// <example>application/xml</example>
public string? MimeType { get; init; }
}

View File

@@ -1,6 +1,6 @@
using DocumentService.Application.ConvertFromPdfA;
using DocumentService.Application.ConvertToPdfA;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using DocumentService.Domain.Common.Exceptions;
using MediatR;
using Microsoft.AspNetCore.Http;

View File

@@ -1,5 +1,6 @@
using DocumentService.Application.AddAnnotation;
using DocumentService.Application.AddStamp;
using DocumentService.Application.Common.DTOs.Requests;
using DocumentService.Application.MergePdfs;
using DocumentService.Domain.Common.Exceptions;
using DocumentService.Domain.Models.ValueObjects;
@@ -158,7 +159,7 @@ public class PdfOperationsController(IMediator mediator) : ControllerBase
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AnnotateFromBase64(
[FromBody] AddAnnotationBase64Command command,
[FromBody] AddAnnotationBase64Request command,
CancellationToken cancellationToken)
{
// Convert Base64 to MemoryStream
@@ -404,116 +405,6 @@ public class AddAnnotationMultipartRequest
public AnnotationOrigin Origin { get; set; } = AnnotationOrigin.BottomLeft;
}
/// <summary>
/// Request DTO for Base64-encoded PDF merge operation (API layer only - converts to MergePdfsCommand)
/// </summary>
public record MergePdfsBase64Request
{
/// <summary>
/// Array of Base64-encoded PDF files (minimum 2 required)
/// </summary>
/// <example>["JVBERi0xLjQK...", "JVBERi0xLjQK..."]</example>
public required List<string> Base64Pdfs { get; init; }
/// <summary>
/// Optional page ranges per PDF (null = all pages).
/// Format: "1-3,5" means pages 1, 2, 3, and 5.
/// If provided, array length must match Base64Pdfs length.
/// </summary>
/// <example>["1-2", "1,3,5", null]</example>
public List<string?>? PageRanges { get; init; }
}
/// <summary>
/// Request DTO for Base64-encoded PDF annotation (API layer only - converts to AddAnnotationCommand)
/// </summary>
public record AddAnnotationBase64Command
{
/// <summary>
/// Base64-encoded PDF file
/// </summary>
/// <example>"JVBERi0xLjQK..."</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// Type of annotation to add
/// </summary>
/// <example>TextMarkup</example>
public required AnnotationType AnnotationType { get; init; }
/// <summary>
/// Target page number (1-indexed)
/// </summary>
/// <example>1</example>
public required int PageNumber { get; init; }
/// <summary>
/// Rectangle X1 coordinate (left)
/// </summary>
/// <example>100.0</example>
public required double X1 { get; init; }
/// <summary>
/// Rectangle Y1 coordinate (top or bottom depending on Origin)
/// </summary>
/// <example>100.0</example>
public required double Y1 { get; init; }
/// <summary>
/// Rectangle X2 coordinate (right). Optional if Width is provided.
/// </summary>
/// <example>200.0</example>
public double? X2 { get; init; }
/// <summary>
/// Rectangle Y2 coordinate (bottom or top depending on Origin). Optional if Height is provided.
/// </summary>
/// <example>120.0</example>
public double? Y2 { get; init; }
/// <summary>
/// Rectangle width. Alternative to X2 (X2 = X1 + Width). Optional if X2 is provided.
/// </summary>
/// <example>100.0</example>
public double? Width { get; init; }
/// <summary>
/// Rectangle height. Alternative to Y2 (Y2 = Y1 + Height). Optional if Y2 is provided.
/// </summary>
/// <example>20.0</example>
public double? Height { get; init; }
/// <summary>
/// Annotation content (required for FreeText and StickyNote)
/// </summary>
/// <example>"Important text to highlight"</example>
public string? Content { get; init; }
/// <summary>
/// Author name (optional)
/// </summary>
/// <example>"John Doe"</example>
public string? Author { get; init; }
/// <summary>
/// Hex color (6 digits, e.g., "FF0000" for red). Optional - defaults vary by annotation type.
/// </summary>
/// <example>"FFFF00"</example>
public string? Color { get; init; }
/// <summary>
/// Text markup style (Highlight, Underline, or Strikeout). Required for TextMarkup annotations.
/// </summary>
/// <example>Highlight</example>
public TextMarkupStyle? TextMarkupStyle { get; init; }
/// <summary>
/// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft
/// </summary>
/// <example>BottomLeft</example>
public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft;
}
/// <summary>
/// Request DTO for multipart/form-data stamp operation
/// </summary>
@@ -618,111 +509,3 @@ public class AddStampMultipartRequest
/// <example>Confidential</example>
public PredefinedStampType? PredefinedType { get; set; }
}
/// <summary>
/// Request DTO for Base64-encoded PDF stamp operation (API layer only - converts to AddStampCommand)
/// </summary>
public record AddStampBase64Request
{
/// <summary>
/// Base64-encoded PDF file
/// </summary>
/// <example>"JVBERi0xLjQK..."</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// Type of stamp (Text, Image, or Predefined)
/// </summary>
/// <example>Text</example>
public required StampType StampType { get; init; }
/// <summary>
/// Target page numbers (1-indexed). Null or empty = all pages.
/// </summary>
/// <example>[1, 3, 5]</example>
public int[]? PageNumbers { get; init; }
/// <summary>
/// Stamp position X coordinate
/// </summary>
/// <example>100.0</example>
public required double X { get; init; }
/// <summary>
/// Stamp position Y coordinate
/// </summary>
/// <example>100.0</example>
public required double Y { get; init; }
/// <summary>
/// Stamp width (optional, auto-size for images if not specified)
/// </summary>
/// <example>200.0</example>
public double? Width { get; init; }
/// <summary>
/// Stamp height (optional, auto-size for images if not specified)
/// </summary>
/// <example>50.0</example>
public double? Height { get; init; }
/// <summary>
/// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft
/// </summary>
/// <example>BottomLeft</example>
public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft;
/// <summary>
/// Text content (required for Text stamps)
/// </summary>
/// <example>"CONFIDENTIAL"</example>
public string? Text { get; init; }
/// <summary>
/// Font name (default: Arial)
/// </summary>
/// <example>"Arial"</example>
public string? FontName { get; init; }
/// <summary>
/// Font size in points (default: 12)
/// </summary>
/// <example>24.0</example>
public double? FontSize { get; init; }
/// <summary>
/// Hex color (6 digits, e.g., "FF0000" for red, default: "000000")
/// </summary>
/// <example>"FF0000"</example>
public string? Color { get; init; }
/// <summary>
/// Opacity (0.0 = transparent, 1.0 = opaque, default: 0.5)
/// </summary>
/// <example>0.5</example>
public double? Opacity { get; init; }
/// <summary>
/// Rotation angle in degrees (0-360, default: 0)
/// </summary>
/// <example>45.0</example>
public double? Rotation { get; init; }
/// <summary>
/// Stamp placement (Foreground = on top, Background = watermark effect, default: Foreground)
/// </summary>
/// <example>Foreground</example>
public StampPlacement Placement { get; init; } = StampPlacement.Foreground;
/// <summary>
/// Base64-encoded image (required for Image stamps, PNG/JPEG)
/// </summary>
/// <example>"iVBORw0KGgoAAAANSUhEUgAA..."</example>
public string? Base64Image { get; init; }
/// <summary>
/// Predefined stamp type (required for Predefined stamps)
/// </summary>
/// <example>Confidential</example>
public PredefinedStampType? PredefinedType { get; init; }
}

View File

@@ -1,7 +1,7 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.ValidatePdf.Queries;
using DocumentService.Application.ValidatePdfA.Queries;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using DocumentService.Domain.Common.Exceptions;
using MediatR;
using Microsoft.AspNetCore.Mvc;

View File

@@ -1,4 +1,5 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.Common.DTOs.Requests;
using DocumentService.Application.SwissQrCode.Queries;
using DocumentService.Domain.Common.Exceptions;
using MediatR;
@@ -100,14 +101,3 @@ public class SwissQrCodeController(IMediator Mediator) : ControllerBase
}
}
/// <summary>
/// Request DTO for Base64-encoded Swiss QR Code extraction
/// </summary>
public record ExtractSwissQrCodeBase64Request
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}

View File

@@ -1,7 +1,7 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.ExtractZugferd;
using DocumentService.Application.HasZugferd.Queries;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using DocumentService.Domain.Common.Exceptions;
using MediatR;
using Microsoft.AspNetCore.Http;

View File

@@ -9,10 +9,10 @@
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>DocumentOperator.API</Product>
<Version>1.0.0</Version>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<InformationalVersion>1.0.0</InformationalVersion>
<Version>1.1.0</Version>
<FileVersion>1.1.0.0</FileVersion>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<InformationalVersion>1.1.0</InformationalVersion>
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
<Description>PDF Document Operations REST API - Validation, Swiss QR Code extraction, attachments, merge, annotation, stamp operations powered by DevExpress Office File API</Description>
<PackageTags>pdf document operator validation swiss-qr-code annotations stamp devexpress</PackageTags>
@@ -32,10 +32,10 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentOperator.Application\DocumentService.Application.csproj" />
<ProjectReference Include="..\DocumentOperator.Domain\DocumentService.Domain.csproj" />
<ProjectReference Include="..\DocumentOperator.Infrastructure\DocumentService.Infrastructure.csproj" />
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
<ProjectReference Include="..\DocumentService.Client\DocumentService.Client.csproj" />
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj" />
<ProjectReference Include="..\DocumentService.Infrastructure\DocumentService.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -9,10 +9,12 @@ namespace DocumentService.Application.Common.DTOs;
/// <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>
/// <param name="IsEncrypted">Ist das PDF passwortgeschützt/verschlüsselt?</param>
public record PdfValidationResult(
int PageCount,
long FileSizeBytes,
double FileSizeMB,
string PdfVersion,
bool HasAttachments,
int AttachmentCount);
int AttachmentCount,
bool IsEncrypted);

View File

@@ -1,40 +1,20 @@
namespace DocumentService.Client.Models.Requests;
namespace DocumentService.Application.Common.DTOs.Requests;
/// <summary>
/// Request DTO for Base64-encoded PDF attachment check
/// </summary>
public record CheckPdfAttachmentsRequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}
public record CheckPdfAttachmentsRequest : PdfBase64RequestBase;
/// <summary>
/// Request DTO for Base64-encoded PDF attachment extraction
/// </summary>
public record ExtractPdfAttachmentsRequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}
public record ExtractPdfAttachmentsRequest : PdfBase64RequestBase;
/// <summary>
/// Request DTO for Base64-encoded PDF with attachments to add
/// </summary>
public record AddAttachmentsRequest
public record AddAttachmentsRequest : PdfBase64RequestBase
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// List of attachments to embed
/// </summary>
@@ -42,7 +22,7 @@ public record AddAttachmentsRequest
}
/// <summary>
/// DTO for attachment file in request
/// DTO for a single attachment file in a request
/// </summary>
public record AttachmentRequestDto
{

View File

@@ -1,9 +1,10 @@
namespace DocumentService.Client.Models.Requests;
namespace DocumentService.Application.Common.DTOs.Requests;
/// <summary>
/// Request DTO for Base64-encoded Swiss QR Code extraction
/// Base record for all single-PDF Base64 request types.
/// Provides the common <see cref="Base64Pdf"/> property shared across endpoints.
/// </summary>
public record ExtractSwissQrCodeBase64Request
public abstract record PdfBase64RequestBase
{
/// <summary>
/// PDF document encoded as Base64 string

View File

@@ -0,0 +1,18 @@
namespace DocumentService.Application.Common.DTOs.Requests;
/// <summary>
/// Request DTO for converting a standard PDF to PDF/A format (Base64 JSON)
/// </summary>
public record ConvertToPdfARequest : PdfBase64RequestBase
{
/// <summary>
/// Target PDF/A conformance level. Defaults to "PDF/A-3b".
/// </summary>
/// <example>PDF/A-3b</example>
public string? PdfALevel { get; init; }
}
/// <summary>
/// Request DTO for converting a PDF/A document back to standard PDF (Base64 JSON)
/// </summary>
public record ConvertFromPdfARequest : PdfBase64RequestBase;

View File

@@ -1,6 +1,6 @@
using DocumentService.Client.Models.ValueObjects;
using DocumentService.Domain.Models.ValueObjects;
namespace DocumentService.Client.Models.Requests;
namespace DocumentService.Application.Common.DTOs.Requests;
/// <summary>
/// Request DTO for Base64-encoded PDF merge operation
@@ -25,14 +25,8 @@ public record MergePdfsBase64Request
/// <summary>
/// Request DTO for Base64-encoded PDF annotation
/// </summary>
public record AddAnnotationBase64Request
public record AddAnnotationBase64Request : PdfBase64RequestBase
{
/// <summary>
/// Base64-encoded PDF file
/// </summary>
/// <example>"JVBERi0xLjQK..."</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// Type of annotation to add
/// </summary>
@@ -115,14 +109,8 @@ public record AddAnnotationBase64Request
/// <summary>
/// Request DTO for Base64-encoded PDF stamp operation
/// </summary>
public record AddStampBase64Request
public record AddStampBase64Request : PdfBase64RequestBase
{
/// <summary>
/// Base64-encoded PDF file
/// </summary>
/// <example>"JVBERi0xLjQK..."</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// Type of stamp (Text, Image, or Predefined)
/// </summary>

View File

@@ -0,0 +1,11 @@
namespace DocumentService.Application.Common.DTOs.Requests;
/// <summary>
/// Request DTO for Base64-encoded PDF validation
/// </summary>
public record ValidatePdfBase64Request : PdfBase64RequestBase;
/// <summary>
/// Request DTO for Base64-encoded PDF/A validation
/// </summary>
public record ValidatePdfABase64Request : PdfBase64RequestBase;

View File

@@ -0,0 +1,6 @@
namespace DocumentService.Application.Common.DTOs.Requests;
/// <summary>
/// Request DTO for Base64-encoded Swiss QR Code extraction
/// </summary>
public record ExtractSwissQrCodeBase64Request : PdfBase64RequestBase;

View File

@@ -0,0 +1,11 @@
namespace DocumentService.Application.Common.DTOs.Requests;
/// <summary>
/// Request DTO for Base64-encoded PDF ZUGFeRD check
/// </summary>
public record HasZugferdRequest : PdfBase64RequestBase;
/// <summary>
/// Request DTO for Base64-encoded PDF ZUGFeRD extraction
/// </summary>
public record ExtractZugferdRequest : PdfBase64RequestBase;

View File

@@ -32,6 +32,10 @@ namespace DocumentService.Application.Common.DTOs;
/// }
/// </example>
public record SwissQrCodeExtractionResult(
#if NET
SwissQrBillDto Bill,
IReadOnlyList<string> RawLines
);
#else
object? Bill,
#endif
IEnumerable<string> RawLines
);

View File

@@ -15,7 +15,7 @@ public interface IPdfProcessor
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty, invalid, or not positioned at the beginning
/// </exception>
Task<PdfMetadata> ValidateAsync(Stream pdfStream);
Task<PdfValidationResult> ValidateAsync(Stream pdfStream);
/// <summary>
/// Validates a PDF/A document and checks conformance level.
@@ -28,7 +28,7 @@ public interface IPdfProcessor
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty, invalid, or not positioned at the beginning
/// </exception>
Task<PdfAMetadata> ValidatePdfAAsync(Stream pdfStream);
Task<PdfAValidationResult> ValidatePdfAAsync(Stream pdfStream);
/// <summary>
/// Checks for embedded files (attachments) in a PDF document and returns detailed metadata.

View File

@@ -13,12 +13,6 @@ public class MappingProfile : Profile
{
public MappingProfile()
{
// PdfMetadata -> PdfValidationResult
CreateMap<PdfMetadata, PdfValidationResult>();
// PdfAMetadata -> PdfAValidationResult
CreateMap<PdfAMetadata, PdfAValidationResult>()
.ForMember(dest => dest.FileSize, opt => opt.MapFrom(src => src.FileSizeBytes));
// Codecrete Bill -> SwissQrBillDto
CreateMap<Bill, SwissQrBillDto>()

View File

@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="DependencyInjection\**" />
<Compile Remove="Features\**" />
<EmbeddedResource Remove="DependencyInjection\**" />
<EmbeddedResource Remove="Features\**" />
<None Remove="DependencyInjection\**" />
<None Remove="Features\**" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="AutoMapper" Version="16.2.0" />
<PackageReference Include="Codecrete.SwissQRBill.Generator" Version="3.4.0" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
<PackageReference Include="AutoMapper" Version="16.2.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="MediatR" Version="14.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net480'">
<PackageReference Include="AutoMapper" Version="16.2.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="MediatR" Version="14.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
<Compile Remove="Common\Interfaces\ISwissQrCodeProcessor.cs" />
<Compile Remove="Common\DTOs\SwissQrBillDto.cs" />
<Compile Remove="Common\Mapping\MappingProfile.cs" />
<Compile Remove="SwissQrCode\**" />
<Compile Remove="HasZugferd\**" />
<Compile Remove="DependencyInjection.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -18,6 +18,7 @@ public record ExtractZugferdCommand : IRequest<ZugferdExtractionResult>
public required Stream PdfStream { get; init; }
}
#if NET
/// <summary>
/// Handler for ExtractZugferdCommand.
/// Extracts ZUGFeRD XML from PDF and returns XML content
@@ -77,6 +78,7 @@ public class ExtractZugferdCommandHandler(
};
}
}
#endif
/// <summary>
/// Validator for ExtractZugferdCommand.

View File

@@ -1,4 +1,3 @@
using AutoMapper;
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.Common.Interfaces;
using MediatR;
@@ -18,20 +17,12 @@ public record ValidatePdfQuery : IRequest<PdfValidationResult>
/// <summary>
/// Handler for ValidatePdfQuery
/// Orchestrates PDF validation using IPdfProcessor and AutoMapper
/// </summary>
public class ValidatePdfQueryHandler(IPdfProcessor PdfProcessor, IMapper Mapper)
public class ValidatePdfQueryHandler(IPdfProcessor pdfProcessor)
: IRequestHandler<ValidatePdfQuery, PdfValidationResult>
{
/// <summary>
/// Validates PDF and returns metadata
/// </summary>
public async Task<PdfValidationResult> Handle(ValidatePdfQuery request, CancellationToken cancellationToken)
{
// 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<PdfValidationResult>(metadata);
return await pdfProcessor.ValidateAsync(request.PdfStream);
}
}

View File

@@ -1,4 +1,3 @@
using AutoMapper;
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.Common.Interfaces;
using MediatR;
@@ -18,20 +17,12 @@ public record ValidatePdfAQuery : IRequest<PdfAValidationResult>
/// <summary>
/// Handler for ValidatePdfAQuery
/// Orchestrates PDF/A validation using IPdfProcessor and AutoMapper
/// </summary>
public class ValidatePdfAQueryHandler(IPdfProcessor PdfProcessor, IMapper Mapper)
public class ValidatePdfAQueryHandler(IPdfProcessor pdfProcessor)
: IRequestHandler<ValidatePdfAQuery, PdfAValidationResult>
{
/// <summary>
/// Validates PDF/A and returns metadata with conformance level
/// </summary>
public async Task<PdfAValidationResult> Handle(ValidatePdfAQuery request, CancellationToken cancellationToken)
{
// 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<PdfAValidationResult>(metadata);
return await pdfProcessor.ValidatePdfAAsync(request.PdfStream);
}
}

View File

@@ -1,5 +1,6 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using Microsoft.Extensions.Logging;
using System.IO.Compression;
using System.Net.Http;

View File

@@ -1,5 +1,5 @@
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using Microsoft.Extensions.Logging;
using System.Net.Http;

View File

@@ -1,5 +1,5 @@
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Net.Http.Headers;

View File

@@ -1,5 +1,6 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using Microsoft.Extensions.Logging;
using System.Net.Http;

View File

@@ -1,5 +1,6 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using Microsoft.Extensions.Logging;
using System.Net.Http;

View File

@@ -1,5 +1,7 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.ExtractZugferd;
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
using Microsoft.Extensions.Logging;
using System.Net.Http;

View File

@@ -42,5 +42,9 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.Common.DTOs.Requests;
namespace DocumentService.Client.Interfaces;
@@ -60,30 +61,4 @@ public interface IPdfAttachmentClient
/// <returns>Modified PDF as stream</returns>
[Obsolete("API endpoint not implemented yet")]
Task<Stream> AddAttachmentsAsync(byte[] pdfBytes, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default);
}
/// <summary>
/// DTO for attachment check result
/// </summary>
public record AttachmentCheckResult
{
/// <summary>Whether the document contains any embedded file attachments.</summary>
public bool HasAttachments { get; init; }
/// <summary>Number of embedded file attachments found.</summary>
public int AttachmentCount { get; init; }
/// <summary>Metadata for each attachment found in the document.</summary>
public List<AttachmentMetadata> Attachments { get; init; } = new();
}
/// <summary>
/// DTO for attachment metadata
/// </summary>
public record AttachmentMetadata
{
/// <summary>Name of the embedded file.</summary>
public string FileName { get; init; } = string.Empty;
/// <summary>MIME type of the embedded file (e.g. "application/xml"), or <c>null</c> if unknown.</summary>
public string? MimeType { get; init; }
/// <summary>Size of the embedded file in bytes.</summary>
public long Size { get; init; }
}
}

View File

@@ -1,4 +1,4 @@
using DocumentService.Client.Models.Requests;
using DocumentService.Application.Common.DTOs.Requests;
namespace DocumentService.Client.Interfaces;

View File

@@ -1,3 +1,5 @@
using DocumentService.Application.Common.DTOs;
namespace DocumentService.Client.Interfaces;
/// <summary>
@@ -36,40 +38,4 @@ public interface IPdfValidationClient
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>PDF/A validation result with conformance level and errors</returns>
Task<PdfAValidationResult> ValidatePdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
}
/// <summary>
/// DTO for PDF validation result
/// </summary>
public record PdfValidationResult
{
/// <summary>Total number of pages in the document.</summary>
public int PageCount { get; init; }
/// <summary>File size in bytes.</summary>
public long FileSizeBytes { get; init; }
/// <summary>PDF specification version (e.g. "1.7").</summary>
public string PdfVersion { get; init; } = string.Empty;
/// <summary>Whether the document is password-protected.</summary>
public bool IsEncrypted { get; init; }
/// <summary>Whether the document contains embedded file attachments.</summary>
public bool HasAttachments { get; init; }
/// <summary>Number of embedded file attachments.</summary>
public int AttachmentCount { get; init; }
}
/// <summary>
/// DTO for PDF/A validation result
/// </summary>
public record PdfAValidationResult
{
/// <summary>Whether the document is fully PDF/A conformant.</summary>
public bool IsValid { get; init; }
/// <summary>Detected PDF/A conformance level (e.g. "PDF/A-3b"), or <c>null</c> if not a PDF/A document.</summary>
public string? PdfAVersion { get; init; }
/// <summary>Total number of pages in the document.</summary>
public int PageCount { get; init; }
/// <summary>List of conformance errors found during validation.</summary>
public List<string> Errors { get; init; } = new();
/// <summary>List of conformance warnings found during validation.</summary>
public List<string> Warnings { get; init; } = new();
}
}

View File

@@ -1,3 +1,5 @@
using DocumentService.Application.Common.DTOs;
namespace DocumentService.Client.Interfaces;
/// <summary>
@@ -24,13 +26,3 @@ public interface ISwissQrCodeClient
Task<SwissQrCodeExtractionResult> ExtractSwissQrCodeAsync(byte[] pdfBytes, bool raw = false, CancellationToken cancellationToken = default);
}
/// <summary>
/// DTO for Swiss QR Code extraction result
/// </summary>
public record SwissQrCodeExtractionResult
{
/// <summary>Parsed Swiss QR bill object. <c>null</c> when <c>raw=true</c> was requested.</summary>
public object? Bill { get; init; }
/// <summary>Raw QR code text lines. Populated when <c>raw=true</c> was requested.</summary>
public List<string> RawLines { get; init; } = new();
}

View File

@@ -1,3 +1,6 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Application.ExtractZugferd;
namespace DocumentService.Client.Interfaces;
/// <summary>
@@ -53,27 +56,3 @@ public interface IZugferdClient
/// <returns>Structured ZUGFeRD extraction result</returns>
Task<ZugferdExtractionResult> ExtractZugferdAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
}
/// <summary>DTO for ZUGFeRD detection result</summary>
public record ZugferdCheckResult
{
/// <summary>Whether the document contains a ZUGFeRD XML attachment.</summary>
public bool HasZugferd { get; init; }
/// <summary>Detected ZUGFeRD version (e.g. "2.1"), or <c>null</c> if not present.</summary>
public string? Version { get; init; }
/// <summary>ZUGFeRD profile name (e.g. "EN 16931"), or <c>null</c> if not present.</summary>
public string? Profile { get; init; }
}
/// <summary>DTO for ZUGFeRD extraction result</summary>
public record ZugferdExtractionResult
{
/// <summary>File name of the extracted XML attachment (e.g. "factur-x.xml").</summary>
public string FileName { get; init; } = string.Empty;
/// <summary>Full XML content of the ZUGFeRD attachment.</summary>
public string XmlContent { get; init; } = string.Empty;
/// <summary>ZUGFeRD version (e.g. "2.1"), or <c>null</c> if not detected.</summary>
public string? Version { get; init; }
/// <summary>ZUGFeRD profile name (e.g. "EN 16931"), or <c>null</c> if not detected.</summary>
public string? Profile { get; init; }
}

View File

@@ -1,31 +0,0 @@
namespace DocumentService.Client.Models.Requests;
/// <summary>
/// Request DTO for converting a standard PDF to PDF/A format (Base64 JSON)
/// </summary>
public record ConvertToPdfARequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// Target PDF/A conformance level. Defaults to "PDF/A-3b".
/// </summary>
/// <example>PDF/A-3b</example>
public string? PdfALevel { get; init; }
}
/// <summary>
/// Request DTO for converting a PDF/A document back to standard PDF (Base64 JSON)
/// </summary>
public record ConvertFromPdfARequest
{
/// <summary>
/// PDF/A document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}

View File

@@ -1,27 +0,0 @@
using DocumentService.Client.Models.ValueObjects;
namespace DocumentService.Client.Models.Requests;
/// <summary>
/// Request DTO for Base64-encoded PDF validation
/// </summary>
public record ValidatePdfBase64Request
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}
/// <summary>
/// Request DTO for Base64-encoded PDF/A validation
/// </summary>
public record ValidatePdfABase64Request
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}

View File

@@ -1,25 +0,0 @@
namespace DocumentService.Client.Models.Requests;
/// <summary>
/// Request DTO for Base64-encoded PDF ZUGFeRD check
/// </summary>
public record HasZugferdRequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}
/// <summary>
/// Request DTO for Base64-encoded PDF ZUGFeRD extraction
/// </summary>
public record ExtractZugferdRequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}

View File

@@ -1,17 +0,0 @@
namespace DocumentService.Client.Models.ValueObjects;
/// <summary>
/// Coordinate origin point for PDF annotations.
/// </summary>
public enum AnnotationOrigin
{
/// <summary>
/// Bottom-left corner (PDF native coordinate system, default)
/// </summary>
BottomLeft,
/// <summary>
/// Top-left corner (common in UI frameworks)
/// </summary>
TopLeft
}

View File

@@ -1,32 +0,0 @@
namespace DocumentService.Client.Models.ValueObjects;
/// <summary>
/// Supported PDF annotation types
/// </summary>
public enum AnnotationType
{
/// <summary>
/// Text markup annotation (highlight, underline, strikeout)
/// </summary>
TextMarkup,
/// <summary>
/// Free text annotation (text box with visible text)
/// </summary>
FreeText,
/// <summary>
/// Sticky note annotation (popup comment icon)
/// </summary>
StickyNote,
/// <summary>
/// Circle shape annotation
/// </summary>
Circle,
/// <summary>
/// Square shape annotation
/// </summary>
Square
}

View File

@@ -1,32 +0,0 @@
namespace DocumentService.Client.Models.ValueObjects;
/// <summary>
/// Predefined stamp types with standard text and styling.
/// </summary>
public enum PredefinedStampType
{
/// <summary>
/// CONFIDENTIAL stamp (red, bold).
/// </summary>
Confidential,
/// <summary>
/// APPROVED stamp (green, bold).
/// </summary>
Approved,
/// <summary>
/// DRAFT stamp (gray, italic).
/// </summary>
Draft,
/// <summary>
/// VOID stamp (red, strikethrough effect).
/// </summary>
Void,
/// <summary>
/// FOR REVIEW stamp (orange, bold).
/// </summary>
ForReview
}

View File

@@ -1,17 +0,0 @@
namespace DocumentService.Client.Models.ValueObjects;
/// <summary>
/// Specifies whether the stamp should appear in the foreground or background.
/// </summary>
public enum StampPlacement
{
/// <summary>
/// Stamp appears on top of existing page content.
/// </summary>
Foreground,
/// <summary>
/// Stamp appears behind existing page content (watermark effect).
/// </summary>
Background
}

View File

@@ -1,22 +0,0 @@
namespace DocumentService.Client.Models.ValueObjects;
/// <summary>
/// Specifies the type of stamp to add to a PDF document.
/// </summary>
public enum StampType
{
/// <summary>
/// Text-based stamp with custom text, font, and color.
/// </summary>
Text,
/// <summary>
/// Image-based stamp (PNG/JPEG overlay).
/// </summary>
Image,
/// <summary>
/// Predefined stamp with standard text (e.g., CONFIDENTIAL, APPROVED).
/// </summary>
Predefined
}

View File

@@ -1,22 +0,0 @@
namespace DocumentService.Client.Models.ValueObjects;
/// <summary>
/// Text markup annotation style (highlight, underline, strikeout)
/// </summary>
public enum TextMarkupStyle
{
/// <summary>
/// Highlight text with background color
/// </summary>
Highlight,
/// <summary>
/// Underline text
/// </summary>
Underline,
/// <summary>
/// Strikeout text (strikethrough)
/// </summary>
Strikeout
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,230 +0,0 @@
# DocumentService.Client - Stream-Based API
## ?? Design Decision: Why Stream Instead of byte[]?
### ? **Advantages of Stream-Based Returns**
| **Aspect** | **Stream** | **byte[]** |
|---|---|---|
| **Memory Efficiency** | ????? | ?? |
| **Flexibility** | ????? | ??? |
| **Large Files** | ? Excellent | ? Poor (OutOfMemoryException risk) |
| **Direct File Save** | ? `CopyToAsync(fileStream)` | ? Must buffer entire file |
| **Streaming to Response** | ? Direct pipe | ? Must load to memory first |
| **Base64 Conversion** | ? Extension method | ? `Convert.ToBase64String()` |
| **Network Transfer** | ? Progressive | ? Buffered |
---
## ?? Extension Methods
### `StreamExtensions` - Converting Streams
```csharp
using DocumentService.Client.Extensions;
// Convert Stream to Base64
using var pdfStream = await client.Operations.MergeAsync(streams);
string base64 = await pdfStream.ToBase64StringAsync();
// Convert Stream to byte[]
using var pdfStream = await client.Operations.MergeAsync(streams);
byte[] bytes = await pdfStream.ToBytesAsync();
// Reset stream position (if seekable)
pdfStream.Reset(); // Position = 0
```
---
## ?? Usage Patterns
### Pattern 1: Direct File Save (Memory Efficient ?)
```csharp
// ? Best for large files - no intermediate buffering
using var pdfStream = await client.Operations.MergeAsync(streams);
using var fileStream = File.Create("output.pdf");
await pdfStream.CopyToAsync(fileStream);
```
### Pattern 2: HTTP Response Streaming (Memory Efficient ?)
```csharp
// ASP.NET Core example
[HttpGet("merge")]
public async Task<IActionResult> MergePdfs()
{
using var mergedStream = await _client.Operations.MergeAsync(streams);
// Stream directly to HTTP response - no buffering
return File(mergedStream, "application/pdf", "merged.pdf");
}
```
### Pattern 3: Base64 Conversion (When Needed)
```csharp
// ?? Only if Base64 is required (e.g., JSON APIs, email attachments)
using var pdfStream = await client.Operations.MergeAsync(streams);
string base64Pdf = await pdfStream.ToBase64StringAsync();
// Send to external API
await externalApi.SendDocumentAsync(new { pdf = base64Pdf });
```
### Pattern 4: Byte Array (Legacy Compatibility)
```csharp
// ?? For legacy code that requires byte[]
using var pdfStream = await client.Operations.MergeAsync(streams);
byte[] pdfBytes = await pdfStream.ToBytesAsync();
// Use with legacy method
legacyService.ProcessPdf(pdfBytes);
```
---
## ?? Performance Comparison
### Scenario: Merging 10 PDFs (100 MB total)
| **Approach** | **Memory Usage** | **Speed** | **Scalability** |
|---|---|---|---|
| **Stream ? File** | ~10 MB | ????? | Excellent |
| **Stream ? HTTP** | ~10 MB | ????? | Excellent |
| **Stream ? byte[]** | ~110 MB | ??? | Limited |
| **byte[] ? File** | ~210 MB | ?? | Poor |
**Conclusion:** Stream-based API reduces memory footprint by **10-20x** for large files.
---
## ?? API Reference
### All Stream-Returning Methods
| **Client** | **Method** | **Return Type** |
|---|---|---|
| **IPdfAttachmentClient** | `ExtractAttachmentsAsync()` | `Task<Stream>` |
| **IPdfAttachmentClient** | `AddAttachmentsAsync()` | `Task<Stream>` |
| **IPdfOperationsClient** | `MergeAsync()` | `Task<Stream>` |
| **IPdfOperationsClient** | `AnnotateAsync()` | `Task<Stream>` |
| **IPdfOperationsClient** | `StampAsync()` | `Task<Stream>` |
**Query Methods** (Metadata only):
- `IPdfValidationClient.ValidatePdfAsync()` ? `Task<PdfValidationResult>`
- `IPdfAttachmentClient.CheckAttachmentsAsync()` ? `Task<AttachmentCheckResult>`
- `ISwissQrCodeClient.ExtractSwissQrCodeAsync()` ? `Task<SwissQrCodeExtractionResult>`
---
## ?? Stream Disposal Best Practices
### ? Correct Usage
```csharp
// Pattern 1: using declaration (C# 8.0+)
using var pdfStream = await client.Operations.MergeAsync(streams);
// Auto-disposed at end of scope
// Pattern 2: using statement
using (var pdfStream = await client.Operations.MergeAsync(streams))
{
// Use stream here
} // Auto-disposed
// Pattern 3: Manual disposal (not recommended)
var pdfStream = await client.Operations.MergeAsync(streams);
try
{
// Use stream
}
finally
{
pdfStream.Dispose();
}
```
### ? Incorrect Usage (Memory Leak)
```csharp
// ? NO using - stream never disposed!
var pdfStream = await client.Operations.MergeAsync(streams);
await pdfStream.CopyToAsync(fileStream);
// Memory leak!
```
---
## ?? .NET Framework Compatibility
### Conditional Compilation for CopyToAsync
```csharp
// StreamExtensions.cs handles this internally
#if NET8_0
await stream.CopyToAsync(memoryStream, cancellationToken);
#else
await stream.CopyToAsync(memoryStream); // .NET Framework doesn't support CancellationToken
#endif
```
### Supported Versions
- ? .NET 8.0 - Full support with CancellationToken
- ? .NET Framework 4.8 - Full support (no CancellationToken in CopyToAsync)
- ? .NET Framework 4.6.2 - Full support (no CancellationToken in CopyToAsync)
---
## ?? Migration from byte[] to Stream
### Before (byte[]-based)
```csharp
byte[] mergedPdf = await client.Operations.MergeAsync(streams);
await File.WriteAllBytesAsync("output.pdf", mergedPdf);
```
### After (Stream-based)
```csharp
using var mergedStream = await client.Operations.MergeAsync(streams);
using var fileStream = File.Create("output.pdf");
await mergedStream.CopyToAsync(fileStream);
```
### If you NEED byte[] (Legacy Code)
```csharp
using DocumentService.Client.Extensions;
using var mergedStream = await client.Operations.MergeAsync(streams);
byte[] mergedPdf = await mergedStream.ToBytesAsync(); // Extension method
```
---
## ?? Summary
? **Stream-based API** for:
- Memory efficiency
- Large file support
- Direct file/HTTP streaming
- Flexibility (convert to byte[]/Base64 when needed)
? **Extension methods** for:
- `Stream.ToBase64StringAsync()`
- `Stream.ToBytesAsync()`
- `Stream.Reset()`
? **Multi-target support**:
- .NET 8.0
- .NET Framework 4.8
- .NET Framework 4.6.2
? **Performance**:
- 10-20x memory reduction for large files
- Progressive streaming (no buffering)
- Scalable for enterprise workloads

View File

@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>

Some files were not shown because too many files have changed in this diff Show More