Granit.BlobStorage & Imaging
Granit.BlobStorage provides a multi-provider blob storage layer with a unified
IBlobStorage API. Cloud providers (S3, Azure Blob, Google Cloud Storage) use native presigned URLs for
Direct-to-Cloud uploads where the application server never touches file bytes.
Server-side providers (FileSystem, Database) use Granit.BlobStorage.Proxy to expose
token-based upload/download endpoints with identical client-side flow. A post-upload
validation pipeline (magic bytes, size) ensures integrity before marking blobs as valid.
Granit.Imaging adds a fluent image processing pipeline (resize, crop, compress, format
conversion, EXIF stripping) powered by Magick.NET.
Why Direct-to-Cloud?
Section titled “Why Direct-to-Cloud?”Traditional file upload architectures stream bytes through the application server, which creates several problems at scale:
| Problem | Traditional upload | Granit Direct-to-Cloud |
|---|---|---|
| Server memory | Entire file buffered in RAM | Server handles only metadata (< 1 KB) |
| Bandwidth costs | Bytes transit through your servers twice (in + out) | Client uploads directly to storage; server bandwidth near zero |
| Horizontal scaling | Upload throughput limited by server capacity | Unlimited — storage provider handles all I/O |
| Timeout risk | Large uploads blocked by server timeouts | Upload goes directly to storage with its own timeout window |
| Infrastructure cost | Need large instances for file processing | Minimal compute — only metadata and validation |
For cloud providers (S3, Azure Blob), bytes never touch the application server.
For server-side providers (FileSystem, Database), Granit.BlobStorage.Proxy streams
bytes through thin proxy endpoints without RAM buffering, keeping the architecture
consistent while minimizing server resource usage.
Package structure
Section titled “Package structure”DirectoryGranit.BlobStorage/ Core abstractions, validation pipeline, domain model
- Granit.BlobStorage.S3 S3 client, native presigned URLs
- Granit.BlobStorage.AzureBlob Azure Blob Storage client, SAS tokens
- Granit.BlobStorage.GoogleCloud Google Cloud Storage client, signed URLs
- Granit.BlobStorage.FileSystem Local file system storage
- Granit.BlobStorage.Database EF Core database storage (small files)
- Granit.BlobStorage.Proxy Token-based proxy endpoints for server-side providers
- Granit.BlobStorage.EntityFrameworkCore Isolated DbContext, EF persistence (metadata)
DirectoryGranit.Imaging/ Image processing abstractions, fluent pipeline API
- Granit.Imaging.MagickNet Magick.NET implementation (JPEG/PNG/WebP/AVIF/GIF/BMP/TIFF)
| Package | Role | Depends on |
|---|---|---|
Granit.BlobStorage | IBlobStorage, IBlobValidator, BlobDescriptor domain | Granit.Core |
Granit.BlobStorage.S3 | S3 native presigned URLs, PrefixBlobKeyStrategy | Granit.BlobStorage |
Granit.BlobStorage.AzureBlob | Azure Blob SAS tokens, Managed Identity support | Granit.BlobStorage |
Granit.BlobStorage.GoogleCloud | Google Cloud Storage signed URLs, Workload Identity support | Granit.BlobStorage |
Granit.BlobStorage.FileSystem | Local file system storage, path traversal protection | Granit.BlobStorage |
Granit.BlobStorage.Database | EF Core database storage (small files, 10 MB default) | Granit.BlobStorage, Granit.Persistence |
Granit.BlobStorage.Proxy | Token-based proxy endpoints for FileSystem/Database | Granit.BlobStorage |
Granit.BlobStorage.EntityFrameworkCore | BlobStorageDbContext, EfBlobDescriptorStore | Granit.BlobStorage, Granit.Persistence |
Granit.Imaging | IImageProcessor, IImagePipeline fluent API | Granit.Core |
Granit.Imaging.MagickNet | MagickNetImageProcessor (singleton) | Granit.Imaging |
Provider comparison
Section titled “Provider comparison”| Provider | Presigned URLs | Max file size | Use case |
|---|---|---|---|
| S3 | Native (AWS SigV4) | Unlimited | Production cloud storage |
| Azure Blob | Native (SAS tokens) | Unlimited | Azure-hosted deployments |
| Google Cloud | Native (V4 signed URLs) | Unlimited | GCP-hosted deployments |
| FileSystem | Via Proxy | Disk capacity | Development, on-premise |
| Database | Via Proxy | 10 MB (configurable) | Small files, regulated environments |
Cloud providers (S3, Azure, Google Cloud) generate native presigned/SAS/signed URLs — the application
server never touches file bytes. Server-side providers (FileSystem, Database) require
Granit.BlobStorage.Proxy which exposes ephemeral token-based endpoints that stream
bytes through to the underlying storage. The client-side upload/download flow is
identical regardless of provider.
Dependency graph
Section titled “Dependency graph”graph TD
BS[Granit.BlobStorage] --> CO[Granit.Core]
S3[Granit.BlobStorage.S3] --> BS
AZ[Granit.BlobStorage.AzureBlob] --> BS
GC[Granit.BlobStorage.GoogleCloud] --> BS
FS[Granit.BlobStorage.FileSystem] --> BS
DB[Granit.BlobStorage.Database] --> BS
DB --> P[Granit.Persistence]
PX[Granit.BlobStorage.Proxy] --> BS
EF[Granit.BlobStorage.EntityFrameworkCore] --> BS
EF --> P
IM[Granit.Imaging] --> CO
MN[Granit.Imaging.MagickNet] --> IM
[DependsOn( typeof(GranitBlobStorageEntityFrameworkCoreModule), typeof(GranitImagingMagickNetModule))]public class AppModule : GranitModule { }builder.AddGranitBlobStorageS3();builder.AddGranitBlobStorageEntityFrameworkCore(options => options.UseNpgsql(builder.Configuration.GetConnectionString("BlobStorage")));{ "BlobStorage": { "ServiceUrl": "https://s3.eu-west-1.amazonaws.com", "AccessKey": "<from-vault>", "SecretKey": "<from-vault>", "Region": "eu-west-1", "DefaultBucket": "myapp-blobs", "ForcePathStyle": false, "TenantIsolation": "Prefix", "UploadUrlExpiry": "00:15:00", "DownloadUrlExpiry": "00:05:00" }}[DependsOn( typeof(GranitBlobStorageAzureBlobModule), typeof(GranitBlobStorageEntityFrameworkCoreModule))]public class AppModule : GranitModule { }builder.AddGranitBlobStorageAzureBlob();builder.AddGranitBlobStorageEntityFrameworkCore(options => options.UseNpgsql(builder.Configuration.GetConnectionString("BlobStorage")));{ "BlobStorage": { "ConnectionString": "<from-vault>", "DefaultContainer": "myapp-blobs", "TenantIsolation": "Prefix" }}For Managed Identity (recommended in production), omit ConnectionString and set:
{ "BlobStorage": { "UseManagedIdentity": true, "ServiceUri": "https://myaccount.blob.core.windows.net", "DefaultContainer": "myapp-blobs" }}[DependsOn( typeof(GranitBlobStorageGoogleCloudModule), typeof(GranitBlobStorageEntityFrameworkCoreModule), typeof(GranitImagingMagickNetModule))]public class AppModule : GranitModule { }builder.AddGranitBlobStorageGoogleCloud();builder.AddGranitBlobStorageEntityFrameworkCore(options => options.UseNpgsql(builder.Configuration.GetConnectionString("BlobStorage")));{ "BlobStorage": { "ProjectId": "my-gcp-project", "DefaultBucket": "myapp-blobs", "TenantIsolation": "Prefix", "UploadUrlExpiry": "00:15:00", "DownloadUrlExpiry": "00:05:00" }}For Workload Identity (recommended in GKE), omit CredentialFilePath — Application Default Credentials are used automatically.
[DependsOn( typeof(GranitBlobStorageFileSystemModule), typeof(GranitBlobStorageProxyModule), typeof(GranitBlobStorageEntityFrameworkCoreModule))]public class AppModule : GranitModule { }builder.AddGranitBlobStorageFileSystem();builder.AddGranitBlobStorageProxy();builder.AddGranitBlobStorageEntityFrameworkCore(options => options.UseNpgsql(builder.Configuration.GetConnectionString("BlobStorage")));
var app = builder.Build();app.MapGranitBlobProxyEndpoints();{ "BlobStorage": { "BasePath": "./blobs", "Proxy": { "BaseUrl": "https://api.example.com", "RoutePrefix": "/api/blobs" } }}[DependsOn( typeof(GranitBlobStorageDatabaseModule), typeof(GranitBlobStorageProxyModule), typeof(GranitBlobStorageEntityFrameworkCoreModule))]public class AppModule : GranitModule { }builder.AddGranitBlobStorageDatabase(options => options.UseNpgsql(builder.Configuration.GetConnectionString("BlobStorage")));builder.AddGranitBlobStorageProxy();builder.AddGranitBlobStorageEntityFrameworkCore(options => options.UseNpgsql(builder.Configuration.GetConnectionString("BlobStorage")));
var app = builder.Build();app.MapGranitBlobProxyEndpoints();{ "BlobStorage": { "MaxBlobSizeBytes": 10485760, "Proxy": { "BaseUrl": "https://api.example.com", "RoutePrefix": "/api/blobs" } }}[DependsOn( typeof(GranitBlobStorageEntityFrameworkCoreModule), typeof(GranitImagingMagickNetModule))]public class AppModule : GranitModule { }builder.AddGranitBlobStorageS3();builder.AddGranitBlobStorageEntityFrameworkCore(options => options.UseNpgsql(builder.Configuration.GetConnectionString("BlobStorage")));{ "BlobStorage": { "ServiceUrl": "http://localhost:9000", "AccessKey": "minioadmin", "SecretKey": "minioadmin", "Region": "us-east-1", "DefaultBucket": "dev-blobs", "ForcePathStyle": true }}Presigned upload flow
Section titled “Presigned upload flow”Cloud providers (S3, Azure) use native presigned URLs. The application server never touches file bytes:
sequenceDiagram
participant C as Client
participant API as Application Server
participant ST as Storage (S3 / Azure)
participant V as Validation Pipeline
C->>API: POST /upload (fileName, contentType, maxBytes)
API->>API: Create BlobDescriptor (Pending)
API->>ST: Generate presigned/SAS URL
ST-->>API: PresignedUploadTicket
API-->>C: { blobId, uploadUrl, headers, expiresAt }
C->>ST: PUT file (presigned URL + required headers)
ST-->>C: 200 OK
C->>API: POST /upload/{blobId}/confirm
API->>ST: HEAD object (size, metadata)
API->>V: Run validators (magic bytes, size)
alt All validators pass
V-->>API: Valid
API->>API: BlobDescriptor → Valid
API-->>C: 200 OK
else Validation fails
V-->>API: Rejected
API->>ST: DELETE object
API->>API: BlobDescriptor → Rejected
API-->>C: 422 (rejection reason)
end
Proxy upload flow (FileSystem / Database)
Section titled “Proxy upload flow (FileSystem / Database)”Server-side providers use Granit.BlobStorage.Proxy which generates ephemeral
token-based URLs. The upload is streamed through the proxy endpoint:
sequenceDiagram
participant C as Client
participant API as Application Server
participant PX as Proxy Endpoints
participant ST as Storage (FS / DB)
C->>API: POST /upload (fileName, contentType, maxBytes)
API->>API: Create BlobDescriptor (Pending)
API->>PX: Generate proxy token
PX-->>API: PresignedUploadTicket (token-based URL)
API-->>C: { blobId, uploadUrl, headers, expiresAt }
C->>PX: PUT /api/blobs/upload/{token} (stream body)
PX->>ST: SaveAsync (stream-through, no RAM buffering)
ST-->>PX: Saved
PX-->>C: 204 No Content
Proxy tokens are single-use, TTL-bound, and backed by IDistributedCache.
IBlobStorage
Section titled “IBlobStorage”The primary entry point for blob operations. All operations are scoped to the current
tenant resolved via ICurrentTenant.
public interface IBlobStorage{ Task<PresignedUploadTicket> InitiateUploadAsync( string containerName, BlobUploadRequest request, CancellationToken cancellationToken = default);
Task<PresignedDownloadUrl> CreateDownloadUrlAsync( string containerName, Guid blobId, DownloadUrlOptions? options = null, CancellationToken cancellationToken = default);
Task<BlobDescriptor?> GetDescriptorAsync( string containerName, Guid blobId, CancellationToken cancellationToken = default);
Task DeleteAsync( string containerName, Guid blobId, string? deletionReason = null, CancellationToken cancellationToken = default);}Upload example
Section titled “Upload example”public class PatientPhotoService(IBlobStorage blobStorage){ public async Task<PresignedUploadTicket> InitiatePhotoUploadAsync( string fileName, CancellationToken cancellationToken) { var request = new BlobUploadRequest( FileName: fileName, ContentType: "image/jpeg", MaxAllowedBytes: 10 * 1024 * 1024); // 10 MB
return await blobStorage.InitiateUploadAsync( "patient-photos", request, cancellationToken) .ConfigureAwait(false); }}The returned PresignedUploadTicket contains everything the frontend needs:
| Property | Description |
|---|---|
BlobId | Stable identifier for polling status and requesting downloads |
UploadUrl | Presigned URL (S3/Azure) or proxy token URL (FileSystem/Database) |
HttpMethod | Always "PUT" |
ExpiresAt | UTC expiry of the presigned URL |
RequiredHeaders | Headers the client must include verbatim (e.g. Content-Type) |
Download example
Section titled “Download example”public async Task<PresignedDownloadUrl> GetPhotoDownloadUrlAsync( Guid blobId, CancellationToken cancellationToken){ return await blobStorage.CreateDownloadUrlAsync( "patient-photos", blobId, new DownloadUrlOptions(DownloadFileName: "patient-photo.jpg"), cancellationToken) .ConfigureAwait(false);}Setting DownloadFileName injects a Content-Disposition: attachment; filename="..." header
into the presigned URL, forcing the browser to download rather than preview.
GDPR deletion
Section titled “GDPR deletion”await blobStorage.DeleteAsync( "patient-photos", blobId, deletionReason: "GDPR Art. 17 erasure request", cancellationToken);The storage object is physically deleted (crypto-shredding). The BlobDescriptor record is
retained in the database for the ISO 27001 3-year audit trail.
BlobDescriptor lifecycle
Section titled “BlobDescriptor lifecycle”stateDiagram-v2
[*] --> Pending : InitiateUploadAsync
Pending --> Uploading : Upload notification
Uploading --> Valid : All validators pass
Uploading --> Rejected : Validation fails
Valid --> Deleted : DeleteAsync (GDPR erasure)
| Status | Storage object | DB record | Description |
|---|---|---|---|
Pending | Not yet uploaded | Exists | Upload ticket issued, waiting for client |
Uploading | Uploaded | Exists | Validation pipeline running |
Valid | Present | Exists | Ready for download |
Rejected | Deleted | Retained | Magic bytes or size check failed |
Deleted | Deleted | Retained (3 years) | GDPR Art. 17 crypto-shredding |
CQRS persistence
Section titled “CQRS persistence”Blob descriptor persistence follows the CQRS pattern with separate reader and writer interfaces. Always inject the specific interface matching your intent:
// Read-only: querying blob metadatapublic class BlobQueryHandler(IBlobDescriptorReader reader) { }
// Write-only: updating blob statepublic class BlobCommandHandler(IBlobDescriptorWriter writer) { }IBlobDescriptorReader also provides specialized queries for background jobs:
| Method | Purpose |
|---|---|
FindAsync(blobId) | Single descriptor by ID |
FindOrphanedAsync(cutoff, batchSize) | Uploads stuck in Pending/Uploading (orphan cleanup) |
FindByContainerBeforeAsync(container, cutoff, batchSize) | GDPR retention cleanup |
Validation pipeline
Section titled “Validation pipeline”Validators run after the file lands on storage. The BlobValidationContext provides
partial access (range read for magic bytes, HEAD for metadata) without buffering the
full file in the application server.
Built-in validators
Section titled “Built-in validators”| Validator | Order | Check |
|---|---|---|
MagicBytesValidator | 10 | Reads first 261 bytes via range read; compares magic-byte signature against declared Content-Type. Detects PDF, JPEG, PNG, GIF, TIFF, DICOM, ZIP. |
MaxSizeValidator | 20 | Compares actual size against MaxAllowedBytes declared at upload time. |
The pipeline is fail-fast: processing stops at the first failing validator.
Custom validator
Section titled “Custom validator”Register application-specific validators (e.g. antivirus scanning) via DI:
public sealed class AntivirusValidator(IAntivirusClient av) : IBlobValidator{ public int Order => 30;
public async Task<BlobValidationResult> ValidateAsync( BlobValidationContext context, CancellationToken cancellationToken = default) { bool isSafe = await av.ScanAsync( context.Descriptor.ObjectKey, cancellationToken) .ConfigureAwait(false);
return isSafe ? BlobValidationResult.Success() : BlobValidationResult.Failure("Malware detected by antivirus scan."); }}builder.Services.AddSingleton<IBlobValidator, AntivirusValidator>();Tenant isolation
Section titled “Tenant isolation”All providers use the same tenant-prefixed object key strategy:
{tenantId}/{containerName}/{yyyy}/{MM}/{blobId}The date components distribute keys across a wider key-space prefix, reducing hot-spot partitions on large buckets.
| Strategy | Key format | Isolation level | Provider support |
|---|---|---|---|
Prefix (default) | {tenantId}/{container}/{yyyy}/{MM}/{blobId} | Key-prefix | All providers |
Container | {container}/{yyyy}/{MM}/{blobId} | One container/bucket per tenant | S3, Azure only |
Single-tenant deployments (no ICurrentTenant) omit the tenant prefix:
{containerName}/{yyyy}/{MM}/{blobId}.
Per-provider isolation details
Section titled “Per-provider isolation details”| Provider | Isolation mechanism |
|---|---|
| S3 | Tenant prefix in S3 object key. Bucket strategy uses one S3 bucket per tenant. |
| Azure Blob | Tenant prefix in blob name. Container strategy uses one Azure container per tenant. |
| Google Cloud | Tenant prefix in GCS object name. Bucket strategy uses one GCS bucket per tenant. |
| FileSystem | Tenant prefix in directory path: {BasePath}/{tenantId}/... |
| Database | Tenant prefix in ObjectKey column + IMultiTenant EF Core query filter |
Domain events
Section titled “Domain events”BlobDescriptor publishes domain events on state transitions:
| Event | Trigger |
|---|---|
BlobValidated(BlobId, ContainerName, VerifiedContentType, SizeBytes) | Blob passes all validators |
BlobRejected(BlobId, ContainerName, RejectionReason) | Validation fails |
BlobDeleted(BlobId, ContainerName, DeletionReason) | GDPR crypto-shredding |
Subscribe via Wolverine handlers to react to blob lifecycle changes (e.g. trigger image processing after validation, notify the user on rejection).
Image processing (Granit.Imaging)
Section titled “Image processing (Granit.Imaging)”IImageProcessor provides a fluent pipeline for image transformations. The pipeline
holds native resources and must be disposed after use.
public class PatientPhotoProcessor(IImageProcessor imageProcessor){ public async Task<ImageResult> CreateThumbnailAsync( Stream sourceImage, CancellationToken cancellationToken) { await using IImagePipeline pipeline = imageProcessor.Load(sourceImage);
return await pipeline .Resize(200, 200, ResizeMode.Crop) .StripMetadata() .Compress(quality: 80) .ConvertTo(ImageFormat.WebP) .ToResultAsync(cancellationToken); }}Pipeline operations
Section titled “Pipeline operations”| Method | Description |
|---|---|
Resize(width, height, mode) | Resize with configurable strategy |
Crop(rectangle) | Crop to rectangular region |
Compress(quality) | Set output quality (0-100) |
ConvertTo(format) | Change output format |
Watermark(data, position, opacity) | Composite watermark overlay |
StripMetadata() | Remove EXIF, IPTC, XMP metadata (GDPR) |
ToResultAsync() | Terminal: encode and return ImageResult |
SaveToStreamAsync(stream) | Terminal: encode and write to stream |
Resize modes
Section titled “Resize modes”| Mode | Behavior |
|---|---|
Max | Fit within bounds, preserving aspect ratio (may be smaller than target) |
Pad | Fit within bounds with transparent padding to exact dimensions |
Crop | Fill exact dimensions by resizing and center-cropping overflow |
Stretch | Stretch to exact dimensions (distorts aspect ratio) |
Min | Resize to minimum bounds covering target dimensions entirely |
Convenience methods
Section titled “Convenience methods”await pipeline.SaveAsWebPAsync(cancellationToken); // ConvertTo(WebP) + ToResultAsyncawait pipeline.SaveAsAvifAsync(cancellationToken); // ConvertTo(AVIF) + ToResultAsyncawait pipeline.SaveAsJpegAsync(cancellationToken); // ConvertTo(JPEG) + ToResultAsyncawait pipeline.SaveAsPngAsync(cancellationToken); // ConvertTo(PNG) + ToResultAsyncSupported formats
Section titled “Supported formats”| Format | Read | Write | Notes |
|---|---|---|---|
| JPEG | Yes | Yes | Lossy, no transparency |
| PNG | Yes | Yes | Lossless, transparency |
| WebP | Yes | Yes | Modern lossy/lossless, smaller than JPEG |
| AVIF | Yes | Yes | Next-gen, best compression ratio |
| GIF | Yes | Yes | 256 colors, animation support |
| BMP | Yes | Yes | Uncompressed bitmap |
| TIFF | Yes | Yes | Lossless, medical imaging |
GDPR: metadata stripping
Section titled “GDPR: metadata stripping”StripMetadata() removes all EXIF, IPTC, and XMP metadata from images. This is
critical for GDPR compliance: uploaded photos often contain GPS coordinates, camera
serial numbers, timestamps, and other personally identifiable information.
await using IImagePipeline pipeline = imageProcessor.Load(uploadedPhoto);ImageResult sanitized = await pipeline .StripMetadata() .SaveAsWebPAsync(cancellationToken);Combining with blob storage
Section titled “Combining with blob storage”A typical pattern: subscribe to BlobValidated events to post-process images
after upload validation:
public static class BlobValidatedHandler{ public static async Task HandleAsync( BlobValidated @event, IBlobStorage blobStorage, IImageProcessor imageProcessor, CancellationToken cancellationToken) { if (@event.ContainerName != "patient-photos") return;
// Download, process, re-upload as thumbnail PresignedDownloadUrl download = await blobStorage .CreateDownloadUrlAsync("patient-photos", @event.BlobId, cancellationToken: cancellationToken) .ConfigureAwait(false);
// ... fetch image from download.Url, process with imageProcessor }}Health check
Section titled “Health check”builder.Services.AddHealthChecks() .AddGranitS3HealthCheck();Verifies S3 connectivity by listing one object in the default bucket (ListObjectsV2
with MaxKeys=1). Tagged ["readiness", "startup"]. Returns Unhealthy on access
denied or unreachable endpoint. Error messages are sanitized — credentials, bucket
names, and endpoint URLs are never exposed.
Google Cloud Storage
Section titled “Google Cloud Storage”builder.Services.AddHealthChecks() .AddGranitGoogleCloudStorageHealthCheck();Verifies GCS connectivity by listing one object in the default bucket. Tagged
["readiness", "startup"]. Returns Unhealthy on access denied or unreachable endpoint.
Configuration reference
Section titled “Configuration reference”{ "BlobStorage": { "ServiceUrl": "https://s3.eu-west-1.amazonaws.com", "AccessKey": "<from-vault>", "SecretKey": "<from-vault>", "Region": "eu-west-1", "DefaultBucket": "myapp-blobs", "ForcePathStyle": false, "TenantIsolation": "Prefix", "UploadUrlExpiry": "00:15:00", "DownloadUrlExpiry": "00:05:00" }}| Property | Default | Description |
|---|---|---|
ServiceUrl | — | S3-compatible endpoint URL (required) |
AccessKey | — | S3 access key (required, inject from Vault) |
SecretKey | — | S3 secret key (required, inject from Vault) |
Region | "us-east-1" | S3 region for request signing |
DefaultBucket | — | S3 bucket name (required) |
ForcePathStyle | true | Path-style URLs (host/bucket/key); required for MinIO |
TenantIsolation | Prefix | Prefix (shared bucket) or Bucket (one per tenant) |
{ "BlobStorage": { "ConnectionString": "<from-vault>", "DefaultContainer": "myapp-blobs", "TenantIsolation": "Prefix" }}| Property | Default | Description |
|---|---|---|
ConnectionString | — | Azure Storage connection string (from Vault) |
DefaultContainer | — | Default blob container name (required) |
UseManagedIdentity | false | Use Azure Managed Identity instead of connection string |
ServiceUri | — | Storage account URI (required when UseManagedIdentity = true) |
TenantIsolation | Prefix | Prefix (shared container) or Container (one per tenant) |
{ "BlobStorage": { "ProjectId": "my-gcp-project", "DefaultBucket": "myapp-blobs", "CredentialFilePath": null, "TenantIsolation": "Prefix" }}| Property | Default | Description |
|---|---|---|
ProjectId | — | GCP project ID (required) |
DefaultBucket | — | Default GCS bucket name (required) |
CredentialFilePath | null | Service account key JSON; ADC when null (Workload Identity) |
TenantIsolation | Prefix | Prefix (shared bucket) or Bucket (one per tenant) |
{ "BlobStorage": { "BasePath": "./blobs" }}| Property | Default | Description |
|---|---|---|
BasePath | — | Root directory for blob storage (required) |
{ "BlobStorage": { "MaxBlobSizeBytes": 10485760 }}| Property | Default | Description |
|---|---|---|
MaxBlobSizeBytes | 10485760 (10 MB) | Maximum blob size accepted by the provider |
{ "BlobStorage": { "Proxy": { "BaseUrl": "https://api.example.com", "RoutePrefix": "/api/blobs", "MaxUploadBytes": 104857600 } }}| Property | Default | Description |
|---|---|---|
BaseUrl | — | Public URL of the API server (required) |
RoutePrefix | "/api/blobs" | Route prefix for proxy endpoints |
MaxUploadBytes | 104857600 (100 MB) | Maximum upload size through proxy |
Common options (all providers)
Section titled “Common options (all providers)”| Property | Default | Description |
|---|---|---|
UploadUrlExpiry | 00:15:00 | Presigned upload URL / proxy token TTL |
DownloadUrlExpiry | 00:05:00 | Presigned download URL / proxy token TTL |
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Module | GranitBlobStorageModule, GranitBlobStorageS3Module, GranitBlobStorageAzureBlobModule, GranitBlobStorageGoogleCloudModule, GranitBlobStorageFileSystemModule, GranitBlobStorageDatabaseModule, GranitBlobStorageProxyModule, GranitBlobStorageEntityFrameworkCoreModule | — |
| Storage | IBlobStorage, IBlobKeyStrategy | Granit.BlobStorage |
| Persistence (CQRS) | IBlobDescriptorReader, IBlobDescriptorWriter, IBlobDescriptorStore | Granit.BlobStorage |
| Domain | BlobDescriptor, BlobStatus, BlobUploadRequest | Granit.BlobStorage |
| Presigned URLs | PresignedUploadTicket, PresignedDownloadUrl, DownloadUrlOptions | Granit.BlobStorage |
| Validation | IBlobValidator, BlobValidationResult, BlobValidationContext, MagicBytesValidator, MaxSizeValidator | Granit.BlobStorage |
| Events | BlobValidated, BlobRejected, BlobDeleted | Granit.BlobStorage |
| S3 options | S3BlobOptions, BlobTenantIsolation | Granit.BlobStorage.S3 |
| Azure options | AzureBlobOptions | Granit.BlobStorage.AzureBlob |
| Google Cloud options | GoogleCloudStorageOptions | Granit.BlobStorage.GoogleCloud |
| FileSystem options | FileSystemBlobOptions | Granit.BlobStorage.FileSystem |
| Database options | DatabaseBlobOptions | Granit.BlobStorage.Database |
| Proxy options | ProxyBlobOptions | Granit.BlobStorage.Proxy |
| Imaging | IImageProcessor, IImagePipeline, ImageResult, ImageFormat, ImageSize | Granit.Imaging |
| Imaging types | ResizeMode, CropRectangle, WatermarkPosition | Granit.Imaging |
| Extensions | AddGranitBlobStorageS3(), AddGranitBlobStorageAzureBlob(), AddGranitBlobStorageGoogleCloud(), AddGranitBlobStorageFileSystem(), AddGranitBlobStorageDatabase(), AddGranitBlobStorageProxy(), AddGranitBlobStorageEntityFrameworkCore(), AddGranitImagingMagickNet(), MapGranitBlobProxyEndpoints() | — |
See also
Section titled “See also”- Configure blob storage — Step-by-step guide
- Pre-signed URL pattern — Architectural pattern
- Persistence module — EF Core interceptors, query filters
- Security module — Authorization, tenant isolation
- Privacy module — GDPR compliance patterns
- Background Jobs module — Orphan cleanup, retention jobs