- name
- binary-file-storage
- description
- Pattern for storing and serving binary files (images, etc.) across both File and Blob storage backends
- domain
- storage, api-design
- confidence
- high
- source
- earned — image support feature implementation
## Context
When adding binary file support (images, attachments) to Squad Places, both FileStorageService and BlobStorageService must be extended consistently. The pattern applies to any new binary content type.
## Patterns
### Storage Interface
Add `Save{Type}Async(Guid ownerId, Guid id, byte[] data, string contentType)` and `Get{Type}Async(Guid ownerId, Guid id)` returning `(byte[] Data, string ContentType)?` to `IBlobStorageService`. Owner-scoped IDs enable natural data isolation.
### FileStorageService
- Store binary at `{basePath}/{type}/{ownerId}/{id}{extension}`
- Store content type in a `.meta` sidecar file at `{basePath}/{type}/{ownerId}/{id}.meta`
- Map content types to extensions in a switch expression
- Create the top-level directory in `InitializeAsync()`; owner subdirectories created on-demand via `Directory.CreateDirectory()`
### BlobStorageService
- Use a dedicated container (e.g., `images`)
- Blob path: `{ownerId}/{id}{extension}` — virtual folder structure
- Set `BlobHttpHeaders.ContentType` on upload
- For retrieval, search by prefix with `GetBlobsAsync(BlobTraits, BlobStates, prefix, CancellationToken)`
- Create container in `InitializeAsync()`
### API Pattern
- `POST /api/{type}` — accept base64 body with owner ID, validate owner exists, store, return relative URL
- `GET /api/{type}/{ownerId}/{id}` — serve with `Results.File(data, contentType)`
- Inline upload on parent entity (e.g., `ImageData` on `PublishArtifactRequest`) as convenience
- **Only relative URLs** — reject absolute http/https URLs to prevent SSRF and ensure all content is hosted
### URL Security
- Validate URLs with regex matching `/api/{type}/{guid}/{guid}` format
- In HTML sanitizer, use `FilterUrl` event to strip non-relative `src` attributes rather than removing the tag entirely
- This is more surgical than scheme-based allowlisting — permits relative paths without custom schemes
### Dockerfile
Add the new data directory to the `mkdir -p` line.
## Examples
See `src/SquadPlaces.Data/FileStorageService.cs` (SaveImageAsync/GetImageAsync) and `src/SquadPlaces.Web/Api/ApiEndpoints.cs` (image endpoints).
## Anti-Patterns
- Don't store binary data as base64 in JSON — decode to bytes first
- Don't use `GetBlobsAsync(prefix:)` named parameter — the .NET 10 API requires positional args
- Don't forget the `.meta` sidecar in file storage — without it, you can't serve with correct Content-Type
Ver no GitHub