Replaced Azure Blob Storage and Storage Queue with local temp folders and an in-memory queue for file storage and async processing. Updated `IFileStorage` and `IJobQueue` interfaces to support the new architecture. Modified `TenantSettings` and `ApplyStampHandler` to use local file paths. Updated `JobProcessorService` to handle in-memory queue jobs. Added file cleanup policies to `LocalFileStorage`. Revised roadmap and documentation to reflect the shift to local-first architecture, emphasizing simplicity, reduced cloud dependencies, and single-server readiness. Logging now uses file-based storage instead of Application Insights. Adjusted production deployment and health check phases to align with the new approach.
2612 lines
80 KiB
Markdown
2612 lines
80 KiB
Markdown
# ?? DocumentOperator - Project Roadmap (Pragmatic Edition)
|
|
|
|
> **Last Updated:** 22.06.2026 (Azure-Referenzen vollständig entfernt) | **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:** In-Memory Queue-based + Background Worker für große Operationen
|
|
3. **? File Storage:** Lokale Temp-Ordner mit IFileStorage Abstraction
|
|
4. **? Resilience:** Polly (Retry, Circuit Breaker, Timeout) für DevExpress Calls
|
|
5. **?? Health Checks:** FRÜH implementieren (Phase 5.5 statt Phase 9)
|
|
6. **? Logging:** Correlation IDs + Seq + File Logging
|
|
7. **? 9 neue NuGet Packages:** EF Core, Polly, BCrypt, Seq, Correlation IDs
|
|
8. **? 6 neue Phasen:** 5.5, 6.5, 8, 9, 10, 11 (insgesamt 11 Phasen statt 9)
|
|
9. **? 11 Key Learnings:** Dokumentiert (statt 5)
|
|
10. **? Technology Stack:** Komplett aktualisiert mit allen neuen Dependencies
|
|
|
|
**Warum diese Änderungen?**
|
|
- **Einfachheit:** Lokale Temp-Ordner (keine Cloud-Abhängigkeiten)
|
|
- **Security:** API-Key Hashing (BCrypt), Rotation möglich
|
|
- **Performance:** Async Processing für große Operationen (keine HTTP Timeouts)
|
|
- **Resilience:** Production-ready (Polly Retry/Circuit Breaker)
|
|
- **Monitoring:** Correlation IDs, Seq, File Logging
|
|
- **Wartbarkeit:** Clean Architecture bleibt pragmatisch, aber production-ready!
|
|
|
|
---
|
|
|
|
## ?? TABLE OF CONTENTS
|
|
|
|
1. [Project Overview](#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) ? In-Memory Queue ? Verarbeitung
|
|
?
|
|
Client Poll: [GET /api/v1/jobs/abc123] ? { "status": "Processing", "progress": 45% }
|
|
?
|
|
Client Poll: [GET /api/v1/jobs/abc123] ? { "status": "Success", "resultUrl": "/download/xyz" }
|
|
```
|
|
|
|
**Typischer Ablauf (Synchron):**
|
|
1. Client sendet PDF als Base64 in JSON + API-Key Header
|
|
2. Tenant Resolution Middleware validiert API-Key (DB-Lookup mit Redis Cache)
|
|
3. API validiert Input (FluentValidation in MediatR Pipeline)
|
|
4. Handler konvertiert PDF ? Byte-Array
|
|
5. DevExpress Service führt Operation durch (mit Polly Retry/Circuit Breaker)
|
|
6. Ergebnis wird in lokalem Temp-Ordner gespeichert
|
|
7. Ergebnis wird als Base64 zurückgegeben (oder Download-Link)
|
|
|
|
---
|
|
|
|
## ??? ARCHITECTURE & DESIGN DECISIONS
|
|
|
|
### Clean Architecture (Pragmatisch!)
|
|
|
|
Wir verwenden **Clean Architecture** mit 4 Layers - **ABER: pragmatisch, nicht dogmatisch!**
|
|
|
|
```
|
|
???????????????????????????????????????
|
|
? API Layer (Endpoints) ? ? HTTP Entry Point
|
|
???????????????????????????????????????
|
|
? Application Layer (Use Cases) ? ? MediatR Handlers, DTOs
|
|
???????????????????????????????????????
|
|
? Infrastructure Layer (Tech Stack) ? ? DevExpress, File I/O
|
|
???????????????????????????????????????
|
|
? Domain Layer (MINIMAL!) ? ? Nur Enums + Value Objects
|
|
???????????????????????????????????????
|
|
```
|
|
|
|
#### Dependency Rule
|
|
|
|
**Abhängigkeiten zeigen immer nach innen:**
|
|
|
|
```
|
|
API ? Application ? Domain
|
|
API ? Infrastructure ? Domain
|
|
Infrastructure ? Application (für Interfaces)
|
|
|
|
Domain ? NICHTS! (No External Dependencies)
|
|
Application ? NUR Domain
|
|
```
|
|
|
|
**Warum Clean Architecture?**
|
|
- ? Testbarkeit (Application Layer kann Services mocken)
|
|
- ? Austauschbarkeit (DevExpress ? anderes PDF-Lib ohne Application zu ändern)
|
|
- ? Separation of Concerns (jede Schicht hat klare Verantwortung)
|
|
|
|
**ABER:**
|
|
- ? Kein Overengineering (nur was wir wirklich brauchen!)
|
|
- ? Keine spekulativen Abstraktionen (erst wenn 2. Use Case es braucht)
|
|
- ? Keine unnötigen Klassen (YAGNI - You Ain't Gonna Need It)
|
|
|
|
---
|
|
|
|
### Domain Layer - Warum so minimal?
|
|
|
|
**Was wir NICHT haben:**
|
|
- ? Keine Datenbank / EF Core
|
|
- ? Keine komplexen Entities mit Business-Logik
|
|
- ? Keine Aggregate Roots, Repositories, etc.
|
|
|
|
**Was wir SIND:**
|
|
- ? Ein **Service** (nicht eine Domain-lastige Business-Anwendung)
|
|
- ? PDF-Operationen = technische Operationen (nicht fachliche Geschäftslogik)
|
|
- ? Daten fließen durch (Input ? Verarbeitung ? Output)
|
|
|
|
**Deshalb: Domain Layer minimal!**
|
|
|
|
**Was bleibt in Domain:**
|
|
1. **Enums** (DocumentOperationType, ProcessingStatus)
|
|
- Pure Business-Konzepte
|
|
- Technologie-unabhängig
|
|
- Wiederverwendbar über alle Layer
|
|
|
|
2. **Value Objects** (Base64String, TenantId, PdfMetadata)
|
|
- Typsicherheit (Base64String statt string)
|
|
- Selbst-validierend (Fehler werfen im Constructor)
|
|
- Immutable (keine Änderungen nach Erstellung)
|
|
|
|
3. **Domain Exceptions** (DomainValidationException, PdfProcessingException, etc.)
|
|
- Für fachliche Fehler
|
|
- Exception Middleware mapped zu HTTP Status Codes
|
|
|
|
**Was wir NICHT in Domain haben:**
|
|
- ? Domain Models (PdfDocument, DocumentAttachment) ? DTOs in Application reichen!
|
|
- ? Constants (ErrorCodes) ? erst wenn wirklich mehrfach gebraucht (YAGNI)
|
|
- ? Services (? Infrastructure)
|
|
|
|
**Fazit:**
|
|
- Domain = **so viel wie nötig, so wenig wie möglich**
|
|
- Wenn wir später merken "das fehlt" ? dann erst hinzufügen (iterativ!)
|
|
|
|
---
|
|
|
|
### CQRS with MediatR
|
|
|
|
**Pattern:** Command Query Responsibility Segregation
|
|
|
|
**Warum MediatR?**
|
|
- ? Klare Trennung: 1 Command/Query = 1 Handler = 1 Verantwortung
|
|
- ? Testbarkeit (Handler kann isoliert getestet werden)
|
|
- ? Pipeline Behaviors (Validation, Logging zentral)
|
|
- ? Kein aufgeblähter Service mit 20 Methoden
|
|
|
|
**CQRS in unserem Kontext:**
|
|
- **Command:** Ändert Daten (ProcessDocument, ApplyStamp, etc.)
|
|
- **Query:** Liest Daten (ValidatePdf ? gibt nur Metadata zurück)
|
|
|
|
**Beispiel:**
|
|
```csharp
|
|
// Query (Read-Only)
|
|
public record ValidatePdfQuery(Base64String PdfContent) : IRequest<PdfMetadata>;
|
|
|
|
// Handler
|
|
public class ValidatePdfHandler : IRequestHandler<ValidatePdfQuery, PdfMetadata>
|
|
{
|
|
private readonly IPdfProcessor _processor;
|
|
|
|
public async Task<PdfMetadata> Handle(ValidatePdfQuery query, CancellationToken ct)
|
|
{
|
|
byte[] bytes = query.PdfContent.ToByteArray();
|
|
var metadata = await _processor.ValidateAsync(bytes);
|
|
return metadata;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Warum Value Objects in Query/Command?**
|
|
- ? Typsicherheit (Base64String vs string)
|
|
- ? Validierung bereits beim Erstellen der Query (nicht im Handler)
|
|
- ? Handler bleibt schlank (keine Validierungs-Boilerplate)
|
|
|
|
---
|
|
|
|
### Vertical Slice Architecture
|
|
|
|
**Statt Horizontal Layers** (Commands/, Handlers/, Validators/):
|
|
|
|
```
|
|
? Horizontal (Schlecht für Wartung):
|
|
Application/
|
|
??? Commands/
|
|
? ??? ValidatePdfCommand.cs
|
|
? ??? ProcessDocumentCommand.cs
|
|
??? Handlers/
|
|
? ??? ValidatePdfHandler.cs
|
|
? ??? ProcessDocumentHandler.cs
|
|
??? Validators/
|
|
??? ValidatePdfValidator.cs
|
|
??? ProcessDocumentValidator.cs
|
|
```
|
|
|
|
**Nutzen wir Vertical Slices** (pro Feature alles zusammen):
|
|
|
|
```
|
|
? Vertical (Gut für Wartung):
|
|
Features/
|
|
??? ValidatePdf/
|
|
? ??? ValidatePdfQuery.cs
|
|
? ??? ValidatePdfHandler.cs
|
|
? ??? ValidatePdfValidator.cs
|
|
??? ProcessDocument/
|
|
??? ProcessDocumentCommand.cs
|
|
??? ProcessDocumentHandler.cs
|
|
??? ProcessDocumentValidator.cs
|
|
```
|
|
|
|
**Vorteile:**
|
|
- ? Zusammengehöriger Code ist zusammen (Cohesion)
|
|
- ? Einfacher zu finden ("Wo ist ValidatePdf?" ? ein Ordner!)
|
|
- ? Einfacher zu ändern (alle Dateien im gleichen Ordner)
|
|
- ? Weniger Merge-Konflikte im Team
|
|
|
|
---
|
|
|
|
### Exception-based Error Handling
|
|
|
|
**Entscheidung:** Keine Result Pattern Library (Ardalis.Result entfernt)
|
|
|
|
**Stattdessen:**
|
|
1. **FluentValidation** für Input-Validierung (DTO-Ebene)
|
|
2. **Domain Exceptions** für fachliche Fehler
|
|
3. **Zentrale Exception Handling Middleware** im API Layer
|
|
|
|
**Warum Exception-basiert?**
|
|
- ? Einfacherer Code (kein `if (result.IsSuccess)` überall)
|
|
- ? Weniger Boilerplate (kein Result<T> 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 LogoFilePath { get; set; } // Lokaler Dateipfad (z.B. "logos/tenant-a/stamp.png")
|
|
public string CertificateFilePath { get; set; } // Lokaler Dateipfad (z.B. "certs/tenant-a/cert.pfx")
|
|
public int RateLimitPerMinute { get; set; } // Rate-Limiting
|
|
|
|
// Navigation
|
|
public Tenant Tenant { get; set; }
|
|
}
|
|
```
|
|
|
|
**Beispiel - Tenant-spezifischer Stamp:**
|
|
```csharp
|
|
public class ApplyStampHandler : IRequestHandler<ApplyStampCommand, byte[]>
|
|
{
|
|
private readonly ITenantContext _tenantContext;
|
|
private readonly IFileStorage _fileStorage; // Lokaler File Storage
|
|
|
|
public async Task<byte[]> Handle(ApplyStampCommand command, CancellationToken ct)
|
|
{
|
|
// Tenant-spezifisches Logo aus DB Settings laden
|
|
var logoPath = _tenantContext.CurrentTenant.Settings.LogoFilePath;
|
|
|
|
// Logo aus lokalem Dateisystem laden
|
|
var logoBytes = await _fileStorage.GetAsync(logoPath);
|
|
|
|
// Stamp mit Logo anwenden
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
**Vorteile:**
|
|
- ? Neue Tenants via Admin-API hinzufügen (ohne Neustart!)
|
|
- ? API-Key Rotation (alten Key invalidieren, neuen generieren)
|
|
- ? Audit-Log (LastUsedAt pro Tenant tracken)
|
|
- ? Rate-Limiting pro Tenant (Redis Counter)
|
|
|
|
---
|
|
|
|
## ??? TECHNOLOGY STACK
|
|
|
|
### Core Framework
|
|
|
|
| Technology | Version | Purpose |
|
|
|------------|---------|---------|
|
|
| **.NET** | 8.0 | Runtime & Framework |
|
|
| **ASP.NET Core** | 8.0 | Web API |
|
|
| **C#** | 12 | Language (Primary Constructors, Record Types) |
|
|
|
|
---
|
|
|
|
### NuGet Packages
|
|
|
|
#### API Layer
|
|
|
|
| Package | Version | Purpose |
|
|
|---------|---------|---------|
|
|
| **Swashbuckle.AspNetCore** | 6.6.2 | Swagger/OpenAPI Documentation |
|
|
| **Serilog.AspNetCore** | 10.0.0 | Strukturiertes Logging |
|
|
| **Serilog.Sinks.File** | 7.0.0 | Log-Datei-Output |
|
|
| **Serilog.Sinks.Seq** | 8.0.0 | **NEU:** Log-Browsing UI (Development) |
|
|
| **Serilog.Enrichers.Environment** | 3.0.1 | Log-Enrichment (MachineName, etc.) |
|
|
| **Serilog.Enrichers.CorrelationId** | 3.0.1 | **NEU:** Correlation IDs für Request-Tracking |
|
|
| **Asp.Versioning.Http** | 8.1.1 | API Versioning (/api/v1/, /api/v2/) |
|
|
| **Microsoft.Extensions.Caching.StackExchangeRedis** | 8.0.28 | Redis Cache (Tenant-Lookups, Rate-Limiting) |
|
|
|
|
#### Application Layer
|
|
|
|
| Package | Version | Purpose |
|
|
|---------|---------|---------|
|
|
| **MediatR** | 14.1.0 | CQRS Pattern Implementation |
|
|
| **FluentValidation** | 12.1.1 | Input Validation (DTOs) |
|
|
| **FluentValidation.DependencyInjectionExtensions** | 12.1.1 | DI Integration |
|
|
|
|
#### Infrastructure Layer
|
|
|
|
| Package | Version | Purpose |
|
|
|---------|---------|---------|
|
|
| **DevExpress.Pdf.Core** | 25.2.8 | PDF-Operationen (Merge, Extract, Sign, etc.) |
|
|
| **DevExpress Universal License** | ? Verfügbar | **Vollzugriff auf alle DevExpress Bibliotheken** |
|
|
| **Microsoft.EntityFrameworkCore** | 8.0.0 | **NEU:** ORM für Tenant-Datenbank |
|
|
| **Microsoft.EntityFrameworkCore.Sqlite** | 8.0.0 | **NEU:** SQLite Provider (Tenant-DB) |
|
|
| **Microsoft.EntityFrameworkCore.Tools** | 8.0.0 | **NEU:** Migrations-Support |
|
|
| **Polly** | 8.5.0 | **NEU:** Resilience (Retry, Circuit Breaker, Timeout) |
|
|
| **BCrypt.Net-Next** | 4.0.3 | **NEU:** API-Key Hashing (Security) |
|
|
| **Microsoft.Extensions.Options.ConfigurationExtensions** | 8.0.0 | Options Pattern |
|
|
|
|
**Hinweis zur DevExpress Lizenz:**
|
|
- ? Universal License vorhanden - wir können **ALLE** DevExpress Pakete nutzen
|
|
- Neben `DevExpress.Pdf.Core` können wir auch weitere Pakete integrieren:
|
|
- `DevExpress.Office.Core` (Word, Excel)
|
|
- `DevExpress.Document.Processor` (erweiterte Dokumenten-Verarbeitung)
|
|
- `DevExpress.Blazor` (falls UI später benötigt wird)
|
|
- Alle weiteren DevExpress Produkte nach Bedarf
|
|
|
|
**Warum diese neuen Pakete?**
|
|
- **EF Core + SQLite:** Tenant-Datenbank (API-Keys, Settings) - skalierbar ohne SQL Server
|
|
- **Polly:** Resilience für DevExpress Calls (Retry bei Transient Errors, Circuit Breaker)
|
|
- **BCrypt:** Sichere API-Key Hashes (NICHT Klartext in DB!)
|
|
- **Seq:** Log-Browsing UI für Development
|
|
- **Correlation IDs:** Request-Tracking über alle Logs (Debugging leichter)
|
|
|
|
#### Domain Layer
|
|
|
|
| Package | Version | Purpose |
|
|
|---------|---------|---------|
|
|
| - | - | **Keine Dependencies!** (Clean Architecture) |
|
|
|
|
#### Tests (neu!)
|
|
|
|
| Package | Version | Purpose |
|
|
|---------|---------|---------|
|
|
| **xUnit** | 2.9.3 | Test Framework |
|
|
| **FluentAssertions** | 7.0.0 | Assertions (result.Should().Be(expected)) |
|
|
| **Moq** | 4.20.72 | Mocking (für Services) |
|
|
| **Microsoft.NET.Test.Sdk** | 17.11.1 | Test SDK |
|
|
| **xunit.runner.visualstudio** | 2.8.2 | Visual Studio Test Runner |
|
|
|
|
---
|
|
|
|
## ?? PROJECT STRUCTURE
|
|
|
|
### Solution Overview
|
|
|
|
```
|
|
DocumentOperator/
|
|
??? DocumentOperator.API/ ? HTTP Entry Point
|
|
??? DocumentOperator.Application/ ? Use Cases (MediatR Handlers)
|
|
??? DocumentOperator.Infrastructure/ ? Technical Implementations
|
|
??? DocumentOperator.Domain/ ? Business Logic (MINIMAL!)
|
|
??? DocumentOperator.Tests/ ? Unit & Integration Tests (NEU!)
|
|
??? ROADMAP.md ? This file
|
|
```
|
|
|
|
---
|
|
|
|
### ?? API Layer (DocumentOperator.API)
|
|
|
|
**Purpose:** HTTP Entry Point, Routing, Middleware
|
|
|
|
**References:**
|
|
- ? Application
|
|
- ? Infrastructure
|
|
- ? Domain
|
|
|
|
**Folder Structure:**
|
|
|
|
```
|
|
DocumentOperator.API/
|
|
??? Endpoints/
|
|
? ??? v1/
|
|
? ??? DocumentEndpoints.cs ? Minimal API Endpoints
|
|
??? Middleware/
|
|
? ??? ExceptionHandlingMiddleware.cs ? Zentrale Exception Handling ?
|
|
??? Configuration/
|
|
? ??? SwaggerConfiguration.cs ? Swagger Setup (API-Key Support)
|
|
??? appsettings.json ? Base Configuration
|
|
??? appsettings.Development.json ? Dev Overrides
|
|
??? Program.cs ? Application Entry Point
|
|
```
|
|
|
|
**Was gehört hierher:**
|
|
- ? HTTP Routing (Minimal APIs)
|
|
- ? Middleware (Exception, Logging)
|
|
- ? Swagger Configuration
|
|
- ? Dependency Injection Setup
|
|
- ? appsettings.json
|
|
|
|
**Was NICHT hierher gehört:**
|
|
- ? Business Logic (? Application)
|
|
- ? PDF-Verarbeitung (? Infrastructure)
|
|
- ? Validierung (? Application: FluentValidation)
|
|
|
|
---
|
|
|
|
### ?? Application Layer (DocumentOperator.Application)
|
|
|
|
**Purpose:** Use Cases, Business Logic Orchestration
|
|
|
|
**References:**
|
|
- ? Domain (ONLY!)
|
|
|
|
**Folder Structure:**
|
|
|
|
```
|
|
DocumentOperator.Application/
|
|
??? Features/ ? Vertical Slices ?
|
|
? ??? Documents/
|
|
? ? ??? ValidatePdf/
|
|
? ? ? ??? ValidatePdfQuery.cs
|
|
? ? ? ??? ValidatePdfHandler.cs
|
|
? ? ? ??? ValidatePdfValidator.cs
|
|
? ? ??? ExtractAttachments/
|
|
? ? ? ??? ExtractAttachmentsCommand.cs
|
|
? ? ? ??? ExtractAttachmentsHandler.cs
|
|
? ? ? ??? ExtractAttachmentsValidator.cs
|
|
? ? ??? ... (weitere Features iterativ)
|
|
? ??? Jobs/ ? **NEU:** Async Processing Features
|
|
? ??? GetJobStatus/
|
|
? ? ??? GetJobStatusQuery.cs
|
|
? ? ??? GetJobStatusHandler.cs
|
|
? ??? SubmitConcatenateJob/
|
|
? ??? SubmitConcatenateJobCommand.cs
|
|
? ??? SubmitConcatenateJobHandler.cs
|
|
??? Common/
|
|
? ??? Interfaces/ ? Abstractions für Infrastructure
|
|
? ? ??? IPdfProcessor.cs
|
|
? ? ??? IFileStorage.cs ? **NEU:** File Storage Abstraction
|
|
? ? ??? IJobQueue.cs ? **NEU:** Queue Abstraction (In-Memory Queue)
|
|
? ? ??? ITenantRepository.cs ? **NEU:** Tenant-DB Abstraction
|
|
? ??? Behaviors/ ? MediatR Pipeline Behaviors
|
|
? ? ??? ValidationBehavior.cs ? FluentValidation Integration
|
|
? ? ??? LoggingBehavior.cs ? **NEU:** Structured Logging mit Correlation IDs
|
|
? ??? DTOs/ ? Data Transfer Objects (API Contracts)
|
|
? ? ??? ValidatePdfRequest.cs
|
|
? ? ??? ValidatePdfResponse.cs
|
|
? ? ??? JobStatusResponse.cs ? **NEU:** Async Job Status
|
|
? ??? Models/ ? **NEU:** Shared Models
|
|
? ??? TenantContext.cs ? Tenant-Info (TenantId, Name, Settings)
|
|
??? DependencyInjection.cs ? Service Registration
|
|
```
|
|
|
|
**Was gehört hierher:**
|
|
- ? MediatR Commands & Queries (pro Feature)
|
|
- ? Handlers (orchestrieren Domain + Infrastructure)
|
|
- ? FluentValidation Validators
|
|
- ? DTOs (API Contracts)
|
|
- ? Interfaces für Infrastructure (Dependency Inversion!)
|
|
- ? Pipeline Behaviors (Validation, Logging)
|
|
|
|
**Was NICHT hierher gehört:**
|
|
- ? DevExpress-spezifischer Code (? Infrastructure)
|
|
- ? File I/O (? Infrastructure)
|
|
- ? HTTP-spezifisches (? API)
|
|
|
|
**Warum keine Infrastructure-Referenz?**
|
|
- Application kennt nur **Interfaces** (`IPdfProcessor`)
|
|
- Infrastructure **implementiert** die Interfaces (`DevExpressPdfProcessor`)
|
|
- API injiziert die Implementierung via DI
|
|
- ? Application bleibt technologie-unabhängig!
|
|
|
|
---
|
|
|
|
### ?? Infrastructure Layer (DocumentOperator.Infrastructure)
|
|
|
|
**Purpose:** Technische Implementierungen
|
|
|
|
**References:**
|
|
- ? Application (für Interfaces)
|
|
- ? Domain
|
|
|
|
**Folder Structure:**
|
|
|
|
```
|
|
DocumentOperator.Infrastructure/
|
|
??? Services/
|
|
? ??? PdfProcessing/
|
|
? ? ??? DevExpressPdfProcessor.cs ? IPdfProcessor Implementation (mit Polly Resilience)
|
|
? ??? FileStorage/
|
|
? ? ??? LocalFileStorage.cs ? **NEU:** IFileStorage Implementation (lokaler Temp-Ordner)
|
|
? ??? Queue/
|
|
? ??? InMemoryJobQueue.cs ? **NEU:** IJobQueue Implementation (In-Memory Queue)
|
|
??? Data/
|
|
? ??? TenantDbContext.cs ? **NEU:** EF Core DbContext (Tenant-DB)
|
|
? ??? Entities/
|
|
? ? ??? Tenant.cs ? **NEU:** Tenant Entity
|
|
? ? ??? TenantSettings.cs ? **NEU:** TenantSettings Entity
|
|
? ??? Repositories/
|
|
? ? ??? TenantRepository.cs ? **NEU:** ITenantRepository Implementation
|
|
? ??? Migrations/ ? **NEU:** EF Core Migrations
|
|
??? BackgroundServices/
|
|
? ??? JobProcessorService.cs ? **NEU:** IHostedService für Async Job Processing
|
|
? ??? TempFileCleanupService.cs ? **NEU:** IHostedService für Temp-File Cleanup (täglich)
|
|
??? Configuration/
|
|
? ??? DocumentOperatorSettings.cs ? Options Pattern Class
|
|
? ??? FileStorageSettings.cs ? **NEU:** File Storage Configuration (Temp-Ordner Pfad)
|
|
? ??? RedisSettings.cs ? **NEU:** Redis Cache Configuration
|
|
??? DependencyInjection.cs ? Service Registration
|
|
```
|
|
|
|
**Was gehört hierher:**
|
|
- ? DevExpress Integration (mit Polly Resilience!)
|
|
- ? **File Storage:** Lokaler Temp-Ordner (IFileStorage Abstraction)
|
|
- ? **Queue:** In-Memory Queue für Async Processing
|
|
- ? **Datenbank:** EF Core + SQLite (Tenant-Management)
|
|
- ? **Background Services:** Job Processing, Temp-File Cleanup
|
|
- ? Options Pattern Classes (Settings)
|
|
|
|
**Was NICHT hierher gehört:**
|
|
- ? Business Logic (? Application)
|
|
- ? HTTP Handling (? API)
|
|
|
|
---
|
|
|
|
### ??? Domain Layer (DocumentOperator.Domain) - MINIMAL!
|
|
|
|
**Purpose:** Business Rules (nur was wirklich gebraucht wird!)
|
|
|
|
**References:**
|
|
- ? **KEINE!** (wichtigste Clean Architecture Regel)
|
|
|
|
**Folder Structure:**
|
|
|
|
```
|
|
DocumentOperator.Domain/
|
|
??? ValueObjects/ ? Immutable, selbst-validierend
|
|
? ??? Base64String.cs
|
|
? ??? TenantId.cs
|
|
? ??? PdfMetadata.cs
|
|
? ??? JobId.cs ? **NEU:** Job-ID für Async Processing
|
|
??? Enums/
|
|
? ??? DocumentOperationType.cs
|
|
? ??? ProcessingStatus.cs ? (Wird jetzt für Async Jobs genutzt!)
|
|
??? Exceptions/ ? Domain-spezifische Exceptions
|
|
??? DomainException.cs
|
|
??? DomainValidationException.cs
|
|
??? NotFoundException.cs
|
|
??? PdfProcessingException.cs
|
|
```
|
|
|
|
**Was gehört hierher:**
|
|
- ? Value Objects (Base64String, TenantId, PdfMetadata, JobId)
|
|
- ? Enums (DocumentOperationType, ProcessingStatus)
|
|
- ? Domain Exceptions
|
|
|
|
**Wichtig:**
|
|
- Domain bleibt **MINIMAL** - keine EF Core Entities hier!
|
|
- Tenant, TenantSettings sind **Infrastructure** Entities (Data/Entities/)
|
|
- Domain kennt nur Value Objects (keine Navigation Properties, kein EF Core)
|
|
|
|
**Was NICHT hierher gehört:**
|
|
- ? Domain Models (PdfDocument, etc.) ? YAGNI! DTOs reichen!
|
|
- ? Constants (ErrorCodes) ? erst wenn mehrfach gebraucht
|
|
- ? Services (? Infrastructure)
|
|
- ? MediatR (? Application)
|
|
- ? JEGLICHE externe Library!
|
|
|
|
---
|
|
|
|
### ?? Tests Layer (DocumentOperator.Tests) - NEU!
|
|
|
|
**Purpose:** Unit & Integration Tests
|
|
|
|
**References:**
|
|
- ? Alle Projekte (API, Application, Infrastructure, Domain)
|
|
|
|
**Folder Structure:**
|
|
|
|
```
|
|
DocumentOperator.Tests/
|
|
??? Unit/
|
|
? ??? Application/
|
|
? ? ??? Features/
|
|
? ? ??? ValidatePdf/
|
|
? ? ??? ValidatePdfHandlerTests.cs
|
|
? ??? Infrastructure/
|
|
? ? ??? Services/
|
|
? ? ??? DevExpressPdfProcessorTests.cs
|
|
? ??? Domain/
|
|
? ??? ValueObjects/
|
|
? ??? Base64StringTests.cs
|
|
??? Integration/
|
|
??? API/
|
|
??? ValidatePdfEndpointTests.cs
|
|
```
|
|
|
|
**Test-Strategie:**
|
|
- ? TDD (Test-Driven Development)
|
|
- ? Unit Tests für Handler (Application Layer)
|
|
- ? Unit Tests für Services (Infrastructure Layer)
|
|
- ? Unit Tests für Value Objects (Domain Layer)
|
|
- ? Integration Tests für Endpoints (API Layer)
|
|
|
|
---
|
|
|
|
## ?? DEVELOPMENT PHILOSOPHY
|
|
|
|
### Pragmatisch, nicht dogmatisch!
|
|
|
|
**Prinzipien:**
|
|
|
|
1. **YAGNI (You Ain't Gonna Need It)**
|
|
- ? Keine spekulativen Abstraktionen
|
|
- ? Keine Klassen "für später"
|
|
- ? Erst wenn 2. Use Case es braucht ? dann Abstrahieren
|
|
|
|
2. **KISS (Keep It Simple, Stupid)**
|
|
- ? Kein Overengineering
|
|
- ? Keine unnötigen Design Patterns
|
|
- ? Einfachster Code der funktioniert
|
|
|
|
3. **Clean Architecture JA, aber pragmatisch**
|
|
- ? Dependency Rule einhalten (wichtig!)
|
|
- ? Separation of Concerns (wichtig!)
|
|
- ? ABER: Nur Abstraktionen die wir wirklich brauchen
|
|
|
|
4. **Test-Driven Development (TDD)**
|
|
- ? Tests schreiben **bevor** Code (Red ? Green ? Refactor)
|
|
- ? Tests als Dokumentation (wie wird es genutzt?)
|
|
- ? Tests als Safety Net (Refactoring ohne Angst)
|
|
|
|
5. **Outside-In Development**
|
|
- ? Von außen nach innen bauen (API ? Service ? Domain)
|
|
- ? Wir sehen sofort was funktioniert (kein "spekulatives" Code)
|
|
- ? Feedback-Loop schneller
|
|
|
|
**Konkret für unser Projekt:**
|
|
- Domain Layer **minimal** (nur Enums + Value Objects + Exceptions)
|
|
- Keine Domain Models (DTOs in Application reichen!)
|
|
- Keine Constants (erst wenn mehrfach gebraucht)
|
|
- Iterativ entwickeln (Feature für Feature)
|
|
- TDD (Test ? Code ? Refactor)
|
|
|
|
---
|
|
|
|
## ??? DEVELOPMENT ROADMAP
|
|
|
|
### ? PHASE 1: Foundation - **COMPLETED**
|
|
|
|
**Bereits erledigt:**
|
|
- [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<PdfMetadata> ValidateAsync(byte[] pdfBytes);
|
|
}
|
|
```
|
|
|
|
**Warum Interface ERST?**
|
|
- Application kennt nur Interface (Dependency Inversion)
|
|
- Infrastructure implementiert
|
|
- TDD: Test ? Interface ? Implementation
|
|
|
|
**Status:** ? **COMPLETED** (17.01.2025)
|
|
- ? Interface erstellt mit XML Comments
|
|
- ? Using Statement korrekt
|
|
- ? Namespace korrekt
|
|
- ? Build erfolgreich
|
|
|
|
---
|
|
|
|
#### ?? Step 3.2: DevExpressPdfProcessor implementieren (mit TDD!) - **NEXT**
|
|
|
|
**Aufgabe:** DevExpress Integration
|
|
|
|
**Flow:**
|
|
1. **Test schreiben** (Red)
|
|
```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<PdfMetadata> ValidateAsync(byte[] pdfBytes)
|
|
{
|
|
using var processor = new PdfDocumentProcessor();
|
|
processor.LoadDocument(pdfBytes);
|
|
|
|
return new PdfMetadata(
|
|
PageCount: processor.Document.Pages.Count,
|
|
FileSizeBytes: pdfBytes.Length,
|
|
// ...
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Test grün machen**
|
|
4. **Refactoring** (falls nötig)
|
|
|
|
**Wo:**
|
|
- Test: `Tests/Unit/Infrastructure/Services/DevExpressPdfProcessorTests.cs`
|
|
- Code: `Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs`
|
|
|
|
**Nach diesem Step:**
|
|
- Wir haben **echten** Code der mit DevExpress arbeitet!
|
|
- Wir wissen welche Exceptions geworfen werden können
|
|
- Wir können Exception Middleware bauen
|
|
|
|
---
|
|
|
|
### ? PHASE 4: Application Layer (erste Feature)
|
|
|
|
**Ziel:** ValidatePdf Feature komplett (Query ? Handler ? Validator)
|
|
|
|
---
|
|
|
|
#### ? Step 4.1: MediatR Setup
|
|
|
|
**Aufgabe:** MediatR + FluentValidation + ValidationBehavior
|
|
|
|
**Was du erstellen wirst:**
|
|
1. `DependencyInjection.cs` (Application Layer)
|
|
2. `ValidationBehavior.cs` (MediatR Pipeline)
|
|
|
|
**Warum jetzt?**
|
|
- Wir brauchen MediatR für Handler
|
|
- ValidationBehavior = zentrale FluentValidation Ausführung
|
|
|
|
---
|
|
|
|
#### ? Step 4.2: ValidatePdf Feature (mit TDD!)
|
|
|
|
**Aufgabe:** Erste komplette Feature-Implementierung
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **ValidatePdfQuery.cs**
|
|
```csharp
|
|
public record ValidatePdfQuery(Base64String PdfContent) : IRequest<PdfMetadata>;
|
|
```
|
|
|
|
2. **ValidatePdfHandler.cs**
|
|
```csharp
|
|
public class ValidatePdfHandler : IRequestHandler<ValidatePdfQuery, PdfMetadata>
|
|
{
|
|
private readonly IPdfProcessor _processor;
|
|
|
|
public async Task<PdfMetadata> Handle(ValidatePdfQuery query, CancellationToken ct)
|
|
{
|
|
byte[] bytes = query.PdfContent.ToByteArray();
|
|
return await _processor.ValidateAsync(bytes);
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **ValidatePdfValidator.cs** (FluentValidation)
|
|
```csharp
|
|
public class ValidatePdfValidator : AbstractValidator<ValidatePdfQuery>
|
|
{
|
|
public ValidatePdfValidator()
|
|
{
|
|
RuleFor(x => x.PdfContent).NotNull();
|
|
}
|
|
}
|
|
```
|
|
|
|
4. **ValidatePdfHandlerTests.cs** (Unit Test)
|
|
|
|
**Wo:** `Application/Features/Documents/ValidatePdf/`
|
|
|
|
**Flow:**
|
|
```
|
|
DTO ? Query (Value Objects) ? ValidationBehavior (FluentValidation)
|
|
? Handler ? IPdfProcessor ? PdfMetadata
|
|
```
|
|
|
|
---
|
|
|
|
### ? PHASE 5: API Layer
|
|
|
|
**Ziel:** HTTP Endpoint + Exception Middleware
|
|
|
|
---
|
|
|
|
#### ? Step 5.1: Exception Handling Middleware
|
|
|
|
**Aufgabe:** Zentrale Exception ? HTTP Response Mapping
|
|
|
|
**Was du erstellen wirst:**
|
|
- **Wo:** `API/Middleware/ExceptionHandlingMiddleware.cs`
|
|
- **Inhalt:**
|
|
```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<IResult> ValidatePdf(
|
|
ValidatePdfRequest request,
|
|
IMediator mediator,
|
|
CancellationToken ct)
|
|
{
|
|
var query = new ValidatePdfQuery(
|
|
Base64String.Create(request.Base64Pdf)
|
|
);
|
|
|
|
var result = await mediator.Send(query, ct);
|
|
return Results.Ok(result);
|
|
}
|
|
}
|
|
```
|
|
|
|
**DTOs:**
|
|
- `ValidatePdfRequest` (Input)
|
|
- `ValidatePdfResponse` (Output) ? oder direkt PdfMetadata?
|
|
|
|
**In Program.cs registrieren:**
|
|
```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<WebApplicationFactory<Program>>
|
|
{
|
|
[Fact]
|
|
public async Task POST_ValidatePdf_ValidPdf_Returns200()
|
|
{
|
|
// Arrange
|
|
var client = _factory.CreateClient();
|
|
var request = new ValidatePdfRequest(Base64Pdf: "...");
|
|
|
|
// Act
|
|
var response = await client.PostAsJsonAsync("/api/v1/documents/validate", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var metadata = await response.Content.ReadFromJsonAsync<PdfMetadata>();
|
|
metadata.PageCount.Should().BeGreaterThan(0);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Warum Integration Test?**
|
|
- Testet kompletten Flow (HTTP ? MediatR ? Service ? Response)
|
|
- Testet Exception Middleware
|
|
- Testet Swagger/OpenAPI
|
|
|
|
---
|
|
|
|
### ? PHASE 5.5: Health Checks & Resilience - **NEW!**
|
|
|
|
**Ziel:** Production-Ready Features FRÜH implementieren
|
|
|
|
**Warum JETZT (nicht Phase 9)?**
|
|
- Health Checks = **Pflicht** für Production (Load Balancer, Kubernetes)
|
|
- Resilience (Polly) = **Pflicht** für externe Dependencies (DevExpress)
|
|
|
|
---
|
|
|
|
#### ? Step 5.5.1: Health Checks
|
|
|
|
**Aufgabe:** `/health` Endpoint für Liveness/Readiness Probes
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **DevExpressPdfHealthCheck.cs**
|
|
```csharp
|
|
public class DevExpressPdfHealthCheck : IHealthCheck
|
|
{
|
|
private readonly IPdfProcessor _processor;
|
|
|
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
// Dummy PDF validieren (Smoke Test)
|
|
byte[] dummyPdf = CreateMinimalPdf();
|
|
await _processor.ValidateAsync(dummyPdf);
|
|
return HealthCheckResult.Healthy("DevExpress PDF OK");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return HealthCheckResult.Unhealthy("DevExpress PDF failed", ex);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Program.cs Registration:**
|
|
```csharp
|
|
builder.Services.AddHealthChecks()
|
|
.AddCheck<DevExpressPdfHealthCheck>("devexpress")
|
|
.AddCheck("self", () => HealthCheckResult.Healthy());
|
|
|
|
app.MapHealthChecks("/health");
|
|
app.MapHealthChecks("/health/ready"); // Kubernetes Readiness
|
|
app.MapHealthChecks("/health/live"); // Kubernetes Liveness
|
|
```
|
|
|
|
**Wo:** `API/HealthChecks/DevExpressPdfHealthCheck.cs`
|
|
|
|
---
|
|
|
|
#### ? Step 5.5.2: Polly Resilience
|
|
|
|
**Aufgabe:** Retry + Circuit Breaker + Timeout für DevExpress Calls
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **DevExpressPdfProcessor erweitern (Polly Policies):**
|
|
```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<Exception>()
|
|
.WaitAndRetryAsync(3, retryAttempt =>
|
|
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
|
|
|
|
// Circuit Breaker: Nach 5 Fehlern für 30 Sekunden öffnen
|
|
_circuitBreakerPolicy = Policy
|
|
.Handle<Exception>()
|
|
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
|
|
|
|
// Timeout: 30 Sekunden max
|
|
_timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(30));
|
|
}
|
|
|
|
public async Task<PdfMetadata> ValidateAsync(byte[] pdfBytes)
|
|
{
|
|
// Alle Policies wrappen
|
|
return await _retryPolicy.ExecuteAsync(async () =>
|
|
await _circuitBreakerPolicy.ExecuteAsync(async () =>
|
|
await _timeoutPolicy.ExecuteAsync(async () =>
|
|
{
|
|
// DevExpress Call
|
|
using var processor = new PdfDocumentProcessor();
|
|
processor.LoadDocument(pdfBytes);
|
|
// ...
|
|
})));
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Logging für Resilience Events:**
|
|
```csharp
|
|
_retryPolicy = Policy
|
|
.Handle<Exception>()
|
|
.WaitAndRetryAsync(3,
|
|
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
|
onRetry: (exception, timespan, retryCount, context) =>
|
|
{
|
|
Log.Warning("Retry {RetryCount} after {Delay}ms: {Exception}",
|
|
retryCount, timespan.TotalMilliseconds, exception.Message);
|
|
});
|
|
```
|
|
|
|
**Warum Polly?**
|
|
- ? **Retry:** DevExpress temporäre Fehler (File Lock, Memory)
|
|
- ? **Circuit Breaker:** DevExpress kaputt ? alle Requests sofort fehlschlagen (kein Retry-Storm)
|
|
- ? **Timeout:** DevExpress hängt ? Request-Timeout (nicht ewig warten)
|
|
|
|
---
|
|
|
|
### ? PHASE 6: Weitere Features (iterativ)
|
|
|
|
**Nach ValidatePdf (als Referenz):**
|
|
|
|
Jedes Feature folgt dem gleichen Pattern:
|
|
1. Interface erweitern (IPdfProcessor)
|
|
2. Service implementieren (DevExpressPdfProcessor) + Test
|
|
3. Command/Query + Handler + Validator
|
|
4. Endpoint erstellen
|
|
5. Integration Test
|
|
|
|
**Features:**
|
|
- [ ] ExtractAttachments (synchron)
|
|
- [ ] ConcatenatePdfs (asynchron - siehe Phase 6.5!)
|
|
- [ ] ApplyStamp (synchron)
|
|
- [ ] EmbedCertificate (synchron)
|
|
|
|
---
|
|
|
|
### ? PHASE 6.5: Async Processing (In-Memory Queue-based) - **NEW!**
|
|
|
|
**Ziel:** Große Operationen asynchron verarbeiten (> 5 Sekunden)
|
|
|
|
**Warum?**
|
|
- ConcatenatePdfs von 50 PDFs = 10+ Sekunden
|
|
- Client wartet nicht ? HTTP Timeout
|
|
- Queue-basiert = skalierbar (Background Worker kann parallel verarbeiten)
|
|
|
|
---
|
|
|
|
#### ? Step 6.5.1: In-Memory Queue Setup
|
|
|
|
**Aufgabe:** Queue für Async Jobs (In-Memory, keine Cloud-Abhängigkeiten)
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **IJobQueue Interface (Application):**
|
|
```csharp
|
|
public interface IJobQueue
|
|
{
|
|
Task<JobId> EnqueueAsync<T>(T jobData) where T : class;
|
|
Task<JobStatus> GetStatusAsync(JobId jobId);
|
|
}
|
|
|
|
public record JobStatus(
|
|
JobId JobId,
|
|
ProcessingStatus Status,
|
|
int Progress,
|
|
string? ResultFilePath,
|
|
string? ErrorMessage);
|
|
```
|
|
|
|
2. **InMemoryJobQueue Implementation (Infrastructure):**
|
|
```csharp
|
|
public class InMemoryJobQueue : IJobQueue
|
|
{
|
|
private readonly ConcurrentQueue<JobData> _queue = new();
|
|
private readonly ConcurrentDictionary<string, JobStatus> _jobStatuses = new();
|
|
|
|
public async Task<JobId> EnqueueAsync<T>(T jobData) where T : class
|
|
{
|
|
var jobId = JobId.Create(Guid.NewGuid().ToString());
|
|
|
|
// Job Message in Queue
|
|
_queue.Enqueue(new JobData
|
|
{
|
|
JobId = jobId,
|
|
Data = jobData,
|
|
Type = typeof(T)
|
|
});
|
|
|
|
// Job Status setzen (Pending)
|
|
_jobStatuses[jobId.Value] = new JobStatus(
|
|
jobId,
|
|
ProcessingStatus.Pending,
|
|
Progress: 0,
|
|
ResultFilePath: null,
|
|
ErrorMessage: null
|
|
);
|
|
|
|
return jobId;
|
|
}
|
|
|
|
public Task<JobStatus> GetStatusAsync(JobId jobId)
|
|
{
|
|
_jobStatuses.TryGetValue(jobId.Value, out var status);
|
|
return Task.FromResult(status ?? throw new NotFoundException($"Job {jobId.Value} not found"));
|
|
}
|
|
|
|
public bool TryDequeue(out JobData jobData)
|
|
{
|
|
return _queue.TryDequeue(out jobData);
|
|
}
|
|
|
|
public void UpdateStatus(JobId jobId, JobStatus status)
|
|
{
|
|
_jobStatuses[jobId.Value] = status;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### ? Step 6.5.2: Background Worker (Job Processor)
|
|
|
|
**Aufgabe:** IHostedService für Job-Verarbeitung
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **JobProcessorService.cs (Infrastructure/BackgroundServices/):**
|
|
```csharp
|
|
public class JobProcessorService : BackgroundService
|
|
{
|
|
private readonly InMemoryJobQueue _jobQueue;
|
|
private readonly IPdfProcessor _pdfProcessor;
|
|
private readonly IFileStorage _fileStorage;
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
// Queue Message abrufen
|
|
if (_jobQueue.TryDequeue(out var jobData))
|
|
{
|
|
try
|
|
{
|
|
// Job Status: Processing
|
|
_jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
|
|
jobData.JobId,
|
|
ProcessingStatus.Processing,
|
|
Progress: 0,
|
|
ResultFilePath: null,
|
|
ErrorMessage: null
|
|
));
|
|
|
|
// PDF-Operation (z.B. Concatenate)
|
|
var result = await _pdfProcessor.ConcatenateAsync(jobData.Data.PdfFiles);
|
|
|
|
// Ergebnis in lokalem Temp-Ordner speichern
|
|
var resultPath = await _fileStorage.SaveAsync(result, $"results/{jobData.JobId}.pdf");
|
|
|
|
// Job Status: Success
|
|
_jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
|
|
jobData.JobId,
|
|
ProcessingStatus.Success,
|
|
Progress: 100,
|
|
ResultFilePath: resultPath,
|
|
ErrorMessage: null
|
|
));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Job Status: Failed
|
|
_jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
|
|
jobData.JobId,
|
|
ProcessingStatus.Failed,
|
|
Progress: 0,
|
|
ResultFilePath: null,
|
|
ErrorMessage: ex.Message
|
|
));
|
|
}
|
|
}
|
|
|
|
await Task.Delay(TimeSpan.FromMilliseconds(100), ct); // Polling-Interval
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Program.cs Registration:**
|
|
```csharp
|
|
builder.Services.AddHostedService<JobProcessorService>();
|
|
```
|
|
|
|
---
|
|
|
|
#### ? Step 6.5.3: Async Endpoints
|
|
|
|
**Aufgabe:** POST ? JobId, GET ? JobStatus
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **POST /api/v1/documents/concatenate (Async):**
|
|
```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 (ResultFilePath: "C:\\Temp\\DocumentOperator\\results\\abc123.pdf")
|
|
|
|
Client: GET /api/v1/jobs/abc123
|
|
? API: { "jobId": "abc123", "status": "Success", "resultUrl": "/download/abc123" }
|
|
|
|
Client: GET /download/abc123
|
|
? API: PDF-Datei aus lokalem Temp-Ordner
|
|
```
|
|
|
|
---
|
|
|
|
### ? PHASE 7: Swagger & API Documentation - **UPDATED!**
|
|
|
|
**Ziel:** Produktionsreife API-Dokumentation
|
|
|
|
**Steps:**
|
|
|
|
#### ? Step 7.1: Swagger Configuration (API-Key Support)
|
|
|
|
**Aufgabe:** API-Key Header in Swagger UI
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **SwaggerConfiguration.cs:**
|
|
```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<string>()
|
|
}
|
|
});
|
|
|
|
// XML Comments
|
|
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
|
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
|
c.IncludeXmlComments(xmlPath);
|
|
|
|
// Example Schemas
|
|
c.SchemaFilter<ExampleSchemaFilter>();
|
|
});
|
|
```
|
|
|
|
2. **XML Comments in Endpoints:**
|
|
```csharp
|
|
/// <summary>
|
|
/// Validates a PDF document and returns metadata
|
|
/// </summary>
|
|
/// <param name="request">PDF as Base64 string</param>
|
|
/// <returns>PDF metadata (page count, file size, etc.)</returns>
|
|
/// <response code="200">PDF is valid, metadata returned</response>
|
|
/// <response code="400">Invalid PDF or Base64 format</response>
|
|
/// <response code="500">Internal server error during validation</response>
|
|
app.MapPost("/api/v1/documents/validate", ValidatePdf)
|
|
.WithName("ValidatePdf")
|
|
.WithTags("Documents")
|
|
.WithOpenApi();
|
|
```
|
|
|
|
---
|
|
|
|
#### ? Step 7.2: Response Examples (Swashbuckle)
|
|
|
|
**Aufgabe:** Beispiel-Responses in Swagger
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **ExampleSchemaFilter.cs:**
|
|
```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<Tenant> Tenants { get; set; }
|
|
public DbSet<TenantSettings> TenantSettings { get; set; }
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<Tenant>(entity =>
|
|
{
|
|
entity.HasKey(e => e.Id);
|
|
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
|
|
entity.Property(e => e.ApiKeyHash).IsRequired().HasMaxLength(500);
|
|
entity.HasOne(e => e.Settings).WithOne(e => e.Tenant).HasForeignKey<TenantSettings>(e => e.TenantId);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **EF Core Migration:**
|
|
```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<Tenant>(cachedTenant);
|
|
}
|
|
else
|
|
{
|
|
// DB Lookup (BCrypt Hash Vergleich)
|
|
tenant = await _tenantRepository.GetByApiKeyAsync(apiKey);
|
|
|
|
if (tenant == null)
|
|
{
|
|
context.Response.StatusCode = 401;
|
|
await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
|
|
return;
|
|
}
|
|
|
|
// Redis Cache befüllen (TTL: 1 Stunde)
|
|
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(tenant), new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
|
|
});
|
|
}
|
|
|
|
if (!tenant.IsActive)
|
|
{
|
|
context.Response.StatusCode = 403;
|
|
await context.Response.WriteAsJsonAsync(new { error = "Tenant is inactive" });
|
|
return;
|
|
}
|
|
|
|
// ITenantContext setzen (Scoped Service)
|
|
var tenantContext = context.RequestServices.GetRequiredService<ITenantContext>();
|
|
tenantContext.SetTenant(tenant);
|
|
|
|
await _next(context);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### ? Step 8.3: Tenant Admin API (CRUD)
|
|
|
|
**Aufgabe:** Admin-Endpoints für Tenant-Management
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **POST /api/v1/admin/tenants (Create Tenant):**
|
|
```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 (Lokale Temp-Ordner) - **NEW!**
|
|
|
|
**Ziel:** File Storage für Temp-Files, Logos, Zertifikate
|
|
|
|
**Warum lokale Temp-Ordner?**
|
|
- ? **Einfachheit:** Keine Cloud-Abhängigkeiten
|
|
- ? **Schnell:** Lokaler Dateizugriff (keine Netzwerk-Latenz)
|
|
- ? **Flexibel:** Bei Bedarf später zu Cloud migrierbar (IFileStorage bleibt!)
|
|
|
|
---
|
|
|
|
#### ? Step 9.1: IFileStorage Interface
|
|
|
|
**Aufgabe:** Abstraction für File Storage
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **IFileStorage.cs (Application/Common/Interfaces/):**
|
|
```csharp
|
|
public interface IFileStorage
|
|
{
|
|
Task<string> SaveAsync(byte[] content, string filename);
|
|
Task<byte[]> GetAsync(string path);
|
|
Task DeleteAsync(string path);
|
|
Task<bool> ExistsAsync(string path);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### ? Step 9.2: LocalFileStorage Implementation
|
|
|
|
**Aufgabe:** Lokaler File Storage Provider
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **LocalFileStorage.cs (Infrastructure/Services/FileStorage/):**
|
|
```csharp
|
|
public class LocalFileStorage : IFileStorage
|
|
{
|
|
private readonly string _basePath;
|
|
|
|
public LocalFileStorage(FileStorageSettings settings)
|
|
{
|
|
_basePath = settings.TempFolderPath ?? Path.Combine(Directory.GetCurrentDirectory(), "TempFiles");
|
|
Directory.CreateDirectory(_basePath);
|
|
}
|
|
|
|
public async Task<string> SaveAsync(byte[] content, string filename)
|
|
{
|
|
var path = Path.Combine(_basePath, filename);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
await File.WriteAllBytesAsync(path, content);
|
|
return path;
|
|
}
|
|
|
|
public async Task<byte[]> GetAsync(string path)
|
|
{
|
|
// Pfad kann absolut oder relativ sein
|
|
var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
|
|
return await File.ReadAllBytesAsync(fullPath);
|
|
}
|
|
|
|
public Task DeleteAsync(string path)
|
|
{
|
|
var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
|
|
if (File.Exists(fullPath))
|
|
{
|
|
File.Delete(fullPath);
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<bool> ExistsAsync(string path)
|
|
{
|
|
var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
|
|
return Task.FromResult(File.Exists(fullPath));
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **FileStorageSettings.cs (Infrastructure/Configuration/):**
|
|
```csharp
|
|
public class FileStorageSettings
|
|
{
|
|
public string TempFolderPath { get; set; } = "TempFiles"; // Default: ./TempFiles
|
|
public int CleanupIntervalHours { get; set; } = 24; // Default: täglich
|
|
public int FileRetentionHours { get; set; } = 24; // Default: 24h Aufbewahrung
|
|
}
|
|
```
|
|
|
|
3. **appsettings.json:**
|
|
```json
|
|
{
|
|
"FileStorage": {
|
|
"TempFolderPath": "C:\\Temp\\DocumentOperator", // Windows Pfad
|
|
"CleanupIntervalHours": 24,
|
|
"FileRetentionHours": 24
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### ? Step 9.3: Temp-File Cleanup Service
|
|
|
|
**Aufgabe:** IHostedService für tägliche Cleanup
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **TempFileCleanupService.cs (Infrastructure/BackgroundServices/):**
|
|
```csharp
|
|
public class TempFileCleanupService : BackgroundService
|
|
{
|
|
private readonly IFileStorage _fileStorage;
|
|
private readonly FileStorageSettings _settings;
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
// Warten auf nächsten Cleanup-Zeitpunkt
|
|
await Task.Delay(TimeSpan.FromHours(_settings.CleanupIntervalHours), ct);
|
|
|
|
try
|
|
{
|
|
// Temp-Files älter als FileRetentionHours löschen
|
|
var tempFolderPath = _settings.TempFolderPath;
|
|
if (Directory.Exists(tempFolderPath))
|
|
{
|
|
var files = Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories);
|
|
var cutoffTime = DateTime.UtcNow.AddHours(-_settings.FileRetentionHours);
|
|
|
|
foreach (var file in files)
|
|
{
|
|
var fileInfo = new FileInfo(file);
|
|
if (fileInfo.CreationTimeUtc < cutoffTime)
|
|
{
|
|
await _fileStorage.DeleteAsync(file);
|
|
Log.Information("Deleted temp file: {Path} (Age: {Hours}h)",
|
|
file, (DateTime.UtcNow - fileInfo.CreationTimeUtc).TotalHours);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "Error during temp file cleanup");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### ? PHASE 10: Logging & Monitoring - **NEW!**
|
|
|
|
**Ziel:** Production-Ready Logging
|
|
|
|
**Steps:**
|
|
|
|
#### ? Step 10.1: Correlation IDs
|
|
|
|
**Aufgabe:** Request-Tracking über alle Logs
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **CorrelationIdMiddleware.cs:**
|
|
```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<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
|
{
|
|
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
|
|
{
|
|
var requestName = typeof(TRequest).Name;
|
|
Log.Information("Handling {RequestName}: {@Request}", requestName, request);
|
|
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var response = await next();
|
|
stopwatch.Stop();
|
|
|
|
Log.Information("Handled {RequestName} in {ElapsedMs}ms", requestName, stopwatch.ElapsedMilliseconds);
|
|
|
|
return response;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### ? Step 10.2: Serilog Configuration (Seq + File)
|
|
|
|
**Aufgabe:** Structured Logging Setup
|
|
|
|
**Was du erstellen wirst:**
|
|
1. **Program.cs Serilog Setup:**
|
|
```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 (File Logging):**
|
|
```json
|
|
{
|
|
"Serilog": {
|
|
"WriteTo": [
|
|
{
|
|
"Name": "File",
|
|
"Args": {
|
|
"path": "C:\\Logs\\DocumentOperator\\log-.txt",
|
|
"rollingInterval": "Day"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### ? PHASE 11: Production Deployment - **UPDATED!**
|
|
|
|
**Ziel:** IIS Deployment + Production Configuration
|
|
|
|
**Steps:**
|
|
- [ ] appsettings.Production.json (Lokale Temp-Ordner, Redis, File Logging)
|
|
- [ ] IIS Web.config (Kestrel Settings)
|
|
- [ ] SSL/TLS Configuration
|
|
- [ ] Redis Connection String (Production) - OPTIONAL (In-Memory Cache Alternative)
|
|
- [ ] Shared Network Drive (bei Multi-Server Setup) - OPTIONAL (lokale Temp-Ordner für Single-Server)
|
|
- [ ] Health Checks für Load Balancer
|
|
- [ ] Rate-Limiting (Redis-based, pro Tenant) - OPTIONAL (In-Memory für Single-Server)
|
|
|
|
---
|
|
|
|
## ?? TESTING STRATEGY
|
|
|
|
### Test-Driven Development (TDD)
|
|
|
|
**Flow:**
|
|
1. **Red:** Test schreiben (schlägt fehl, weil Code noch nicht existiert)
|
|
2. **Green:** Code schreiben (Test wird grün)
|
|
3. **Refactor:** Code verbessern (Test bleibt grün)
|
|
|
|
**Warum TDD?**
|
|
- ? Tests als Dokumentation (wie wird es genutzt?)
|
|
- ? Tests als Safety Net (Refactoring ohne Angst)
|
|
- ? Besseres Design (testbarer Code = guter Code)
|
|
- ? Keine "vergessenen" Tests (Test kommt ZUERST)
|
|
|
|
---
|
|
|
|
### Test-Pyramide
|
|
|
|
```
|
|
/\
|
|
/ \ E2E Tests (wenige)
|
|
/ \
|
|
/------\ Integration Tests (einige)
|
|
/ \
|
|
/----------\ Unit Tests (viele)
|
|
/ \
|
|
```
|
|
|
|
**Konkret:**
|
|
- **Unit Tests (viele):**
|
|
- Value Objects (Base64String.Create() wirft Exception?)
|
|
- Handlers (ValidatePdfHandler ruft IPdfProcessor auf?)
|
|
- Services (DevExpressPdfProcessor gibt Metadata zurück?)
|
|
|
|
- **Integration Tests (einige):**
|
|
- Endpoints (HTTP POST ? 200 OK + JSON?)
|
|
- MediatR Pipeline (ValidationBehavior funktioniert?)
|
|
|
|
- **E2E Tests (wenige/keine):**
|
|
- Haben wir nicht (API ist selbst der "Top-Level")
|
|
|
|
---
|
|
|
|
### Test-Abdeckung
|
|
|
|
**Ziel:** >80% Code Coverage (aber nicht um jeden Preis!)
|
|
|
|
**Was testen:**
|
|
- ? Value Objects (Validierung)
|
|
- ? Handlers (Business Logic)
|
|
- ? Services (DevExpress Integration)
|
|
- ? Endpoints (HTTP Responses)
|
|
- ? Exception Middleware (Error Mapping)
|
|
|
|
**Was NICHT testen:**
|
|
- ? DTOs (keine Logik)
|
|
- ? Enums (keine Logik)
|
|
- ? Program.cs (Startup Code)
|
|
|
|
---
|
|
|
|
### Test-Naming Convention
|
|
|
|
**Pattern:** `MethodName_Scenario_ExpectedResult`
|
|
|
|
**Beispiele:**
|
|
```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 - In-Memory Queue Setup (IJobQueue Interface + InMemoryJobQueue)
|
|
- Step 6.5.2 - Background Worker (JobProcessorService - IHostedService)
|
|
- Step 6.5.3 - Async Endpoints (POST ? JobId, GET ? Status)
|
|
|
|
- **Phase 7:** Swagger & API Documentation (erweitert!)
|
|
- Step 7.1 - Swagger Configuration (API-Key Support, XML Comments)
|
|
- Step 7.2 - Response Examples (Swashbuckle)
|
|
|
|
- **Phase 8:** Multi-Tenancy (DB-based!) (**NEU!**)
|
|
- Step 8.1 - EF Core Setup (SQLite, Migrations)
|
|
- Step 8.2 - Tenant Resolution Middleware (Redis Cache)
|
|
- Step 8.3 - Tenant Admin API (CRUD)
|
|
|
|
- **Phase 9:** File Storage (**NEU!**)
|
|
- Step 9.1 - IFileStorage Interface
|
|
- Step 9.2 - LocalFileStorage Implementation (lokale Temp-Ordner)
|
|
- Step 9.3 - Temp-File Cleanup Service (IHostedService)
|
|
|
|
- **Phase 10:** Logging & Monitoring (**NEU!**)
|
|
- Step 10.1 - Correlation IDs (Request-Tracking)
|
|
- Step 10.2 - Serilog Configuration (Seq + File Logging)
|
|
|
|
- **Phase 11:** Production Deployment
|
|
- appsettings.Production.json
|
|
- IIS Deployment
|
|
- Rate-Limiting (Redis-based)
|
|
|
|
### ?? Hinweise zum aktuellen Stand
|
|
|
|
1. **Infrastructure Services:**
|
|
- Ordner existieren (PdfProcessing, FileStorage, DocumentValidation)
|
|
- **Aber:** Alle leer
|
|
- ?? **Action:** DevExpressPdfProcessor.cs implementieren (Step 3.2 - IN PROGRESS)
|
|
|
|
2. **DevExpress Universal License:**
|
|
- ? **Verfügbar!** Wir können alle DevExpress Pakete nutzen
|
|
- Aktuell nur: `DevExpress.Pdf.Core`
|
|
- Bei Bedarf können weitere Pakete hinzugefügt werden
|
|
|
|
---
|
|
|
|
## ?? KEY LEARNINGS & DECISIONS
|
|
|
|
### 1. Domain Layer minimal halten
|
|
|
|
**Entscheidung:** Nur Enums + Value Objects + Exceptions (ABER: EF Core Entities in Infrastructure!)
|
|
|
|
**Warum:**
|
|
- Domain = Business-Konzepte (technologie-unabhängig)
|
|
- Tenant/TenantSettings sind **Infrastructure** Entities (EF Core, Navigation Properties)
|
|
- Service-Anwendung (nicht Domain-lastig)
|
|
- YAGNI (You Ain't Gonna Need It)
|
|
|
|
**Was bedeutet das?**
|
|
- Domain/ValueObjects/TenantId.cs ? Value Object (immutable, validierend)
|
|
- Infrastructure/Data/Entities/Tenant.cs ? EF Core Entity (mit Navigation Properties)
|
|
- Domain kennt KEINE EF Core Dependencies!
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Volle Domain Models (PdfDocument, DocumentAttachment, etc.)
|
|
- **Nachteile:** Overengineering, unnötige Komplexität
|
|
|
|
---
|
|
|
|
### 2. Outside-In Development
|
|
|
|
**Entscheidung:** Infrastructure ? Application ? API
|
|
|
|
**Warum:**
|
|
- Wir sehen **echten** Code sofort (DevExpress Integration)
|
|
- Keine Spekulation (wir wissen welche Exceptions geworfen werden)
|
|
- Schnellerer Feedback-Loop
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Domain ? Application ? Infrastructure ? API
|
|
- **Nachteile:** Viel "spekulativer" Code ohne echte Implementation
|
|
|
|
---
|
|
|
|
### 3. Exception-based Error Handling
|
|
|
|
**Entscheidung:** Keine Result Pattern Library
|
|
|
|
**Warum:**
|
|
- Einfacherer Code (kein Result<T> Boilerplate)
|
|
- Zentrales Error Handling (Middleware)
|
|
- Standard .NET Exception-Flow
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Ardalis.Result oder FluentResults
|
|
- **Nachteile:** Extra Package, mehr Boilerplate
|
|
|
|
---
|
|
|
|
### 4. TDD (Test-Driven Development)
|
|
|
|
**Entscheidung:** Test ? Code ? Refactor
|
|
|
|
**Warum:**
|
|
- Besseres Design (testbarer Code)
|
|
- Tests als Dokumentation
|
|
- Safety Net für Refactoring
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Code ? Test (Test-After)
|
|
- **Nachteile:** Tests werden oft vergessen, schlechteres Design
|
|
|
|
---
|
|
|
|
### 5. Vertical Slice Architecture
|
|
|
|
**Entscheidung:** Pro Feature alles zusammen
|
|
|
|
**Warum:**
|
|
- Zusammengehöriger Code ist zusammen
|
|
- Einfacher zu finden und zu ändern
|
|
- Besser für Teams (weniger Merge-Konflikte)
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Horizontal Layers (Commands/, Handlers/, Validators/)
|
|
- **Nachteile:** Code über viele Ordner verteilt
|
|
|
|
---
|
|
|
|
### 6. Multi-Tenancy: Database-based (statt appsettings.json) - **NEU!**
|
|
|
|
**Entscheidung:** EF Core + SQLite für Tenant-Management
|
|
|
|
**Warum:**
|
|
- **Skalierbar:** Neue Tenants ohne Neustart
|
|
- **Security:** API-Key Hashing (BCrypt, NICHT Klartext!)
|
|
- **Audit-Log:** LastUsedAt pro Tenant
|
|
- **Rate-Limiting:** Redis Counter pro Tenant
|
|
- **Redis Cache:** API-Key Lookups cached (Performance!)
|
|
|
|
**Alternative wäre gewesen:**
|
|
- appsettings.json mit API-Keys
|
|
- **Nachteile:** Nicht skalierbar, keine Rotation, kein Audit-Log
|
|
|
|
---
|
|
|
|
### 7. Async Processing: In-Memory Queue-based (für große Operationen) - **NEU!**
|
|
|
|
**Entscheidung:** In-Memory Queue + Background Worker
|
|
|
|
**Warum:**
|
|
- ConcatenatePdfs von 50 PDFs = 10+ Sekunden
|
|
- Synchron = HTTP Timeout
|
|
- In-Memory Queue = einfach, keine Cloud-Abhängigkeiten
|
|
- ProcessingStatus Enum wird jetzt genutzt!
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Alle Operationen synchron
|
|
- **Nachteile:** HTTP Timeouts, nicht skalierbar
|
|
|
|
**Hinweis:**
|
|
- In-Memory Queue = nicht persistent (bei Server-Neustart gehen Jobs verloren)
|
|
- Für Production später: Redis Queue oder RabbitMQ (aber IJobQueue bleibt!)
|
|
- Für Single-Server Setup: In-Memory Queue ausreichend
|
|
|
|
---
|
|
|
|
### 8. File Storage: Lokale Temp-Ordner (statt Cloud) - **NEU!**
|
|
|
|
**Entscheidung:** IFileStorage Interface + LocalFileStorage
|
|
|
|
**Warum:**
|
|
- **Einfachheit:** Keine Cloud-Abhängigkeiten (Azure Blob Storage)
|
|
- **Schnell:** Lokaler Dateizugriff (keine Netzwerk-Latenz)
|
|
- **Flexibel:** IFileStorage Abstraction bleibt (später zu Cloud migrierbar!)
|
|
- **TempFileCleanupService:** Automatische Cleanup (täglich)
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Cloud Storage (Azure Blob, AWS S3)
|
|
- **Nachteile:** Cloud-Abhängigkeit, Kosten, Komplexität
|
|
|
|
**Hinweis:**
|
|
- Für Multi-Server Setup: Shared Network Drive (UNC-Pfad) statt lokale Ordner
|
|
- IFileStorage Interface bleibt gleich (austauschbar!)
|
|
|
|
---
|
|
|
|
### 9. Resilience: Polly (Retry, Circuit Breaker, Timeout) - **NEU!**
|
|
|
|
**Entscheidung:** Polly für DevExpress Calls
|
|
|
|
**Warum:**
|
|
- **Retry:** Transient Errors (File Lock, Memory)
|
|
- **Circuit Breaker:** DevExpress kaputt ? alle Requests sofort fehlschlagen
|
|
- **Timeout:** DevExpress hängt ? nicht ewig warten
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Keine Resilience
|
|
- **Nachteile:** Produktionsausfälle bei transient errors
|
|
|
|
---
|
|
|
|
### 10. Health Checks: FRÜH implementieren (Phase 5.5, nicht Phase 9) - **NEU!**
|
|
|
|
**Entscheidung:** Health Checks direkt nach erstem Endpoint
|
|
|
|
**Warum:**
|
|
- **Pflicht** für Production (Load Balancer, Kubernetes)
|
|
- `/health` Endpoint = Liveness/Readiness Probes
|
|
- DevExpressPdfHealthCheck = Smoke Test (PDF validieren)
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Health Checks in Phase 9 (Production-Ready)
|
|
- **Nachteile:** Zu spät! Load Balancer braucht Health Checks sofort
|
|
|
|
---
|
|
|
|
### 11. Logging: Correlation IDs + Seq + File Logging - **NEU!**
|
|
|
|
**Entscheidung:** Structured Logging mit Correlation IDs
|
|
|
|
**Warum:**
|
|
- **Correlation IDs:** Request-Tracking über alle Logs (Debugging leichter)
|
|
- **Seq:** Log-Browsing UI (Development)
|
|
- **File Logging:** Production Logs (keine Cloud-Abhängigkeit)
|
|
- **LoggingBehavior:** MediatR Pipeline Behavior (automatisches Logging)
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Nur Console Logging (keine Correlation IDs)
|
|
- **Nachteile:** Debugging schwierig, keine Request-Zusammenhänge, keine persistente Log-Speicherung
|
|
|
|
### 5. Vertical Slice Architecture
|
|
|
|
**Entscheidung:** Pro Feature alles zusammen
|
|
|
|
**Warum:**
|
|
- Zusammengehöriger Code ist zusammen
|
|
- Einfacher zu finden und zu ändern
|
|
- Besser für Teams (weniger Merge-Konflikte)
|
|
|
|
**Alternative wäre gewesen:**
|
|
- Horizontal Layers (Commands/, Handlers/, Validators/)
|
|
- **Nachteile:** Code über viele Ordner verteilt
|
|
|
|
---
|
|
|
|
## ?? REFERENCES & BEST PRACTICES
|
|
|
|
### Documentation
|
|
|
|
- [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
|
|
- [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 (In-Memory Queue)** - **NEU:** In-Memory Queue + Background Worker
|
|
- ? **File Storage Abstraction (Lokale Temp-Ordner)** - **NEU:** IFileStorage + LocalFileStorage
|
|
- ? **Correlation IDs** - **NEU:** Request-Tracking über alle Logs
|
|
- ? **Health Checks** - **NEU:** Load Balancer Support
|
|
- ? Options Pattern für Configuration
|
|
- ? Dependency Injection
|
|
- ? Async/Await überall
|
|
- ? Nullable Reference Types
|
|
- ? Record Types für DTOs (C# 12)
|
|
- ? Primary Constructors (.NET 8)
|
|
- ? Structured Logging (Serilog + Seq + File Logging)
|
|
|
|
---
|
|
|
|
## ?? UPDATE LOG
|
|
|
|
| Date | Phase | Changes |
|
|
|------|-------|---------|
|
|
| 2024-XX-XX | Phase 1 | Project setup, dependencies, folder structure |
|
|
| 2024-XX-XX | Phase 1 | Configuration, Serilog, Options Pattern |
|
|
| 2024-XX-XX | Phase 1 | ? Phase 1 completed |
|
|
| 2024-XX-XX | Phase 2 | ? Step 2.1 completed - Domain Exceptions created |
|
|
| 17.01.2025 | Roadmap | ?? **ROADMAP komplett überarbeitet** (Pragmatisch, Outside-In, TDD) |
|
|
| 17.01.2025 | Phase 2 | ? Step 2.2 completed - Enums erstellt |
|
|
| 17.01.2025 | Phase 2 | ? Step 2.3 completed - Value Objects erstellt |
|
|
| 17.01.2025 | Phase 2 | ? **Phase 2 (Domain Layer) komplett abgeschlossen!** |
|
|
| 17.01.2025 | Phase 3 | ? Step 3.1 completed - IPdfProcessor Interface erstellt |
|
|
| 17.01.2025 | Roadmap | ?? **ROADMAP Status-Update** - Aktueller Projektstand dokumentiert |
|
|
| 17.01.2025 | Infrastructure | ?? **DevExpress Universal License** hinzugefügt - Vollzugriff auf alle Pakete |
|
|
| 17.01.2025 | Phase 3 | ?? **Step 3.2 gestartet** - DevExpressPdfProcessor (TDD) |
|
|
| 17.01.2025 | Application | ? Step 3.2.1 - ProcessDocument Ordner gelöscht (Cleanup) |
|
|
| 17.01.2025 | Tests | ? Step 3.2.2 - Test-Ordnerstruktur erstellt, UnitTest1.cs gelöscht |
|
|
| 17.01.2025 | Tests | ? Step 3.2.3 - Test-PDF (valid.pdf) als Embedded Resource hinzugefügt |
|
|
| 17.01.2025 | Tests | ? Step 3.2.4 - DevExpressPdfProcessorTests.cs erstellt (TDD Red - 6 Tests) |
|
|
| 17.01.2025 | Roadmap | ?? **ROADMAP MAJOR UPDATE** - Production-Ready Features hinzugefügt! |
|
|
| 17.01.2025 | Architecture | ? **Multi-Tenancy:** Database-based (EF Core + SQLite + Redis Cache) |
|
|
| 17.01.2025 | Architecture | ? **Async Processing:** In-Memory Queue-based + Background Worker |
|
|
| 17.01.2025 | Architecture | ? **File Storage:** Lokale Temp-Ordner mit IFileStorage Abstraction |
|
|
| 17.01.2025 | Architecture | ? **Resilience:** Polly (Retry, Circuit Breaker, Timeout) |
|
|
| 17.01.2025 | Architecture | ? **Health Checks:** Früh implementieren (Phase 5.5 statt Phase 9) |
|
|
| 17.01.2025 | Architecture | ? **Logging:** Correlation IDs + Seq + File Logging |
|
|
| 17.01.2025 | Technology Stack | ? **9 neue NuGet Packages hinzugefügt** (EF Core, Polly, BCrypt, Seq - OHNE Azure) |
|
|
| 17.01.2025 | Roadmap | ? **6 neue Phasen** (5.5, 6.5, 8, 9, 10, 11) - insgesamt 11 Phasen statt 9 |
|
|
| 17.01.2025 | Documentation | ? **10 Key Learnings & Decisions** dokumentiert (statt 5) |
|
|
| 17.01.2025 | Roadmap | ?? **Azure Services entfernt** - Lokale Temp-Ordner + In-Memory Queue stattdessen
|
|
| 22.06.2026 | Dokumentation | ? **Azure-Referenzen vollständig entfernt** - Alle Azure-Referenzen bereinigt (PROJECT_STATUS.md + ROADMAP.md)
|
|
|
|
---
|
|
|
|
**END OF ROADMAP**
|
|
|
|
*This document is a living document and will be updated as development progresses.*
|