Production-grade HTTP API client with runtime proxy generation, automatic retry, CancellationToken support, file uploads, form data, response headers, and per-request timeouts. A feature-rich, extensible alternative to Refit.
$ dotnet add package Royce.ApiClientA modern, production-grade HTTP API client library for .NET that generates clients at runtime from interface definitions. Complete with file uploads, enhanced downloads with progress tracking, and enterprise features!
✨ Progress Reporting - Real-time download progress
✨ Stream to File - Memory-efficient downloads
✨ Range Downloads - Download specific byte ranges
✨ Resume Support - Auto-resume interrupted downloads
✨ Pre-Download Info - Check file size before downloading
✨ Cancellation - Cancel downloads anytime
✨ File Upload - Single/multiple files (v1.1)
✨ CancellationToken - Cancel requests (v1.1)
✨ Response Headers - Access metadata (v1.1)
✨ Per-Request Timeout - Override per endpoint (v1.1)
dotnet add package RoyceLark.ApiClient
using RoyceLark.ApiClient;
using RoyceLark.ApiClient.Attributes;
public interface IUserApi
{
[Get("/users/{id}")]
Task<User> GetUser(int id);
[Post("/upload")]
Task<string> UploadFile([Multipart] FileContent file);
}
var api = ApiClient.Create<IUserApi>("https://api.example.com");
var user = await api.GetUser(123);
using RoyceLark.ApiClient.Extensions;
var httpClient = new HttpClient();
var progress = new Progress<DownloadProgress>(p =>
{
Console.WriteLine($"{p.ProgressPercentage:F1}% - {p.BytesDownloadedFormatted}");
});
var result = await httpClient.DownloadFileAsync(
"https://example.com/file.zip",
progress);
var result = await httpClient.DownloadToFileAsync(
"https://example.com/video.mp4",
"/path/to/video.mp4",
progress);
Console.WriteLine($"Saved: {result.SizeFormatted} to {result.FilePath}");
var result = await httpClient.DownloadWithResumeAsync(
url: "https://example.com/huge.zip",
filePath: "huge.zip",
progress: progress,
maxRetries: 5);
// Download bytes 1000-2000
var result = await httpClient.DownloadRangeAsync(url, 1000, 2000);
// Download from 5000 to end
var result = await httpClient.DownloadRangeAsync(url, 5000);
// Check file size
var size = await httpClient.GetFileSizeAsync(url);
Console.WriteLine($"File size: {size} bytes");
// Check resume support
var supportsResume = await httpClient.SupportsRangeDownloadsAsync(url);
using var cts = new CancellationTokenSource();
var result = await httpClient.DownloadToFileAsync(
url,
path,
progress,
cts.Token);
[Post("/upload")]
Task<string> Upload([Multipart] FileContent file);
// From file
var file = FileContent.FromFile("document.pdf");
await api.Upload(file);
// From bytes
var file = FileContent.FromBytes("file.jpg", bytes, "image/jpeg");
await api.Upload(file);
// From stream
var file = FileContent.FromStream("video.mp4", stream, "video/mp4");
await api.Upload(file);
[Post("/upload-multiple")]
Task<string> UploadMultiple([Multipart] MultipartContent files);
var content = new MultipartContent();
content.AddFile(FileContent.FromFile("file1.pdf"));
content.AddFile(FileContent.FromFile("file2.jpg"));
content.AddField("description", "My files");
await api.UploadMultiple(content);
[Get("/users")]
Task<List<User>> GetUsers(CancellationToken ct = default);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var users = await api.GetUsers(cts.Token);
[Get("/users/{id}")]
Task<ApiResponse<User>> GetUser(int id);
var response = await api.GetUser(123);
Console.WriteLine($"ETag: {response.GetHeader("ETag")}");
var user = response.Content;
[Get("/users/{id}", TimeoutSeconds = 5)]
Task<User> GetUserFast(int id);
[Get("/reports", TimeoutSeconds = 300)]
Task<Report> GenerateReport();
[Get("/users")]
Task<List<User>> Search(
[Query] string search,
[Query] int page = 1);
// Generates: /users?search=john&page=1
var users = await api.Search("john", 1);
[Post("/login")]
Task<Token> Login([Form] LoginData data);
var token = await api.Login(new LoginData
{
Username = "user",
Password = "pass"
});
[Get] [Post] [Put] [Delete] [Patch]
[Body] [Query] [Header] [Multipart] [Form]
Task Task<T> ValueTask<T> Task<ApiResponse<T>>
DownloadFileAsync() - With progressDownloadToFileAsync() - Stream to diskDownloadRangeAsync() - Partial downloadDownloadWithResumeAsync() - Auto-resumeGetFileSizeAsync() - Check sizeSupportsRangeDownloadsAsync() - Check resumeservices.AddApiClient<IUserApi>(options =>
{
options.BaseUrl = "https://api.example.com";
options.Timeout = TimeSpan.FromSeconds(30);
options.RetryPolicy.MaxRetryAttempts = 3;
});
try
{
var user = await api.GetUser(999);
}
catch (ApiException ex)
{
Console.WriteLine($"Status: {ex.StatusCode}");
Console.WriteLine($"Content: {ex.Content}");
}
catch (OperationCanceledException)
{
Console.WriteLine("Cancelled");
}
catch (TimeoutException)
{
Console.WriteLine("Timeout");
}
| Feature | CoreApiClient | Refit | RestSharp |
|---|---|---|---|
| Interface-based | ✅ | ✅ | ❌ |
| Download progress | ✅ | ❌ | ⚠️ |
| Stream to file | ✅ | ❌ | ⚠️ |
| Resume downloads | ✅ | ❌ | ❌ |
| Per-request timeout | ✅ | ❌ | ✅ |
| Built-in retry | ✅ | Extension | ✅ |
RoyceLark.ApiClient provides:
✅ Complete HTTP client functionality
✅ File upload (all sources)
✅ Enhanced downloads (progress, resume)
✅ CancellationToken support
✅ Response headers access
✅ Retry & timeout policies
✅ Production-ready
Everything you need for HTTP APIs! 🚀
MIT License
Made with ❤️ for the .NET community