Hexagonal Architecture — Ports & Adapters
Definition
Section titled “Definition”Hexagonal architecture separates business logic (the “core”) from infrastructure details (databases, cloud services, frameworks) via ports (interfaces) and adapters (interchangeable implementations). The core only knows about ports; adapters are wired at composition time (DI).
In Granit, each functional module (BlobStorage, Features, BackgroundJobs,
Webhooks, Settings) follows this pattern: a “core” package defines the ports,
and separate packages (*.EntityFrameworkCore, *.S3) provide the adapters.
Diagram
Section titled “Diagram”classDiagram
direction LR
class IBlobStorage {
+InitiateUploadAsync()
+CreateDownloadUrlAsync()
+DeleteAsync()
}
class IBlobDescriptorReader {
+FindAsync()
}
class IBlobDescriptorWriter {
+SaveAsync()
+UpdateAsync()
}
class IBlobStoreProvider {
+SaveAsync()
+OpenReadAsync()
+DeleteAsync()
}
class IBlobKeyStrategy {
+BuildObjectKey()
+ResolveBucketName()
}
class IBlobValidator {
+ValidateAsync()
}
class DefaultBlobStorage {
core adapter
}
class EfBlobDescriptorStore {
EF Core adapter
}
class S3BlobClient {
S3 adapter
}
class PrefixBlobKeyStrategy {
S3 adapter
}
class MagicBytesValidator {
built-in adapter
}
IBlobStorage <|.. DefaultBlobStorage
DefaultBlobStorage --> IBlobDescriptorReader
DefaultBlobStorage --> IBlobDescriptorWriter
DefaultBlobStorage --> IBlobStoreProvider
DefaultBlobStorage --> IBlobKeyStrategy
DefaultBlobStorage --> IBlobValidator
IBlobDescriptorReader <|.. EfBlobDescriptorStore
IBlobDescriptorWriter <|.. EfBlobDescriptorStore
IBlobStoreProvider <|.. S3BlobClient
IBlobKeyStrategy <|.. PrefixBlobKeyStrategy
IBlobValidator <|.. MagicBytesValidator
Implementation in Granit
Section titled “Implementation in Granit”BlobStorage (primary example)
Section titled “BlobStorage (primary example)”| Port (interface) | File | Adapter(s) |
|---|---|---|
IBlobStorage | src/Granit.BlobStorage/IBlobStorage.cs | DefaultBlobStorage (orchestrator) |
IBlobDescriptorReader / IBlobDescriptorWriter | src/Granit.BlobStorage/ | EfBlobDescriptorStore in Granit.BlobStorage.EntityFrameworkCore |
IBlobStoreProvider / IPresignedUrlProvider | src/Granit.BlobStorage/Internal/ | S3BlobClient in Granit.BlobStorage.S3 (implements both) |
IBlobKeyStrategy | src/Granit.BlobStorage/IBlobKeyStrategy.cs | PrefixBlobKeyStrategy in Granit.BlobStorage.S3 |
IBlobValidator | src/Granit.BlobStorage/IBlobValidator.cs | MagicBytesValidator, MaxSizeValidator (built-in) + custom |
Same pattern across other modules
Section titled “Same pattern across other modules”| Module | Port | Adapters |
|---|---|---|
| Features | IFeatureStoreReader / IFeatureStoreWriter | InMemoryFeatureStore, EfCoreFeatureStore |
| BackgroundJobs | IBackgroundJobStoreReader / IBackgroundJobStoreWriter | InMemoryBackgroundJobStore, EfBackgroundJobStore |
| Webhooks | IWebhookSubscriptionReader / IWebhookSubscriptionWriter | EfWebhookSubscriptionStore |
| Settings | ISettingStoreReader / ISettingStoreWriter | EfCoreSettingStore |
| Caching | IFusionCache | FusionCache (via Granit.Caching) |
| Encryption | IStringEncryptionProvider | AesStringEncryptionProvider |
Rationale
Section titled “Rationale”| Problem | Solution |
|---|---|
| Coupling to a cloud provider (S3, Azure Blob) | Ports allow swapping adapters without touching the core |
| Unit tests requiring a database | InMemoryFeatureStore and InMemoryBackgroundJobStore implement Reader/Writer interfaces, replacing EF Core in tests |
| ISO 27001 compliance — ability to migrate from S3-compatible storage to a sovereign provider | A provider package implementing IBlobStoreProvider is sufficient; the core and every consumer stay untouched |
| Independent NuGet packages | The core (Granit.BlobStorage) has no dependency on EF Core or the AWS SDK |
Usage example
Section titled “Usage example”Ports are internal to the package that owns them — adapters ship as
first-party provider packages, and hosts select one through its registration
extension. Swapping AWS S3 for MinIO reuses the same adapter against a
different endpoint (ServiceUrl + ForcePathStyle on S3BlobOptions):
builder.AddGranitBlobStorageS3();
// The rest of the application code remains unchangedIBlobStorage blobStorage = serviceProvider.GetRequiredService<IBlobStorage>();PresignedUploadTicket ticket = await blobStorage.InitiateUploadAsync( "medical-documents", new BlobUploadRequest("mri-report.pdf", "application/pdf", MaxAllowedBytes: 50_000_000), cancellationToken);