Skip to content

Object Storage ​

Gryd.IO now provides a shared object storage module through Gryd.Infrastructure.

This module centralizes cloud file operations for:

  • GrydAuth profile image upload
  • GrydReports file 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 ​

  • AmazonS3
  • GoogleCloudStorage
  • AzureBlobStorage

Main Contracts ​

  • IObjectStorageService: application-facing facade
  • IObjectStorageProvider: provider adapter contract
  • IObjectStorageProviderResolver: 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:

FieldEffectProvider mechanism
ResponseContentDispositionThe Content-Disposition the storage returnsS3 ResponseHeaderOverrides, Azure SAS rscd, GCS response-content-disposition
ResponseContentTypeThe Content-Type the storage returnsS3 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 on http, matching the endpoint, instead of on https against a port that never spoke TLS.
  • GrydAuth still accepts GrydAuth:Storage:AwsS3 for backward compatibility, but GrydStorage is the recommended source of truth.

Released under the MIT License.