HttpClient

.NETフレームワークに組み込まれた標準HTTPクライアント。HttpClientFactoryによる依存性注入とライフサイクル管理をサポート。非同期通信、HTTP/2対応、自動圧縮、詳細なタイムアウト設定機能を提供。エンタープライズアプリケーションで推奨される標準実装。

HTTPクライアントC#.NET非同期IHttpClientFactory

GitHub概要

dotnet/runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

スター17,028
ウォッチ458
フォーク5,203
作成日:2019年9月24日
言語:C#
ライセンス:MIT License

トピックス

dotnethacktoberfesthelp-wanted

スター履歴

dotnet/runtime Star History
データ取得日時: 2025/10/22 10:03

ライブラリ

HttpClient

概要

HttpClientは、.NET標準のHTTPクライアントライブラリです。.NET Framework 4.5以降および.NET Core/.NET 5以降で利用可能で、非同期プログラミング(async/await)を完全サポートし、IHttpClientFactoryとの統合により高パフォーマンスとリソース効率を実現します。2025年においても、.NET開発者にとって最も推奨されるHTTPクライアントソリューションです。

詳細

HttpClientは.NET Frameworkの初期バージョンからWebRequestの後継として開発され、現代的なHTTP通信のニーズに対応しています。特に.NET Core/.NET 5以降では、IHttpClientFactoryとの統合により、ソケット枯渇問題やDNS変更問題が解決され、エンタープライズレベルのアプリケーションでも安心して使用できます。System.Text.Jsonとの統合やPollyとの連携により、堅牢なHTTP通信が可能です。

主な特徴

  • 非同期処理: async/awaitパターンの完全サポート
  • IHttpClientFactory: 依存性注入とライフサイクル管理
  • 高パフォーマンス: 接続プールとソケット再利用
  • JSON統合: System.Text.Jsonによる効率的なJSON処理
  • 認証サポート: Bearer Token、JWT、カスタム認証対応
  • エラーハンドリング: 包括的な例外処理機能
  • HTTP/2対応: 最新プロトコルサポート
  • リトライポリシー: Pollyライブラリとの連携
  • タイムアウト制御: 詳細なタイムアウト設定
  • ヘッダー管理: カスタムヘッダーとユーザーエージェント設定

メリット・デメリット

メリット

  • .NET標準ライブラリで追加依存関係が不要
  • IHttpClientFactoryによる最適化されたリソース管理
  • async/await対応による優れた非同期処理
  • 依存性注入(DI)との優れた統合
  • Microsoft公式サポートによる信頼性と継続的更新
  • System.Text.Jsonとの統合による高速JSON処理

デメリット

  • 旧バージョン(.NET Framework 4.x未満)では利用不可
  • 基本的な使用法でもある程度のボイラープレートコードが必要
  • 高度な機能(リトライ、サーキットブレーカーなど)には外部ライブラリが必要
  • 同期処理メソッドは非推奨(デッドロック回避のため)
  • 単体テストでのモック化がやや複雑

参考ページ

書き方の例

基本的なセットアップ(IHttpClientFactory)

// Program.cs (.NET 6以降)
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);

// IHttpClientFactoryの登録
builder.Services.AddHttpClient();

// 名前付きHttpClientの登録
builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
    client.Timeout = TimeSpan.FromSeconds(30);
});

// 型付きHttpClientの登録
builder.Services.AddHttpClient<ApiService>();

var app = builder.Build();

// サービスクラス例
public class ApiService
{
    private readonly HttpClient _httpClient;
    
    public ApiService(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = new Uri("https://api.example.com/");
    }
}

基本的なリクエスト

public class HttpClientService
{
    private readonly HttpClient _httpClient;
    
    public HttpClientService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    // GETリクエスト
    public async Task<string> GetDataAsync()
    {
        try
        {
            var response = await _httpClient.GetAsync("https://api.example.com/data");
            response.EnsureSuccessStatusCode();
            
            return await response.Content.ReadAsStringAsync();
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"Request error: {ex.Message}");
            throw;
        }
    }
    
    // POSTリクエスト
    public async Task<string> PostDataAsync(object data)
    {
        var json = JsonSerializer.Serialize(data);
        var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
        
        var response = await _httpClient.PostAsync("https://api.example.com/users", content);
        response.EnsureSuccessStatusCode();
        
        return await response.Content.ReadAsStringAsync();
    }
    
    // PUTリクエスト
    public async Task<bool> UpdateDataAsync(int id, object data)
    {
        var json = JsonSerializer.Serialize(data);
        var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
        
        var response = await _httpClient.PutAsync($"https://api.example.com/users/{id}", content);
        return response.IsSuccessStatusCode;
    }
    
    // DELETEリクエスト
    public async Task<bool> DeleteDataAsync(int id)
    {
        var response = await _httpClient.DeleteAsync($"https://api.example.com/users/{id}");
        return response.IsSuccessStatusCode;
    }
}

認証処理

public class AuthenticatedHttpService
{
    private readonly HttpClient _httpClient;
    
    public AuthenticatedHttpService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    // Bearer Token認証
    public void SetBearerToken(string token)
    {
        _httpClient.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
    }
    
    // Basic認証
    public void SetBasicAuth(string username, string password)
    {
        var credentials = Convert.ToBase64String(
            System.Text.Encoding.ASCII.GetBytes($"{username}:{password}"));
        
        _httpClient.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
    }
    
    // JWT Token取得とセット
    public async Task<string> LoginAndGetTokenAsync(string username, string password)
    {
        var loginData = new { Username = username, Password = password };
        var json = JsonSerializer.Serialize(loginData);
        var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
        
        var response = await _httpClient.PostAsync("/auth/login", content);
        response.EnsureSuccessStatusCode();
        
        var responseContent = await response.Content.ReadAsStringAsync();
        var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(responseContent);
        
        // Tokenをヘッダーに設定
        SetBearerToken(tokenResponse.AccessToken);
        
        return tokenResponse.AccessToken;
    }
    
    // カスタム認証ヘッダー
    public async Task<string> GetWithCustomAuthAsync(string apiKey)
    {
        using var request = new HttpRequestMessage(HttpMethod.Get, "/api/secure-data");
        request.Headers.Add("X-API-Key", apiKey);
        request.Headers.Add("X-Client-Version", "1.0");
        
        var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();
        
        return await response.Content.ReadAsStringAsync();
    }
}

public class TokenResponse
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
    public int ExpiresIn { get; set; }
}

JSON処理(System.Text.Json統合)

public class JsonHttpService
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializerOptions _jsonOptions;
    
    public JsonHttpService(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = true,
            PropertyNameCaseInsensitive = true
        };
    }
    
    // オブジェクトの送信
    public async Task<TResponse> PostJsonAsync<TRequest, TResponse>(string endpoint, TRequest data)
    {
        var json = JsonSerializer.Serialize(data, _jsonOptions);
        var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
        
        var response = await _httpClient.PostAsync(endpoint, content);
        response.EnsureSuccessStatusCode();
        
        var responseJson = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<TResponse>(responseJson, _jsonOptions);
    }
    
    // JSONレスポンスの取得
    public async Task<T> GetJsonAsync<T>(string endpoint)
    {
        var response = await _httpClient.GetAsync(endpoint);
        response.EnsureSuccessStatusCode();
        
        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<T>(json, _jsonOptions);
    }
    
    // ストリーミングJSON処理(大きなレスポンス用)
    public async Task<T> GetJsonStreamAsync<T>(string endpoint)
    {
        var response = await _httpClient.GetAsync(endpoint);
        response.EnsureSuccessStatusCode();
        
        using var stream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<T>(stream, _jsonOptions);
    }
    
    // 実用例
    public async Task<User> CreateUserAsync(CreateUserRequest request)
    {
        return await PostJsonAsync<CreateUserRequest, User>("/api/users", request);
    }
    
    public async Task<List<User>> GetUsersAsync()
    {
        return await GetJsonAsync<List<User>>("/api/users");
    }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class CreateUserRequest
{
    public string Name { get; set; }
    public string Email { get; set; }
}

エラーハンドリング

public class RobustHttpService
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<RobustHttpService> _logger;
    
    public RobustHttpService(HttpClient httpClient, ILogger<RobustHttpService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }
    
    public async Task<ApiResult<T>> SafeGetAsync<T>(string endpoint)
    {
        try
        {
            using var response = await _httpClient.GetAsync(endpoint);
            
            if (response.IsSuccessStatusCode)
            {
                var json = await response.Content.ReadAsStringAsync();
                var data = JsonSerializer.Deserialize<T>(json);
                
                return ApiResult<T>.Success(data);
            }
            
            // HTTPステータスコード別の処理
            return response.StatusCode switch
            {
                HttpStatusCode.NotFound => ApiResult<T>.Error("リソースが見つかりません"),
                HttpStatusCode.Unauthorized => ApiResult<T>.Error("認証が必要です"),
                HttpStatusCode.Forbidden => ApiResult<T>.Error("アクセスが拒否されました"),
                HttpStatusCode.BadRequest => ApiResult<T>.Error("リクエストが無効です"),
                HttpStatusCode.InternalServerError => ApiResult<T>.Error("サーバーエラーが発生しました"),
                _ => ApiResult<T>.Error($"予期しないエラー: {response.StatusCode}")
            };
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "HTTP request failed for endpoint: {Endpoint}", endpoint);
            return ApiResult<T>.Error($"ネットワークエラー: {ex.Message}");
        }
        catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
        {
            _logger.LogWarning("Request timeout for endpoint: {Endpoint}", endpoint);
            return ApiResult<T>.Error("リクエストがタイムアウトしました");
        }
        catch (TaskCanceledException ex)
        {
            _logger.LogWarning("Request cancelled for endpoint: {Endpoint}", endpoint);
            return ApiResult<T>.Error("リクエストがキャンセルされました");
        }
        catch (JsonException ex)
        {
            _logger.LogError(ex, "JSON deserialization failed for endpoint: {Endpoint}", endpoint);
            return ApiResult<T>.Error("レスポンスの解析に失敗しました");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error for endpoint: {Endpoint}", endpoint);
            return ApiResult<T>.Error("予期しないエラーが発生しました");
        }
    }
    
    // リトライ処理付きリクエスト
    public async Task<T> GetWithRetryAsync<T>(string endpoint, int maxRetries = 3)
    {
        for (int attempt = 1; attempt <= maxRetries; attempt++)
        {
            try
            {
                var response = await _httpClient.GetAsync(endpoint);
                response.EnsureSuccessStatusCode();
                
                var json = await response.Content.ReadAsStringAsync();
                return JsonSerializer.Deserialize<T>(json);
            }
            catch (HttpRequestException ex) when (attempt < maxRetries)
            {
                _logger.LogWarning("Attempt {Attempt} failed, retrying... Error: {Error}", 
                    attempt, ex.Message);
                
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // 指数バックオフ
            }
        }
        
        throw new Exception($"Failed after {maxRetries} attempts");
    }
}

public class ApiResult<T>
{
    public bool IsSuccess { get; set; }
    public T Data { get; set; }
    public string ErrorMessage { get; set; }
    
    public static ApiResult<T> Success(T data) => new() { IsSuccess = true, Data = data };
    public static ApiResult<T> Error(string message) => new() { IsSuccess = false, ErrorMessage = message };
}

非同期処理とパフォーマンス最適化

public class PerformantHttpService
{
    private readonly HttpClient _httpClient;
    private readonly SemaphoreSlim _semaphore;
    
    public PerformantHttpService(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _semaphore = new SemaphoreSlim(10, 10); // 同時実行数を制限
    }
    
    // 複数URL並行取得
    public async Task<Dictionary<string, string>> GetMultipleUrlsAsync(IEnumerable<string> urls)
    {
        var tasks = urls.Select(async url =>
        {
            await _semaphore.WaitAsync();
            try
            {
                var response = await _httpClient.GetStringAsync(url);
                return new KeyValuePair<string, string>(url, response);
            }
            finally
            {
                _semaphore.Release();
            }
        });
        
        var results = await Task.WhenAll(tasks);
        return results.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
    }
    
    // CancellationToken活用
    public async Task<string> GetWithCancellationAsync(string endpoint, CancellationToken cancellationToken = default)
    {
        using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(
            cancellationToken, timeoutCts.Token);
        
        try
        {
            var response = await _httpClient.GetAsync(endpoint, combinedCts.Token);
            response.EnsureSuccessStatusCode();
            
            return await response.Content.ReadAsStringAsync();
        }
        catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested)
        {
            throw new TimeoutException("Request timed out after 30 seconds");
        }
    }
    
    // ストリーミングダウンロード
    public async Task DownloadLargeFileAsync(string url, string filePath, 
        IProgress<long> progress = null, CancellationToken cancellationToken = default)
    {
        using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
        response.EnsureSuccessStatusCode();
        
        var contentLength = response.Content.Headers.ContentLength;
        
        using var stream = await response.Content.ReadAsStreamAsync();
        using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
        
        var buffer = new byte[8192];
        long totalBytesRead = 0;
        int bytesRead;
        
        while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
        {
            await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
            totalBytesRead += bytesRead;
            
            progress?.Report(totalBytesRead);
        }
    }
    
    // バッチ処理
    public async Task<List<T>> ProcessBatchAsync<T>(IEnumerable<string> endpoints, int batchSize = 5)
    {
        var results = new List<T>();
        var batches = endpoints.Chunk(batchSize);
        
        foreach (var batch in batches)
        {
            var batchTasks = batch.Select(async endpoint =>
            {
                var response = await _httpClient.GetStringAsync(endpoint);
                return JsonSerializer.Deserialize<T>(response);
            });
            
            var batchResults = await Task.WhenAll(batchTasks);
            results.AddRange(batchResults);
            
            // バッチ間の待機(レート制限対策)
            await Task.Delay(100);
        }
        
        return results;
    }
    
    // HTTP/2対応の並行リクエスト
    public async Task<List<ApiResponse>> GetConcurrentHttp2Async(List<string> endpoints)
    {
        // HTTP/2では同一接続で複数リクエストを並行処理可能
        var tasks = endpoints.Select(async endpoint =>
        {
            try
            {
                var response = await _httpClient.GetAsync(endpoint);
                return new ApiResponse
                {
                    Endpoint = endpoint,
                    StatusCode = response.StatusCode,
                    Content = await response.Content.ReadAsStringAsync(),
                    Headers = response.Headers.ToDictionary(h => h.Key, h => h.Value.FirstOrDefault())
                };
            }
            catch (Exception ex)
            {
                return new ApiResponse
                {
                    Endpoint = endpoint,
                    Error = ex.Message
                };
            }
        });
        
        return (await Task.WhenAll(tasks)).ToList();
    }
}

public class ApiResponse
{
    public string Endpoint { get; set; }
    public HttpStatusCode StatusCode { get; set; }
    public string Content { get; set; }
    public Dictionary<string, string> Headers { get; set; }
    public string Error { get; set; }
}

// 拡張メソッド(.NET 6以降でChunkが利用可能、以前のバージョン用)
public static class EnumerableExtensions
{
    public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> source, int size)
    {
        var chunk = new List<T>(size);
        foreach (var item in source)
        {
            chunk.Add(item);
            if (chunk.Count == size)
            {
                yield return chunk.ToArray();
                chunk.Clear();
            }
        }
        if (chunk.Count > 0)
            yield return chunk.ToArray();
    }
}