Refit
Type-safe REST library for .NET. Automatically generates HTTP client implementation from interface definitions. Declarative API design inspired by Retrofit (Java/Android). Provides JSON.NET, System.Text.Json support, authentication, error handling, and reactive stream integration.
GitHub Overview
reactiveui/refit
The automatic type-safe REST library for .NET Core, Xamarin and .NET. Heavily inspired by Square's Retrofit library, Refit turns your REST API into a live interface.
Topics
Star History
Library
Refit
Overview
Refit is "a type-safe REST library for .NET" developed as one of the most popular HTTP client libraries in C# applications. With the concept of "The automatic type-safe REST library for .NET Core, Xamarin and .NET," it automatically generates HTTP client code from interface definitions. Through attribute-based API definitions, strong type safety, Newtonsoft.Json and System.Text.Json support, and complete integration with HttpClientFactory, it enables highly productive REST API communication in modern .NET application development.
Details
Refit 2025 edition is a mature library with over 10 years of development experience as the standard for REST API client development in the .NET ecosystem. Automatic code generation based on interface definitions completely eliminates manual HTTP client implementation. With broad platform support including .NET 8+, .NET Framework, Xamarin, and MAUI, and integration with HttpClientFactory, it fully supports modern dependency injection patterns. Advanced features such as custom serializers, authentication handlers, and policy-based retry functionality meet enterprise-level API integration requirements. Source Generator support enables build-time optimization and performance improvements.
Key Features
- Automatic Code Generation: Automatically generates HTTP client implementation from interface definitions
- Strong Type Safety: Compile-time type checking and IntelliSense support
- HttpClientFactory Integration: Modern .NET DI patterns and lifecycle management
- Flexible Serialization: JSON, XML, and custom serializer support
- Rich Authentication Methods: Bearer Token, API Key, and custom authentication headers
- Comprehensive Platform Support: .NET 8+, Framework, Xamarin, and MAUI
Pros and Cons
Pros
- Intuitive and declarative API design through interface definitions
- Complete elimination of boilerplate code through automatic code generation
- Significant reduction in runtime errors through strong type safety
- Complete compliance with modern .NET development patterns through HttpClientFactory integration
- Support for complex API requirements through rich customization features
- Overwhelming adoption in .NET community with abundant learning resources
Cons
- Complex API specifications may lead to cumbersome attribute configuration
- Limited flexibility when custom HTTP processing is required
- Build time may increase slightly when using Source Generator
- Difficult to handle dynamic API specification changes
- Tracing auto-generated code during debugging can be complex
- Learning curve is relatively low but requires familiarity with attribute usage
Reference Pages
Code Examples
Installation and Basic Setup
<!-- Package Reference (.csproj file) -->
<PackageReference Include="Refit" Version="7.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="7.0.0" />
<!-- JSON support (if needed) -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- or System.Text.Json (default) -->
<!-- Logging functionality (recommended) -->
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<!-- Source Generator support (.NET 5+) -->
<PackageReference Include="Refit.SourceGenerator" Version="7.0.0" PrivateAssets="all" />
API Interface Definition and Basic HTTP Requests
using Refit;
using System.Text.Json.Serialization;
// Data model definitions
public class User
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("email")]
public string Email { get; set; } = string.Empty;
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("updated_at")]
public DateTime? UpdatedAt { get; set; }
}
public class CreateUserRequest
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("email")]
public string Email { get; set; } = string.Empty;
[JsonPropertyName("age")]
public int? Age { get; set; }
}
public class UpdateUserRequest
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("age")]
public int? Age { get; set; }
}
public class UsersResponse
{
[JsonPropertyName("users")]
public List<User> Users { get; set; } = new();
[JsonPropertyName("pagination")]
public PaginationInfo Pagination { get; set; } = new();
}
public class PaginationInfo
{
[JsonPropertyName("page")]
public int Page { get; set; }
[JsonPropertyName("limit")]
public int Limit { get; set; }
[JsonPropertyName("total")]
public int Total { get; set; }
[JsonPropertyName("has_more")]
public bool HasMore { get; set; }
}
// API interface definition
public interface IUserApi
{
// GET request - Get users list
[Get("/users")]
Task<UsersResponse> GetUsersAsync(
[Query] int page = 1,
[Query] int limit = 10,
CancellationToken cancellationToken = default);
// GET request - Get specific user
[Get("/users/{id}")]
Task<User> GetUserAsync(
int id,
CancellationToken cancellationToken = default);
// POST request - Create user
[Post("/users")]
Task<User> CreateUserAsync(
[Body] CreateUserRequest request,
CancellationToken cancellationToken = default);
// PUT request - Update user
[Put("/users/{id}")]
Task<User> UpdateUserAsync(
int id,
[Body] UpdateUserRequest request,
CancellationToken cancellationToken = default);
// DELETE request - Delete user
[Delete("/users/{id}")]
Task DeleteUserAsync(
int id,
CancellationToken cancellationToken = default);
// Request with custom headers
[Get("/users/{id}")]
Task<User> GetUserWithCustomHeadersAsync(
int id,
[Header("X-Request-ID")] string requestId,
[Header("Authorization")] string authorization,
CancellationToken cancellationToken = default);
// Form data submission
[Post("/users/bulk-import")]
[Multipart]
Task<ApiResponse<string>> ImportUsersAsync(
[AliasAs("file")] StreamPart csvFile,
[AliasAs("overwrite")] bool overwrite = false,
CancellationToken cancellationToken = default);
// Raw HTTP response access
[Get("/users/{id}")]
Task<ApiResponse<User>> GetUserWithResponseAsync(
int id,
CancellationToken cancellationToken = default);
}
// Program.cs or Startup.cs configuration
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using System.Text.Json;
var builder = Host.CreateApplicationBuilder(args);
// JSON serializer configuration
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
// Refit client configuration
builder.Services.AddRefitClient<IUserApi>(new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(jsonOptions)
})
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://api.example.com/v1");
c.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
c.Timeout = TimeSpan.FromSeconds(30);
});
// Logging configuration
builder.Services.AddLogging(configure =>
{
configure.AddConsole();
configure.SetMinimumLevel(LogLevel.Information);
});
var host = builder.Build();
// Usage example
await using var scope = host.Services.CreateAsyncScope();
var userApi = scope.ServiceProvider.GetRequiredService<IUserApi>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
try
{
// Get users list
logger.LogInformation("=== Getting Users List ===");
var usersResponse = await userApi.GetUsersAsync(page: 1, limit: 5);
logger.LogInformation($"Retrieved users count: {usersResponse.Users.Count}");
foreach (var user in usersResponse.Users)
{
logger.LogInformation($"User: {user.Id} - {user.Name} ({user.Email})");
}
// Get specific user
if (usersResponse.Users.Any())
{
var firstUserId = usersResponse.Users.First().Id;
logger.LogInformation($"\n=== Getting User Details (ID: {firstUserId}) ===");
var userDetail = await userApi.GetUserAsync(firstUserId);
logger.LogInformation($"User Detail: {userDetail.Name} - {userDetail.Email}");
logger.LogInformation($"Created: {userDetail.CreatedAt:yyyy-MM-dd HH:mm:ss}");
}
// Create user
logger.LogInformation("\n=== Creating User ===");
var newUserRequest = new CreateUserRequest
{
Name = "John Doe",
Email = "[email protected]",
Age = 30
};
var createdUser = await userApi.CreateUserAsync(newUserRequest);
logger.LogInformation($"Created User: {createdUser.Id} - {createdUser.Name}");
// Update user
logger.LogInformation($"\n=== Updating User (ID: {createdUser.Id}) ===");
var updateRequest = new UpdateUserRequest
{
Name = "John Doe (Updated)",
Email = "[email protected]"
};
var updatedUser = await userApi.UpdateUserAsync(createdUser.Id, updateRequest);
logger.LogInformation($"Updated User: {updatedUser.Name} - {updatedUser.Email}");
}
catch (ApiException ex)
{
logger.LogError($"API Error: {ex.StatusCode} - {ex.Content}");
}
catch (HttpRequestException ex)
{
logger.LogError($"HTTP Request Error: {ex.Message}");
}
catch (Exception ex)
{
logger.LogError($"Unexpected Error: {ex.Message}");
}
await host.RunAsync();
Authentication and Header Management
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Refit;
using System.Net.Http.Headers;
// Authentication configuration
public class ApiAuthSettings
{
public string ApiKey { get; set; } = string.Empty;
public string BaseUrl { get; set; } = string.Empty;
public string UserAgent { get; set; } = "MyApp/1.0";
public int TimeoutSeconds { get; set; } = 30;
}
// Authenticated API interface
public interface IAuthenticatedApi
{
[Get("/profile")]
Task<UserProfile> GetProfileAsync(CancellationToken cancellationToken = default);
[Get("/admin/users")]
[Headers("Authorization: Bearer")]
Task<List<User>> GetAdminUsersAsync(
[Header("Authorization")] string bearerToken,
CancellationToken cancellationToken = default);
[Post("/auth/refresh")]
Task<AuthTokenResponse> RefreshTokenAsync(
[Body] RefreshTokenRequest request,
CancellationToken cancellationToken = default);
[Get("/protected-resource")]
Task<ProtectedData> GetProtectedDataAsync(
[Header("X-API-Key")] string apiKey,
[Header("X-Request-ID")] string requestId,
CancellationToken cancellationToken = default);
}
public class UserProfile
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("username")]
public string Username { get; set; } = string.Empty;
[JsonPropertyName("email")]
public string Email { get; set; } = string.Empty;
[JsonPropertyName("roles")]
public List<string> Roles { get; set; } = new();
}
public class AuthTokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; } = string.Empty;
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; } = string.Empty;
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; } = "Bearer";
}
public class RefreshTokenRequest
{
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; } = string.Empty;
}
public class ProtectedData
{
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; }
}
// Custom authentication handler
public class AuthenticationHandler : DelegatingHandler
{
private readonly ITokenService _tokenService;
private readonly ILogger<AuthenticationHandler> _logger;
public AuthenticationHandler(ITokenService tokenService, ILogger<AuthenticationHandler> logger)
{
_tokenService = tokenService;
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Get and set token
var token = await _tokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
var response = await base.SendAsync(request, cancellationToken);
// If 401 Unauthorized, refresh token and retry
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
_logger.LogInformation("Access token expired, attempting refresh...");
var refreshed = await _tokenService.RefreshTokenAsync();
if (refreshed)
{
var newToken = await _tokenService.GetAccessTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
response.Dispose();
response = await base.SendAsync(request, cancellationToken);
}
}
return response;
}
}
// Token management service
public interface ITokenService
{
Task<string?> GetAccessTokenAsync();
Task<bool> RefreshTokenAsync();
Task SetTokensAsync(string accessToken, string refreshToken);
}
public class TokenService : ITokenService
{
private readonly IAuthenticatedApi _authApi;
private readonly ILogger<TokenService> _logger;
private string? _accessToken;
private string? _refreshToken;
private DateTime _tokenExpiry;
public TokenService(IAuthenticatedApi authApi, ILogger<TokenService> logger)
{
_authApi = authApi;
_logger = logger;
}
public async Task<string?> GetAccessTokenAsync()
{
if (DateTime.UtcNow >= _tokenExpiry)
{
await RefreshTokenAsync();
}
return _accessToken;
}
public async Task<bool> RefreshTokenAsync()
{
if (string.IsNullOrEmpty(_refreshToken))
{
_logger.LogWarning("No refresh token available");
return false;
}
try
{
var request = new RefreshTokenRequest { RefreshToken = _refreshToken };
var response = await _authApi.RefreshTokenAsync(request);
await SetTokensAsync(response.AccessToken, response.RefreshToken);
_logger.LogInformation("Token refreshed successfully");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh token");
return false;
}
}
public async Task SetTokensAsync(string accessToken, string refreshToken)
{
_accessToken = accessToken;
_refreshToken = refreshToken;
_tokenExpiry = DateTime.UtcNow.AddMinutes(55); // Usually 1 hour, 5 minutes buffer
await Task.CompletedTask; // Add persistence logic if needed
}
}
// Service registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApiClients(this IServiceCollection services, IConfiguration configuration)
{
// Configuration registration
services.Configure<ApiAuthSettings>(configuration.GetSection("ApiAuth"));
// Token service registration
services.AddScoped<ITokenService, TokenService>();
services.AddTransient<AuthenticationHandler>();
// Non-authenticated API client (for authentication)
services.AddRefitClient<IAuthenticatedApi>("auth")
.ConfigureHttpClient((serviceProvider, client) =>
{
var settings = serviceProvider.GetRequiredService<IOptions<ApiAuthSettings>>().Value;
client.BaseAddress = new Uri(settings.BaseUrl);
client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);
client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
});
// Authenticated API client
services.AddRefitClient<IAuthenticatedApi>()
.ConfigureHttpClient((serviceProvider, client) =>
{
var settings = serviceProvider.GetRequiredService<IOptions<ApiAuthSettings>>().Value;
client.BaseAddress = new Uri(settings.BaseUrl);
client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);
client.DefaultRequestHeaders.Add("X-API-Key", settings.ApiKey);
client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
})
.AddHttpMessageHandler<AuthenticationHandler>();
return services;
}
}
// Usage example
public class AuthenticatedApiExample
{
private readonly IAuthenticatedApi _api;
private readonly ITokenService _tokenService;
private readonly ILogger<AuthenticatedApiExample> _logger;
public AuthenticatedApiExample(
IAuthenticatedApi api,
ITokenService tokenService,
ILogger<AuthenticatedApiExample> logger)
{
_api = api;
_tokenService = tokenService;
_logger = logger;
}
public async Task RunExamplesAsync()
{
try
{
// Get profile (automatic auth header addition)
_logger.LogInformation("=== Getting Profile ===");
var profile = await _api.GetProfileAsync();
_logger.LogInformation($"Profile: {profile.Username} ({profile.Email})");
_logger.LogInformation($"Roles: {string.Join(", ", profile.Roles)}");
// Get admin users list (explicit Bearer Token)
_logger.LogInformation("\n=== Getting Admin Users List ===");
var token = await _tokenService.GetAccessTokenAsync();
var adminUsers = await _api.GetAdminUsersAsync($"Bearer {token}");
_logger.LogInformation($"Admin Users Count: {adminUsers.Count}");
// Access protected resource (API Key + Request ID)
_logger.LogInformation("\n=== Accessing Protected Resource ===");
var requestId = Guid.NewGuid().ToString();
var protectedData = await _api.GetProtectedDataAsync("your-api-key", requestId);
_logger.LogInformation($"Protected Data: {protectedData.Data}");
_logger.LogInformation($"Timestamp: {protectedData.Timestamp:yyyy-MM-dd HH:mm:ss}");
}
catch (ApiException ex)
{
_logger.LogError($"API Error: {ex.StatusCode} - {ex.Content}");
if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
_logger.LogWarning("Authentication failed. Please check your credentials.");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred");
}
}
}
File Upload/Download and Streaming
using Microsoft.Extensions.DependencyInjection;
using Refit;
using System.Net.Http.Headers;
// File operations API interface
public interface IFileApi
{
// File upload (multipart)
[Post("/files/upload")]
[Multipart]
Task<FileUploadResponse> UploadFileAsync(
[AliasAs("file")] StreamPart file,
[AliasAs("description")] string description,
[AliasAs("category")] string category = "general",
CancellationToken cancellationToken = default);
// Multiple file upload
[Post("/files/bulk-upload")]
[Multipart]
Task<BulkUploadResponse> UploadMultipleFilesAsync(
[AliasAs("files")] IEnumerable<StreamPart> files,
[AliasAs("metadata")] string metadata,
CancellationToken cancellationToken = default);
// File download (stream)
[Get("/files/{fileId}/download")]
Task<HttpContent> DownloadFileAsync(
string fileId,
CancellationToken cancellationToken = default);
// Get file information
[Get("/files/{fileId}")]
Task<FileInfo> GetFileInfoAsync(
string fileId,
CancellationToken cancellationToken = default);
// Get files list
[Get("/files")]
Task<FileListResponse> GetFilesAsync(
[Query] int page = 1,
[Query] int limit = 20,
[Query] string? category = null,
CancellationToken cancellationToken = default);
// Delete file
[Delete("/files/{fileId}")]
Task DeleteFileAsync(
string fileId,
CancellationToken cancellationToken = default);
// Image resize/conversion
[Get("/files/{fileId}/resize")]
Task<HttpContent> ResizeImageAsync(
string fileId,
[Query] int width,
[Query] int height,
[Query] string format = "jpeg",
[Query] int quality = 85,
CancellationToken cancellationToken = default);
// Streaming upload
[Post("/files/stream-upload")]
Task<FileUploadResponse> StreamUploadAsync(
[Body] HttpContent content,
[Header("Content-Type")] string contentType,
[Header("X-File-Name")] string fileName,
[Header("X-File-Size")] long fileSize,
CancellationToken cancellationToken = default);
}
// Data models
public class FileUploadResponse
{
[JsonPropertyName("file_id")]
public string FileId { get; set; } = string.Empty;
[JsonPropertyName("file_name")]
public string FileName { get; set; } = string.Empty;
[JsonPropertyName("file_size")]
public long FileSize { get; set; }
[JsonPropertyName("content_type")]
public string ContentType { get; set; } = string.Empty;
[JsonPropertyName("download_url")]
public string DownloadUrl { get; set; } = string.Empty;
[JsonPropertyName("uploaded_at")]
public DateTime UploadedAt { get; set; }
}
public class BulkUploadResponse
{
[JsonPropertyName("uploaded_files")]
public List<FileUploadResponse> UploadedFiles { get; set; } = new();
[JsonPropertyName("failed_files")]
public List<FailedFileUpload> FailedFiles { get; set; } = new();
[JsonPropertyName("total_count")]
public int TotalCount { get; set; }
[JsonPropertyName("success_count")]
public int SuccessCount { get; set; }
}
public class FailedFileUpload
{
[JsonPropertyName("file_name")]
public string FileName { get; set; } = string.Empty;
[JsonPropertyName("error")]
public string Error { get; set; } = string.Empty;
}
public class FileInfo
{
[JsonPropertyName("file_id")]
public string FileId { get; set; } = string.Empty;
[JsonPropertyName("file_name")]
public string FileName { get; set; } = string.Empty;
[JsonPropertyName("file_size")]
public long FileSize { get; set; }
[JsonPropertyName("content_type")]
public string ContentType { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("category")]
public string Category { get; set; } = string.Empty;
[JsonPropertyName("upload_date")]
public DateTime UploadDate { get; set; }
[JsonPropertyName("download_count")]
public int DownloadCount { get; set; }
}
public class FileListResponse
{
[JsonPropertyName("files")]
public List<FileInfo> Files { get; set; } = new();
[JsonPropertyName("pagination")]
public PaginationInfo Pagination { get; set; } = new();
}
// File operations service
public class FileService
{
private readonly IFileApi _fileApi;
private readonly ILogger<FileService> _logger;
public FileService(IFileApi fileApi, ILogger<FileService> logger)
{
_fileApi = fileApi;
_logger = logger;
}
// Single file upload
public async Task<FileUploadResponse> UploadFileAsync(
string filePath,
string description,
string category = "general")
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"File not found: {filePath}");
}
var fileName = Path.GetFileName(filePath);
var contentType = GetContentType(fileName);
_logger.LogInformation($"Uploading file: {fileName} ({contentType})");
await using var fileStream = File.OpenRead(filePath);
var streamPart = new StreamPart(fileStream, fileName, contentType);
var response = await _fileApi.UploadFileAsync(streamPart, description, category);
_logger.LogInformation($"File uploaded successfully: {response.FileId}");
_logger.LogInformation($"Download URL: {response.DownloadUrl}");
return response;
}
// Multiple file upload
public async Task<BulkUploadResponse> UploadMultipleFilesAsync(
IEnumerable<string> filePaths,
string metadata)
{
var streamParts = new List<StreamPart>();
var fileStreams = new List<FileStream>();
try
{
foreach (var filePath in filePaths)
{
if (File.Exists(filePath))
{
var fileName = Path.GetFileName(filePath);
var contentType = GetContentType(fileName);
var fileStream = File.OpenRead(filePath);
fileStreams.Add(fileStream);
streamParts.Add(new StreamPart(fileStream, fileName, contentType));
_logger.LogInformation($"Added file to bulk upload: {fileName}");
}
else
{
_logger.LogWarning($"File not found, skipping: {filePath}");
}
}
if (!streamParts.Any())
{
throw new InvalidOperationException("No valid files found for upload");
}
_logger.LogInformation($"Starting bulk upload of {streamParts.Count} files");
var response = await _fileApi.UploadMultipleFilesAsync(streamParts, metadata);
_logger.LogInformation($"Bulk upload completed: {response.SuccessCount}/{response.TotalCount} files uploaded");
if (response.FailedFiles.Any())
{
_logger.LogWarning("Failed uploads:");
foreach (var failed in response.FailedFiles)
{
_logger.LogWarning($" {failed.FileName}: {failed.Error}");
}
}
return response;
}
finally
{
// Stream cleanup
foreach (var stream in fileStreams)
{
await stream.DisposeAsync();
}
}
}
// File download
public async Task<string> DownloadFileAsync(string fileId, string downloadPath)
{
_logger.LogInformation($"Downloading file: {fileId}");
// Get file information
var fileInfo = await _fileApi.GetFileInfoAsync(fileId);
var fileName = fileInfo.FileName;
var fullPath = Path.Combine(downloadPath, fileName);
// Create directory if it doesn't exist
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Download file
var content = await _fileApi.DownloadFileAsync(fileId);
await using var fileStream = File.Create(fullPath);
await content.CopyToAsync(fileStream);
_logger.LogInformation($"File downloaded: {fullPath} ({fileInfo.FileSize} bytes)");
return fullPath;
}
// Streaming upload (for large files)
public async Task<FileUploadResponse> StreamUploadAsync(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"File not found: {filePath}");
}
var fileInfo = new System.IO.FileInfo(filePath);
var fileName = fileInfo.Name;
var contentType = GetContentType(fileName);
_logger.LogInformation($"Starting stream upload: {fileName} ({fileInfo.Length} bytes)");
await using var fileStream = File.OpenRead(filePath);
var content = new StreamContent(fileStream);
content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
var response = await _fileApi.StreamUploadAsync(
content,
contentType,
fileName,
fileInfo.Length);
_logger.LogInformation($"Stream upload completed: {response.FileId}");
return response;
}
// Download resized image
public async Task<string> DownloadResizedImageAsync(
string fileId,
int width,
int height,
string downloadPath,
string format = "jpeg",
int quality = 85)
{
_logger.LogInformation($"Downloading resized image: {fileId} ({width}x{height})");
var content = await _fileApi.ResizeImageAsync(fileId, width, height, format, quality);
var fileName = $"{fileId}_resized_{width}x{height}.{format}";
var fullPath = Path.Combine(downloadPath, fileName);
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
await using var fileStream = File.Create(fullPath);
await content.CopyToAsync(fileStream);
_logger.LogInformation($"Resized image downloaded: {fullPath}");
return fullPath;
}
// Get files list
public async Task<FileListResponse> GetFilesAsync(
int page = 1,
int limit = 20,
string? category = null)
{
_logger.LogInformation($"Getting files list: page={page}, limit={limit}, category={category}");
var response = await _fileApi.GetFilesAsync(page, limit, category);
_logger.LogInformation($"Retrieved {response.Files.Count} files");
return response;
}
// Delete file
public async Task DeleteFileAsync(string fileId)
{
_logger.LogInformation($"Deleting file: {fileId}");
await _fileApi.DeleteFileAsync(fileId);
_logger.LogInformation($"File deleted successfully: {fileId}");
}
// Content-Type inference
private static string GetContentType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".pdf" => "application/pdf",
".txt" => "text/plain",
".json" => "application/json",
".xml" => "application/xml",
".zip" => "application/zip",
".csv" => "text/csv",
".mp4" => "video/mp4",
".mp3" => "audio/mpeg",
_ => "application/octet-stream"
};
}
}
// Usage example
public class FileOperationsExample
{
private readonly FileService _fileService;
private readonly ILogger<FileOperationsExample> _logger;
public FileOperationsExample(FileService fileService, ILogger<FileOperationsExample> logger)
{
_fileService = fileService;
_logger = logger;
}
public async Task RunExamplesAsync()
{
try
{
// Single file upload
_logger.LogInformation("=== Single File Upload ===");
var uploadedFile = await _fileService.UploadFileAsync(
@"C:\temp\sample.jpg",
"Sample image file",
"images");
// Get files list
_logger.LogInformation("\n=== Getting Files List ===");
var filesList = await _fileService.GetFilesAsync(1, 10, "images");
foreach (var file in filesList.Files)
{
_logger.LogInformation($"File: {file.FileName} - {file.FileSize} bytes - {file.Category}");
}
// File download
_logger.LogInformation("\n=== File Download ===");
var downloadedPath = await _fileService.DownloadFileAsync(
uploadedFile.FileId,
@"C:\temp\downloads");
_logger.LogInformation($"Downloaded to: {downloadedPath}");
// Download resized image
_logger.LogInformation("\n=== Resized Image Download ===");
var resizedPath = await _fileService.DownloadResizedImageAsync(
uploadedFile.FileId,
200,
200,
@"C:\temp\downloads",
"png",
90);
_logger.LogInformation($"Resized image downloaded to: {resizedPath}");
// Multiple file upload
_logger.LogInformation("\n=== Multiple File Upload ===");
var filePaths = new[]
{
@"C:\temp\file1.txt",
@"C:\temp\file2.pdf",
@"C:\temp\file3.jpg"
};
var bulkUploadResult = await _fileService.UploadMultipleFilesAsync(
filePaths,
"Bulk upload test");
_logger.LogInformation($"Bulk upload completed: {bulkUploadResult.SuccessCount} files uploaded");
}
catch (ApiException ex)
{
_logger.LogError($"API Error: {ex.StatusCode} - {ex.Content}");
}
catch (Exception ex)
{
_logger.LogError(ex, "File operation error");
}
}
}
Error Handling and Retry Functionality
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Extensions.Http;
using Refit;
using System.Net;
// Error handling API interface
public interface IReliableApi
{
[Get("/reliable-endpoint")]
Task<ApiResponse<DataResponse>> GetDataWithResponseAsync(CancellationToken cancellationToken = default);
[Get("/unreliable-endpoint")]
Task<DataResponse> GetUnreliableDataAsync(CancellationToken cancellationToken = default);
[Post("/batch-process")]
Task<BatchProcessResponse> ProcessBatchAsync(
[Body] BatchProcessRequest request,
CancellationToken cancellationToken = default);
[Get("/status")]
Task<ServiceStatus> GetServiceStatusAsync(CancellationToken cancellationToken = default);
}
public class DataResponse
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; }
}
public class BatchProcessRequest
{
[JsonPropertyName("items")]
public List<string> Items { get; set; } = new();
[JsonPropertyName("options")]
public ProcessingOptions Options { get; set; } = new();
}
public class ProcessingOptions
{
[JsonPropertyName("parallel")]
public bool Parallel { get; set; } = true;
[JsonPropertyName("timeout_seconds")]
public int TimeoutSeconds { get; set; } = 300;
}
public class BatchProcessResponse
{
[JsonPropertyName("job_id")]
public string JobId { get; set; } = string.Empty;
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("processed_count")]
public int ProcessedCount { get; set; }
[JsonPropertyName("failed_count")]
public int FailedCount { get; set; }
[JsonPropertyName("errors")]
public List<ProcessingError> Errors { get; set; } = new();
}
public class ProcessingError
{
[JsonPropertyName("item")]
public string Item { get; set; } = string.Empty;
[JsonPropertyName("error")]
public string Error { get; set; } = string.Empty;
}
public class ServiceStatus
{
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("version")]
public string Version { get; set; } = string.Empty;
[JsonPropertyName("uptime_seconds")]
public long UptimeSeconds { get; set; }
}
// Error handling and retry service
public class ReliableApiService
{
private readonly IReliableApi _api;
private readonly ILogger<ReliableApiService> _logger;
public ReliableApiService(IReliableApi api, ILogger<ReliableApiService> logger)
{
_api = api;
_logger = logger;
}
// Comprehensive error handling
public async Task<T?> SafeApiCallAsync<T>(
Func<Task<T>> apiCall,
string operationName,
T? defaultValue = default)
{
try
{
_logger.LogInformation($"Starting {operationName}");
var result = await apiCall();
_logger.LogInformation($"{operationName} completed successfully");
return result;
}
catch (ApiException ex)
{
_logger.LogError($"{operationName} API error: {ex.StatusCode}");
_logger.LogError($"Error content: {ex.Content}");
return ex.StatusCode switch
{
HttpStatusCode.NotFound =>
{
_logger.LogWarning($"Resource not found for {operationName}");
return defaultValue;
},
HttpStatusCode.Unauthorized =>
{
_logger.LogError($"Authentication failed for {operationName}");
throw new UnauthorizedAccessException($"Authentication required for {operationName}");
},
HttpStatusCode.Forbidden =>
{
_logger.LogError($"Access forbidden for {operationName}");
throw new UnauthorizedAccessException($"Access denied for {operationName}");
},
HttpStatusCode.TooManyRequests =>
{
_logger.LogWarning($"Rate limit exceeded for {operationName}");
throw new InvalidOperationException($"Rate limit exceeded for {operationName}");
},
HttpStatusCode.InternalServerError =>
{
_logger.LogError($"Server error for {operationName}");
throw new InvalidOperationException($"Server error occurred during {operationName}");
},
_ =>
{
_logger.LogError($"Unexpected HTTP error {ex.StatusCode} for {operationName}");
throw new InvalidOperationException($"API call failed for {operationName}: {ex.StatusCode}");
}
};
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, $"Network error during {operationName}");
throw new InvalidOperationException($"Network error during {operationName}: {ex.Message}");
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError($"Timeout during {operationName}");
throw new TimeoutException($"Operation {operationName} timed out");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error during {operationName}");
throw new InvalidOperationException($"Unexpected error during {operationName}: {ex.Message}");
}
}
// Error handling with ApiResponse
public async Task<(bool Success, T? Data, string? Error)> SafeApiCallWithResponseAsync<T>(
Func<Task<ApiResponse<T>>> apiCall,
string operationName)
{
try
{
_logger.LogInformation($"Starting {operationName} with response details");
var response = await apiCall();
if (response.IsSuccessStatusCode)
{
_logger.LogInformation($"{operationName} successful - Status: {response.StatusCode}");
return (true, response.Content, null);
}
else
{
var error = $"HTTP {response.StatusCode}: {response.ReasonPhrase}";
_logger.LogWarning($"{operationName} failed - {error}");
return (false, default, error);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Exception during {operationName}");
return (false, default, ex.Message);
}
}
// Batch processing error handling
public async Task<BatchProcessResponse> ProcessBatchWithErrorHandlingAsync(
List<string> items,
ProcessingOptions? options = null)
{
options ??= new ProcessingOptions();
_logger.LogInformation($"Starting batch processing of {items.Count} items");
var request = new BatchProcessRequest
{
Items = items,
Options = options
};
var (success, response, error) = await SafeApiCallWithResponseAsync(
() => _api.GetDataWithResponseAsync(),
"BatchProcess"
);
if (!success || response == null)
{
_logger.LogError($"Batch processing failed: {error}");
// Fallback: individual processing
return await ProcessItemsIndividuallyAsync(items);
}
var batchResponse = await _api.ProcessBatchAsync(request);
if (batchResponse.FailedCount > 0)
{
_logger.LogWarning($"Batch processing completed with {batchResponse.FailedCount} failures");
foreach (var processingError in batchResponse.Errors)
{
_logger.LogWarning($"Failed item: {processingError.Item} - {processingError.Error}");
}
}
return batchResponse;
}
// Individual processing fallback
private async Task<BatchProcessResponse> ProcessItemsIndividuallyAsync(List<string> items)
{
_logger.LogInformation("Falling back to individual item processing");
var processedCount = 0;
var failedCount = 0;
var errors = new List<ProcessingError>();
foreach (var item in items)
{
var (success, _, error) = await SafeApiCallWithResponseAsync(
() => _api.GetDataWithResponseAsync(),
$"ProcessItem-{item}"
);
if (success)
{
processedCount++;
}
else
{
failedCount++;
errors.Add(new ProcessingError
{
Item = item,
Error = error ?? "Unknown error"
});
}
// Short delay to avoid rate limiting
await Task.Delay(100);
}
return new BatchProcessResponse
{
JobId = Guid.NewGuid().ToString(),
Status = failedCount == 0 ? "completed" : "partial_failure",
ProcessedCount = processedCount,
FailedCount = failedCount,
Errors = errors
};
}
// Service health check
public async Task<bool> CheckServiceHealthAsync()
{
try
{
var status = await SafeApiCallAsync(
() => _api.GetServiceStatusAsync(),
"HealthCheck",
new ServiceStatus { Status = "unknown" });
if (status?.Status == "healthy")
{
_logger.LogInformation($"Service is healthy - Version: {status.Version}, Uptime: {status.UptimeSeconds}s");
return true;
}
else
{
_logger.LogWarning($"Service status: {status?.Status ?? "unknown"}");
return false;
}
}
catch
{
_logger.LogError("Health check failed");
return false;
}
}
}
// Polly retry policy configuration
public static class RetryPolicyExtensions
{
public static IServiceCollection AddApiClientsWithRetry(this IServiceCollection services, IConfiguration configuration)
{
// Basic retry policy
var basicRetryPolicy = HttpPolicyExtensions
.HandleTransientHttpError() // HttpRequestException and HTTP 5XX, 408
.OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
var logger = context.GetLogger();
logger?.LogWarning($"Retry {retryCount} after {timespan}s due to: {outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString()}");
});
// Circuit Breaker policy
var circuitBreakerPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (exception, duration) =>
{
Console.WriteLine($"Circuit breaker opened for {duration}");
},
onReset: () =>
{
Console.WriteLine("Circuit breaker reset");
});
// Timeout policy
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);
// Combined policy
var resilientPolicy = Policy.WrapAsync(basicRetryPolicy, circuitBreakerPolicy, timeoutPolicy);
// API client registration
services.AddRefitClient<IReliableApi>()
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri(configuration["ApiSettings:BaseUrl"] ?? "https://api.example.com");
c.DefaultRequestHeaders.Add("User-Agent", "ReliableClient/1.0");
})
.AddPolicyHandler(resilientPolicy);
services.AddScoped<ReliableApiService>();
return services;
}
}
// Usage example
public class ErrorHandlingExample
{
private readonly ReliableApiService _service;
private readonly ILogger<ErrorHandlingExample> _logger;
public ErrorHandlingExample(ReliableApiService service, ILogger<ErrorHandlingExample> logger)
{
_service = service;
_logger = logger;
}
public async Task RunExamplesAsync()
{
// Service health check
_logger.LogInformation("=== Service Health Check ===");
var isHealthy = await _service.CheckServiceHealthAsync();
_logger.LogInformation($"Service healthy: {isHealthy}");
if (!isHealthy)
{
_logger.LogWarning("Service is not healthy, proceeding with caution");
}
// Safe API call example
_logger.LogInformation("\n=== Safe API Call ===");
var data = await _service.SafeApiCallAsync(
async () =>
{
var api = _service.GetType()
.GetField("_api", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?
.GetValue(_service) as IReliableApi;
return await api!.GetUnreliableDataAsync();
},
"GetUnreliableData",
new DataResponse { Id = "fallback", Data = "fallback data", Timestamp = DateTime.UtcNow });
if (data != null)
{
_logger.LogInformation($"Retrieved data: {data.Id} - {data.Data}");
}
// Batch processing example
_logger.LogInformation("\n=== Batch Processing ===");
var items = Enumerable.Range(1, 10).Select(i => $"item-{i}").ToList();
var batchResult = await _service.ProcessBatchWithErrorHandlingAsync(items);
_logger.LogInformation($"Batch processing result: {batchResult.Status}");
_logger.LogInformation($"Processed: {batchResult.ProcessedCount}, Failed: {batchResult.FailedCount}");
}
}