Refit
.NET向けの型安全なREST ライブラリ。インターフェース定義から自動的にHTTPクライアント実装を生成。Retrofit(Java/Android)からインスパイアされた宣言的API設計。JSON.NET、System.Text.Json対応、認証、エラーハンドリング、リアクティブストリーム統合を提供。
GitHub概要
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.
トピックス
スター履歴
ライブラリ
Refit
概要
Refitは「.NET向けの型安全なREST ライブラリ」として開発された、C#アプリケーションにおける最も人気のあるHTTPクライアントライブラリの一つです。「The automatic type-safe REST library for .NET Core, Xamarin and .NET」をコンセプトに、インターフェース定義からHTTPクライアントコードを自動生成。属性ベースのAPI定義、強力な型安全性、Newtonsoft.Json・System.Text.Json対応、HttpClientFactoryとの完全統合により、現代的な.NETアプリケーション開発において生産性の高いREST API通信を実現します。
詳細
Refit 2025年版は.NETエコシステムにおけるREST API クライアント開発の標準として10年以上の開発実績を持つ成熟したライブラリです。インターフェース定義に基づく自動コード生成により、手動でのHTTPクライアント実装を完全に排除。.NET 8+、.NET Framework、Xamarin、MAUI等の幅広いプラットフォーム対応と、HttpClientFactoryとの統合により現代的な依存性注入パターンを完全サポート。カスタムシリアライザー、認証ハンドラー、ポリシーベースのリトライ機能等の高度な機能により、エンタープライズレベルのAPI統合要件に対応。Source Generatorサポートにより、ビルド時最適化とパフォーマンス向上を実現します。
主な特徴
- 自動コード生成: インターフェース定義からHTTPクライアント実装を自動生成
- 強力な型安全性: コンパイル時型チェックとIntelliSense サポート
- HttpClientFactory統合: 現代的な.NET DIパターンとライフサイクル管理
- 柔軟なシリアライゼーション: JSON、XML、カスタムシリアライザー対応
- 豊富な認証方式: Bearer Token、API Key、カスタム認証ヘッダー
- 包括的なプラットフォーム対応: .NET 8+、Framework、Xamarin、MAUI
メリット・デメリット
メリット
- インターフェース定義による直感的で宣言的なAPI設計
- 自動コード生成により手動実装のボイラープレートコードを完全排除
- 強力な型安全性によりランタイムエラーを大幅削減
- HttpClientFactoryとの統合で現代的な.NET開発パターンに完全対応
- 豊富なカスタマイゼーション機能により複雑なAPI要件にも対応
- .NETコミュニティでの圧倒的な採用実績と豊富な学習リソース
デメリット
- 複雑なAPI仕様の場合、属性設定が煩雑になる可能性
- カスタムHTTP処理が必要な場合は、柔軟性に制限
- Source Generator使用時は、ビルド時間が若干増加
- 動的なAPI仕様変更に対する対応が困難
- デバッグ時に自動生成コードの追跡が複雑な場合がある
- 学習コストは比較的低いが、属性の使い方に慣れが必要
参考ページ
書き方の例
インストールと基本セットアップ
<!-- Package Reference (.csproj ファイル) -->
<PackageReference Include="Refit" Version="7.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="7.0.0" />
<!-- JSON サポート (必要に応じて) -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- または System.Text.Json (デフォルト) -->
<!-- ログ機能 (推奨) -->
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<!-- Source Generator サポート (.NET 5+) -->
<PackageReference Include="Refit.SourceGenerator" Version="7.0.0" PrivateAssets="all" />
API インターフェース定義と基本的な HTTP リクエスト
using Refit;
using System.Text.Json.Serialization;
// データモデル定義
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 インターフェース定義
public interface IUserApi
{
// GET リクエスト - ユーザー一覧取得
[Get("/users")]
Task<UsersResponse> GetUsersAsync(
[Query] int page = 1,
[Query] int limit = 10,
CancellationToken cancellationToken = default);
// GET リクエスト - 特定ユーザー取得
[Get("/users/{id}")]
Task<User> GetUserAsync(
int id,
CancellationToken cancellationToken = default);
// POST リクエスト - ユーザー作成
[Post("/users")]
Task<User> CreateUserAsync(
[Body] CreateUserRequest request,
CancellationToken cancellationToken = default);
// PUT リクエスト - ユーザー更新
[Put("/users/{id}")]
Task<User> UpdateUserAsync(
int id,
[Body] UpdateUserRequest request,
CancellationToken cancellationToken = default);
// DELETE リクエスト - ユーザー削除
[Delete("/users/{id}")]
Task DeleteUserAsync(
int id,
CancellationToken cancellationToken = default);
// カスタムヘッダー付きリクエスト
[Get("/users/{id}")]
Task<User> GetUserWithCustomHeadersAsync(
int id,
[Header("X-Request-ID")] string requestId,
[Header("Authorization")] string authorization,
CancellationToken cancellationToken = default);
// フォームデータ送信
[Post("/users/bulk-import")]
[Multipart]
Task<ApiResponse<string>> ImportUsersAsync(
[AliasAs("file")] StreamPart csvFile,
[AliasAs("overwrite")] bool overwrite = false,
CancellationToken cancellationToken = default);
// 生のHTTPレスポンス取得
[Get("/users/{id}")]
Task<ApiResponse<User>> GetUserWithResponseAsync(
int id,
CancellationToken cancellationToken = default);
}
// Program.cs または Startup.cs での設定
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using System.Text.Json;
var builder = Host.CreateApplicationBuilder(args);
// JSON シリアライザー設定
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
// Refit クライアント設定
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);
});
// ログ設定
builder.Services.AddLogging(configure =>
{
configure.AddConsole();
configure.SetMinimumLevel(LogLevel.Information);
});
var host = builder.Build();
// 使用例
await using var scope = host.Services.CreateAsyncScope();
var userApi = scope.ServiceProvider.GetRequiredService<IUserApi>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
try
{
// ユーザー一覧取得
logger.LogInformation("=== ユーザー一覧取得 ===");
var usersResponse = await userApi.GetUsersAsync(page: 1, limit: 5);
logger.LogInformation($"取得したユーザー数: {usersResponse.Users.Count}");
foreach (var user in usersResponse.Users)
{
logger.LogInformation($"User: {user.Id} - {user.Name} ({user.Email})");
}
// 特定ユーザー取得
if (usersResponse.Users.Any())
{
var firstUserId = usersResponse.Users.First().Id;
logger.LogInformation($"\n=== ユーザー詳細取得 (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}");
}
// ユーザー作成
logger.LogInformation("\n=== ユーザー作成 ===");
var newUserRequest = new CreateUserRequest
{
Name = "田中太郎",
Email = "[email protected]",
Age = 30
};
var createdUser = await userApi.CreateUserAsync(newUserRequest);
logger.LogInformation($"Created User: {createdUser.Id} - {createdUser.Name}");
// ユーザー更新
logger.LogInformation($"\n=== ユーザー更新 (ID: {createdUser.Id}) ===");
var updateRequest = new UpdateUserRequest
{
Name = "田中太郎(更新後)",
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();
認証とヘッダー管理
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Refit;
using System.Net.Http.Headers;
// 認証設定
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;
}
// 認証付きAPIインターフェース
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; }
}
// カスタム認証ハンドラー
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)
{
// トークンの取得と設定
var token = await _tokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
var response = await base.SendAsync(request, cancellationToken);
// 401 Unauthorized の場合、トークンをリフレッシュして再試行
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;
}
}
// トークン管理サービス
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); // 通常1時間、5分のバッファ
await Task.CompletedTask; // 必要に応じて永続化処理
}
}
// サービス登録
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApiClients(this IServiceCollection services, IConfiguration configuration)
{
// 設定の登録
services.Configure<ApiAuthSettings>(configuration.GetSection("ApiAuth"));
// トークンサービスの登録
services.AddScoped<ITokenService, TokenService>();
services.AddTransient<AuthenticationHandler>();
// 認証無しAPIクライアント (認証用)
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);
});
// 認証付きAPIクライアント
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;
}
}
// 使用例
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
{
// プロファイル取得 (自動認証ヘッダー追加)
_logger.LogInformation("=== プロファイル取得 ===");
var profile = await _api.GetProfileAsync();
_logger.LogInformation($"Profile: {profile.Username} ({profile.Email})");
_logger.LogInformation($"Roles: {string.Join(", ", profile.Roles)}");
// 管理者ユーザー一覧取得 (明示的なBearer Token)
_logger.LogInformation("\n=== 管理者ユーザー一覧取得 ===");
var token = await _tokenService.GetAccessTokenAsync();
var adminUsers = await _api.GetAdminUsersAsync($"Bearer {token}");
_logger.LogInformation($"Admin Users Count: {adminUsers.Count}");
// 保護されたリソースへのアクセス (API Key + Request ID)
_logger.LogInformation("\n=== 保護されたリソースアクセス ===");
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");
}
}
}
ファイルアップロード・ダウンロードとストリーミング
using Microsoft.Extensions.DependencyInjection;
using Refit;
using System.Net.Http.Headers;
// ファイル操作用 API インターフェース
public interface IFileApi
{
// ファイルアップロード (マルチパート)
[Post("/files/upload")]
[Multipart]
Task<FileUploadResponse> UploadFileAsync(
[AliasAs("file")] StreamPart file,
[AliasAs("description")] string description,
[AliasAs("category")] string category = "general",
CancellationToken cancellationToken = default);
// 複数ファイルアップロード
[Post("/files/bulk-upload")]
[Multipart]
Task<BulkUploadResponse> UploadMultipleFilesAsync(
[AliasAs("files")] IEnumerable<StreamPart> files,
[AliasAs("metadata")] string metadata,
CancellationToken cancellationToken = default);
// ファイルダウンロード (ストリーム)
[Get("/files/{fileId}/download")]
Task<HttpContent> DownloadFileAsync(
string fileId,
CancellationToken cancellationToken = default);
// ファイル情報取得
[Get("/files/{fileId}")]
Task<FileInfo> GetFileInfoAsync(
string fileId,
CancellationToken cancellationToken = default);
// ファイル一覧取得
[Get("/files")]
Task<FileListResponse> GetFilesAsync(
[Query] int page = 1,
[Query] int limit = 20,
[Query] string? category = null,
CancellationToken cancellationToken = default);
// ファイル削除
[Delete("/files/{fileId}")]
Task DeleteFileAsync(
string fileId,
CancellationToken cancellationToken = default);
// 画像リサイズ・変換
[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);
// ストリーミングアップロード
[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);
}
// データモデル
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();
}
// ファイル操作サービス
public class FileService
{
private readonly IFileApi _fileApi;
private readonly ILogger<FileService> _logger;
public FileService(IFileApi fileApi, ILogger<FileService> logger)
{
_fileApi = fileApi;
_logger = logger;
}
// 単一ファイルアップロード
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;
}
// 複数ファイルアップロード
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
{
// ストリームのクリーンアップ
foreach (var stream in fileStreams)
{
await stream.DisposeAsync();
}
}
}
// ファイルダウンロード
public async Task<string> DownloadFileAsync(string fileId, string downloadPath)
{
_logger.LogInformation($"Downloading file: {fileId}");
// ファイル情報を取得
var fileInfo = await _fileApi.GetFileInfoAsync(fileId);
var fileName = fileInfo.FileName;
var fullPath = Path.Combine(downloadPath, fileName);
// ディレクトリが存在しない場合は作成
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// ファイルをダウンロード
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;
}
// ストリーミングアップロード (大容量ファイル用)
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;
}
// 画像リサイズダウンロード
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;
}
// ファイル一覧取得
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;
}
// ファイル削除
public async Task DeleteFileAsync(string fileId)
{
_logger.LogInformation($"Deleting file: {fileId}");
await _fileApi.DeleteFileAsync(fileId);
_logger.LogInformation($"File deleted successfully: {fileId}");
}
// Content-Type の推定
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"
};
}
}
// 使用例
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
{
// 単一ファイルアップロード
_logger.LogInformation("=== 単一ファイルアップロード ===");
var uploadedFile = await _fileService.UploadFileAsync(
@"C:\temp\sample.jpg",
"サンプル画像ファイル",
"images");
// ファイル一覧取得
_logger.LogInformation("\n=== ファイル一覧取得 ===");
var filesList = await _fileService.GetFilesAsync(1, 10, "images");
foreach (var file in filesList.Files)
{
_logger.LogInformation($"File: {file.FileName} - {file.FileSize} bytes - {file.Category}");
}
// ファイルダウンロード
_logger.LogInformation("\n=== ファイルダウンロード ===");
var downloadedPath = await _fileService.DownloadFileAsync(
uploadedFile.FileId,
@"C:\temp\downloads");
_logger.LogInformation($"Downloaded to: {downloadedPath}");
// 画像リサイズダウンロード
_logger.LogInformation("\n=== 画像リサイズダウンロード ===");
var resizedPath = await _fileService.DownloadResizedImageAsync(
uploadedFile.FileId,
200,
200,
@"C:\temp\downloads",
"png",
90);
_logger.LogInformation($"Resized image downloaded to: {resizedPath}");
// 複数ファイルアップロード
_logger.LogInformation("\n=== 複数ファイルアップロード ===");
var filePaths = new[]
{
@"C:\temp\file1.txt",
@"C:\temp\file2.pdf",
@"C:\temp\file3.jpg"
};
var bulkUploadResult = await _fileService.UploadMultipleFilesAsync(
filePaths,
"バルクアップロードテスト");
_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");
}
}
}
エラーハンドリングとリトライ機能
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Extensions.Http;
using Refit;
using System.Net;
// エラーハンドリング用 API インターフェース
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; }
}
// エラーハンドリング・リトライサービス
public class ReliableApiService
{
private readonly IReliableApi _api;
private readonly ILogger<ReliableApiService> _logger;
public ReliableApiService(IReliableApi api, ILogger<ReliableApiService> logger)
{
_api = api;
_logger = logger;
}
// 包括的なエラーハンドリング
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}");
}
}
// 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);
}
}
// バッチ処理エラーハンドリング
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}");
// フォールバック: 個別処理
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;
}
// 個別処理フォールバック
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"
});
}
// 短い間隔を開けてレート制限を回避
await Task.Delay(100);
}
return new BatchProcessResponse
{
JobId = Guid.NewGuid().ToString(),
Status = failedCount == 0 ? "completed" : "partial_failure",
ProcessedCount = processedCount,
FailedCount = failedCount,
Errors = errors
};
}
// サービス健全性チェック
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 リトライポリシー設定
public static class RetryPolicyExtensions
{
public static IServiceCollection AddApiClientsWithRetry(this IServiceCollection services, IConfiguration configuration)
{
// 基本リトライポリシー
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 ポリシー
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");
});
// タイムアウトポリシー
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);
// 組み合わせポリシー
var resilientPolicy = Policy.WrapAsync(basicRetryPolicy, circuitBreakerPolicy, timeoutPolicy);
// API クライアントの登録
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;
}
}
// 使用例
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()
{
// サービス健全性チェック
_logger.LogInformation("=== サービス健全性チェック ===");
var isHealthy = await _service.CheckServiceHealthAsync();
_logger.LogInformation($"Service healthy: {isHealthy}");
if (!isHealthy)
{
_logger.LogWarning("Service is not healthy, proceeding with caution");
}
// 安全なAPI呼び出し例
_logger.LogInformation("\n=== 安全なAPI呼び出し ===");
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}");
}
// バッチ処理例
_logger.LogInformation("\n=== バッチ処理 ===");
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}");
}
}