Appearance
Object Storage
Gryd.IO now provides a shared object storage module through Gryd.Infrastructure.
This module centralizes cloud file operations for:
GrydAuthprofile image uploadGrydReportsfile persistence when using cloud backends- Any custom module that needs upload/download/delete/presigned URLs
Registration
Register once in your host application:
csharp
using Gryd.Infrastructure.Extensions;
builder.Services.AddGrydObjectStorage(builder.Configuration);appsettings.json
Configuration section: GrydStorage
json
{
"GrydStorage": {
"DefaultProvider": "AmazonS3",
"AmazonS3": {
"BucketName": "my-bucket",
"Region": "us-east-1",
"AccessKeyId": "",
"SecretAccessKey": "",
"DefaultPrefix": "app"
},
"GoogleCloudStorage": {
"BucketName": "my-gcs-bucket",
"CredentialsFilePath": "./secrets/gcp-service-account.json",
"DefaultPrefix": "app"
},
"AzureBlobStorage": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=...",
"ContainerName": "app-files",
"CreateContainerIfNotExists": true,
"DefaultPrefix": "app"
}
}
}Supported Providers
AmazonS3GoogleCloudStorageAzureBlobStorage
Main Contracts
IObjectStorageService: application-facing facadeIObjectStorageProvider: provider adapter contractIObjectStorageProviderResolver: strategy resolver by provider kind
Reading an object's metadata
GetDescriptorAsync answers with an ObjectStorageObjectDescriptor — the size, the content type and the custom metadata — or null when the object is not there. A missing object is Success(null), not a failure: "it is not there" is an answer, and a failure is reserved for not having been able to ask (a credential, a network, a bucket that was never configured).
csharp
var descriptor = await storage.GetDescriptorAsync(new ObjectStorageDescribeRequest
{
ObjectKey = objectKey
});
if (descriptor.IsSuccess && descriptor.Data is null)
{
// The object never arrived.
}It exists for the size. Confirming that an upload landed means comparing what the client declared against what the bucket actually holds, and OpenReadAsync would defeat the reason the bytes were sent straight to the bucket in the first place: every provider answers this from a HEAD, so asking for the descriptor costs what asking ExistsAsync costs and returns strictly more.
Presigned download: overriding the response headers
ObjectStoragePresignedUrlRequest carries two optional fields that reshape the response of a signed download. Both are ignored on ObjectStorageSignedUrlOperation.Upload, where there is no response to reshape:
| Field | Effect | Provider mechanism |
|---|---|---|
ResponseContentDisposition | The Content-Disposition the storage returns | S3 ResponseHeaderOverrides, Azure SAS rscd, GCS response-content-disposition |
ResponseContentType | The Content-Type the storage returns | S3 ResponseHeaderOverrides, Azure SAS rsct, GCS response-content-type |
Without them the browser saves the object under its storage key, so a user who uploaded contrato assinado.pdf gets a3f9….pdf back.
Build the disposition with ContentDispositionHeader.Attachment(fileName), which applies the RFC 5987 encoding a filename with an accent, a space or a comma requires — rescisão, 2ª via.pdf survives the round trip, a raw value does not:
csharp
var url = await storage.GeneratePresignedUrlAsync(new ObjectStoragePresignedUrlRequest
{
ObjectKey = objectKey,
ExpiresIn = TimeSpan.FromMinutes(5),
Operation = ObjectStorageSignedUrlOperation.Download,
ResponseContentDisposition = ContentDispositionHeader.Attachment("rescisão, 2ª via.pdf"),
ResponseContentType = "application/octet-stream"
});Both values are part of the signature: editing them in the emitted URL invalidates it rather than changing the header, which is what stops the link from becoming a way to rename — or re-type — somebody else's download.
X-Content-Type-Options: nosniff is not expressible this way. The set of headers a presigned URL may override is closed and identical across the three providers — content-type, content-language, expires, cache-control, content-disposition and content-encoding — and custom metadata comes back as x-amz-meta-*, not as the header. nosniff belongs to a bucket or CDN response policy. The combination that does hold without it is attachment plus application/octet-stream: together they stop the browser rendering anything, HTML and SVG included.
Notes
- The module is provider-agnostic from Application layer perspective.
- New providers can be added without changing callers (OCP).
- A plaintext
ServiceUrl(MinIO or an S3-compatible gateway on a developer machine or in CI) is honoured by the presigned URL as well as by the API calls: the signed link comes back onhttp, matching the endpoint, instead of onhttpsagainst a port that never spoke TLS. GrydAuthstill acceptsGrydAuth:Storage:AwsS3for backward compatibility, butGrydStorageis the recommended source of truth.