From d50e30f7ac33900403ca40e6cd2523a36f029478 Mon Sep 17 00:00:00 2001 From: OlgunR Date: Mon, 22 Jun 2026 13:35:48 +0200 Subject: [PATCH] Update roadmap with production-ready architecture The roadmap has been updated to reflect a shift towards a scalable, resilient, and production-ready architecture. Key changes include: - Multi-tenancy with EF Core, SQLite, and Redis Cache. - Async processing using Azure Storage Queue and workers. - File storage abstraction with Azure Blob and local storage. - Resilience with Polly (retry, circuit breaker, timeout). - Early health checks for Kubernetes readiness/liveness. - Enhanced logging with Correlation IDs, Seq, and App Insights. - Expanded roadmap to 11 phases with new production features. - Added Swagger updates for API-Key auth and response examples. - Introduced EF Core tenant management with CRUD operations. - Added background services for async jobs and temp cleanup. - Updated testing strategy for resilience and async processing. - Documented 11 key learnings and updated best practices. --- DocumentOperator.API/ROADMAP.md | 1198 +++++++++++++++++++++++++++++-- 1 file changed, 1130 insertions(+), 68 deletions(-) diff --git a/DocumentOperator.API/ROADMAP.md b/DocumentOperator.API/ROADMAP.md index 847476e..7331d04 100644 --- a/DocumentOperator.API/ROADMAP.md +++ b/DocumentOperator.API/ROADMAP.md @@ -1,6 +1,31 @@ # ?? DocumentOperator - Project Roadmap (Pragmatic Edition) -> **Last Updated:** 17.01.2025 | **Status:** In Development | **Phase:** 2 (Domain Layer - Minimal) +> **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! --- @@ -72,10 +97,13 @@ Der Service bietet folgende PDF-Operationen: ### Business Workflow +**Synchroner Flow (kleine Operationen < 5 Sekunden):** ``` Client Application ? -[HTTP Request] - JSON mit Base64-PDF +[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) ? @@ -84,13 +112,29 @@ DocumentOperator API (Minimal API Endpoint) [HTTP Response] - JSON mit verarbeitetem PDF (Base64) ``` -**Typischer Ablauf:** -1. Client sendet PDF als Base64 in JSON -2. API validiert Input (FluentValidation in MediatR Pipeline) -3. Handler konvertiert PDF ? Byte-Array -4. DevExpress Service führt Operation durch -5. Temporäre Dateien werden erstellt/bereinigt (falls nötig) -6. Ergebnis wird als Base64 zurückgegeben +**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) --- @@ -348,45 +392,92 @@ app.MapPost("/api/v1/documents/validate", async ( --- -### Multi-Tenancy via API-Keys +### 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 API-Keys?** -- ? Einfach für Service-to-Service Communication -- ? Security + Tenant-Identification kombiniert -- ? Swagger-kompatibel (für BB-Tests) -- ? Einfaches Rate-Limiting pro Tenant (später) +**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 (wird später implementiert) +TenantResolutionMiddleware ? -API-Key validieren (aus appsettings.json oder Redis) +Redis Cache Lookup (Key: "tenant:abc123") + ? Cache Hit: Tenant-Info geladen (1ms) + ? Cache Miss: DB Lookup ? Redis Cache befüllen (TTL: 1 Stunde) ? -Tenant-Info auflösen (TenantId, TenantName, IsActive) +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 laden - var logoPath = $"logos/{_tenantContext.TenantId}/stamp.png"; + // 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 // ... @@ -394,6 +485,12 @@ public class ApplyStampHandler : IRequestHandler } ``` +**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 @@ -417,9 +514,11 @@ public class ApplyStampHandler : IRequestHandler | **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 (später) | +| **Microsoft.Extensions.Caching.StackExchangeRedis** | 8.0.28 | Redis Cache (Tenant-Lookups, Rate-Limiting) | #### Application Layer @@ -435,6 +534,12 @@ public class ApplyStampHandler : IRequestHandler |---------|---------|---------| | **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:** @@ -445,6 +550,14 @@ public class ApplyStampHandler : IRequestHandler - `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 | @@ -531,23 +644,37 @@ DocumentOperator.API/ DocumentOperator.Application/ ??? Features/ ? Vertical Slices ? ? ??? Documents/ -? ??? ValidatePdf/ -? ? ??? ValidatePdfQuery.cs -? ? ??? ValidatePdfHandler.cs -? ? ??? ValidatePdfValidator.cs -? ??? ExtractAttachments/ -? ? ??? ExtractAttachmentsCommand.cs -? ? ??? ExtractAttachmentsHandler.cs -? ? ??? ExtractAttachmentsValidator.cs -? ??? ... (weitere Features iterativ) +? ? ??? 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 +? ? ??? 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 ``` @@ -586,17 +713,38 @@ DocumentOperator.Application/ DocumentOperator.Infrastructure/ ??? Services/ ? ??? PdfProcessing/ -? ??? DevExpressPdfProcessor.cs ? IPdfProcessor Implementation +? ? ??? 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 -? ??? ApiKeySettings.cs -? ??? TenantInfo.cs -??? DependencyInjection.cs ? Service Registration +? ??? 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 -- ? File System Zugriffe (Temp-Files) +- ? 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:** @@ -620,9 +768,10 @@ DocumentOperator.Domain/ ? ??? Base64String.cs ? ??? TenantId.cs ? ??? PdfMetadata.cs +? ??? JobId.cs ? **NEU:** Job-ID für Async Processing ??? Enums/ ? ??? DocumentOperationType.cs -? ??? ProcessingStatus.cs +? ??? ProcessingStatus.cs ? (Wird jetzt für Async Jobs genutzt!) ??? Exceptions/ ? Domain-spezifische Exceptions ??? DomainException.cs ??? DomainValidationException.cs @@ -631,10 +780,15 @@ DocumentOperator.Domain/ ``` **Was gehört hierher:** -- ? Value Objects (Base64String, TenantId, PdfMetadata) +- ? 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 @@ -1148,6 +1302,125 @@ app.MapDocumentEndpoints(); --- +### ? 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):** @@ -1160,45 +1433,663 @@ Jedes Feature folgt dem gleichen Pattern: 5. Integration Test **Features:** -- [ ] ExtractAttachments -- [ ] ConcatenatePdfs -- [ ] ApplyStamp -- [ ] EmbedCertificate +- [ ] ExtractAttachments (synchron) +- [ ] ConcatenatePdfs (asynchron - siehe Phase 6.5!) +- [ ] ApplyStamp (synchron) +- [ ] EmbedCertificate (synchron) --- -### ? PHASE 7: Swagger & API Documentation +### ? 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:** -- [ ] Swagger Configuration (API-Key Support) -- [ ] XML Comments für Endpoints -- [ ] Response Examples (Swashbuckle) + +#### ? 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(); + ``` --- -### ? PHASE 8: Multi-Tenancy (später) +#### ? Step 7.2: Response Examples (Swashbuckle) -**Ziel:** Mandantenfähigkeit implementieren +**Aufgabe:** Beispiel-Responses in Swagger -**Steps:** -- [ ] ITenantContext Interface (Application) -- [ ] TenantContext Implementation (Infrastructure) -- [ ] TenantResolutionMiddleware (API) -- [ ] Tenant-spezifische Settings (Logo, Zertifikat) +**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 9: Production-Ready +### ? PHASE 8: Multi-Tenancy (Database-based) - **UPDATED!** -**Ziel:** Deployment vorbereiten +**Ziel:** Mandantenfähigkeit mit DB + Redis **Steps:** -- [ ] Health Checks -- [ ] appsettings.Production.json -- [ ] IIS Deployment (Web.config) -- [ ] Redis Integration (Distributed Cache) + +#### ? 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) --- @@ -1330,10 +2221,10 @@ public async Task POST_ValidatePdf_InvalidPdf_Returns400() { } ### ? Pending - **Phase 3:** Infrastructure Layer - - Step 3.2 - DevExpressPdfProcessor Implementation + - Step 3.2 - DevExpressPdfProcessor Implementation (mit Polly Resilience!) - **Phase 4:** Application Layer - - Step 4.1 - MediatR Setup (DependencyInjection.cs, ValidationBehavior.cs) + - Step 4.1 - MediatR Setup (DependencyInjection.cs, ValidationBehavior.cs, LoggingBehavior.cs) - Step 4.2 - ValidatePdf Feature (Query, Handler, Validator) - **Phase 5:** API Layer @@ -1341,7 +2232,42 @@ public async Task POST_ValidatePdf_InvalidPdf_Returns400() { } - Step 5.2 - Minimal API Endpoint - Step 5.3 - Integration Test -- **Phase 6-9:** Weitere Features, Swagger, Multi-Tenancy, Production +- **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 @@ -1361,13 +2287,19 @@ public async Task POST_ValidatePdf_InvalidPdf_Returns400() { } ### 1. Domain Layer minimal halten -**Entscheidung:** Nur Enums + Value Objects + Exceptions +**Entscheidung:** Nur Enums + Value Objects + Exceptions (ABER: EF Core Entities in Infrastructure!) **Warum:** -- Kein EF Core / Entities +- 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 @@ -1434,6 +2366,114 @@ public async Task POST_ValidatePdf_InvalidPdf_Returns400() { } --- +### 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 @@ -1446,6 +2486,12 @@ public async Task POST_ValidatePdf_InvalidPdf_Returns400() { } - [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 @@ -1456,13 +2502,19 @@ public async Task POST_ValidatePdf_InvalidPdf_Returns400() { } - ? 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) +- ? Structured Logging (Serilog + Seq + Application Insights) --- @@ -1486,6 +2538,16 @@ public async Task POST_ValidatePdf_InvalidPdf_Returns400() { } | 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) ---