Refit

.NET向けの型安全なREST ライブラリ。インターフェース定義から自動的にHTTPクライアント実装を生成。Retrofit(Java/Android)からインスパイアされた宣言的API設計。JSON.NET、System.Text.Json対応、認証、エラーハンドリング、リアクティブストリーム統合を提供。

HTTPクライアント.NETC#型安全REST API自動生成

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.

スター9,285
ウォッチ163
フォーク770
作成日:2013年7月22日
言語:C#
ライセンス:MIT License

トピックス

c-sharpdotnetdotnet-corehttpjsonxamarinxml

スター履歴

reactiveui/refit Star History
データ取得日時: 2025/10/22 09:55

ライブラリ

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}");
    }
}