- Enhanced the architecture documentation to recommend direct upload over legacy proxy upload for improved performance and scalability. - Added detailed comparisons of upload patterns, including throughput, memory usage, and latency. - Updated API endpoint documentation to reflect new direct upload methods and their benefits. - Included examples for direct upload flow and bucket directory structure to aid developers in implementation.
Storage Service
A .NET 10 microservice for file storage management supporting MinIO and Aliyun OSS.
Features
- Multi-provider Storage: MinIO (S3-compatible) and Aliyun OSS
- Runtime Provider Switching: Switch between MinIO and Aliyun via environment variable
- Complete File CRUD: Upload, download, delete, list files
- Pre-signed URLs: Secure time-limited download/upload URLs
- User Quotas: Storage capacity and file count limits per user
- Inter-service Communication: JWT validation via IAM Service with caching
Quick Start
Prerequisites
- .NET 10 SDK
- Docker & Docker Compose
- PostgreSQL (or Neon)
- MinIO (or Aliyun OSS account)
Run with Docker
cd services/storage-service-net
docker-compose up -d
Access: http://localhost:5002/swagger
Run Locally
cd services/storage-service-net
# Install dependencies
dotnet restore
# Run migrations (first time)
dotnet ef database update --project src/StorageService.Infrastructure --startup-project src/StorageService.API
# Start the service
dotnet run --project src/StorageService.API
Configuration
Environment Variables
| Variable | Description | Default |
|---|---|---|
Storage__Provider |
Provider selection: minio or aliyun |
minio |
Storage__DefaultBucket |
Default bucket name | storage |
Storage__MaxFileSizeBytes |
Maximum file size | 104857600 (100MB) |
Storage__PreSignedUrlExpirationSeconds |
Pre-signed URL expiration | 3600 |
MinIO Configuration
| Variable | Description | Default |
|---|---|---|
Storage__MinIO__Endpoint |
MinIO server endpoint | localhost:9000 |
Storage__MinIO__AccessKey |
Access key | - |
Storage__MinIO__SecretKey |
Secret key | - |
Storage__MinIO__UseSSL |
Enable SSL | false |
Aliyun OSS Configuration
| Variable | Description | Default |
|---|---|---|
Storage__AliyunOSS__Endpoint |
OSS endpoint | - |
Storage__AliyunOSS__AccessKeyId |
Access key ID | - |
Storage__AliyunOSS__AccessKeySecret |
Access key secret | - |
Storage__AliyunOSS__Region |
OSS region | - |
IAM Service Configuration
| Variable | Description | Default |
|---|---|---|
IamService__BaseUrl |
IAM Service URL | http://localhost:5001 |
IamService__ServiceName |
Service identifier | storage-service |
IamService__TimeoutSeconds |
Request timeout | 30 |
IamService__CacheDurationSeconds |
User info cache TTL | 300 |
IamService__HealthCheckCacheDurationSeconds |
Health check cache TTL | 60 |
API Endpoints
Files (Legacy - Proxy Upload)
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/files/upload |
Upload a file via backend (max 100MB) |
GET |
/api/v1/files |
List user's files with pagination |
GET |
/api/v1/files/{id} |
Get file metadata by ID |
GET |
/api/v1/files/{id}/download-url |
Get pre-signed download URL |
DELETE |
/api/v1/files/{id} |
Delete a file (soft delete) |
Direct Upload (Recommended for Scale)
Benefits of Direct Upload:
- Backend doesn't handle file binary → 90%+ load reduction
- Can handle millions of concurrent uploads
- Faster upload speed (no intermediary)
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/storage/sign-upload |
Get pre-signed PUT URL for direct upload |
POST |
/api/v1/storage/confirm-upload |
Confirm upload and save metadata |
Quota
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/quota |
Get current user's storage quota |
Direct Upload Example (Recommended)
Step 1: Get Pre-signed URL
curl -X POST "http://localhost:5002/api/v1/storage/sign-upload" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fileName": "document.pdf",
"fileSizeBytes": 1048576,
"contentType": "application/pdf",
"accessLevel": "private"
}'
Response:
{
"success": true,
"data": {
"uploadUrl": "http://minio:9000/storage/private/user123/20260113/abc12345_document.pdf?X-Amz-...",
"objectKey": "private/user123/20260113/abc12345_document.pdf",
"expiresAt": "2026-01-13T21:49:33Z"
}
}
Step 2: Upload Directly to MinIO
curl -X PUT "${uploadUrl}" \
-H "Content-Type: application/pdf" \
--data-binary @document.pdf
Step 3: Confirm Upload
curl -X POST "http://localhost:5002/api/v1/storage/confirm-upload" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"objectKey": "private/user123/20260113/abc12345_document.pdf",
"fileName": "document.pdf",
"fileSizeBytes": 1048576,
"contentType": "application/pdf",
"accessLevel": "private"
}'
Response:
{
"success": true,
"data": {
"fileId": "550e8400-e29b-41d4-a716-446655440000",
"metadata": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"fileName": "document.pdf",
"contentType": "application/pdf",
"fileSizeBytes": 1048576,
"provider": "MinIO",
"accessLevel": "Private",
"uploadedAt": "2026-01-13T20:49:33Z"
}
}
}
Bucket Directory Structure
Files are organized by access level and user ID with the following format:
{bucket}/
├── private/{userId}/{date}/{fileId}_{filename} → Owner access only (via pre-signed URL)
├── public/{userId}/{date}/{fileId}_{filename} → Publicly accessible
└── shared/{userId}/{date}/{fileId}_{filename} → Controlled by sharing rules
Object Key Format Details
| Component | Description | Example |
|---|---|---|
{bucket} |
Bucket name (from config) | goodgo |
{accessLevel} |
Access level prefix | private, public, shared |
{userId} |
Uploader's user ID | user123 |
{date} |
Upload date (UTC) | 20260113 |
{fileId} |
First 8 chars of GUID | a1b2c3d4 |
{filename} |
Sanitized file name | document.pdf |
Real-World Example
goodgo/
├── private/
│ └── user123/
│ └── 20260113/
│ ├── a1b2c3d4_document.pdf
│ └── e5f6g7h8_image.jpg
├── public/
│ └── user456/
│ └── 20260113/
│ └── i9j0k1l2_avatar.png
└── shared/
└── user789/
└── 20260113/
└── m3n4o5p6_presentation.pptx
Note
: The object key is returned in the
/sign-uploadresponse and must be sent back when calling/confirm-upload.
Legacy Upload Example (Via Backend)
curl -X POST "http://localhost:5002/api/v1/files/upload" \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf"
Inter-Service Communication
The service communicates with IAM Service for user validation. This section explains the configuration required for proper inter-service communication in Docker environments.
Docker Network Configuration
When running in Docker Compose, services communicate via Docker network using container names:
# docker-compose.yml
storage-service:
environment:
# IMPORTANT: Use container name for inter-service communication
- IamService__BaseUrl=http://iam-service-net:8080
- IamService__ServiceName=storage-service
Note
: The URL
http://iam-service-net:8080uses:
iam-service-net= Docker container name (notlocalhost)8080= Internal container port (not exposed port5001)
JWT Token Issuer Configuration
To ensure JWT tokens work across services, IAM Service must use a fixed issuer URI:
# IAM Service docker-compose.yml
iam-service-net:
environment:
- IdentityServer__IssuerUri=http://iam-service
This ensures tokens issued by IAM Service have a consistent iss claim regardless of how the client accesses it.
Communication Flow
┌─────────────────┐ JWT Token (iss: http://iam-service) ┌─────────────────┐
│ Storage │ ──────────────────────────────────────────► │ IAM Service │
│ Service │ ◄────────────────────────────────────────── │ │
│ :8080 │ User info + Roles/Permissions │ :8080 │
└─────────────────┘ └─────────────────┘
│ Docker Network: goodgo-network │
└──────────────────────────────────────────────────────────────┘
Headers & Caching
- Headers:
Authorization: Bearer <token>,X-Service-Name: storage-service - User Info Cache: 5 minutes (300 seconds)
- Health Check Cache: 1 minute (60 seconds)
- Resilience: Polly retry (3x) + circuit breaker
Available Methods (IIamServiceClient)
| Method | Description |
|---|---|
ValidateUserAsync |
Validate JWT token and get user info |
GetUserByIdAsync |
Get user by ID |
GetUserRolesAsync |
Get user's assigned roles |
GetUserPermissionsAsync |
Get user's permissions |
HasPermissionAsync |
Check if user has specific permission |
HasRoleAsync |
Check if user has specific role |
CheckHealthAsync |
Check IAM Service health status |
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
401 Unauthorized |
JWT issuer mismatch | Set IdentityServer__IssuerUri in IAM Service |
Connection refused |
Wrong URL or container not running | Use container name, not localhost |
Name resolution failed |
Container not in same network | Ensure both services use goodgo-network |
Timeout |
IAM Service slow/unhealthy | Check IAM health: curl http://iam-service-net:8080/health |
Database Migrations
# Create new migration
dotnet ef migrations add MigrationName \
--project src/StorageService.Infrastructure \
--startup-project src/StorageService.API
# Apply migrations
dotnet ef database update \
--project src/StorageService.Infrastructure \
--startup-project src/StorageService.API
Testing
# Run all tests
dotnet test
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
Project Structure
services/storage-service-net/
├── src/
│ ├── StorageService.API/ # Controllers, Commands, Queries
│ ├── StorageService.Domain/ # Entities, Repository interfaces
│ └── StorageService.Infrastructure/# Providers, DbContext, Repositories
├── tests/
│ ├── StorageService.UnitTests/
│ └── StorageService.FunctionalTests/
├── docs/
│ ├── en/ # English documentation
│ └── vi/ # Vietnamese documentation
├── docker-compose.yml
├── Dockerfile
└── README.md
License
MIT