# ?? DocumentOperator - Project Roadmap (Pragmatic Edition) > **Last Updated:** 17.01.2025 (Updated with Resilience, Async Processing, Multi-Tenancy DB) | **Status:** In Development | **Phase:** 3 (Infrastructure Layer) --- ## ?? 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:** Queue-based (Azure Storage Queue + Background Worker) für große Operationen 3. **? File Storage:** Azure Blob Storage + IFileStorage Abstraction (Multi-Server fähig!) 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 + Application Insights 7. **? 11 neue NuGet Packages:** EF Core, Polly, Azure Storage, 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?** - **Skalierbarkeit:** Multi-Server Support (Azure Blob, Redis Cache) - **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, Application Insights - **Wartbarkeit:** Clean Architecture bleibt pragmatisch, aber production-ready! --- ## ?? TABLE OF CONTENTS 1. [Project Overview](#project-overview) 2. [Architecture & Design Decisions](#architecture--design-decisions) 3. [Development Philosophy](#development-philosophy) 4. [Technology Stack](#technology-stack) 5. [Project Structure](#project-structure) 6. [Development Roadmap](#development-roadmap) 7. [Testing Strategy](#testing-strategy) 8. [Current Status](#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) ? Azure Storage 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 Azure Blob Storage gespeichert (Multi-Server!) 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:** ```csharp // Query (Read-Only) public record ValidatePdfQuery(Base64String PdfContent) : IRequest; // Handler public class ValidatePdfHandler : IRequestHandler { private readonly IPdfProcessor _processor; public async Task 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:** ```csharp 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:** ```csharp // 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 LogoBlobPath { get; set; } // Azure Blob Storage Pfad public string CertificateBlobPath { get; set; } // Azure Blob Storage Pfad public int RateLimitPerMinute { get; set; } // Rate-Limiting // Navigation public Tenant Tenant { get; set; } } ``` **Beispiel - Tenant-spezifischer Stamp:** ```csharp public class ApplyStampHandler : IRequestHandler { private readonly ITenantContext _tenantContext; private readonly IFileStorage _fileStorage; // Azure Blob Storage public async Task Handle(ApplyStampCommand command, CancellationToken ct) { // Tenant-spezifisches Logo aus DB Settings laden var logoPath = _tenantContext.CurrentTenant.Settings.LogoBlobPath; // Logo aus Azure Blob Storage 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 | | **Azure.Storage.Blobs** | 12.22.3 | **NEU:** Azure Blob Storage (File Storage) | | **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 - **Azure Blob Storage:** Multi-Server File Storage (Logos, Zertifikate, Temp-Files) - **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 (Production: Application Insights) - **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 (Azure Storage 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/ ? ? ??? AzureBlobFileStorage.cs ? **NEU:** IFileStorage Implementation (Azure Blob) ? ? ??? LocalFileStorage.cs ? **NEU:** IFileStorage Implementation (Dev/Test) ? ??? Queue/ ? ??? AzureStorageJobQueue.cs ? **NEU:** IJobQueue Implementation (Azure Storage 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 ? ??? AzureBlobSettings.cs ? **NEU:** Azure Blob Storage Configuration ? ??? AzureQueueSettings.cs ? **NEU:** Azure Storage Queue Configuration ? ??? RedisSettings.cs ? **NEU:** Redis Cache Configuration ??? DependencyInjection.cs ? Service Registration ``` **Was gehört hierher:** - ? DevExpress Integration (mit Polly Resilience!) - ? **File Storage:** Azure Blob Storage + Local File Storage (Abstraction!) - ? **Queue:** Azure Storage Queue für Async Processing - ? **Datenbank:** EF Core + SQLite (Tenant-Management) - ? **Background Services:** Job Processing, Temp-File Cleanup - ? Options Pattern Classes (Settings) - ? 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:** - [x] Solution erstellt (4 Projekte) - [x] Dependencies korrekt (Clean Architecture Dependency Rule) - [x] NuGet Packages installiert - [x] Folder-Struktur erstellt - [x] appsettings.json konfiguriert - [x] Options Pattern Classes erstellt - [x] 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. [x] `DomainException.cs` (Basis-Exception) 2. [x] `DomainValidationException.cs` (Value Object Validierung) 3. [x] `NotFoundException.cs` (Resource nicht gefunden) 4. [x] `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:** ```csharp 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:** ```csharp 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:** ```csharp using DocumentOperator.Domain.Models.ValueObjects; namespace DocumentOperator.Application.Common.Interfaces; public interface IPdfProcessor { Task 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) ```csharp [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) ```csharp public class DevExpressPdfProcessor : IPdfProcessor { public async Task 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** ```csharp public record ValidatePdfQuery(Base64String PdfContent) : IRequest; ``` 2. **ValidatePdfHandler.cs** ```csharp public class ValidatePdfHandler : IRequestHandler { private readonly IPdfProcessor _processor; public async Task Handle(ValidatePdfQuery query, CancellationToken ct) { byte[] bytes = query.PdfContent.ToByteArray(); return await _processor.ValidateAsync(bytes); } } ``` 3. **ValidatePdfValidator.cs** (FluentValidation) ```csharp public class ValidatePdfValidator : AbstractValidator { 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:** ```csharp 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:** ```csharp 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 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:** ```csharp 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:** ```csharp public class ValidatePdfEndpointTests : IClassFixture> { [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(); 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** ```csharp public class DevExpressPdfHealthCheck : IHealthCheck { private readonly IPdfProcessor _processor; public async Task 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:** ```csharp builder.Services.AddHealthChecks() .AddCheck("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):** ```csharp 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() .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); // Circuit Breaker: Nach 5 Fehlern für 30 Sekunden öffnen _circuitBreakerPolicy = Policy .Handle() .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)); // Timeout: 30 Sekunden max _timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(30)); } public async Task 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:** ```csharp _retryPolicy = Policy .Handle() .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 (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 horizontal skalieren) --- #### ? Step 6.5.1: Azure Storage Queue Setup **Aufgabe:** Queue für Async Jobs **Was du erstellen wirst:** 1. **IJobQueue Interface (Application):** ```csharp public interface IJobQueue { Task EnqueueAsync(T jobData) where T : class; Task GetStatusAsync(JobId jobId); } public record JobStatus( JobId JobId, ProcessingStatus Status, int Progress, string? ResultBlobPath, string? ErrorMessage); ``` 2. **AzureStorageJobQueue Implementation (Infrastructure):** ```csharp public class AzureStorageJobQueue : IJobQueue { private readonly QueueClient _queueClient; private readonly TableClient _tableClient; // Job Status Tracking public async Task EnqueueAsync(T jobData) { var jobId = JobId.Create(Guid.NewGuid().ToString()); // Job Message in Queue await _queueClient.SendMessageAsync(JsonSerializer.Serialize(jobData)); // Job Status in Table Storage (Pending) await _tableClient.AddEntityAsync(new JobStatusEntity { PartitionKey = jobId.Value, RowKey = jobId.Value, Status = ProcessingStatus.Pending }); return jobId; } } ``` --- #### ? Step 6.5.2: Background Worker (Job Processor) **Aufgabe:** IHostedService für Job-Verarbeitung **Was du erstellen wirst:** 1. **JobProcessorService.cs (Infrastructure/BackgroundServices/):** ```csharp public class JobProcessorService : BackgroundService { private readonly QueueClient _queueClient; private readonly IPdfProcessor _pdfProcessor; private readonly IFileStorage _fileStorage; protected override async Task ExecuteAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { // Queue Message abrufen var message = await _queueClient.ReceiveMessageAsync(); if (message.Value != null) { // Job verarbeiten var jobData = JsonSerializer.Deserialize(message.Value.Body); try { // PDF-Operation var result = await _pdfProcessor.ConcatenateAsync(jobData.PdfFiles); // Ergebnis in Blob Storage var blobPath = await _fileStorage.SaveAsync(result, $"results/{jobData.JobId}.pdf"); // Job Status: Success await UpdateJobStatusAsync(jobData.JobId, ProcessingStatus.Success, blobPath); // Message löschen await _queueClient.DeleteMessageAsync(message.Value.MessageId, message.Value.PopReceipt); } catch (Exception ex) { // Job Status: Failed await UpdateJobStatusAsync(jobData.JobId, ProcessingStatus.Failed, errorMessage: ex.Message); } } await Task.Delay(TimeSpan.FromSeconds(1), ct); // Polling-Interval } } } ``` 2. **Program.cs Registration:** ```csharp builder.Services.AddHostedService(); ``` --- #### ? Step 6.5.3: Async Endpoints **Aufgabe:** POST ? JobId, GET ? JobStatus **Was du erstellen wirst:** 1. **POST /api/v1/documents/concatenate (Async):** ```csharp 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}:** ```csharp 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 (ResultBlobPath: "/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 Blob Storage ``` --- ### ? 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:** ```csharp 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() } }); // XML Comments var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); // Example Schemas c.SchemaFilter(); }); ``` 2. **XML Comments in Endpoints:** ```csharp /// /// Validates a PDF document and returns metadata /// /// PDF as Base64 string /// PDF metadata (page count, file size, etc.) /// PDF is valid, metadata returned /// Invalid PDF or Base64 format /// Internal server error during validation 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:** ```csharp 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:** ```csharp public class TenantDbContext : DbContext { public DbSet Tenants { get; set; } public DbSet TenantSettings { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(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(e => e.TenantId); }); } } ``` 2. **EF Core Migration:** ```bash 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:** ```csharp 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(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(); 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):** ```csharp 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 (Azure Blob) - **NEW!** **Ziel:** Multi-Server fähiges File Storage **Steps:** #### ? Step 9.1: IFileStorage Interface **Aufgabe:** Abstraction für File Storage **Was du erstellen wirst:** 1. **IFileStorage.cs (Application/Common/Interfaces/):** ```csharp public interface IFileStorage { Task SaveAsync(byte[] content, string filename); Task GetAsync(string path); Task DeleteAsync(string path); Task ExistsAsync(string path); } ``` --- #### ? Step 9.2: Azure Blob Storage Implementation **Aufgabe:** Azure Blob Storage Provider **Was du erstellen wirst:** 1. **AzureBlobFileStorage.cs (Infrastructure/Services/FileStorage/):** ```csharp public class AzureBlobFileStorage : IFileStorage { private readonly BlobContainerClient _containerClient; public AzureBlobFileStorage(AzureBlobSettings settings) { var serviceClient = new BlobServiceClient(settings.ConnectionString); _containerClient = serviceClient.GetBlobContainerClient(settings.ContainerName); _containerClient.CreateIfNotExists(); } public async Task SaveAsync(byte[] content, string filename) { var blobClient = _containerClient.GetBlobClient(filename); await blobClient.UploadAsync(new BinaryData(content), overwrite: true); return blobClient.Uri.ToString(); } public async Task GetAsync(string path) { var blobClient = _containerClient.GetBlobClient(path); var response = await blobClient.DownloadContentAsync(); return response.Value.Content.ToArray(); } public async Task DeleteAsync(string path) { var blobClient = _containerClient.GetBlobClient(path); await blobClient.DeleteIfExistsAsync(); } } ``` 2. **LocalFileStorage.cs (für Dev/Test):** ```csharp public class LocalFileStorage : IFileStorage { private readonly string _basePath; public LocalFileStorage() { _basePath = Path.Combine(Directory.GetCurrentDirectory(), "LocalStorage"); Directory.CreateDirectory(_basePath); } public async Task 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 GetAsync(string path) { return await File.ReadAllBytesAsync(path); } public Task DeleteAsync(string path) { File.Delete(path); return Task.CompletedTask; } } ``` --- #### ? Step 9.3: Temp-File Cleanup Service **Aufgabe:** IHostedService für tägliche Cleanup **Was du erstellen wirst:** 1. **TempFileCleanupService.cs (Infrastructure/BackgroundServices/):** ```csharp public class TempFileCleanupService : BackgroundService { private readonly IFileStorage _fileStorage; protected override async Task ExecuteAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { // Täglich um 3 Uhr nachts var now = DateTime.UtcNow; var next3AM = now.Date.AddDays(1).AddHours(3); var delay = next3AM - now; await Task.Delay(delay, ct); // Temp-Files älter als 24h löschen var tempFiles = await _fileStorage.ListAsync("temp/"); foreach (var file in tempFiles) { if (file.CreatedAt < DateTime.UtcNow.AddHours(-24)) { await _fileStorage.DeleteAsync(file.Path); Log.Information("Deleted temp file: {Path}", file.Path); } } } } } ``` --- ### ? 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:** ```csharp 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):** ```csharp public class LoggingBehavior : IPipelineBehavior { public async Task Handle(TRequest request, RequestHandlerDelegate 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:** ```csharp 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 (Application Insights):** ```json { "Serilog": { "WriteTo": [ { "Name": "ApplicationInsights", "Args": { "connectionString": "InstrumentationKey=..." } } ] } } ``` --- ### ? PHASE 11: Production Deployment - **UPDATED!** **Ziel:** IIS Deployment + Production Configuration **Steps:** - [ ] appsettings.Production.json (Azure Blob, Redis, Application Insights) - [ ] IIS Web.config (Kestrel Settings) - [ ] SSL/TLS Configuration - [ ] Redis Connection String (Production) - [ ] Azure Blob Storage Connection String - [ ] Health Checks für Kubernetes/Load Balancer - [ ] Rate-Limiting (Redis-based, pro Tenant) --- ## ?? 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:** ```csharp // 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 - **IN PROGRESS**) - ? 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) ### ?? In Progress - **Phase 3, Step 3.2:** DevExpressPdfProcessor (TDD) - **NEXT:** Step 3.2.5 - DevExpressPdfProcessor.cs implementieren (TDD Green Phase) - **Progress:** 4/7 Mini-Steps abgeschlossen ### ? Pending - **Phase 3:** Infrastructure Layer - Step 3.2 - DevExpressPdfProcessor Implementation (mit Polly Resilience!) - **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 - Azure Storage Queue Setup - Step 6.5.2 - Background Worker (JobProcessorService) - 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 - Azure Blob Storage + Local File Storage - Step 9.3 - Temp-File Cleanup Service - **Phase 10:** Logging & Monitoring (**NEU!**) - Step 10.1 - Correlation IDs (Request-Tracking) - Step 10.2 - Serilog Configuration (Seq + Application Insights) - **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: Queue-based (für große Operationen) - **NEU!** **Entscheidung:** Azure Storage Queue + Background Worker **Warum:** - ConcatenatePdfs von 50 PDFs = 10+ Sekunden - Synchron = HTTP Timeout - Queue = skalierbar (Worker horizontal skalieren) - ProcessingStatus Enum wird jetzt genutzt! **Alternative wäre gewesen:** - Alle Operationen synchron - **Nachteile:** HTTP Timeouts, nicht skalierbar --- ### 8. File Storage: Azure Blob (statt Local Files) - **NEU!** **Entscheidung:** IFileStorage Interface + Azure Blob + Local (Dev) **Warum:** - **Multi-Server:** Load Balancer mit 3 API-Instanzen - **Shared Storage:** Azure Blob = alle Server greifen auf gleiche Files zu - **Abstraction:** LocalFileStorage für Dev/Test - **Automatic Cleanup:** TempFileCleanupService (IHostedService) **Alternative wäre gewesen:** - Temp-Files auf lokalem Server - **Nachteile:** Multi-Server nicht möglich, Disk voll nach 1 Monat --- ### 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 + Application Insights - **NEU!** **Entscheidung:** Structured Logging mit Correlation IDs **Warum:** - **Correlation IDs:** Request-Tracking über alle Logs (Debugging leichter) - **Seq:** Log-Browsing UI (Development) - **Application Insights:** Production Monitoring (Azure) - **LoggingBehavior:** MediatR Pipeline Behavior (automatisches Logging) **Alternative wäre gewesen:** - Nur File Logging (keine Correlation IDs) - **Nachteile:** Debugging schwierig, keine Request-Zusammenhänge ### 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 - [Clean Architecture (Uncle Bob)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [MediatR Documentation](https://github.com/jbogard/MediatR) - [FluentValidation Docs](https://docs.fluentvalidation.net/) - [DevExpress PDF API](https://docs.devexpress.com/OfficeFileAPI/114877/pdf-document-api) - [ASP.NET Core Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis) - [RFC 7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807) - [xUnit Documentation](https://xunit.net/) - [FluentAssertions Documentation](https://fluentassertions.com/) - [Polly Documentation](https://www.pollydocs.org/) - **NEU:** Resilience Patterns - [EF Core Documentation](https://learn.microsoft.com/en-us/ef/core/) - **NEU:** ORM für Tenant-DB - [Azure Blob Storage Documentation](https://learn.microsoft.com/en-us/azure/storage/blobs/) - **NEU:** File Storage - [Azure Storage Queue Documentation](https://learn.microsoft.com/en-us/azure/storage/queues/) - **NEU:** Async Processing - [Serilog Documentation](https://serilog.net/) - **NEU:** Structured Logging - [Seq Documentation](https://docs.datalust.co/docs) - **NEU:** Log Browsing UI ### 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 (Queue-based)** - **NEU:** Azure Storage Queue + Background Worker - ? **File Storage Abstraction (Azure Blob)** - **NEU:** Multi-Server fähig - ? **Correlation IDs** - **NEU:** Request-Tracking über alle Logs - ? **Health Checks** - **NEU:** Kubernetes/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 + Application Insights) --- ## ?? 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 | 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:** Queue-based (Azure Storage Queue + Background Worker) | | 17.01.2025 | Architecture | ? **File Storage:** Azure Blob Storage + 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 + Application Insights | | 17.01.2025 | Technology Stack | ? **11 neue NuGet Packages hinzugefügt** (EF Core, Polly, Azure Storage, BCrypt, Seq) | | 17.01.2025 | Roadmap | ? **6 neue Phasen** (5.5, 6.5, 8, 9, 10, 11) - insgesamt 11 Phasen statt 9 | | 17.01.2025 | Documentation | ? **11 Key Learnings & Decisions** dokumentiert (statt 5) --- **END OF ROADMAP** *This document is a living document and will be updated as development progresses.*