Cross-cutting Concerns
Middleware Pipeline
Every HTTP request passes through these middleware layers in order:
flowchart TD
A[HTTP Request] --> B[GlobalExceptionMiddleware\nCatches unhandled exceptions → ProblemDetails]
B --> C[RequestIdMiddleware\nAdds X-Request-ID header]
C --> D[LocalisationMiddleware\nSets Accept-Language culture]
D --> E[Authentication\nJWT validation]
E --> F[TenantResolutionMiddleware\nExtracts vendor_id / customer_id from JWT claims]
F --> G[TokenBlacklistMiddleware\nRejects revoked JTI tokens]
G --> H[Authorization\nPolicy enforcement]
H --> I[RateLimiter]
I --> J[IdempotencyMiddleware\nChecks Idempotency-Key header → Redis]
J -->|duplicate request| Z1[Return cached response]
J -->|new request| K[ETagMiddleware\nSets ETag / handles If-None-Match]
K --> L[Controller]
Caching Strategy
flowchart TD
A[Query via MediatR] --> B{Implements ICacheableQuery?}
B -->|No| C[Execute handler directly]
B -->|Yes| D[CachingBehaviour checks Redis]
D -->|Hit| E[Return cached result]
D -->|Miss| F[Execute handler]
F --> G[Store result in Redis with TTL + version key]
G --> E
subgraph Cache Invalidation
H[Command handler runs] --> I[Increment version key in Redis]
I --> J[Next read: version mismatch → cache miss]
end
subgraph Special Cases
K[Geocoding: CachingGeocodingService uses IMemoryCache in-process]
L[Idempotency keys: IdempotencyMiddleware uses Redis directly]
end
Multi-tenancy
flowchart TD
A[JWT Token] --> B[TenantResolutionMiddleware]
B --> C{Role claim}
C -->|VendorAdmin| D[SetTenantContext\nvendor_id + vendor_type + user_id]
C -->|User/Customer| E[No vendor claims set\nCustomerAccess policy enforced downstream]
C -->|PlatformAdmin| F[SetPlatformAdminContext\nvia dynamic dispatch]
D --> G[ITenantContextService]
E --> G
F --> G
G --> H[TenantValidationBehavior in MediatR pipeline]
H --> I[Handler: GetTenantContext\nScopes all DB queries by VendorId + VendorType]
Idempotency
sequenceDiagram
participant Client
participant API as IdempotencyMiddleware
participant Redis
Client->>API: POST /orders Idempotency-Key: abc123
API->>Redis: GET idempotency:abc123
alt Key not found
Redis-->>API: null
API->>API: Process request
API->>Redis: SET idempotency:abc123 {status, body} TTL=24h
API-->>Client: 201 Created
else Key found
Redis-->>API: Cached response
API-->>Client: 201 Created (replayed — no duplicate order)
end
Strongly-Typed IDs
All domain identifiers use strongly-typed value objects to prevent ID confusion across aggregates:
| Type | Wraps | Used in |
|---|---|---|
VendorId |
Guid |
Vendor, SmartVase, Bouquet, Employee |
BouquetId |
Guid |
Bouquet, OrderLine, Favourite, Cart |
CustomerId |
Guid |
Customer, Order, Favourite, Subscription |
OrderId |
Guid |
Order, OrderLine |
VaseId |
Guid |
SmartVase, Bouquet |
UserId |
Guid |
User |