PresignedUrlClient.Serialization.SystemTextJson
2.3.0
See the version list below for details.
dotnet add package PresignedUrlClient.Serialization.SystemTextJson --version 2.3.0
NuGet\Install-Package PresignedUrlClient.Serialization.SystemTextJson -Version 2.3.0
<PackageReference Include="PresignedUrlClient.Serialization.SystemTextJson" Version="2.3.0" />
<PackageVersion Include="PresignedUrlClient.Serialization.SystemTextJson" Version="2.3.0" />
<PackageReference Include="PresignedUrlClient.Serialization.SystemTextJson" />
paket add PresignedUrlClient.Serialization.SystemTextJson --version 2.3.0
#r "nuget: PresignedUrlClient.Serialization.SystemTextJson, 2.3.0"
#:package PresignedUrlClient.Serialization.SystemTextJson@2.3.0
#addin nuget:?package=PresignedUrlClient.Serialization.SystemTextJson&version=2.3.0
#tool nuget:?package=PresignedUrlClient.Serialization.SystemTextJson&version=2.3.0
๐ PresignedUrlClient
A resilient, production-ready .NET Standard 2.1 client library for S3 Presigned URL Services with built-in async/await, circuit breaker, retry logic, automatic failover, first-class storage operations, and automatic S3 integration for multipart uploads.
๐ Table of Contents
- โจ Features
- ๐๏ธ Architecture
- ๐ Quick Start
- ๐ฆ Installation
- โ๏ธ Configuration
- ๐ Usage Examples
- ๐ Storage Operations
- ๐ก๏ธ Resilience Patterns
- ๐ฆ Multipart Uploads
- ๐จ Error Handling
- ๐งช Testing
- ๐ค Contributing
- ๐ License
โ Status
v2.3.0 - Production Ready ๐
โจ NEW: Automatic S3 integration for multipart uploads - supports both backend architectures!
โจ NEW: Placeholder uploadId detection - library automatically calls S3 when needed!
โจ NEW: XML response parsing - extracts real uploadId from S3 responses!
๐ Backward Compatible: All v2.2.0 code continues to work - zero breaking changes!
โจ Features
Core Capabilities
- ๐ Presigned URL Generation - GET and PUT operations for S3 objects
- โก Async/Await Support - Full async methods with CancellationToken (v2.0)
- ๐ Storage Operations - First-class upload/download with progress tracking (NEW in v2.1)
- ๐ฆ Multipart Upload - Complete workflow for large files (5MB - 5TB)
- ๐ API Key Authentication - Secure X-API-Key header authentication
- โ๏ธ Service Configuration Discovery - Automatic bucket and region detection
- ๐ฏ Type-Safe Models - Strongly-typed requests/responses with built-in validation
Resilience & Reliability โญ
- ๐ Automatic Retry Logic - Configurable retry attempts with exponential backoff
- ๐ก๏ธ Circuit Breaker Pattern - Prevents cascading failures when service is down
- โฑ๏ธ Timeout Management - Configurable request timeouts with cancellation support
- ๐จ Comprehensive Error Handling - Specific exception types (400, 401, 403, 500)
- ๐ Zero External Dependencies - Only .NET Standard 2.1 + ResilientHttpClient.Core
Storage Operations (v2.1) + Enhanced Progress (v2.2) + S3 Integration (v2.3) โญ
- ๐ค Upload Files - Stream-based upload with continuous progress tracking (fixed in v2.2!)
- ๐ฅ Download Files - Stream-based download with progress tracking
- ๐ Automatic S3 Integration - Handles both backend-initiated and client-initiated multipart uploads (v2.3)
- ๐ข Multipart Workflow - Complete initiate โ upload parts โ complete/abort with per-part progress
- โ ETag Validation - Data integrity verification
- ๐ Hybrid Progress - Rich (
IProgress<UploadProgress>) + Simple (IProgress<double>) options (NEW v2.2) - โ Cancellation Support - Graceful operation cancellation
Developer Experience
- ๐ Dependency Injection Ready - First-class Microsoft.Extensions.DependencyInjection support
- โ 95%+ Code Coverage - 205 tests (100% pass rate) covering all scenarios including new progress tracking
- ๐ XML Documentation - IntelliSense-friendly API documentation
- ๐จ Clean Architecture - SOLID principles with clear separation of concerns
- ๐ง Configuration Binding - Seamless appsettings.json integration
- ๐ Enhanced Sample Project - Real upload/download with visual success/failure tracking and comprehensive test results
๐๏ธ Architecture
graph TB
subgraph "๐ฏ Client Application"
A[Your Service/Controller]
end
subgraph "๐ฆ PresignedUrlClient Library"
B[IPresignedUrlService<br/>Interface]
C[PresignedUrlService<br/>Implementation]
D[IResilientHttpClient<br/>Resilient HTTP Layer]
E[RequestBuilder<br/>JSON Serialization]
F[ResponseParser<br/>Error Mapping]
end
subgraph "๐ก๏ธ Resilience Layer"
G[Retry Logic<br/>3 attempts]
H[Circuit Breaker<br/>Failure Detection]
I[Timeout Handler<br/>30s default]
end
subgraph "๐ External Service"
J[S3 Presigned URL<br/>Service API]
end
A -->|Dependency Injection| B
B -.Implements.- C
C -->|Uses| D
C -->|Builds Requests| E
C -->|Parses Responses| F
D -->|Applies| G
D -->|Monitors| H
D -->|Enforces| I
D -->|HTTPS POST/GET| J
style B fill:#e1f5ff,stroke:#0066cc,stroke-width:2px
style D fill:#fff4e1,stroke:#ff9900,stroke-width:2px
style J fill:#ffe1e1,stroke:#cc0000,stroke-width:2px
style G fill:#e8f5e9,stroke:#4caf50
style H fill:#e8f5e9,stroke:#4caf50
style I fill:#e8f5e9,stroke:#4caf50
๐ Project Structure
PresignedUrlClient/
โโโ src/
โ โโโ PresignedUrlClient.Abstractions/ # ๐ Interfaces, Models, Exceptions
โ โ โโโ IPresignedUrlService.cs
โ โ โโโ IPresignedUrlConfig.cs
โ โ โโโ Models/ # Request & Response DTOs
โ โ โโโ Enums/ # S3Operation enum
โ โ โโโ Exceptions/ # Custom exception hierarchy
โ โ
โ โโโ PresignedUrlClient.Core/ # โ๏ธ Implementation Logic
โ โ โโโ PresignedUrlService.cs # Main service implementation
โ โ โโโ Configuration/ # Options & validation
โ โ โโโ Internal/ # RequestBuilder, ResponseParser
โ โ
โ โโโ PresignedUrlClient.DependencyInjection/ # ๐ DI Extensions
โ โโโ ServiceCollectionExtensions.cs
โ
โโโ tests/
โ โโโ PresignedUrlClient.Core.Tests/ # ๐งช ~60 Unit & Integration Tests
โ โโโ PresignedUrlClient.DependencyInjection.Tests/ # ~15 DI Tests
โ โโโ PresignedUrlClient.Serialization.SystemTextJson.Tests/ # ~7 Serialization Tests
โ โโโ PresignedUrlClient.Serialization.NewtonsoftJson.Tests/ # ~7 Serialization Tests
โ
โโโ docs/
โโโ PLANNING.md # Architecture decisions
โโโ TASKS.md # Development tracker
๐ Quick Start
๐ Requirements
- .NET Standard 2.1+ compatible framework:
- โ .NET Core 3.0, 3.1
- โ .NET 5, 6, 7, 8, 9+
- โ .NET Framework 4.8+ (with .NET Standard 2.1 support)
- .NET SDK 6.0+ for building and testing
๐ฆ Installation
Core Package (URL Generation Only)
# Minimum installation for presigned URL generation
dotnet add reference path/to/PresignedUrlClient.DependencyInjection
Storage Package (Upload/Download) - NEW v2.1 โญ
# Full installation including storage operations
dotnet add reference path/to/PresignedUrlClient.DependencyInjection
dotnet add reference path/to/PresignedUrlClient.Storage.DependencyInjection
Step 2: Register Services
Option A: In Program.cs (.NET 6+)
using PresignedUrlClient.DependencyInjection;
using PresignedUrlClient.Storage.DependencyInjection; // NEW v2.1
var builder = WebApplication.CreateBuilder(args);
// Register PresignedUrlClient services
builder.Services.AddPresignedUrlClient(options =>
{
options.BaseUrl = "https://presigned-url-service.example.com";
options.ApiKey = builder.Configuration["PresignedUrlService:ApiKey"]!;
options.DefaultExpiresIn = 3600; // 1 hour default
// โญ Resilience settings (optional - these are defaults)
options.RetryCount = 3; // 3 retry attempts
options.RetryDelayMilliseconds = 1000; // 1 second between retries
options.CircuitBreakerThreshold = 5; // Open circuit after 5 failures
options.CircuitBreakerDurationSeconds = 60; // Keep circuit open for 60s
});
// โญ NEW v2.1: Register Storage services for upload/download
builder.Services.AddPresignedUrlStorage(options =>
{
options.DefaultTimeout = TimeSpan.FromMinutes(30);
options.BufferSize = 81920; // 80KB
options.MultipartThreshold = 5 * 1024 * 1024; // 5MB
});
var app = builder.Build();
Option B: In Startup.cs (.NET Core 3.1, .NET 5)
using PresignedUrlClient.DependencyInjection;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddPresignedUrlClient(options =>
{
options.BaseUrl = "https://presigned-url-service.example.com";
options.ApiKey = Configuration["PresignedUrlService:ApiKey"]!;
});
}
}
Step 3: Inject and Use
using PresignedUrlClient.Abstractions;
using PresignedUrlClient.Abstractions.Enums;
using PresignedUrlClient.Abstractions.Models;
public class FileService
{
private readonly IPresignedUrlService _presignedUrlService;
public FileService(IPresignedUrlService presignedUrlService)
{
_presignedUrlService = presignedUrlService;
}
public PresignedUrlResponse GetDownloadUrl(string bucket, string key)
{
var request = new PresignedUrlRequest(
bucket: bucket,
key: key,
operation: S3Operation.GetObject,
expiresIn: 3600
);
return _presignedUrlService.GeneratePresignedUrl(request);
}
public PresignedUrlResponse GetUploadUrl(string bucket, string key, string contentType)
{
var request = new PresignedUrlRequest(
bucket: bucket,
key: key,
operation: S3Operation.PutObject,
contentType: contentType,
expiresIn: 3600
);
return _presignedUrlService.GeneratePresignedUrl(request);
}
}
โ๏ธ Configuration
Option 1: Direct Configuration (Code-Based)
services.AddPresignedUrlClient(options =>
{
// Required settings
options.BaseUrl = "https://presigned-url-service.example.com";
options.ApiKey = "your-api-key";
// Optional settings with defaults
options.DefaultExpiresIn = 3600; // Default: 3600 (1 hour)
options.Timeout = TimeSpan.FromSeconds(30); // Default: 30 seconds
// โญ Resilience settings (NEW in v1.0.0)
options.RetryCount = 3; // Default: 3 attempts
options.RetryDelayMilliseconds = 1000; // Default: 1000ms (1 second)
options.CircuitBreakerThreshold = 5; // Default: 5 consecutive failures
options.CircuitBreakerDurationSeconds = 60; // Default: 60 seconds
});
Option 2: Configuration Binding (appsettings.json)
appsettings.json:
{
"PresignedUrlClient": {
"BaseUrl": "https://presigned-url-service.example.com",
"ApiKey": "your-api-key",
"DefaultExpiresIn": 3600,
"Timeout": "00:00:30",
// Resilience configuration
"RetryCount": 5,
"RetryDelayMilliseconds": 2000,
"CircuitBreakerThreshold": 10,
"CircuitBreakerDurationSeconds": 120
}
}
Program.cs:
builder.Services.AddPresignedUrlClient(
builder.Configuration.GetSection("PresignedUrlClient")
);
โ๏ธ Configuration Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
BaseUrl |
string |
Required | Base URL of the presigned URL service |
ApiKey |
string |
Required | API key for authentication |
DefaultExpiresIn |
int |
3600 |
Default URL expiration (seconds) |
Timeout |
TimeSpan |
30s |
HTTP request timeout |
RetryCount |
int |
3 |
Number of retry attempts for transient failures |
RetryDelayMilliseconds |
int |
1000 |
Delay between retry attempts (milliseconds) |
CircuitBreakerThreshold |
int |
5 |
Consecutive failures before circuit opens |
CircuitBreakerDurationSeconds |
int |
60 |
How long circuit stays open (seconds) |
๐ก๏ธ Resilience Patterns
The library includes built-in resilience patterns powered by ResilientHttpClient.Core to handle transient failures and prevent cascading errors.
๐ Automatic Retry Logic
sequenceDiagram
participant App as Your Application
participant Client as PresignedUrlClient
participant Retry as Retry Handler
participant Service as S3 URL Service
App->>Client: GeneratePresignedUrl()
Client->>Retry: Execute Request
Retry->>Service: Attempt 1
Service-->>Retry: โ Timeout
Note over Retry: Wait 1 second
Retry->>Service: Attempt 2
Service-->>Retry: โ 500 Error
Note over Retry: Wait 1 second
Retry->>Service: Attempt 3
Service-->>Retry: โ
200 OK
Retry-->>Client: Success
Client-->>App: Return Response
Retried Errors:
- โฑ๏ธ Timeouts (
TaskCanceledException) - ๐ Network errors (
HttpRequestException) - ๐ฅ Server errors (500, 503)
Not Retried:
- โ Client errors (400, 401, 403, 404)
- โ Success responses (200, 201)
๐ก๏ธ Circuit Breaker Pattern
stateDiagram-v2
[*] --> Closed: Initial State
Closed --> Open: 5 consecutive failures
Closed --> Closed: Successful request
Closed --> Closed: Failed request (count < 5)
Open --> HalfOpen: After 60 seconds
Open --> Open: Any request (fast fail)
HalfOpen --> Closed: Test request succeeds
HalfOpen --> Open: Test request fails
note right of Closed
Normal operation
All requests allowed
end note
note right of Open
Service assumed down
Fail fast (no network calls)
end note
note right of HalfOpen
Testing recovery
One request allowed
end note
Benefits:
- ๐ Fast Fail - Don't wait for timeouts when service is down
- ๐ฐ Resource Protection - Save network/CPU resources
- ๐ Auto-Recovery - Automatically detect when service recovers
โ๏ธ Customizing Resilience Behavior
// For a critical operation - more aggressive retry
services.AddPresignedUrlClient(options =>
{
options.RetryCount = 5; // Try 5 times
options.RetryDelayMilliseconds = 500; // Retry quickly (500ms)
options.CircuitBreakerThreshold = 10; // More tolerant of failures
});
// For a non-critical operation - fail fast
services.AddPresignedUrlClient(options =>
{
options.RetryCount = 1; // Only 1 retry
options.RetryDelayMilliseconds = 100; // Short delay
options.CircuitBreakerThreshold = 3; // Trip quickly
});
๐ Usage Examples
๐ฅ Get Presigned URL for Download (GET)
using PresignedUrlClient.Abstractions;
using PresignedUrlClient.Abstractions.Enums;
using PresignedUrlClient.Abstractions.Models;
// Inject IPresignedUrlService into your service/controller
public class DocumentService
{
private readonly IPresignedUrlService _urlService;
public DocumentService(IPresignedUrlService urlService)
{
_urlService = urlService;
}
public PresignedUrlResponse GetDownloadLink(string bucket, string key)
{
var request = new PresignedUrlRequest(
bucket: bucket,
key: key,
operation: S3Operation.GetObject,
expiresIn: 3600 // URL valid for 1 hour
);
var response = _urlService.GeneratePresignedUrl(request);
// Response contains:
// - response.Url: The presigned URL
// - response.ExpiresIn: Seconds until expiration (3600)
// - response.ExpiresAt: Exact DateTime when URL expires
return response;
}
}
Response Example:
{
"url": "https://my-bucket.s3.amazonaws.com/documents/report.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"expiresIn": 3600,
"expiresAt": "2025-10-09T14:30:00Z"
}
๐ค Get Presigned URL for Upload (PUT)
public async Task<string> UploadFile(Stream fileStream, string fileName)
{
// 1. Get presigned URL for upload
var request = new PresignedUrlRequest(
bucket: "uploads-bucket",
key: $"users/{userId}/{fileName}",
operation: S3Operation.PutObject,
contentType: "image/jpeg",
expiresIn: 1800 // URL valid for 30 minutes
);
var response = _urlService.GeneratePresignedUrl(request);
// 2. Upload file directly to S3 using the presigned URL
using var httpClient = new HttpClient();
using var content = new StreamContent(fileStream);
content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
var uploadResponse = await httpClient.PutAsync(response.Url, content);
uploadResponse.EnsureSuccessStatusCode();
return response.Url.Split('?')[0]; // Return permanent S3 URL (without query params)
}
โ๏ธ Get Service Configuration
public void DisplayServiceInfo()
{
var config = _urlService.GetConfiguration();
Console.WriteLine($"โ
Service: {config.Service}");
Console.WriteLine($"๐ฆ Version: {config.Version}");
Console.WriteLine($"\n๐ Available Buckets:");
foreach (var (bucketName, bucketConfig) in config.Buckets)
{
Console.WriteLine($"\n ๐ชฃ {bucketName}");
Console.WriteLine($" Region: {bucketConfig.Region}");
Console.WriteLine($" Max Expiry: {bucketConfig.MaxExpiry}s");
Console.WriteLine($" Operations: {string.Join(", ", bucketConfig.AllowedOperations)}");
}
}
Output Example:
โ
Service: S3 Presigned URL Service
๐ฆ Version: 1.0.0
๐ Available Buckets:
๐ชฃ documents
Region: us-east-1
Max Expiry: 86400s
Operations: GetObject, PutObject
๐ชฃ media
Region: eu-west-1
Max Expiry: 3600s
Operations: GetObject
โก Async/Await Support (NEW in v2.0.0)
All service methods now have async counterparts with CancellationToken support for better scalability and responsiveness.
๐ฅ Async Download URL Generation
public async Task<PresignedUrlResponse> GetDownloadLinkAsync(string bucket, string key, CancellationToken cancellationToken = default)
{
var request = new PresignedUrlRequest(
bucket: bucket,
key: key,
operation: S3Operation.GetObject,
expiresIn: 3600
);
// Use async method for non-blocking I/O
var response = await _urlService.GeneratePresignedUrlAsync(request, cancellationToken);
return response;
}
๐ค Async Upload with Cancellation
public async Task<string> UploadFileAsync(Stream fileStream, string fileName, CancellationToken cancellationToken)
{
// 1. Get presigned URL asynchronously
var request = new PresignedUrlRequest(
bucket: "uploads-bucket",
key: $"users/{userId}/{fileName}",
operation: S3Operation.PutObject,
contentType: "image/jpeg",
expiresIn: 1800
);
var response = await _urlService.GeneratePresignedUrlAsync(request, cancellationToken);
// 2. Upload file to S3
using var httpClient = new HttpClient();
using var content = new StreamContent(fileStream);
content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
var uploadResponse = await httpClient.PutAsync(response.Url, content, cancellationToken);
uploadResponse.EnsureSuccessStatusCode();
return response.Url.Split('?')[0];
}
โ๏ธ Async Configuration Retrieval
public async Task<ConfigurationResponse> GetServiceInfoAsync(CancellationToken cancellationToken = default)
{
// Non-blocking configuration fetch
var config = await _urlService.GetConfigurationAsync(cancellationToken);
return config;
}
๐ Concurrent Async Operations
public async Task<IEnumerable<PresignedUrlResponse>> GenerateMultipleUrlsAsync(
IEnumerable<string> keys,
CancellationToken cancellationToken = default)
{
var tasks = keys.Select(key =>
_urlService.GeneratePresignedUrlAsync(
new PresignedUrlRequest("my-bucket", key, S3Operation.GetObject),
cancellationToken
)
);
// Execute all requests concurrently
var results = await Task.WhenAll(tasks);
return results;
}
โฑ๏ธ Async with Timeout
public async Task<PresignedUrlResponse> GetUrlWithTimeoutAsync(string key)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var request = new PresignedUrlRequest("my-bucket", key, S3Operation.GetObject);
return await _urlService.GeneratePresignedUrlAsync(request, cts.Token);
}
catch (OperationCanceledException)
{
// Handle timeout/cancellation
throw new TimeoutException("Request timed out after 5 seconds");
}
}
๐ Storage Operations (v2.1) + Enhanced Progress (v2.2) โญ
Complete file upload and download operations with universal progress tracking, built on top of presigned URLs.
What's New in v2.2.0:
- โ Upload progress now reports continuously (not just 0% and 100%)
- โ
Simple
PercentProgressoption for easy progress bars - โ Multipart uploads support per-part progress tracking
- โ Use rich OR simple OR both progress reporters simultaneously
- โ 100% backward compatible - no breaking changes
๐ค Upload File with Progress
using PresignedUrlClient.Storage;
using PresignedUrlClient.Storage.Models;
public class StorageService
{
private readonly IStorageService _storage;
public StorageService(IStorageService storage)
{
_storage = storage;
}
public async Task<UploadResult> UploadFileAsync(Stream fileStream, string bucket, string key)
{
var options = new UploadOptions
{
ContentType = "application/pdf",
Progress = new Progress<UploadProgress>(p =>
{
Console.WriteLine($"Uploaded: {p.BytesUploaded}/{p.TotalBytes} bytes ({p.PercentComplete:F1}%)");
})
};
var result = await _storage.UploadAsync(bucket, key, fileStream, options);
Console.WriteLine($"โ
Upload complete! ETag: {result.ETag}");
Console.WriteLine($" Duration: {result.Duration.TotalSeconds:F2}s");
Console.WriteLine($" Bytes: {result.BytesUploaded}");
return result;
}
}
๐ฅ Download File with Progress
public async Task<DownloadResult> DownloadFileAsync(string bucket, string key, string localPath)
{
using var outputStream = File.Create(localPath);
var options = new DownloadOptions
{
Progress = new Progress<DownloadProgress>(p =>
{
Console.WriteLine($"Downloaded: {p.BytesDownloaded}/{p.TotalBytes} bytes ({p.PercentComplete:F1}%)");
})
};
var result = await _storage.DownloadAsync(bucket, key, outputStream, options);
Console.WriteLine($"โ
Download complete!");
Console.WriteLine($" ETag: {result.ETag}");
Console.WriteLine($" ContentType: {result.ContentType}");
Console.WriteLine($" Size: {result.BytesDownloaded} bytes");
return result;
}
๐ Progress Reporting Options
The library provides two ways to track upload/download progress:
Option 1: Rich Progress (Detailed Information)
Get detailed progress information including bytes transferred, percentages, and multipart details:
var options = new UploadOptions
{
Progress = new Progress<UploadProgress>(p =>
{
Console.WriteLine($"Uploaded: {p.BytesUploaded:N0} / {p.TotalBytes:N0} bytes");
Console.WriteLine($"Progress: {p.PercentComplete:F1}%");
if (p.IsMultipart)
{
Console.WriteLine($"Part: {p.CurrentPart} / {p.TotalParts}");
}
})
};
Option 2: Simple Percentage (Easy to Use)
For simple scenarios where you only need the completion percentage:
var options = new UploadOptions
{
PercentProgress = new Progress<double>(percent =>
Console.WriteLine($"Upload: {percent:F1}%"))
};
Use Both (Best of Both Worlds)
You can use both progress reporters simultaneously:
var options = new UploadOptions
{
Progress = richProgressHandler, // For detailed UI/logging
PercentProgress = simpleProgressBar // For progress bar
};
Available for:
- โ
Upload operations (
UploadOptions.Progress/UploadOptions.PercentProgress) - โ
Download operations (
DownloadOptions.Progress/DownloadOptions.PercentProgress) - โ
Multipart part uploads (
IMultipartUploadService.UploadPartAsyncwithIProgress<long>)
๐ข Multipart Upload (Low-Level Control)
For fine-grained control over multipart uploads:
using PresignedUrlClient.Storage;
public class MultipartService
{
private readonly IMultipartUploadService _multipart;
public MultipartService(IMultipartUploadService multipart)
{
_multipart = multipart;
}
public async Task UploadLargeFileAsync(string filePath, string bucket, string key)
{
const int chunkSize = 5 * 1024 * 1024; // 5MB chunks
// 1. Initiate
var uploadId = await _multipart.InitiateAsync(bucket, key, "application/octet-stream");
try
{
// 2. Upload parts
using var fileStream = File.OpenRead(filePath);
var parts = new List<PartInfo>();
int partNumber = 1;
while (fileStream.Position < fileStream.Length)
{
var chunk = new byte[Math.Min(chunkSize, fileStream.Length - fileStream.Position)];
await fileStream.ReadAsync(chunk, 0, chunk.Length);
using var chunkStream = new MemoryStream(chunk);
var partResult = await _multipart.UploadPartAsync(
bucket, key, uploadId, partNumber, chunkStream);
parts.Add(new PartInfo(partNumber, partResult.ETag));
partNumber++;
Console.WriteLine($"Uploaded part {partNumber-1}, ETag: {partResult.ETag}");
}
// 3. Complete
await _multipart.CompleteAsync(bucket, key, uploadId, parts);
Console.WriteLine("โ
Multipart upload complete!");
}
catch
{
// Abort on error
await _multipart.AbortAsync(bucket, key, uploadId);
throw;
}
}
}
๐ฆ Multipart Uploads (URL Generation)
Generate presigned URLs for multipart upload workflows (low-level S3 operations).
๐ Multipart Upload Flow
sequenceDiagram
participant App as Your Application
participant Client as PresignedUrlClient
participant S3 as S3 Service
Note over App,S3: 1๏ธโฃ Initiate Multipart Upload
App->>Client: InitiateMultipartUpload()
Client->>S3: POST (initiate)
S3-->>Client: uploadId
Client-->>App: MultipartInitiateResponse
Note over App,S3: 2๏ธโฃ Upload Parts (parallel)
loop For each part (1-10,000)
App->>Client: GetUploadPartUrl(partNumber)
Client->>S3: GET presigned URL
S3-->>Client: partUrl
Client-->>App: PresignedUrlResponse
App->>S3: PUT file chunk to partUrl
S3-->>App: ETag header
end
Note over App,S3: 3๏ธโฃ Complete Upload
App->>Client: GetCompleteMultipartUrl(parts)
Client->>S3: POST (complete)
S3-->>Client: completeUrl
Client-->>App: MultipartCompleteResponse
App->>S3: POST ETags to completeUrl
S3-->>App: โ
Upload Complete
๐ป Complete Example
public async Task<string> UploadLargeFile(string filePath, string bucket, string key)
{
const int chunkSize = 5 * 1024 * 1024; // 5MB per part
var parts = new List<MultipartPartInfo>();
// Step 1: Initiate multipart upload
var initiateRequest = new MultipartInitiateRequest(
bucket: bucket,
key: key,
contentType: "application/octet-stream"
);
var initiateResponse = _urlService.InitiateMultipartUpload(initiateRequest);
string uploadId = initiateResponse.UploadId;
try
{
// Step 2: Upload parts (can be parallelized!)
using var fileStream = File.OpenRead(filePath);
int partNumber = 1;
while (fileStream.Position < fileStream.Length)
{
// Get presigned URL for this part
var partRequest = new MultipartUploadPartRequest(
bucket: bucket,
key: key,
uploadId: uploadId,
partNumber: partNumber
);
var partResponse = _urlService.GetUploadPartUrl(partRequest);
// Read chunk and upload
byte[] buffer = new byte[Math.Min(chunkSize, fileStream.Length - fileStream.Position)];
await fileStream.ReadAsync(buffer, 0, buffer.Length);
using var httpClient = new HttpClient();
using var content = new ByteArrayContent(buffer);
var uploadResponse = await httpClient.PutAsync(partResponse.Url, content);
// S3 returns ETag in response header
string eTag = uploadResponse.Headers.ETag.Tag;
parts.Add(new MultipartPartInfo(partNumber, eTag));
partNumber++;
}
// Step 3: Complete the upload
var completeRequest = new MultipartCompleteRequest(
bucket: bucket,
key: key,
uploadId: uploadId,
parts: parts
);
var completeResponse = _urlService.GetCompleteMultipartUrl(completeRequest);
// Finalize upload
using var completeClient = new HttpClient();
var finalizeResponse = await completeClient.PostAsync(completeResponse.Url, null);
finalizeResponse.EnsureSuccessStatusCode();
return $"s3://{bucket}/{key}";
}
catch (Exception)
{
// Step 4 (Error): Abort upload to clean up
var abortRequest = new MultipartAbortRequest(bucket, key, uploadId);
var abortResponse = _urlService.GetAbortMultipartUrl(abortRequest);
using var abortClient = new HttpClient();
await abortClient.DeleteAsync(abortResponse.Url);
throw;
}
}
๐ Multipart Upload Limits
| Property | Min | Max | Notes |
|---|---|---|---|
| File Size | 5 MB | 5 TB | Use multipart for files > 100MB |
| Part Size | 5 MB | 5 GB | Except last part (can be < 5MB) |
| Parts | 1 | 10,000 | Part numbers must be sequential |
| Upload Duration | - | 7 days | Incomplete uploads auto-deleted |
๐จ Error Handling
The library provides specific exception types for different error scenarios, making it easy to handle failures gracefully.
Exception Hierarchy
graph TD
A[Exception] --> B[PresignedUrlException]
B --> C[PresignedUrlBadRequestException<br/>HTTP 400]
B --> D[PresignedUrlAuthenticationException<br/>HTTP 401]
B --> E[PresignedUrlAuthorizationException<br/>HTTP 403]
B --> F[PresignedUrlServiceException<br/>HTTP 500/503]
style A fill:#f9f9f9
style B fill:#fff4e1
style C fill:#ffe1e1
style D fill:#ffe1e1
style E fill:#ffe1e1
style F fill:#ffe1e1
๐ฏ Exception Types & When They Occur
| Exception | HTTP Code | Cause | Retry? |
|---|---|---|---|
PresignedUrlBadRequestException |
400 | Invalid bucket/key, missing fields | โ No |
PresignedUrlAuthenticationException |
401 | Invalid or missing API key | โ No |
PresignedUrlAuthorizationException |
403 | No permission to access bucket | โ No |
PresignedUrlServiceException |
500, 503 | Service down, network error | โ Yes (auto) |
๐ป Error Handling Example
using PresignedUrlClient.Abstractions.Exceptions;
public async Task<PresignedUrlResponse> GetSecureDownloadUrl(string bucket, string key)
{
try
{
var request = new PresignedUrlRequest(bucket, key, S3Operation.GetObject);
return _urlService.GeneratePresignedUrl(request);
}
catch (PresignedUrlBadRequestException ex)
{
// 400 - Invalid request parameters
_logger.LogError($"Invalid request: {ex.Message}");
_logger.LogError($"Error code: {ex.ErrorCode}");
// Likely a coding error - fix the request parameters
throw new ArgumentException($"Invalid S3 parameters: {ex.Message}", ex);
}
catch (PresignedUrlAuthenticationException ex)
{
// 401 - API key is invalid or missing
_logger.LogCritical($"Authentication failed: {ex.Message}");
// Check your API key configuration
throw new InvalidOperationException("Service authentication failed. Check API key.", ex);
}
catch (PresignedUrlAuthorizationException ex)
{
// 403 - No permission to access this bucket
_logger.LogWarning($"Access denied to {bucket}/{key}: {ex.Message}");
// User doesn't have permission - return friendly error
return null; // Or throw custom exception
}
catch (PresignedUrlServiceException ex)
{
// 500/503 - Service error (already retried automatically)
_logger.LogError($"Service unavailable after retries: {ex.Message}");
_logger.LogError($"Status: {ex.StatusCode}");
// Service is down - use fallback or queue for later
await _queue.EnqueueForRetry(bucket, key);
throw;
}
catch (HttpRequestException ex)
{
// Network error (DNS, connection refused, etc.)
_logger.LogError($"Network error: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
// Timeout after all retries
_logger.LogError($"Request timeout after {_options.RetryCount} retries");
throw;
}
}
๐ Exception Properties
All exceptions inherit from PresignedUrlException and include:
public class PresignedUrlException : Exception
{
public string? ErrorCode { get; } // API error code (e.g., "INVALID_BUCKET")
public HttpStatusCode? StatusCode { get; } // HTTP status code
public string? ResponseBody { get; } // Full API response for debugging
}
Access exception details:
catch (PresignedUrlBadRequestException ex)
{
Console.WriteLine($"Error Code: {ex.ErrorCode}"); // "INVALID_BUCKET_NAME"
Console.WriteLine($"Status: {ex.StatusCode}"); // 400
Console.WriteLine($"Message: {ex.Message}"); // Human-readable message
Console.WriteLine($"Response: {ex.ResponseBody}"); // Full JSON response
}
๐ Sample Project Demonstrations
The sample console application (samples/PresignedUrlClient.Sample.Console) now includes real upload/download demonstrations showcasing production-ready patterns.
๐ฏ What's Included
Synchronous Examples (v1.x Compatible):
- Example 1: Generate presigned URL for GetObject
- Example 2: Generate presigned URL for PutObject
- Example 3: Real HTTP Upload โญ - Actual file upload to S3 with ETag verification
- Example 4: Real HTTP Download โญ - Actual file download from S3 with content preview and file saving
- Example 5: Get service configuration
- Example 6: Multipart upload workflow
- Example 7: Error handling
- Example 8: Complete Upload-Download-Verify Roundtrip โญ - End-to-end integration test
Async Examples (NEW in v2.0):
- Example 9: Async URL generation with CancellationToken
- Example 10: Concurrent async operations
- Example 11: Async with timeout and cancellation
- Example 12: Async configuration retrieval
- Example 13: Async multipart workflow
โญ New in This Release: Real Upload/Download
Example 3 - Upload File now performs:
- โ Actual HTTP PUT request to S3
- โ Uploads generated test content with timestamp
- โ Sets proper Content-Type headers
- โ Displays ETag from S3 response
- โ Full status reporting
- โ Creates file for Example 4 to download
Example 4 - Download File now performs:
- โ Actual HTTP GET request to S3
- โ Downloads file uploaded in Example 3
- โ Displays content preview (first 100 chars)
- โ Saves to temp directory
- โ Comprehensive error handling
Example 8 - Complete Roundtrip demonstrates:
- โ Upload test file with unique ID
- โ Download the same file back
- โ Verify content integrity byte-by-byte
- โ Comprehensive summary report
- โ Perfect for integration testing
๐จ Enhanced Visual Feedback
The sample app now features prominent success/failure banners for each test:
- ๐ข Green SUCCESS banners - Clear visual confirmation when examples pass
- ๐ด Red FAILURE banners - Immediate visibility when examples fail
- ๐ Test Results Summary - Comprehensive report at the end showing:
- โ Passed tests count
- โ Failed tests count
- โญ๏ธ Skipped tests count
- Overall pass/fail status with color-coded summary
Example output:
================================================================================
โ
SUCCESS: Example 3 Complete
File uploaded successfully to S3!
================================================================================
๐ Test Results:
โ
Passed: 8
โ Failed: 0
โญ๏ธ Skipped: 2
โโโโโโโโโโโโโโโโโโโโ
๐ Total: 10
๐ Running the Samples
cd samples/PresignedUrlClient.Sample.Console
dotnet run
Configuration: Update appsettings.json with your service URL and API key.
For more details, see samples/README.md and SAMPLE_UPLOAD_DOWNLOAD_IMPLEMENTATION.md.
๐งช Testing
The library includes comprehensive test coverage with 205 tests (100% pass rate, 95%+ code coverage) across all layers.
Test Distribution
| Category | Tests | Coverage | Status |
|---|---|---|---|
| Unit Tests | ~155 | Models, Services, Builders, Parsing, Storage, Progress Tracking | โ 100% Pass |
| Integration Tests | ~27 | HTTP Communication, Storage Operations (WireMock) | โ 100% Pass |
| DI Tests | ~23 | Service Registration, Config Binding, Serialization | โ 100% Pass |
| Total | 205 | 95%+ Line Coverage | โ 100% Pass |
Running Tests
# Run all tests
dotnet test
# Run in Release mode
dotnet test --configuration Release
# Run with detailed output
dotnet test --logger "console;verbosity=detailed"
# Run specific test project
dotnet test tests/PresignedUrlClient.Core.Tests
Test Categories
๐ Unit Tests
- โ Request/Response model validation
- โ Constructor parameter validation
- โ Configuration options validation
- โ Request builder (JSON serialization)
- โ Response parser (error mapping)
- โ Exception hierarchy
๐ Integration Tests (WireMock)
- โ GET presigned URL generation
- โ PUT presigned URL generation
- โ API key header validation
- โ HTTP error scenarios (400, 401, 403, 500)
- โ Configuration discovery
- โ Multipart upload workflows
๐ Dependency Injection Tests
- โ Service registration
- โ Options configuration
- โ Configuration binding
- โ Validation at startup
- โ IResilientHttpClient integration
๐ค Contributing
Contributions are welcome! This library follows:
- โ SOLID Principles - Clean separation of concerns
- โ TDD Approach - Tests written alongside features
- โ YAGNI - Only implement what's needed (MVP scope)
- โ KISS - Simple, straightforward implementations
Development Guidelines
- Run tests before committing:
dotnet test - Follow existing patterns: Check
PLANNING.mdfor architecture decisions - Add tests for new features: Maintain 100% pass rate
- Update documentation: Keep README in sync with code changes
๐ License
This project is proprietary and closed source. All rights reserved.
See the LICENSE file for full terms and conditions.
โ ๏ธ NOTICE: Unauthorized copying, distribution, or use of this software is strictly prohibited.
๐ Acknowledgments
- ResilientHttpClient.Core - Provides the resilience layer (GitHub)
- WireMock.Net - Used for integration testing
- FluentAssertions & xUnit - Testing framework
๐ Additional Resources
- ๐ Architecture Documentation
- ๐ Development Tasks
- ๐ Changelog
- ๐ Report Issues
<div align="center">
Built with โค๏ธ using .NET Standard 2.1
Made for developers who need reliable, resilient S3 presigned URL generation ๐
</div>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- PresignedUrlClient.Abstractions (>= 2.3.0)
- System.Text.Json (>= 8.0.6)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on PresignedUrlClient.Serialization.SystemTextJson:
| Package | Downloads |
|---|---|
|
PresignedUrlClient.DependencyInjection
Dependency injection extensions for the PresignedUrlClient library. Provides easy registration of services with Microsoft.Extensions.DependencyInjection. |
GitHub repositories
This package is not used by any popular GitHub repositories.
v2.0.0: Compatible with async methods. No breaking changes.