Files
DocumentService/DocumentOperator.API/ROADMAP.md
OlgunR 0c16294f79 Update ROADMAP.md: Phase 3 completion, Phase 4 start
Updated the ROADMAP.md file to reflect the completion of Phase 3 (Infrastructure Layer) and the transition to Phase 4 (Application Layer).

Marked Step 3.2 (DevExpressPdfProcessor implementation) as completed, including sub-steps such as creating tests, implementing the processor, and preparing for execution.

Added a new Phase 4 section detailing next steps, including MediatR setup. Removed references to incomplete Phase 3 steps, as they are now finished.

Highlighted a major update noting production-ready features and added architectural notes for multi-tenancy, async processing, file storage, and resilience using Polly.
2026-06-23 11:52:49 +02:00

80 KiB

?? DocumentOperator - Project Roadmap (Pragmatic Edition)

Last Updated: 17.01.2025 (Phase 3 abgeschlossen - DevExpressPdfProcessor) | Status: In Development | Phase: 4 (Application Layer - NEXT)


?? MAJOR UPDATE - Production-Ready Features Added!

Was ist neu in diesem Update?

  1. ? Multi-Tenancy: Database-based (EF Core + SQLite + Redis Cache) statt appsettings.json
  2. ? Async Processing: In-Memory Queue-based + Background Worker für große Operationen
  3. ? File Storage: Lokale Temp-Ordner mit IFileStorage Abstraction
  4. ? Resilience: Polly (Retry, Circuit Breaker, Timeout) für DevExpress Calls
  5. ?? Health Checks: FRÜH implementieren (Phase 5.5 statt Phase 9)
  6. ? Logging: Correlation IDs + Seq + File Logging
  7. ? 9 neue NuGet Packages: EF Core, Polly, BCrypt, Seq, Correlation IDs
  8. ? 6 neue Phasen: 5.5, 6.5, 8, 9, 10, 11 (insgesamt 11 Phasen statt 9)
  9. ? 11 Key Learnings: Dokumentiert (statt 5)
  10. ? Technology Stack: Komplett aktualisiert mit allen neuen Dependencies

Warum diese Änderungen?

  • Einfachheit: Lokale Temp-Ordner (keine Cloud-Abhängigkeiten)
  • Security: API-Key Hashing (BCrypt), Rotation möglich
  • Performance: Async Processing für große Operationen (keine HTTP Timeouts)
  • Resilience: Production-ready (Polly Retry/Circuit Breaker)
  • Monitoring: Correlation IDs, Seq, File Logging
  • Wartbarkeit: Clean Architecture bleibt pragmatisch, aber production-ready!

?? TABLE OF CONTENTS

  1. Project Overview
  2. Architecture & Design Decisions
  3. Development Philosophy
  4. Technology Stack
  5. Project Structure
  6. Development Roadmap
  7. Testing Strategy
  8. Current Status

?? PROJECT OVERVIEW

Vision & Purpose

DocumentOperator ist ein zentralisierter REST API Service für PDF-Dokumenten-Operationen in einer Multi-Tenant DMS-Umgebung.

Problem Statement

Aktuell:

  • Verschiedene DMS-Kunden bei unterschiedlichen Mandanten
  • Jede Anwendung implementiert PDF-Operationen redundant
  • Keine zentrale Stelle für Dokumenten-Verarbeitung
  • Wartungsaufwand multipliziert sich mit jeder Anwendung

Lösung:

  • Ein zentraler Service für alle PDF-Operationen
  • Wiederverwendbar über HTTP REST API
  • Mandantenfähig (Multi-Tenancy)
  • Wartbar an einer Stelle

Core Features

Der Service bietet folgende PDF-Operationen:

1. PDF Validierung

  • Prüfung auf gültiges PDF-Format
  • Korruptions-Erkennung
  • Metadaten-Extraktion (Seitenzahl, Größe, Version, Anhänge)

2. Attachment-Extraktion

  • Erkennung von eingebetteten Anhängen
  • Extraktion in temporären Ordner
  • Rückgabe als Base64 oder Download-Link

3. PDF-Konkatenation

  • Zusammenführen mehrerer PDFs
  • Reihenfolge konfigurierbar
  • Seitenzahl-Optimierung

4. Stempel/Wasserzeichen

  • Aufbringen von Stamps (Logo, Text)
  • Positions-Konfiguration
  • Mandanten-spezifische Logos

5. Zertifikat-Einbettung

  • PFX-Zertifikate als Attachment einbetten
  • Digitale Signatur-Vorbereitung
  • Workflow-Integration (Ergebnisbericht ? Zertifikat ? Siegel)

Business Workflow

Synchroner Flow (kleine Operationen < 5 Sekunden):

Client Application
    ?
[HTTP Request] - JSON mit Base64-PDF + X-API-Key Header
    ?
Tenant Resolution Middleware ? API-Key ? Tenant aus DB (Redis Cache)
    ?
DocumentOperator API (Minimal API Endpoint)
    ?
[FluentValidation] ? [MediatR Handler] ? [DevExpress Service] ? [Ergebnis]
    ?
[HTTP Response] - JSON mit verarbeitetem PDF (Base64)

Asynchroner Flow (große Operationen > 5 Sekunden):

Client Application
    ?
[HTTP POST /api/v1/documents/concatenate] - Große Operation
    ?
[Returns: { "jobId": "abc123", "status": "Pending" }] - Sofort
    ?
Background Worker (IHostedService) ? In-Memory Queue ? Verarbeitung
    ?
Client Poll: [GET /api/v1/jobs/abc123] ? { "status": "Processing", "progress": 45% }
    ?
Client Poll: [GET /api/v1/jobs/abc123] ? { "status": "Success", "resultUrl": "/download/xyz" }

Typischer Ablauf (Synchron):

  1. Client sendet PDF als Base64 in JSON + API-Key Header
  2. Tenant Resolution Middleware validiert API-Key (DB-Lookup mit Redis Cache)
  3. API validiert Input (FluentValidation in MediatR Pipeline)
  4. Handler konvertiert PDF ? Byte-Array
  5. DevExpress Service führt Operation durch (mit Polly Retry/Circuit Breaker)
  6. Ergebnis wird in lokalem Temp-Ordner gespeichert
  7. Ergebnis wird als Base64 zurückgegeben (oder Download-Link)

??? ARCHITECTURE & DESIGN DECISIONS

Clean Architecture (Pragmatisch!)

Wir verwenden Clean Architecture mit 4 Layers - ABER: pragmatisch, nicht dogmatisch!

???????????????????????????????????????
?         API Layer (Endpoints)       ?  ? HTTP Entry Point
???????????????????????????????????????
?     Application Layer (Use Cases)   ?  ? MediatR Handlers, DTOs
???????????????????????????????????????
?   Infrastructure Layer (Tech Stack) ?  ? DevExpress, File I/O
???????????????????????????????????????
?  Domain Layer (MINIMAL!)            ?  ? Nur Enums + Value Objects
???????????????????????????????????????

Dependency Rule

Abhängigkeiten zeigen immer nach innen:

API ? Application ? Domain
API ? Infrastructure ? Domain
Infrastructure ? Application (für Interfaces)

Domain ? NICHTS! (No External Dependencies)
Application ? NUR Domain

Warum Clean Architecture?

  • ? Testbarkeit (Application Layer kann Services mocken)
  • ? Austauschbarkeit (DevExpress ? anderes PDF-Lib ohne Application zu ändern)
  • ? Separation of Concerns (jede Schicht hat klare Verantwortung)

ABER:

  • ? Kein Overengineering (nur was wir wirklich brauchen!)
  • ? Keine spekulativen Abstraktionen (erst wenn 2. Use Case es braucht)
  • ? Keine unnötigen Klassen (YAGNI - You Ain't Gonna Need It)

Domain Layer - Warum so minimal?

Was wir NICHT haben:

  • ? Keine Datenbank / EF Core
  • ? Keine komplexen Entities mit Business-Logik
  • ? Keine Aggregate Roots, Repositories, etc.

Was wir SIND:

  • ? Ein Service (nicht eine Domain-lastige Business-Anwendung)
  • ? PDF-Operationen = technische Operationen (nicht fachliche Geschäftslogik)
  • ? Daten fließen durch (Input ? Verarbeitung ? Output)

Deshalb: Domain Layer minimal!

Was bleibt in Domain:

  1. Enums (DocumentOperationType, ProcessingStatus)

    • Pure Business-Konzepte
    • Technologie-unabhängig
    • Wiederverwendbar über alle Layer
  2. Value Objects (Base64String, TenantId, PdfMetadata)

    • Typsicherheit (Base64String statt string)
    • Selbst-validierend (Fehler werfen im Constructor)
    • Immutable (keine Änderungen nach Erstellung)
  3. Domain Exceptions (DomainValidationException, PdfProcessingException, etc.)

    • Für fachliche Fehler
    • Exception Middleware mapped zu HTTP Status Codes

Was wir NICHT in Domain haben:

  • ? Domain Models (PdfDocument, DocumentAttachment) ? DTOs in Application reichen!
  • ? Constants (ErrorCodes) ? erst wenn wirklich mehrfach gebraucht (YAGNI)
  • ? Services (? Infrastructure)

Fazit:

  • Domain = so viel wie nötig, so wenig wie möglich
  • Wenn wir später merken "das fehlt" ? dann erst hinzufügen (iterativ!)

CQRS with MediatR

Pattern: Command Query Responsibility Segregation

Warum MediatR?

  • ? Klare Trennung: 1 Command/Query = 1 Handler = 1 Verantwortung
  • ? Testbarkeit (Handler kann isoliert getestet werden)
  • ? Pipeline Behaviors (Validation, Logging zentral)
  • ? Kein aufgeblähter Service mit 20 Methoden

CQRS in unserem Kontext:

  • Command: Ändert Daten (ProcessDocument, ApplyStamp, etc.)
  • Query: Liest Daten (ValidatePdf ? gibt nur Metadata zurück)

Beispiel:

// Query (Read-Only)
public record ValidatePdfQuery(Base64String PdfContent) : IRequest<PdfMetadata>;

// Handler
public class ValidatePdfHandler : IRequestHandler<ValidatePdfQuery, PdfMetadata>
{
    private readonly IPdfProcessor _processor;

    public async Task<PdfMetadata> Handle(ValidatePdfQuery query, CancellationToken ct)
    {
        byte[] bytes = query.PdfContent.ToByteArray();
        var metadata = await _processor.ValidateAsync(bytes);
        return metadata;
    }
}

Warum Value Objects in Query/Command?

  • ? Typsicherheit (Base64String vs string)
  • ? Validierung bereits beim Erstellen der Query (nicht im Handler)
  • ? Handler bleibt schlank (keine Validierungs-Boilerplate)

Vertical Slice Architecture

Statt Horizontal Layers (Commands/, Handlers/, Validators/):

? Horizontal (Schlecht für Wartung):
Application/
??? Commands/
?   ??? ValidatePdfCommand.cs
?   ??? ProcessDocumentCommand.cs
??? Handlers/
?   ??? ValidatePdfHandler.cs
?   ??? ProcessDocumentHandler.cs
??? Validators/
    ??? ValidatePdfValidator.cs
    ??? ProcessDocumentValidator.cs

Nutzen wir Vertical Slices (pro Feature alles zusammen):

? Vertical (Gut für Wartung):
Features/
??? ValidatePdf/
?   ??? ValidatePdfQuery.cs
?   ??? ValidatePdfHandler.cs
?   ??? ValidatePdfValidator.cs
??? ProcessDocument/
    ??? ProcessDocumentCommand.cs
    ??? ProcessDocumentHandler.cs
    ??? ProcessDocumentValidator.cs

Vorteile:

  • ? Zusammengehöriger Code ist zusammen (Cohesion)
  • ? Einfacher zu finden ("Wo ist ValidatePdf?" ? ein Ordner!)
  • ? Einfacher zu ändern (alle Dateien im gleichen Ordner)
  • ? Weniger Merge-Konflikte im Team

Exception-based Error Handling

Entscheidung: Keine Result Pattern Library (Ardalis.Result entfernt)

Stattdessen:

  1. FluentValidation für Input-Validierung (DTO-Ebene)
  2. Domain Exceptions für fachliche Fehler
  3. Zentrale Exception Handling Middleware im API Layer

Warum Exception-basiert?

  • ? Einfacherer Code (kein if (result.IsSuccess) überall)
  • ? Weniger Boilerplate (kein Result Wrapping)
  • ? Standard .NET Exception-Flow (jeder kennt es)
  • ? Zentrales Error Handling = wartbar an einer Stelle
  • ? Ein Package weniger (keine Extra-Lib)

Flow:

HTTP Request
    ?
FluentValidation (MediatR ValidationBehavior)
  ? Bei Fehler: ValidationException ? Middleware ? HTTP 400
    ?
Handler
  ? Bei Fehler: DomainException ? Middleware ? HTTP 400/404/500
    ?
Middleware (Exception Handler)
  ? Mappt Exception Type ? HTTP Status Code
  ? Loggt Exception (Serilog)
  ? Gibt Problem Details (RFC 7807) zurück
    ?
HTTP Response (JSON)

Exception Types:

  • FluentValidation.ValidationException ? HTTP 400 (Bad Request)
  • DomainValidationException ? HTTP 400 (Bad Request)
  • NotFoundException ? HTTP 404 (Not Found)
  • PdfProcessingException ? HTTP 500 (Internal Server Error)
  • Exception (Catch-All) ? HTTP 500

Warum zentral?

  • Alle Fehler an einer Stelle behandelt
  • Konsistente Error-Responses (Problem Details Format)
  • Handler bleiben schlank (kein Try/Catch in jedem Handler)
  • Logging zentral (Serilog)

Minimal APIs (statt Controllers)

Warum Minimal APIs?

  • ? .NET 8 Best Practice (Microsoft empfiehlt es)
  • ? Weniger Boilerplate (keine Controller-Klassen)
  • ? Direkte Endpoint-Definition (funktionaler Stil)
  • ? Swagger funktioniert 1:1 (WithOpenApi())
  • ? Bessere Performance (weniger Abstraktion)

Beispiel:

app.MapPost("/api/v1/documents/validate", async (
    ValidatePdfRequest request,
    IMediator mediator,
    CancellationToken ct) =>
{
    // DTO ? Query (Value Objects erstellen)
    var query = new ValidatePdfQuery(
        Base64String.Create(request.Base64Pdf)
    );

    // MediatR Handler aufrufen
    var result = await mediator.Send(query, ct);

    // HTTP 200 + JSON Response
    return Results.Ok(result);
})
.WithName("ValidatePdf")
.WithTags("Documents")
.WithOpenApi();

Flow:

  1. HTTP Request kommt rein
  2. ASP.NET Core deserialisiert JSON ? DTO
  3. Endpoint ruft MediatR auf
  4. MediatR Pipeline: Validation ? Handler ? Response
  5. Endpoint gibt Result zurück (Results.Ok())

Multi-Tenancy via API-Keys (Database-based)

Konzept:

  • Jeder Mandant (Customer A, B, C...) hat eigenen API-Key
  • API-Key wird in HTTP Header gesendet: X-API-Key: customer-a-key-12345
  • API-Keys werden in Datenbank gespeichert (SQLite für Einfachheit)
  • Redis Cache für schnelle API-Key Lookups (Performance!)
  • Middleware resolved API-Key ? Tenant-Context
  • Tenant-spezifische Einstellungen (Logo für Stamps, Zertifikat, etc.)

Warum Database-based (statt appsettings.json)?

  • ? Skalierbar: Neue Tenants ohne Neustart hinzufügen
  • ? Security: API-Key Rotation möglich (gehashed in DB!)
  • ? Audit-Log: Wer hat wann was aufgerufen?
  • ? Rate-Limiting: Pro Tenant konfigurierbar (Redis Sliding Window)
  • ? Tenant-Management: CRUD-API für API-Keys (Admin-Endpoint)

Warum SQLite (statt SQL Server)?

  • ? Einfache Deployment (keine separate DB-Server)
  • ? Wenige Daten (nur Tenant-Tabelle + Settings)
  • ? Migrations-Support (EF Core)
  • ? Production-ready (für < 10.000 Requests/Sekunde ausreichend)

Flow:

HTTP Request mit Header "X-API-Key: abc123"
    ?
TenantResolutionMiddleware
    ?
Redis Cache Lookup (Key: "tenant:abc123")
  ? Cache Hit: Tenant-Info geladen (1ms)
  ? Cache Miss: DB Lookup ? Redis Cache befüllen (TTL: 1 Stunde)
    ?
API-Key Hash validieren (BCrypt)
    ?
Tenant.IsActive prüfen (inaktive Tenants ? HTTP 403)
    ?
ITenantContext setzen (Scoped Service)
    ?
Handler nutzt ITenantContext.TenantId

Datenbank-Schema:

// Tenant-Tabelle (EF Core Entity)
public class Tenant
{
    public Guid Id { get; set; }
    public string Name { get; set; }           // "Customer A"
    public string ApiKeyHash { get; set; }     // BCrypt Hash
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? LastUsedAt { get; set; }

    // Navigation
    public TenantSettings Settings { get; set; }
}

// Tenant-Settings (1:1 Beziehung)
public class TenantSettings
{
    public Guid Id { get; set; }
    public Guid TenantId { get; set; }
    public string LogoFilePath { get; set; }       // Lokaler Dateipfad (z.B. "logos/tenant-a/stamp.png")
    public string CertificateFilePath { get; set; } // Lokaler Dateipfad (z.B. "certs/tenant-a/cert.pfx")
    public int RateLimitPerMinute { get; set; }     // Rate-Limiting

    // Navigation
    public Tenant Tenant { get; set; }
}

Beispiel - Tenant-spezifischer Stamp:

public class ApplyStampHandler : IRequestHandler<ApplyStampCommand, byte[]>
{
    private readonly ITenantContext _tenantContext;
    private readonly IFileStorage _fileStorage; // Lokaler File Storage

    public async Task<byte[]> Handle(ApplyStampCommand command, CancellationToken ct)
    {
        // Tenant-spezifisches Logo aus DB Settings laden
        var logoPath = _tenantContext.CurrentTenant.Settings.LogoFilePath;

        // Logo aus lokalem Dateisystem laden
        var logoBytes = await _fileStorage.GetAsync(logoPath);

        // Stamp mit Logo anwenden
        // ...
    }
}

Vorteile:

  • ? Neue Tenants via Admin-API hinzufügen (ohne Neustart!)
  • ? API-Key Rotation (alten Key invalidieren, neuen generieren)
  • ? Audit-Log (LastUsedAt pro Tenant tracken)
  • ? Rate-Limiting pro Tenant (Redis Counter)

??? TECHNOLOGY STACK

Core Framework

Technology Version Purpose
.NET 8.0 Runtime & Framework
ASP.NET Core 8.0 Web API
C# 12 Language (Primary Constructors, Record Types)

NuGet Packages

API Layer

Package Version Purpose
Swashbuckle.AspNetCore 6.6.2 Swagger/OpenAPI Documentation
Serilog.AspNetCore 10.0.0 Strukturiertes Logging
Serilog.Sinks.File 7.0.0 Log-Datei-Output
Serilog.Sinks.Seq 8.0.0 NEU: Log-Browsing UI (Development)
Serilog.Enrichers.Environment 3.0.1 Log-Enrichment (MachineName, etc.)
Serilog.Enrichers.CorrelationId 3.0.1 NEU: Correlation IDs für Request-Tracking
Asp.Versioning.Http 8.1.1 API Versioning (/api/v1/, /api/v2/)
Microsoft.Extensions.Caching.StackExchangeRedis 8.0.28 Redis Cache (Tenant-Lookups, Rate-Limiting)

Application Layer

Package Version Purpose
MediatR 14.1.0 CQRS Pattern Implementation
FluentValidation 12.1.1 Input Validation (DTOs)
FluentValidation.DependencyInjectionExtensions 12.1.1 DI Integration

Infrastructure Layer

Package Version Purpose
DevExpress.Pdf.Core 25.2.8 PDF-Operationen (Merge, Extract, Sign, etc.)
DevExpress Universal License ? Verfügbar Vollzugriff auf alle DevExpress Bibliotheken
Microsoft.EntityFrameworkCore 8.0.0 NEU: ORM für Tenant-Datenbank
Microsoft.EntityFrameworkCore.Sqlite 8.0.0 NEU: SQLite Provider (Tenant-DB)
Microsoft.EntityFrameworkCore.Tools 8.0.0 NEU: Migrations-Support
Polly 8.5.0 NEU: Resilience (Retry, Circuit Breaker, Timeout)
BCrypt.Net-Next 4.0.3 NEU: API-Key Hashing (Security)
Microsoft.Extensions.Options.ConfigurationExtensions 8.0.0 Options Pattern

Hinweis zur DevExpress Lizenz:

  • ? Universal License vorhanden - wir können ALLE DevExpress Pakete nutzen
  • Neben DevExpress.Pdf.Core können wir auch weitere Pakete integrieren:
    • DevExpress.Office.Core (Word, Excel)
    • DevExpress.Document.Processor (erweiterte Dokumenten-Verarbeitung)
    • DevExpress.Blazor (falls UI später benötigt wird)
    • Alle weiteren DevExpress Produkte nach Bedarf

Warum diese neuen Pakete?

  • EF Core + SQLite: Tenant-Datenbank (API-Keys, Settings) - skalierbar ohne SQL Server
  • Polly: Resilience für DevExpress Calls (Retry bei Transient Errors, Circuit Breaker)
  • BCrypt: Sichere API-Key Hashes (NICHT Klartext in DB!)
  • Seq: Log-Browsing UI für Development
  • Correlation IDs: Request-Tracking über alle Logs (Debugging leichter)

Domain Layer

Package Version Purpose
- - Keine Dependencies! (Clean Architecture)

Tests (neu!)

Package Version Purpose
xUnit 2.9.3 Test Framework
FluentAssertions 7.0.0 Assertions (result.Should().Be(expected))
Moq 4.20.72 Mocking (für Services)
Microsoft.NET.Test.Sdk 17.11.1 Test SDK
xunit.runner.visualstudio 2.8.2 Visual Studio Test Runner

?? PROJECT STRUCTURE

Solution Overview

DocumentOperator/
??? DocumentOperator.API/              ? HTTP Entry Point
??? DocumentOperator.Application/      ? Use Cases (MediatR Handlers)
??? DocumentOperator.Infrastructure/   ? Technical Implementations
??? DocumentOperator.Domain/           ? Business Logic (MINIMAL!)
??? DocumentOperator.Tests/            ? Unit & Integration Tests (NEU!)
??? ROADMAP.md                         ? This file

?? API Layer (DocumentOperator.API)

Purpose: HTTP Entry Point, Routing, Middleware

References:

  • ? Application
  • ? Infrastructure
  • ? Domain

Folder Structure:

DocumentOperator.API/
??? Endpoints/
?   ??? v1/
?       ??? DocumentEndpoints.cs        ? Minimal API Endpoints
??? Middleware/
?   ??? ExceptionHandlingMiddleware.cs  ? Zentrale Exception Handling ?
??? Configuration/
?   ??? SwaggerConfiguration.cs         ? Swagger Setup (API-Key Support)
??? appsettings.json                    ? Base Configuration
??? appsettings.Development.json        ? Dev Overrides
??? Program.cs                          ? Application Entry Point

Was gehört hierher:

  • ? HTTP Routing (Minimal APIs)
  • ? Middleware (Exception, Logging)
  • ? Swagger Configuration
  • ? Dependency Injection Setup
  • ? appsettings.json

Was NICHT hierher gehört:

  • ? Business Logic (? Application)
  • ? PDF-Verarbeitung (? Infrastructure)
  • ? Validierung (? Application: FluentValidation)

?? Application Layer (DocumentOperator.Application)

Purpose: Use Cases, Business Logic Orchestration

References:

  • ? Domain (ONLY!)

Folder Structure:

DocumentOperator.Application/
??? Features/                           ? Vertical Slices ?
?   ??? Documents/
?   ?   ??? ValidatePdf/
?   ?   ?   ??? ValidatePdfQuery.cs
?   ?   ?   ??? ValidatePdfHandler.cs
?   ?   ?   ??? ValidatePdfValidator.cs
?   ?   ??? ExtractAttachments/
?   ?   ?   ??? ExtractAttachmentsCommand.cs
?   ?   ?   ??? ExtractAttachmentsHandler.cs
?   ?   ?   ??? ExtractAttachmentsValidator.cs
?   ?   ??? ... (weitere Features iterativ)
?   ??? Jobs/                            ? **NEU:** Async Processing Features
?       ??? GetJobStatus/
?       ?   ??? GetJobStatusQuery.cs
?       ?   ??? GetJobStatusHandler.cs
?       ??? SubmitConcatenateJob/
?           ??? SubmitConcatenateJobCommand.cs
?           ??? SubmitConcatenateJobHandler.cs
??? Common/
?   ??? Interfaces/                     ? Abstractions für Infrastructure
?   ?   ??? IPdfProcessor.cs
?   ?   ??? IFileStorage.cs             ? **NEU:** File Storage Abstraction
?   ?   ??? IJobQueue.cs                ? **NEU:** Queue Abstraction (In-Memory Queue)
?   ?   ??? ITenantRepository.cs        ? **NEU:** Tenant-DB Abstraction
?   ??? Behaviors/                      ? MediatR Pipeline Behaviors
?   ?   ??? ValidationBehavior.cs       ? FluentValidation Integration
?   ?   ??? LoggingBehavior.cs          ? **NEU:** Structured Logging mit Correlation IDs
?   ??? DTOs/                           ? Data Transfer Objects (API Contracts)
?   ?   ??? ValidatePdfRequest.cs
?   ?   ??? ValidatePdfResponse.cs
?   ?   ??? JobStatusResponse.cs        ? **NEU:** Async Job Status
?   ??? Models/                         ? **NEU:** Shared Models
?       ??? TenantContext.cs            ? Tenant-Info (TenantId, Name, Settings)
??? DependencyInjection.cs              ? Service Registration

Was gehört hierher:

  • ? MediatR Commands & Queries (pro Feature)
  • ? Handlers (orchestrieren Domain + Infrastructure)
  • ? FluentValidation Validators
  • ? DTOs (API Contracts)
  • ? Interfaces für Infrastructure (Dependency Inversion!)
  • ? Pipeline Behaviors (Validation, Logging)

Was NICHT hierher gehört:

  • ? DevExpress-spezifischer Code (? Infrastructure)
  • ? File I/O (? Infrastructure)
  • ? HTTP-spezifisches (? API)

Warum keine Infrastructure-Referenz?

  • Application kennt nur Interfaces (IPdfProcessor)
  • Infrastructure implementiert die Interfaces (DevExpressPdfProcessor)
  • API injiziert die Implementierung via DI
  • ? Application bleibt technologie-unabhängig!

?? Infrastructure Layer (DocumentOperator.Infrastructure)

Purpose: Technische Implementierungen

References:

  • ? Application (für Interfaces)
  • ? Domain

Folder Structure:

DocumentOperator.Infrastructure/
??? Services/
?   ??? PdfProcessing/
?   ?   ??? DevExpressPdfProcessor.cs       ? IPdfProcessor Implementation (mit Polly Resilience)
?   ??? FileStorage/
?   ?   ??? LocalFileStorage.cs             ? **NEU:** IFileStorage Implementation (lokaler Temp-Ordner)
?   ??? Queue/
?       ??? InMemoryJobQueue.cs             ? **NEU:** IJobQueue Implementation (In-Memory Queue)
??? Data/
?   ??? TenantDbContext.cs                  ? **NEU:** EF Core DbContext (Tenant-DB)
?   ??? Entities/
?   ?   ??? Tenant.cs                       ? **NEU:** Tenant Entity
?   ?   ??? TenantSettings.cs               ? **NEU:** TenantSettings Entity
?   ??? Repositories/
?   ?   ??? TenantRepository.cs             ? **NEU:** ITenantRepository Implementation
?   ??? Migrations/                         ? **NEU:** EF Core Migrations
??? BackgroundServices/
?   ??? JobProcessorService.cs              ? **NEU:** IHostedService für Async Job Processing
?   ??? TempFileCleanupService.cs           ? **NEU:** IHostedService für Temp-File Cleanup (täglich)
??? Configuration/
?   ??? DocumentOperatorSettings.cs         ? Options Pattern Class
?   ??? FileStorageSettings.cs              ? **NEU:** File Storage Configuration (Temp-Ordner Pfad)
?   ??? RedisSettings.cs                    ? **NEU:** Redis Cache Configuration
??? DependencyInjection.cs                  ? Service Registration

Was gehört hierher:

  • ? DevExpress Integration (mit Polly Resilience!)
  • ? File Storage: Lokaler Temp-Ordner (IFileStorage Abstraction)
  • ? Queue: In-Memory Queue für Async Processing
  • ? Datenbank: EF Core + SQLite (Tenant-Management)
  • ? Background Services: Job Processing, Temp-File Cleanup
  • ? Options Pattern Classes (Settings)

Was NICHT hierher gehört:

  • ? Business Logic (? Application)
  • ? HTTP Handling (? API)

??? Domain Layer (DocumentOperator.Domain) - MINIMAL!

Purpose: Business Rules (nur was wirklich gebraucht wird!)

References:

  • ? KEINE! (wichtigste Clean Architecture Regel)

Folder Structure:

DocumentOperator.Domain/
??? ValueObjects/                       ? Immutable, selbst-validierend
?   ??? Base64String.cs
?   ??? TenantId.cs
?   ??? PdfMetadata.cs
?   ??? JobId.cs                        ? **NEU:** Job-ID für Async Processing
??? Enums/
?   ??? DocumentOperationType.cs
?   ??? ProcessingStatus.cs              ? (Wird jetzt für Async Jobs genutzt!)
??? Exceptions/                         ? Domain-spezifische Exceptions
    ??? DomainException.cs
    ??? DomainValidationException.cs
    ??? NotFoundException.cs
    ??? PdfProcessingException.cs

Was gehört hierher:

  • ? Value Objects (Base64String, TenantId, PdfMetadata, JobId)
  • ? Enums (DocumentOperationType, ProcessingStatus)
  • ? Domain Exceptions

Wichtig:

  • Domain bleibt MINIMAL - keine EF Core Entities hier!
  • Tenant, TenantSettings sind Infrastructure Entities (Data/Entities/)
  • Domain kennt nur Value Objects (keine Navigation Properties, kein EF Core)

Was NICHT hierher gehört:

  • ? Domain Models (PdfDocument, etc.) ? YAGNI! DTOs reichen!
  • ? Constants (ErrorCodes) ? erst wenn mehrfach gebraucht
  • ? Services (? Infrastructure)
  • ? MediatR (? Application)
  • ? JEGLICHE externe Library!

?? Tests Layer (DocumentOperator.Tests) - NEU!

Purpose: Unit & Integration Tests

References:

  • ? Alle Projekte (API, Application, Infrastructure, Domain)

Folder Structure:

DocumentOperator.Tests/
??? Unit/
?   ??? Application/
?   ?   ??? Features/
?   ?       ??? ValidatePdf/
?   ?           ??? ValidatePdfHandlerTests.cs
?   ??? Infrastructure/
?   ?   ??? Services/
?   ?       ??? DevExpressPdfProcessorTests.cs
?   ??? Domain/
?       ??? ValueObjects/
?           ??? Base64StringTests.cs
??? Integration/
    ??? API/
        ??? ValidatePdfEndpointTests.cs

Test-Strategie:

  • ? TDD (Test-Driven Development)
  • ? Unit Tests für Handler (Application Layer)
  • ? Unit Tests für Services (Infrastructure Layer)
  • ? Unit Tests für Value Objects (Domain Layer)
  • ? Integration Tests für Endpoints (API Layer)

?? DEVELOPMENT PHILOSOPHY

Pragmatisch, nicht dogmatisch!

Prinzipien:

  1. YAGNI (You Ain't Gonna Need It)

    • ? Keine spekulativen Abstraktionen
    • ? Keine Klassen "für später"
    • ? Erst wenn 2. Use Case es braucht ? dann Abstrahieren
  2. KISS (Keep It Simple, Stupid)

    • ? Kein Overengineering
    • ? Keine unnötigen Design Patterns
    • ? Einfachster Code der funktioniert
  3. Clean Architecture JA, aber pragmatisch

    • ? Dependency Rule einhalten (wichtig!)
    • ? Separation of Concerns (wichtig!)
    • ? ABER: Nur Abstraktionen die wir wirklich brauchen
  4. Test-Driven Development (TDD)

    • ? Tests schreiben bevor Code (Red ? Green ? Refactor)
    • ? Tests als Dokumentation (wie wird es genutzt?)
    • ? Tests als Safety Net (Refactoring ohne Angst)
  5. Outside-In Development

    • ? Von außen nach innen bauen (API ? Service ? Domain)
    • ? Wir sehen sofort was funktioniert (kein "spekulatives" Code)
    • ? Feedback-Loop schneller

Konkret für unser Projekt:

  • Domain Layer minimal (nur Enums + Value Objects + Exceptions)
  • Keine Domain Models (DTOs in Application reichen!)
  • Keine Constants (erst wenn mehrfach gebraucht)
  • Iterativ entwickeln (Feature für Feature)
  • TDD (Test ? Code ? Refactor)

??? DEVELOPMENT ROADMAP

? PHASE 1: Foundation - COMPLETED

Bereits erledigt:

  • Solution erstellt (4 Projekte)
  • Dependencies korrekt (Clean Architecture Dependency Rule)
  • NuGet Packages installiert
  • Folder-Struktur erstellt
  • appsettings.json konfiguriert
  • Options Pattern Classes erstellt
  • Serilog Setup (Program.cs)

? PHASE 2: Domain Layer (Minimal) - COMPLETED

Ziel: Nur was wirklich gebraucht wird!

Status: ? Alle Steps abgeschlossen!


? Step 2.1: Domain Exceptions erstellen - COMPLETED

Bereits erstellt:

  1. DomainException.cs (Basis-Exception)
  2. DomainValidationException.cs (Value Object Validierung)
  3. NotFoundException.cs (Resource nicht gefunden)
  4. PdfProcessingException.cs (PDF-spezifische Fehler)

Wo: Domain/Common/Exceptions/


? Step 2.2: Enums erstellen - COMPLETED

Aufgabe: Aufzählungen für Business-Konzepte

Warum JETZT (vor Value Objects)?

  • Enums haben keine Dependencies
  • Werden in Value Objects gebraucht (z.B. PdfMetadata)
  • Schneller Erfolg (5 Minuten Arbeit)

Was du tun wirst:

  1. DocumentOperationType.cs erstellen

    • Wo: Domain/Models/Enums/DocumentOperationType.cs
    • Inhalt:
      namespace DocumentOperator.Domain.Models.Enums;
      
      public enum DocumentOperationType
      {
          Validate,
          ExtractAttachments,
          Concatenate,
          ApplyStamp,
          EmbedCertificate
      }
      
    • Warum: Definiert welche Operationen unser Service kann
    • Wo gebraucht: Später in Commands/DTOs
  2. ProcessingStatus.cs erstellen

    • Wo: Domain/Models/Enums/ProcessingStatus.cs
    • Inhalt:
      namespace DocumentOperator.Domain.Models.Enums;
      
      public enum ProcessingStatus
      {
          Pending,
          Processing,
          Success,
          Failed
      }
      
    • Warum: Status für asynchrone Operationen (später: Queue)
    • Wo gebraucht: Response DTOs

Nach diesem Step:

  • Ich prüfe deine Dateien
  • Wir haken Step 2.2 ab in ROADMAP.md
  • Weiter zu Step 2.3 (Value Objects)

Status: ? COMPLETED (17.01.2025)

  • ? DocumentOperationType.cs erstellt
  • ? ProcessingStatus.cs erstellt
  • ? Build erfolgreich

? Step 2.3: Value Objects erstellen - COMPLETED

Aufgabe: Typsichere, selbst-validierende Wert-Objekte

Warum Value Objects?

  • ? Typsicherheit: Base64String statt string
  • ? Validierung an einer Stelle (Constructor)
  • ? Immutable (keine Änderungen nach Erstellung)
  • ? Wiederverwendbar (in Domain, Application, Infrastructure)

Was du erstellt hast:

  1. Base64String.cs ?

    • Factory Method: Create(string value)
    • Validierung: Gültiges Base64-Format
    • Konvertierung: ToByteArray(), FromByteArray(byte[])
    • Wirft DomainValidationException bei Fehler
  2. TenantId.cs ?

    • Factory Method: Create(string value)
    • Validierung: Nicht leer, Max 100 Zeichen
    • Normalisierung: .ToLowerInvariant()
    • Wirft DomainValidationException bei Fehler
  3. PdfMetadata.cs ?

    • Properties: PageCount, FileSizeBytes, PdfVersion, HasAttachments, AttachmentCount
    • Computed Property: FileSizeMB
    • Keine Validierung (nur Daten-Container)

Wo: Domain/Models/ValueObjects/

Status: ? COMPLETED (17.01.2025)

  • ? Base64String.cs erstellt (sealed, Factory Methods, Validierung, Equality)
  • ? TenantId.cs erstellt (sealed, Normalisierung, Validierung, Equality)
  • ? PdfMetadata.cs erstellt (sealed, Computed Property, ToString())
  • ? Build erfolgreich

?? Phase 2 (Domain Layer) komplett abgeschlossen!


? PHASE 3: Infrastructure Layer (Outside-In!) - NEXT

Ziel: DevExpress Services implementieren (wir sehen echten Code!)


? Step 3.1: IPdfProcessor Interface erstellen - COMPLETED

Aufgabe: Abstraction für PDF-Operationen

Was du erstellt hast:

  • Wo: Application/Common/Interfaces/IPdfProcessor.cs ?
  • Inhalt:
    using DocumentOperator.Domain.Models.ValueObjects;
    
    namespace DocumentOperator.Application.Common.Interfaces;
    
    public interface IPdfProcessor
    {
        Task<PdfMetadata> ValidateAsync(byte[] pdfBytes);
    }
    

Warum Interface ERST?

  • Application kennt nur Interface (Dependency Inversion)
  • Infrastructure implementiert
  • TDD: Test ? Interface ? Implementation

Status: ? COMPLETED (17.01.2025)

  • ? Interface erstellt mit XML Comments
  • ? Using Statement korrekt
  • ? Namespace korrekt
  • ? Build erfolgreich

?? Step 3.2: DevExpressPdfProcessor implementieren (mit TDD!) - NEXT

Aufgabe: DevExpress Integration

Flow:

  1. Test schreiben (Red)

    [Fact]
    public async Task ValidateAsync_ValidPdf_ReturnMetadata()
    {
        // Arrange
        var processor = new DevExpressPdfProcessor();
        byte[] validPdf = CreateDummyPdf();
    
        // Act
        var metadata = await processor.ValidateAsync(validPdf);
    
        // Assert
        metadata.PageCount.Should().BeGreaterThan(0);
    }
    
  2. Implementation schreiben (Green)

    public class DevExpressPdfProcessor : IPdfProcessor
    {
        public async Task<PdfMetadata> ValidateAsync(byte[] pdfBytes)
        {
            using var processor = new PdfDocumentProcessor();
            processor.LoadDocument(pdfBytes);
    
            return new PdfMetadata(
                PageCount: processor.Document.Pages.Count,
                FileSizeBytes: pdfBytes.Length,
                // ...
            );
        }
    }
    
  3. Test grün machen

  4. Refactoring (falls nötig)

Wo:

  • Test: Tests/Unit/Infrastructure/Services/DevExpressPdfProcessorTests.cs
  • Code: Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs

Nach diesem Step:

  • Wir haben echten Code der mit DevExpress arbeitet!
  • Wir wissen welche Exceptions geworfen werden können
  • Wir können Exception Middleware bauen

? PHASE 4: Application Layer (erste Feature)

Ziel: ValidatePdf Feature komplett (Query ? Handler ? Validator)


? Step 4.1: MediatR Setup

Aufgabe: MediatR + FluentValidation + ValidationBehavior

Was du erstellen wirst:

  1. DependencyInjection.cs (Application Layer)
  2. ValidationBehavior.cs (MediatR Pipeline)

Warum jetzt?

  • Wir brauchen MediatR für Handler
  • ValidationBehavior = zentrale FluentValidation Ausführung

? Step 4.2: ValidatePdf Feature (mit TDD!)

Aufgabe: Erste komplette Feature-Implementierung

Was du erstellen wirst:

  1. ValidatePdfQuery.cs

    public record ValidatePdfQuery(Base64String PdfContent) : IRequest<PdfMetadata>;
    
  2. ValidatePdfHandler.cs

    public class ValidatePdfHandler : IRequestHandler<ValidatePdfQuery, PdfMetadata>
    {
        private readonly IPdfProcessor _processor;
    
        public async Task<PdfMetadata> Handle(ValidatePdfQuery query, CancellationToken ct)
        {
            byte[] bytes = query.PdfContent.ToByteArray();
            return await _processor.ValidateAsync(bytes);
        }
    }
    
  3. ValidatePdfValidator.cs (FluentValidation)

    public class ValidatePdfValidator : AbstractValidator<ValidatePdfQuery>
    {
        public ValidatePdfValidator()
        {
            RuleFor(x => x.PdfContent).NotNull();
        }
    }
    
  4. ValidatePdfHandlerTests.cs (Unit Test)

Wo: Application/Features/Documents/ValidatePdf/

Flow:

DTO ? Query (Value Objects) ? ValidationBehavior (FluentValidation) 
    ? Handler ? IPdfProcessor ? PdfMetadata

? PHASE 5: API Layer

Ziel: HTTP Endpoint + Exception Middleware


? Step 5.1: Exception Handling Middleware

Aufgabe: Zentrale Exception ? HTTP Response Mapping

Was du erstellen wirst:

  • Wo: API/Middleware/ExceptionHandlingMiddleware.cs
  • Inhalt:
    public class ExceptionHandlingMiddleware
    {
        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (DomainValidationException ex)
            {
                await HandleDomainValidationExceptionAsync(context, ex);
            }
            catch (PdfProcessingException ex)
            {
                await HandlePdfProcessingExceptionAsync(context, ex);
            }
            // ... weitere Exceptions
        }
    
        private static Task HandleDomainValidationExceptionAsync(...)
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
    
            var problemDetails = new ProblemDetails
            {
                Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
                Title = "Validation Error",
                Status = 400,
                Detail = ex.Message
            };
    
            return context.Response.WriteAsJsonAsync(problemDetails);
        }
    }
    

Warum jetzt?

  • Wir kennen jetzt alle Exceptions (aus Infrastructure Step)
  • Wir können sie zu HTTP Status Codes mappen

? Step 5.2: Minimal API Endpoint

Aufgabe: HTTP Endpoint für ValidatePdf

Was du erstellen wirst:

  • Wo: API/Endpoints/v1/DocumentEndpoints.cs
  • Inhalt:
    public static class DocumentEndpoints
    {
        public static void MapDocumentEndpoints(this IEndpointRouteBuilder app)
        {
            var group = app.MapGroup("/api/v1/documents")
                .WithTags("Documents")
                .WithOpenApi();
    
            group.MapPost("/validate", ValidatePdf);
        }
    
        private static async Task<IResult> ValidatePdf(
            ValidatePdfRequest request,
            IMediator mediator,
            CancellationToken ct)
        {
            var query = new ValidatePdfQuery(
                Base64String.Create(request.Base64Pdf)
            );
    
            var result = await mediator.Send(query, ct);
            return Results.Ok(result);
        }
    }
    

DTOs:

  • ValidatePdfRequest (Input)
  • ValidatePdfResponse (Output) ? oder direkt PdfMetadata?

In Program.cs registrieren:

app.MapDocumentEndpoints();

? Step 5.3: Integration Test

Aufgabe: End-to-End Test (HTTP ? Handler ? Service)

Was du erstellen wirst:

  • Wo: Tests/Integration/API/ValidatePdfEndpointTests.cs
  • Inhalt:
    public class ValidatePdfEndpointTests : IClassFixture<WebApplicationFactory<Program>>
    {
        [Fact]
        public async Task POST_ValidatePdf_ValidPdf_Returns200()
        {
            // Arrange
            var client = _factory.CreateClient();
            var request = new ValidatePdfRequest(Base64Pdf: "...");
    
            // Act
            var response = await client.PostAsJsonAsync("/api/v1/documents/validate", request);
    
            // Assert
            response.StatusCode.Should().Be(HttpStatusCode.OK);
            var metadata = await response.Content.ReadFromJsonAsync<PdfMetadata>();
            metadata.PageCount.Should().BeGreaterThan(0);
        }
    }
    

Warum Integration Test?

  • Testet kompletten Flow (HTTP ? MediatR ? Service ? Response)
  • Testet Exception Middleware
  • Testet Swagger/OpenAPI

? PHASE 5.5: Health Checks & Resilience - NEW!

Ziel: Production-Ready Features FRÜH implementieren

Warum JETZT (nicht Phase 9)?

  • Health Checks = Pflicht für Production (Load Balancer, Kubernetes)
  • Resilience (Polly) = Pflicht für externe Dependencies (DevExpress)

? Step 5.5.1: Health Checks

Aufgabe: /health Endpoint für Liveness/Readiness Probes

Was du erstellen wirst:

  1. DevExpressPdfHealthCheck.cs

    public class DevExpressPdfHealthCheck : IHealthCheck
    {
        private readonly IPdfProcessor _processor;
    
        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct)
        {
            try
            {
                // Dummy PDF validieren (Smoke Test)
                byte[] dummyPdf = CreateMinimalPdf();
                await _processor.ValidateAsync(dummyPdf);
                return HealthCheckResult.Healthy("DevExpress PDF OK");
            }
            catch (Exception ex)
            {
                return HealthCheckResult.Unhealthy("DevExpress PDF failed", ex);
            }
        }
    }
    
  2. Program.cs Registration:

    builder.Services.AddHealthChecks()
        .AddCheck<DevExpressPdfHealthCheck>("devexpress")
        .AddCheck("self", () => HealthCheckResult.Healthy());
    
    app.MapHealthChecks("/health");
    app.MapHealthChecks("/health/ready"); // Kubernetes Readiness
    app.MapHealthChecks("/health/live");  // Kubernetes Liveness
    

Wo: API/HealthChecks/DevExpressPdfHealthCheck.cs


? Step 5.5.2: Polly Resilience

Aufgabe: Retry + Circuit Breaker + Timeout für DevExpress Calls

Was du erstellen wirst:

  1. DevExpressPdfProcessor erweitern (Polly Policies):

    public class DevExpressPdfProcessor : IPdfProcessor
    {
        private readonly IAsyncPolicy _retryPolicy;
        private readonly IAsyncPolicy _circuitBreakerPolicy;
        private readonly IAsyncPolicy _timeoutPolicy;
    
        public DevExpressPdfProcessor()
        {
            // Retry: 3x mit Exponential Backoff
            _retryPolicy = Policy
                .Handle<Exception>()
                .WaitAndRetryAsync(3, retryAttempt => 
                    TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
    
            // Circuit Breaker: Nach 5 Fehlern für 30 Sekunden öffnen
            _circuitBreakerPolicy = Policy
                .Handle<Exception>()
                .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
    
            // Timeout: 30 Sekunden max
            _timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(30));
        }
    
        public async Task<PdfMetadata> ValidateAsync(byte[] pdfBytes)
        {
            // Alle Policies wrappen
            return await _retryPolicy.ExecuteAsync(async () =>
                await _circuitBreakerPolicy.ExecuteAsync(async () =>
                    await _timeoutPolicy.ExecuteAsync(async () =>
                    {
                        // DevExpress Call
                        using var processor = new PdfDocumentProcessor();
                        processor.LoadDocument(pdfBytes);
                        // ...
                    })));
        }
    }
    
  2. Logging für Resilience Events:

    _retryPolicy = Policy
        .Handle<Exception>()
        .WaitAndRetryAsync(3, 
            retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
            onRetry: (exception, timespan, retryCount, context) =>
            {
                Log.Warning("Retry {RetryCount} after {Delay}ms: {Exception}", 
                    retryCount, timespan.TotalMilliseconds, exception.Message);
            });
    

Warum Polly?

  • ? Retry: DevExpress temporäre Fehler (File Lock, Memory)
  • ? Circuit Breaker: DevExpress kaputt ? alle Requests sofort fehlschlagen (kein Retry-Storm)
  • ? Timeout: DevExpress hängt ? Request-Timeout (nicht ewig warten)

? PHASE 6: Weitere Features (iterativ)

Nach ValidatePdf (als Referenz):

Jedes Feature folgt dem gleichen Pattern:

  1. Interface erweitern (IPdfProcessor)
  2. Service implementieren (DevExpressPdfProcessor) + Test
  3. Command/Query + Handler + Validator
  4. Endpoint erstellen
  5. Integration Test

Features:

  • ExtractAttachments (synchron)
  • ConcatenatePdfs (asynchron - siehe Phase 6.5!)
  • ApplyStamp (synchron)
  • EmbedCertificate (synchron)

? PHASE 6.5: Async Processing (In-Memory Queue-based) - NEW!

Ziel: Große Operationen asynchron verarbeiten (> 5 Sekunden)

Warum?

  • ConcatenatePdfs von 50 PDFs = 10+ Sekunden
  • Client wartet nicht ? HTTP Timeout
  • Queue-basiert = skalierbar (Background Worker kann parallel verarbeiten)

? Step 6.5.1: In-Memory Queue Setup

Aufgabe: Queue für Async Jobs (In-Memory, keine Cloud-Abhängigkeiten)

Was du erstellen wirst:

  1. IJobQueue Interface (Application):

    public interface IJobQueue
    {
        Task<JobId> EnqueueAsync<T>(T jobData) where T : class;
        Task<JobStatus> GetStatusAsync(JobId jobId);
    }
    
    public record JobStatus(
        JobId JobId,
        ProcessingStatus Status,
        int Progress,
        string? ResultFilePath,
        string? ErrorMessage);
    
  2. InMemoryJobQueue Implementation (Infrastructure):

    public class InMemoryJobQueue : IJobQueue
    {
        private readonly ConcurrentQueue<JobData> _queue = new();
        private readonly ConcurrentDictionary<string, JobStatus> _jobStatuses = new();
    
        public async Task<JobId> EnqueueAsync<T>(T jobData) where T : class
        {
            var jobId = JobId.Create(Guid.NewGuid().ToString());
    
            // Job Message in Queue
            _queue.Enqueue(new JobData
            {
                JobId = jobId,
                Data = jobData,
                Type = typeof(T)
            });
    
            // Job Status setzen (Pending)
            _jobStatuses[jobId.Value] = new JobStatus(
                jobId,
                ProcessingStatus.Pending,
                Progress: 0,
                ResultFilePath: null,
                ErrorMessage: null
            );
    
            return jobId;
        }
    
        public Task<JobStatus> GetStatusAsync(JobId jobId)
        {
            _jobStatuses.TryGetValue(jobId.Value, out var status);
            return Task.FromResult(status ?? throw new NotFoundException($"Job {jobId.Value} not found"));
        }
    
        public bool TryDequeue(out JobData jobData)
        {
            return _queue.TryDequeue(out jobData);
        }
    
        public void UpdateStatus(JobId jobId, JobStatus status)
        {
            _jobStatuses[jobId.Value] = status;
        }
    }
    

? Step 6.5.2: Background Worker (Job Processor)

Aufgabe: IHostedService für Job-Verarbeitung

Was du erstellen wirst:

  1. JobProcessorService.cs (Infrastructure/BackgroundServices/):

    public class JobProcessorService : BackgroundService
    {
        private readonly InMemoryJobQueue _jobQueue;
        private readonly IPdfProcessor _pdfProcessor;
        private readonly IFileStorage _fileStorage;
    
        protected override async Task ExecuteAsync(CancellationToken ct)
        {
            while (!ct.IsCancellationRequested)
            {
                // Queue Message abrufen
                if (_jobQueue.TryDequeue(out var jobData))
                {
                    try
                    {
                        // Job Status: Processing
                        _jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
                            jobData.JobId,
                            ProcessingStatus.Processing,
                            Progress: 0,
                            ResultFilePath: null,
                            ErrorMessage: null
                        ));
    
                        // PDF-Operation (z.B. Concatenate)
                        var result = await _pdfProcessor.ConcatenateAsync(jobData.Data.PdfFiles);
    
                        // Ergebnis in lokalem Temp-Ordner speichern
                        var resultPath = await _fileStorage.SaveAsync(result, $"results/{jobData.JobId}.pdf");
    
                        // Job Status: Success
                        _jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
                            jobData.JobId,
                            ProcessingStatus.Success,
                            Progress: 100,
                            ResultFilePath: resultPath,
                            ErrorMessage: null
                        ));
                    }
                    catch (Exception ex)
                    {
                        // Job Status: Failed
                        _jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
                            jobData.JobId,
                            ProcessingStatus.Failed,
                            Progress: 0,
                            ResultFilePath: null,
                            ErrorMessage: ex.Message
                        ));
                    }
                }
    
                            await Task.Delay(TimeSpan.FromMilliseconds(100), ct); // Polling-Interval
                        }
                    }
                }
    
  2. Program.cs Registration:

    builder.Services.AddHostedService<JobProcessorService>();
    

? Step 6.5.3: Async Endpoints

Aufgabe: POST ? JobId, GET ? JobStatus

Was du erstellen wirst:

  1. POST /api/v1/documents/concatenate (Async):

    app.MapPost("/api/v1/documents/concatenate", async (
        ConcatenateRequest request,
        IJobQueue jobQueue,
        CancellationToken ct) =>
    {
        var jobId = await jobQueue.EnqueueAsync(new ConcatenateJobData
        {
            PdfFiles = request.PdfFiles,
            TenantId = tenantContext.TenantId
        });
    
        return Results.Accepted($"/api/v1/jobs/{jobId}", new { jobId, status = "Pending" });
    });
    
  2. GET /api/v1/jobs/{jobId}:

    app.MapGet("/api/v1/jobs/{jobId}", async (
        string jobId,
        IJobQueue jobQueue,
        CancellationToken ct) =>
    {
        var status = await jobQueue.GetStatusAsync(JobId.Create(jobId));
        return Results.Ok(status);
    });
    

Flow:

Client: POST /api/v1/documents/concatenate
  ? API: { "jobId": "abc123", "status": "Pending" } (HTTP 202 Accepted)

Background Worker: Verarbeitet Job aus Queue
  ? Status-Update: Processing (Progress: 50%)

Client: GET /api/v1/jobs/abc123
  ? API: { "jobId": "abc123", "status": "Processing", "progress": 50 }

Background Worker: Job fertig
  ? Status-Update: Success (ResultFilePath: "C:\\Temp\\DocumentOperator\\results\\abc123.pdf")

Client: GET /api/v1/jobs/abc123
  ? API: { "jobId": "abc123", "status": "Success", "resultUrl": "/download/abc123" }

Client: GET /download/abc123
  ? API: PDF-Datei aus lokalem Temp-Ordner

? PHASE 7: Swagger & API Documentation - UPDATED!

Ziel: Produktionsreife API-Dokumentation

Steps:

? Step 7.1: Swagger Configuration (API-Key Support)

Aufgabe: API-Key Header in Swagger UI

Was du erstellen wirst:

  1. SwaggerConfiguration.cs:

    builder.Services.AddSwaggerGen(c =>
    {
        // API-Key Security
        c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
        {
            Type = SecuritySchemeType.ApiKey,
            In = ParameterLocation.Header,
            Name = "X-API-Key",
            Description = "Enter your API Key (from Tenant DB)"
        });
    
        c.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "ApiKey" }
                },
                Array.Empty<string>()
            }
        });
    
        // XML Comments
        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        c.IncludeXmlComments(xmlPath);
    
        // Example Schemas
        c.SchemaFilter<ExampleSchemaFilter>();
    });
    
  2. XML Comments in Endpoints:

    /// <summary>
    /// Validates a PDF document and returns metadata
    /// </summary>
    /// <param name="request">PDF as Base64 string</param>
    /// <returns>PDF metadata (page count, file size, etc.)</returns>
    /// <response code="200">PDF is valid, metadata returned</response>
    /// <response code="400">Invalid PDF or Base64 format</response>
    /// <response code="500">Internal server error during validation</response>
    app.MapPost("/api/v1/documents/validate", ValidatePdf)
        .WithName("ValidatePdf")
        .WithTags("Documents")
        .WithOpenApi();
    

? Step 7.2: Response Examples (Swashbuckle)

Aufgabe: Beispiel-Responses in Swagger

Was du erstellen wirst:

  1. ExampleSchemaFilter.cs:
    public class ExampleSchemaFilter : ISchemaFilter
    {
        public void Apply(OpenApiSchema schema, SchemaFilterContext context)
        {
            if (context.Type == typeof(ValidatePdfRequest))
            {
                schema.Example = new OpenApiObject
                {
                    ["base64Pdf"] = new OpenApiString("JVBERi0xLjQKJeLjz9MK...")
                };
            }
        }
    }
    

? PHASE 8: Multi-Tenancy (Database-based) - UPDATED!

Ziel: Mandantenfähigkeit mit DB + Redis

Steps:

? Step 8.1: EF Core Setup (SQLite)

Aufgabe: Tenant-Datenbank

Was du erstellen wirst:

  1. TenantDbContext.cs:

    public class TenantDbContext : DbContext
    {
        public DbSet<Tenant> Tenants { get; set; }
        public DbSet<TenantSettings> TenantSettings { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Tenant>(entity =>
            {
                entity.HasKey(e => e.Id);
                entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
                entity.Property(e => e.ApiKeyHash).IsRequired().HasMaxLength(500);
                entity.HasOne(e => e.Settings).WithOne(e => e.Tenant).HasForeignKey<TenantSettings>(e => e.TenantId);
            });
        }
    }
    
  2. EF Core Migration:

    dotnet ef migrations add InitialCreate --project Infrastructure --startup-project API
    dotnet ef database update --project Infrastructure --startup-project API
    

? Step 8.2: Tenant Resolution Middleware

Aufgabe: API-Key ? Tenant auflösen (mit Redis Cache)

Was du erstellen wirst:

  1. TenantResolutionMiddleware.cs:
    public class TenantResolutionMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IDistributedCache _cache; // Redis
        private readonly ITenantRepository _tenantRepository;
    
        public async Task InvokeAsync(HttpContext context)
        {
            var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
    
            if (string.IsNullOrEmpty(apiKey))
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsJsonAsync(new { error = "Missing X-API-Key header" });
                return;
            }
    
            // Redis Cache Lookup
            var cacheKey = $"tenant:{apiKey}";
            var cachedTenant = await _cache.GetStringAsync(cacheKey);
    
            Tenant tenant;
            if (cachedTenant != null)
            {
                tenant = JsonSerializer.Deserialize<Tenant>(cachedTenant);
            }
            else
            {
                // DB Lookup (BCrypt Hash Vergleich)
                tenant = await _tenantRepository.GetByApiKeyAsync(apiKey);
    
                if (tenant == null)
                {
                    context.Response.StatusCode = 401;
                    await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
                    return;
                }
    
                // Redis Cache befüllen (TTL: 1 Stunde)
                await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(tenant), new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
                });
            }
    
            if (!tenant.IsActive)
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsJsonAsync(new { error = "Tenant is inactive" });
                return;
            }
    
            // ITenantContext setzen (Scoped Service)
            var tenantContext = context.RequestServices.GetRequiredService<ITenantContext>();
            tenantContext.SetTenant(tenant);
    
            await _next(context);
        }
    }
    

? Step 8.3: Tenant Admin API (CRUD)

Aufgabe: Admin-Endpoints für Tenant-Management

Was du erstellen wirst:

  1. POST /api/v1/admin/tenants (Create Tenant):
    app.MapPost("/api/v1/admin/tenants", async (CreateTenantRequest request, ITenantRepository repo) =>
    {
        var apiKey = GenerateApiKey(); // Zufälliger Key
        var apiKeyHash = BCrypt.Net.BCrypt.HashPassword(apiKey);
    
        var tenant = new Tenant
        {
            Id = Guid.NewGuid(),
            Name = request.Name,
            ApiKeyHash = apiKeyHash,
            IsActive = true,
            CreatedAt = DateTime.UtcNow
        };
    
        await repo.AddAsync(tenant);
    
        return Results.Created($"/api/v1/admin/tenants/{tenant.Id}", new
        {
            tenant.Id,
            tenant.Name,
            apiKey // Nur EINMAL zurückgeben! (Client muss speichern)
        });
    })
    .RequireAuthorization("Admin"); // Nur für Admins!
    

? PHASE 9: File Storage (Lokale Temp-Ordner) - NEW!

Ziel: File Storage für Temp-Files, Logos, Zertifikate

Warum lokale Temp-Ordner?

  • ? Einfachheit: Keine Cloud-Abhängigkeiten
  • ? Schnell: Lokaler Dateizugriff (keine Netzwerk-Latenz)
  • ? Flexibel: Bei Bedarf später zu Cloud migrierbar (IFileStorage bleibt!)

? Step 9.1: IFileStorage Interface

Aufgabe: Abstraction für File Storage

Was du erstellen wirst:

  1. IFileStorage.cs (Application/Common/Interfaces/):
    public interface IFileStorage
    {
        Task<string> SaveAsync(byte[] content, string filename);
        Task<byte[]> GetAsync(string path);
        Task DeleteAsync(string path);
        Task<bool> ExistsAsync(string path);
    }
    

? Step 9.2: LocalFileStorage Implementation

Aufgabe: Lokaler File Storage Provider

Was du erstellen wirst:

  1. LocalFileStorage.cs (Infrastructure/Services/FileStorage/):

    public class LocalFileStorage : IFileStorage
    {
        private readonly string _basePath;
    
        public LocalFileStorage(FileStorageSettings settings)
        {
            _basePath = settings.TempFolderPath ?? Path.Combine(Directory.GetCurrentDirectory(), "TempFiles");
            Directory.CreateDirectory(_basePath);
        }
    
        public async Task<string> SaveAsync(byte[] content, string filename)
        {
            var path = Path.Combine(_basePath, filename);
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            await File.WriteAllBytesAsync(path, content);
            return path;
        }
    
        public async Task<byte[]> GetAsync(string path)
        {
            // Pfad kann absolut oder relativ sein
            var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
            return await File.ReadAllBytesAsync(fullPath);
        }
    
        public Task DeleteAsync(string path)
        {
            var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
            if (File.Exists(fullPath))
            {
                File.Delete(fullPath);
            }
            return Task.CompletedTask;
        }
    
        public Task<bool> ExistsAsync(string path)
        {
            var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
            return Task.FromResult(File.Exists(fullPath));
        }
    }
    
  2. FileStorageSettings.cs (Infrastructure/Configuration/):

    public class FileStorageSettings
    {
        public string TempFolderPath { get; set; } = "TempFiles"; // Default: ./TempFiles
        public int CleanupIntervalHours { get; set; } = 24; // Default: täglich
        public int FileRetentionHours { get; set; } = 24; // Default: 24h Aufbewahrung
    }
    
  3. appsettings.json:

    {
      "FileStorage": {
        "TempFolderPath": "C:\\Temp\\DocumentOperator", // Windows Pfad
        "CleanupIntervalHours": 24,
        "FileRetentionHours": 24
            }
          }
          ```
    
      ---
    
    

? Step 9.3: Temp-File Cleanup Service

Aufgabe: IHostedService für tägliche Cleanup

Was du erstellen wirst:

  1. TempFileCleanupService.cs (Infrastructure/BackgroundServices/):
    public class TempFileCleanupService : BackgroundService
    {
        private readonly IFileStorage _fileStorage;
        private readonly FileStorageSettings _settings;
    
        protected override async Task ExecuteAsync(CancellationToken ct)
        {
            while (!ct.IsCancellationRequested)
            {
                // Warten auf nächsten Cleanup-Zeitpunkt
                await Task.Delay(TimeSpan.FromHours(_settings.CleanupIntervalHours), ct);
    
                try
                {
                    // Temp-Files älter als FileRetentionHours löschen
                    var tempFolderPath = _settings.TempFolderPath;
                    if (Directory.Exists(tempFolderPath))
                    {
                        var files = Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories);
                        var cutoffTime = DateTime.UtcNow.AddHours(-_settings.FileRetentionHours);
    
                        foreach (var file in files)
                        {
                            var fileInfo = new FileInfo(file);
                            if (fileInfo.CreationTimeUtc < cutoffTime)
                            {
                                await _fileStorage.DeleteAsync(file);
                                Log.Information("Deleted temp file: {Path} (Age: {Hours}h)", 
                                    file, (DateTime.UtcNow - fileInfo.CreationTimeUtc).TotalHours);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Log.Error(ex, "Error during temp file cleanup");
                }
            }
        }
    }
    

? PHASE 10: Logging & Monitoring - NEW!

Ziel: Production-Ready Logging

Steps:

? Step 10.1: Correlation IDs

Aufgabe: Request-Tracking über alle Logs

Was du erstellen wirst:

  1. CorrelationIdMiddleware.cs:

    public class CorrelationIdMiddleware
    {
        private readonly RequestDelegate _next;
    
        public async Task InvokeAsync(HttpContext context)
        {
            var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault() 
                ?? Guid.NewGuid().ToString();
    
            context.TraceIdentifier = correlationId;
            context.Response.Headers.Add("X-Correlation-ID", correlationId);
    
            using (LogContext.PushProperty("CorrelationId", correlationId))
            {
                await _next(context);
            }
        }
    }
    
  2. LoggingBehavior (MediatR Pipeline):

    public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    {
        public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
        {
            var requestName = typeof(TRequest).Name;
            Log.Information("Handling {RequestName}: {@Request}", requestName, request);
    
            var stopwatch = Stopwatch.StartNew();
            var response = await next();
            stopwatch.Stop();
    
            Log.Information("Handled {RequestName} in {ElapsedMs}ms", requestName, stopwatch.ElapsedMilliseconds);
    
            return response;
        }
    }
    

? Step 10.2: Serilog Configuration (Seq + File)

Aufgabe: Structured Logging Setup

Was du erstellen wirst:

  1. Program.cs Serilog Setup:

    Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Information()
        .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
        .Enrich.FromLogContext()
        .Enrich.WithMachineName()
        .Enrich.WithEnvironmentName()
        .WriteTo.Console()
        .WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
        .WriteTo.Seq("http://localhost:5341") // Dev: Seq UI
        .CreateLogger();
    
  2. appsettings.Production.json (File Logging):

    {
      "Serilog": {
        "WriteTo": [
          {
            "Name": "File",
            "Args": {
              "path": "C:\\Logs\\DocumentOperator\\log-.txt",
              "rollingInterval": "Day"
            }
          }
        ]
      }
    }
    

? PHASE 11: Production Deployment - UPDATED!

Ziel: IIS Deployment + Production Configuration

Steps:

  • appsettings.Production.json (Lokale Temp-Ordner, Redis, File Logging)
  • IIS Web.config (Kestrel Settings)
  • SSL/TLS Configuration
  • Redis Connection String (Production) - OPTIONAL (In-Memory Cache Alternative)
  • Shared Network Drive (bei Multi-Server Setup) - OPTIONAL (lokale Temp-Ordner für Single-Server)
  • Health Checks für Load Balancer
  • Rate-Limiting (Redis-based, pro Tenant) - OPTIONAL (In-Memory für Single-Server)

?? TESTING STRATEGY

Test-Driven Development (TDD)

Flow:

  1. Red: Test schreiben (schlägt fehl, weil Code noch nicht existiert)
  2. Green: Code schreiben (Test wird grün)
  3. Refactor: Code verbessern (Test bleibt grün)

Warum TDD?

  • ? Tests als Dokumentation (wie wird es genutzt?)
  • ? Tests als Safety Net (Refactoring ohne Angst)
  • ? Besseres Design (testbarer Code = guter Code)
  • ? Keine "vergessenen" Tests (Test kommt ZUERST)

Test-Pyramide

        /\
       /  \      E2E Tests (wenige)
      /    \
     /------\    Integration Tests (einige)
    /        \
   /----------\  Unit Tests (viele)
  /            \

Konkret:

  • Unit Tests (viele):

    • Value Objects (Base64String.Create() wirft Exception?)
    • Handlers (ValidatePdfHandler ruft IPdfProcessor auf?)
    • Services (DevExpressPdfProcessor gibt Metadata zurück?)
  • Integration Tests (einige):

    • Endpoints (HTTP POST ? 200 OK + JSON?)
    • MediatR Pipeline (ValidationBehavior funktioniert?)
  • E2E Tests (wenige/keine):

    • Haben wir nicht (API ist selbst der "Top-Level")

Test-Abdeckung

Ziel: >80% Code Coverage (aber nicht um jeden Preis!)

Was testen:

  • ? Value Objects (Validierung)
  • ? Handlers (Business Logic)
  • ? Services (DevExpress Integration)
  • ? Endpoints (HTTP Responses)
  • ? Exception Middleware (Error Mapping)

Was NICHT testen:

  • ? DTOs (keine Logik)
  • ? Enums (keine Logik)
  • ? Program.cs (Startup Code)

Test-Naming Convention

Pattern: MethodName_Scenario_ExpectedResult

Beispiele:

// Value Object Tests
[Fact]
public void Create_EmptyString_ThrowsDomainValidationException() { }

[Fact]
public void Create_ValidBase64_ReturnsBase64String() { }

// Handler Tests
[Fact]
public async Task Handle_ValidPdf_ReturnsPdfMetadata() { }

[Fact]
public async Task Handle_InvalidPdf_ThrowsPdfProcessingException() { }

// Endpoint Tests
[Fact]
public async Task POST_ValidatePdf_ValidPdf_Returns200() { }

[Fact]
public async Task POST_ValidatePdf_InvalidPdf_Returns400() { }

?? CURRENT STATUS

? Completed

  • Phase 1: Foundation & Clean Architecture Setup ?

    • Solution Structure ?
    • Dependencies ?
    • NuGet Packages ?
    • Folder Structure ?
    • Configuration (appsettings.json) ?
    • Serilog Setup ?
    • Program.cs Setup ?
  • Phase 2: Domain Layer (Minimal) ?

    • ? Step 2.1 - Domain Exceptions (4 Exceptions erstellt)
      • DomainException.cs
      • DomainValidationException.cs
      • NotFoundException.cs
      • PdfProcessingException.cs
    • ? Step 2.2 - Enums (DocumentOperationType, ProcessingStatus)
    • ? Step 2.3 - Value Objects (Base64String, TenantId, PdfMetadata)
  • Phase 3: Infrastructure Layer (Outside-In!) ?

    • ? Step 3.1 - IPdfProcessor Interface erstellt
    • ? Step 3.2 - DevExpressPdfProcessor implementieren (TDD - COMPLETED)
      • ? Step 3.2.1 - ProcessDocument Ordner gelöscht (Application Layer cleanup)
      • ? Step 3.2.2 - Test-Ordnerstruktur erstellt (Unit/Infrastructure/Services/PdfProcessing)
      • ? Step 3.2.3 - Test-PDF Datei hinzugefügt (valid.pdf als Embedded Resource)
      • ? Step 3.2.4 - DevExpressPdfProcessorTests.cs erstellt (TDD Red Phase - 6 Tests)
      • ? Step 3.2.5 - DevExpressPdfProcessor.cs implementiert (TDD Green Phase)
      • ? Step 3.2.6 - Build erfolgreich, Tests bereit zum Ausführen

?? In Progress

  • Phase 4: Application Layer (MediatR Setup)
    • NEXT: Step 4.1 - MediatR Setup (DependencyInjection, ValidationBehavior, LoggingBehavior)
    • Status: Phase 3 abgeschlossen, bereit für Application Layer!

? Pending

  • Phase 4: Application Layer

    • Step 4.1 - MediatR Setup (DependencyInjection.cs, ValidationBehavior.cs, LoggingBehavior.cs)
    • Step 4.2 - ValidatePdf Feature (Query, Handler, Validator)
  • Phase 5: API Layer

    • Step 5.1 - Exception Handling Middleware
    • Step 5.2 - Minimal API Endpoint
    • Step 5.3 - Integration Test
  • Phase 5.5: Health Checks & Resilience (NEU!)

    • Step 5.5.1 - Health Checks (DevExpressPdfHealthCheck)
    • Step 5.5.2 - Polly Resilience (Retry, Circuit Breaker, Timeout)
  • Phase 6: Weitere Features (synchron)

    • ExtractAttachments
    • ApplyStamp
    • EmbedCertificate
  • Phase 6.5: Async Processing (NEU!)

    • Step 6.5.1 - In-Memory Queue Setup (IJobQueue Interface + InMemoryJobQueue)
    • Step 6.5.2 - Background Worker (JobProcessorService - IHostedService)
    • Step 6.5.3 - Async Endpoints (POST ? JobId, GET ? Status)
  • Phase 7: Swagger & API Documentation (erweitert!)

    • Step 7.1 - Swagger Configuration (API-Key Support, XML Comments)
    • Step 7.2 - Response Examples (Swashbuckle)
  • Phase 8: Multi-Tenancy (DB-based!) (NEU!)

    • Step 8.1 - EF Core Setup (SQLite, Migrations)
    • Step 8.2 - Tenant Resolution Middleware (Redis Cache)
    • Step 8.3 - Tenant Admin API (CRUD)
  • Phase 9: File Storage (NEU!)

    • Step 9.1 - IFileStorage Interface
    • Step 9.2 - LocalFileStorage Implementation (lokale Temp-Ordner)
    • Step 9.3 - Temp-File Cleanup Service (IHostedService)
  • Phase 10: Logging & Monitoring (NEU!)

    • Step 10.1 - Correlation IDs (Request-Tracking)
    • Step 10.2 - Serilog Configuration (Seq + File Logging)
  • Phase 11: Production Deployment

    • appsettings.Production.json
    • IIS Deployment
    • Rate-Limiting (Redis-based)

?? Hinweise zum aktuellen Stand

  1. Infrastructure Services:

    • Ordner existieren (PdfProcessing, FileStorage, DocumentValidation)
    • Aber: Alle leer
    • ?? Action: DevExpressPdfProcessor.cs implementieren (Step 3.2 - IN PROGRESS)
  2. DevExpress Universal License:

    • ? Verfügbar! Wir können alle DevExpress Pakete nutzen
    • Aktuell nur: DevExpress.Pdf.Core
    • Bei Bedarf können weitere Pakete hinzugefügt werden

?? KEY LEARNINGS & DECISIONS

1. Domain Layer minimal halten

Entscheidung: Nur Enums + Value Objects + Exceptions (ABER: EF Core Entities in Infrastructure!)

Warum:

  • Domain = Business-Konzepte (technologie-unabhängig)
  • Tenant/TenantSettings sind Infrastructure Entities (EF Core, Navigation Properties)
  • Service-Anwendung (nicht Domain-lastig)
  • YAGNI (You Ain't Gonna Need It)

Was bedeutet das?

  • Domain/ValueObjects/TenantId.cs ? Value Object (immutable, validierend)
  • Infrastructure/Data/Entities/Tenant.cs ? EF Core Entity (mit Navigation Properties)
  • Domain kennt KEINE EF Core Dependencies!

Alternative wäre gewesen:

  • Volle Domain Models (PdfDocument, DocumentAttachment, etc.)
  • Nachteile: Overengineering, unnötige Komplexität

2. Outside-In Development

Entscheidung: Infrastructure ? Application ? API

Warum:

  • Wir sehen echten Code sofort (DevExpress Integration)
  • Keine Spekulation (wir wissen welche Exceptions geworfen werden)
  • Schnellerer Feedback-Loop

Alternative wäre gewesen:

  • Domain ? Application ? Infrastructure ? API
  • Nachteile: Viel "spekulativer" Code ohne echte Implementation

3. Exception-based Error Handling

Entscheidung: Keine Result Pattern Library

Warum:

  • Einfacherer Code (kein Result Boilerplate)
  • Zentrales Error Handling (Middleware)
  • Standard .NET Exception-Flow

Alternative wäre gewesen:

  • Ardalis.Result oder FluentResults
  • Nachteile: Extra Package, mehr Boilerplate

4. TDD (Test-Driven Development)

Entscheidung: Test ? Code ? Refactor

Warum:

  • Besseres Design (testbarer Code)
  • Tests als Dokumentation
  • Safety Net für Refactoring

Alternative wäre gewesen:

  • Code ? Test (Test-After)
  • Nachteile: Tests werden oft vergessen, schlechteres Design

5. Vertical Slice Architecture

Entscheidung: Pro Feature alles zusammen

Warum:

  • Zusammengehöriger Code ist zusammen
  • Einfacher zu finden und zu ändern
  • Besser für Teams (weniger Merge-Konflikte)

Alternative wäre gewesen:

  • Horizontal Layers (Commands/, Handlers/, Validators/)
  • Nachteile: Code über viele Ordner verteilt

6. Multi-Tenancy: Database-based (statt appsettings.json) - NEU!

Entscheidung: EF Core + SQLite für Tenant-Management

Warum:

  • Skalierbar: Neue Tenants ohne Neustart
  • Security: API-Key Hashing (BCrypt, NICHT Klartext!)
  • Audit-Log: LastUsedAt pro Tenant
  • Rate-Limiting: Redis Counter pro Tenant
  • Redis Cache: API-Key Lookups cached (Performance!)

Alternative wäre gewesen:

  • appsettings.json mit API-Keys
  • Nachteile: Nicht skalierbar, keine Rotation, kein Audit-Log

7. Async Processing: In-Memory Queue-based (für große Operationen) - NEU!

Entscheidung: In-Memory Queue + Background Worker

Warum:

  • ConcatenatePdfs von 50 PDFs = 10+ Sekunden
  • Synchron = HTTP Timeout
  • In-Memory Queue = einfach, keine Cloud-Abhängigkeiten
  • ProcessingStatus Enum wird jetzt genutzt!

Alternative wäre gewesen:

  • Alle Operationen synchron
  • Nachteile: HTTP Timeouts, nicht skalierbar

Hinweis:

  • In-Memory Queue = nicht persistent (bei Server-Neustart gehen Jobs verloren)
  • Für Production später: Redis Queue oder RabbitMQ (aber IJobQueue bleibt!)
  • Für Single-Server Setup: In-Memory Queue ausreichend

8. File Storage: Lokale Temp-Ordner (statt Cloud) - NEU!

Entscheidung: IFileStorage Interface + LocalFileStorage

Warum:

  • Einfachheit: Keine Cloud-Abhängigkeiten (Azure Blob Storage)
  • Schnell: Lokaler Dateizugriff (keine Netzwerk-Latenz)
  • Flexibel: IFileStorage Abstraction bleibt (später zu Cloud migrierbar!)
  • TempFileCleanupService: Automatische Cleanup (täglich)

Alternative wäre gewesen:

  • Cloud Storage (Azure Blob, AWS S3)
  • Nachteile: Cloud-Abhängigkeit, Kosten, Komplexität

Hinweis:

  • Für Multi-Server Setup: Shared Network Drive (UNC-Pfad) statt lokale Ordner
  • IFileStorage Interface bleibt gleich (austauschbar!)

9. Resilience: Polly (Retry, Circuit Breaker, Timeout) - NEU!

Entscheidung: Polly für DevExpress Calls

Warum:

  • Retry: Transient Errors (File Lock, Memory)
  • Circuit Breaker: DevExpress kaputt ? alle Requests sofort fehlschlagen
  • Timeout: DevExpress hängt ? nicht ewig warten

Alternative wäre gewesen:

  • Keine Resilience
  • Nachteile: Produktionsausfälle bei transient errors

10. Health Checks: FRÜH implementieren (Phase 5.5, nicht Phase 9) - NEU!

Entscheidung: Health Checks direkt nach erstem Endpoint

Warum:

  • Pflicht für Production (Load Balancer, Kubernetes)
  • /health Endpoint = Liveness/Readiness Probes
  • DevExpressPdfHealthCheck = Smoke Test (PDF validieren)

Alternative wäre gewesen:

  • Health Checks in Phase 9 (Production-Ready)
  • Nachteile: Zu spät! Load Balancer braucht Health Checks sofort

11. Logging: Correlation IDs + Seq + File Logging - NEU!

Entscheidung: Structured Logging mit Correlation IDs

Warum:

  • Correlation IDs: Request-Tracking über alle Logs (Debugging leichter)
  • Seq: Log-Browsing UI (Development)
  • File Logging: Production Logs (keine Cloud-Abhängigkeit)
  • LoggingBehavior: MediatR Pipeline Behavior (automatisches Logging)

Alternative wäre gewesen:

  • Nur Console Logging (keine Correlation IDs)
  • Nachteile: Debugging schwierig, keine Request-Zusammenhänge, keine persistente Log-Speicherung

5. Vertical Slice Architecture

Entscheidung: Pro Feature alles zusammen

Warum:

  • Zusammengehöriger Code ist zusammen
  • Einfacher zu finden und zu ändern
  • Besser für Teams (weniger Merge-Konflikte)

Alternative wäre gewesen:

  • Horizontal Layers (Commands/, Handlers/, Validators/)
  • Nachteile: Code über viele Ordner verteilt

?? REFERENCES & BEST PRACTICES

Documentation

Best Practices Applied

  • ? Clean Architecture (pragmatisch!)
  • ? CQRS with MediatR
  • ? Vertical Slice Architecture
  • ? Value Objects (DDD)
  • ? Exception-based Error Handling
  • ? Minimal APIs (.NET 8)
  • ? TDD (Test-Driven Development)
  • ? Resilience Patterns (Polly) - NEU: Retry, Circuit Breaker, Timeout
  • ? Multi-Tenancy (Database-based) - NEU: EF Core + SQLite + Redis Cache
  • ? Async Processing (In-Memory Queue) - NEU: In-Memory Queue + Background Worker
  • ? File Storage Abstraction (Lokale Temp-Ordner) - NEU: IFileStorage + LocalFileStorage
  • ? Correlation IDs - NEU: Request-Tracking über alle Logs
  • ? Health Checks - NEU: Load Balancer Support
  • ? Options Pattern für Configuration
  • ? Dependency Injection
  • ? Async/Await überall
  • ? Nullable Reference Types
  • ? Record Types für DTOs (C# 12)
  • ? Primary Constructors (.NET 8)
  • ? Structured Logging (Serilog + Seq + File Logging)

?? UPDATE LOG

Date Phase Changes
2024-XX-XX Phase 1 Project setup, dependencies, folder structure
2024-XX-XX Phase 1 Configuration, Serilog, Options Pattern
2024-XX-XX Phase 1 ? Phase 1 completed
2024-XX-XX Phase 2 ? Step 2.1 completed - Domain Exceptions created
17.01.2025 Roadmap ?? ROADMAP komplett überarbeitet (Pragmatisch, Outside-In, TDD)
17.01.2025 Phase 2 ? Step 2.2 completed - Enums erstellt
17.01.2025 Phase 2 ? Step 2.3 completed - Value Objects erstellt
17.01.2025 Phase 2 ? Phase 2 (Domain Layer) komplett abgeschlossen!
17.01.2025 Phase 3 ? Step 3.1 completed - IPdfProcessor Interface erstellt
17.01.2025 Roadmap ?? ROADMAP Status-Update - Aktueller Projektstand dokumentiert
17.01.2025 Infrastructure ?? DevExpress Universal License hinzugefügt - Vollzugriff auf alle Pakete
17.01.2025 Phase 3 ?? Step 3.2 gestartet - DevExpressPdfProcessor (TDD)
17.01.2025 Application ? Step 3.2.1 - ProcessDocument Ordner gelöscht (Cleanup)
17.01.2025 Tests ? Step 3.2.2 - Test-Ordnerstruktur erstellt, UnitTest1.cs gelöscht
17.01.2025 Tests ? Step 3.2.3 - Test-PDF (valid.pdf) als Embedded Resource hinzugefügt
17.01.2025 Tests ? Step 3.2.4 - DevExpressPdfProcessorTests.cs erstellt (TDD Red - 6 Tests)
17.01.2025 Infrastructure ? Step 3.2.5 - DevExpressPdfProcessor.cs implementiert (TDD Green Phase)
17.01.2025 Phase 3 ? Step 3.2 abgeschlossen - DevExpressPdfProcessor vollständig implementiert!
17.01.2025 Roadmap ?? ROADMAP Status-Update - Phase 3 abgeschlossen, bereit für Phase 4!
17.01.2025 Roadmap ?? ROADMAP MAJOR UPDATE - Production-Ready Features hinzugefügt!
17.01.2025 Architecture ? Multi-Tenancy: Database-based (EF Core + SQLite + Redis Cache)
17.01.2025 Architecture ? Async Processing: In-Memory Queue-based + Background Worker
17.01.2025 Architecture ? File Storage: Lokale Temp-Ordner mit IFileStorage Abstraction
17.01.2025 Architecture ? Resilience: Polly (Retry, Circuit Breaker, Timeout)
17.01.2025 Architecture ? Health Checks: Früh implementieren (Phase 5.5 statt Phase 9)
17.01.2025 Architecture ? Logging: Correlation IDs + Seq + File Logging
17.01.2025 Technology Stack ? 9 neue NuGet Packages hinzugefügt (EF Core, Polly, BCrypt, Seq - OHNE Azure)
17.01.2025 Roadmap ? 6 neue Phasen (5.5, 6.5, 8, 9, 10, 11) - insgesamt 11 Phasen statt 9
17.01.2025 Documentation ? 10 Key Learnings & Decisions dokumentiert (statt 5)
17.01.2025 Roadmap ?? Azure Services entfernt - Lokale Temp-Ordner + In-Memory Queue stattdessen
22.06.2026 Dokumentation ? Azure-Referenzen vollständig entfernt - Alle Azure-Referenzen bereinigt (PROJECT_STATUS.md + ROADMAP.md)

END OF ROADMAP

This document is a living document and will be updated as development progresses.