HTTPClient Cache
GitHub概要
thomasgalliker/HttpClient.Caching
Caching extensions for .NET HttpClient
スター24
ウォッチ1
フォーク7
作成日:2018年11月18日
言語:C#
ライセンス:MIT License
トピックス
cachecachinghttpclientmemorycache
スター履歴
データ取得日時: 2025/10/22 08:06
キャッシュライブラリ
HTTPClient Cache
概要
HTTPClient Cacheは、.NET Framework/.NET CoreのSystem.Net.Http.HttpClientにキャッシュ機能を追加するライブラリ群です。RFC2616 HTTPキャッシング標準に準拠し、HTTPレスポンスの効率的なキャッシングを実現します。DelegatingHandlerパターンやIMemoryCacheとの統合により、既存のHttpClientコードに最小限の変更でキャッシュ機能を追加できます。
詳細
.NET環境でのHTTPキャッシュは、HttpClient.Cachingライブラリ、Marvin.HttpCacheライブラリ、ASP.NET Coreの組み込みキャッシュ機能など、複数のアプローチが存在します。これらはHTTPヘッダー(Cache-Control、ETag、Last-Modifiedなど)を活用し、条件付きGETリクエストやレスポンスキャッシングを自動化します。
主要なライブラリとアプローチ
- HttpClient.Caching: Thomas Gallikerによる軽量キャッシング拡張ライブラリ
- Marvin.HttpCache: Kevin Dockxによる包括的なRFC2616実装
- ASP.NET Core Response Caching: フレームワーク組み込みのサーバーサイドキャッシュ
- Custom DelegatingHandler: IMemoryCacheと統合したカスタム実装
- Polly: キャッシュポリシーとの統合によるレジリエンス向上
技術的特徴
- RFC2616準拠: 標準的なHTTPキャッシング仕様の完全実装
- 条件付きリクエスト: ETag、Last-Modifiedを活用した効率的な再検証
- DelegatingHandler統合: HttpClientパイプラインへの透明な統合
- メモリキャッシュ: IMemoryCacheによる高速なインメモリキャッシング
- ディスクキャッシュ: 永続化可能なディスクベースキャッシング
メリット・デメリット
メリット
- 標準準拠: RFC2616による確立されたキャッシング標準の活用
- 透明性: 既存のHttpClientコードへの影響を最小化
- 高いパフォーマンス: ネットワーク呼び出しの削減による大幅な速度向上
- 柔軟性: カスタムキャッシュポリシーとストレージ実装の対応
- ASP.NET統合: フレームワークとの深い統合によるスケーラブルなソリューション
- デバッグ支援: HttpClientの既存のログとトレース機能との統合
デメリット
- 複雑性: HTTPキャッシング仕様の理解とデバッグの困難さ
- メモリ消費: 大量のレスポンスデータによるメモリ使用量増加
- 一貫性: 分散環境でのキャッシュ無効化の課題
- 設定の複雑さ: 適切なキャッシュ戦略の選択と調整の困難性
参考ページ
- HttpClient.Caching GitHub
- Marvin.HttpCache GitHub
- ASP.NET Core Response Caching
- .NET Caching Documentation
書き方の例
HttpClient.Cachingライブラリの使用
using System;
using System.Net.Http;
using System.Threading.Tasks;
using HttpClientCaching;
public class HttpClientCachingExample
{
public async Task BasicCachingExample()
{
// InMemoryCacheHandlerの作成
var cacheHandler = new InMemoryCacheHandler()
{
InnerHandler = new HttpClientHandler()
};
// HttpClientにキャッシュハンドラーを設定
using var httpClient = new HttpClient(cacheHandler);
// 最初のリクエスト(キャッシュから取得)
var response1 = await httpClient.GetAsync("https://api.example.com/data");
var content1 = await response1.Content.ReadAsStringAsync();
// 二回目のリクエスト(キャッシュから取得、ネットワーク呼び出しなし)
var response2 = await httpClient.GetAsync("https://api.example.com/data");
var content2 = await response2.Content.ReadAsStringAsync();
Console.WriteLine($"First request: {content1.Length} characters");
Console.WriteLine($"Second request: {content2.Length} characters");
Console.WriteLine($"Same content: {content1 == content2}");
}
}
カスタムDelegatingHandlerによるキャッシュ実装
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
public class CachingDelegatingHandler : DelegatingHandler
{
private readonly IMemoryCache _cache;
private readonly ILogger<CachingDelegatingHandler> _logger;
private readonly TimeSpan _defaultCacheDuration;
public CachingDelegatingHandler(
IMemoryCache cache,
ILogger<CachingDelegatingHandler> logger,
TimeSpan? defaultCacheDuration = null)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_defaultCacheDuration = defaultCacheDuration ?? TimeSpan.FromMinutes(5);
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// GETリクエストのみキャッシュ対象
if (request.Method != HttpMethod.Get)
{
return await base.SendAsync(request, cancellationToken);
}
var cacheKey = GenerateCacheKey(request);
// キャッシュから取得を試行
if (_cache.TryGetValue(cacheKey, out HttpResponseMessage cachedResponse))
{
_logger.LogInformation("Cache hit for {RequestUri}", request.RequestUri);
return CloneResponse(cachedResponse);
}
// キャッシュミス - 実際のリクエストを実行
_logger.LogInformation("Cache miss for {RequestUri}", request.RequestUri);
var response = await base.SendAsync(request, cancellationToken);
// 成功したレスポンスをキャッシュ
if (response.IsSuccessStatusCode)
{
var cacheDuration = GetCacheDuration(response);
var responseToCache = await CloneResponseAsync(response);
_cache.Set(cacheKey, responseToCache, cacheDuration);
_logger.LogInformation("Cached response for {RequestUri} for {Duration}",
request.RequestUri, cacheDuration);
}
return response;
}
private string GenerateCacheKey(HttpRequestMessage request)
{
return $"HttpCache:{request.Method}:{request.RequestUri}";
}
private TimeSpan GetCacheDuration(HttpResponseMessage response)
{
// Cache-Controlヘッダーから有効期限を取得
if (response.Headers.CacheControl?.MaxAge.HasValue == true)
{
return response.Headers.CacheControl.MaxAge.Value;
}
// デフォルトのキャッシュ期間を使用
return _defaultCacheDuration;
}
private async Task<HttpResponseMessage> CloneResponseAsync(HttpResponseMessage original)
{
var cloned = new HttpResponseMessage(original.StatusCode)
{
Content = new ByteArrayContent(await original.Content.ReadAsByteArrayAsync()),
ReasonPhrase = original.ReasonPhrase,
RequestMessage = original.RequestMessage,
Version = original.Version
};
// ヘッダーをコピー
foreach (var header in original.Headers)
{
cloned.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (original.Content != null)
{
foreach (var header in original.Content.Headers)
{
cloned.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
return cloned;
}
private HttpResponseMessage CloneResponse(HttpResponseMessage original)
{
// 簡略化されたクローン(実際の実装ではasyncクローンを推奨)
return original;
}
}
ASP.NET CoreでのHttpClientFactory統合
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Caching.Memory;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// メモリキャッシュの追加
services.AddMemoryCache();
// HttpClientFactoryの設定
services.AddHttpClient("CachedClient")
.AddHttpMessageHandler<CachingDelegatingHandler>();
// カスタムキャッシュハンドラーの登録
services.AddTransient<CachingDelegatingHandler>();
// 名前付きHttpClientの設定例
services.AddHttpClient("ApiClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
})
.AddHttpMessageHandler<CachingDelegatingHandler>();
}
}
// サービスでの使用例
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("CachedClient");
}
public async Task<string> GetDataAsync()
{
var response = await _httpClient.GetAsync("data");
return await response.Content.ReadAsStringAsync();
}
}
ETagを活用した条件付きリクエスト
public class ETagCachingHandler : DelegatingHandler
{
private readonly IMemoryCache _cache;
public ETagCachingHandler(IMemoryCache cache)
{
_cache = cache;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.Method != HttpMethod.Get)
{
return await base.SendAsync(request, cancellationToken);
}
var cacheKey = request.RequestUri.ToString();
// キャッシュされたETagを確認
if (_cache.TryGetValue($"{cacheKey}:etag", out string cachedETag))
{
// If-None-Matchヘッダーを追加
request.Headers.IfNoneMatch.ParseAdd(cachedETag);
}
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.NotModified)
{
// 304 Not Modified - キャッシュからレスポンスを取得
if (_cache.TryGetValue(cacheKey, out HttpResponseMessage cachedResponse))
{
return cachedResponse;
}
}
else if (response.IsSuccessStatusCode)
{
// 新しいレスポンスをキャッシュ
var etag = response.Headers.ETag?.Tag;
if (!string.IsNullOrEmpty(etag))
{
var clonedResponse = await CloneResponseAsync(response);
_cache.Set(cacheKey, clonedResponse);
_cache.Set($"{cacheKey}:etag", etag);
}
}
return response;
}
private async Task<HttpResponseMessage> CloneResponseAsync(HttpResponseMessage original)
{
// レスポンスのクローン実装
var content = await original.Content.ReadAsByteArrayAsync();
var cloned = new HttpResponseMessage(original.StatusCode)
{
Content = new ByteArrayContent(content)
};
foreach (var header in original.Headers)
{
cloned.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return cloned;
}
}
Pollyと統合したレジリエントキャッシング
using Polly;
using Polly.Extensions.Http;
using Microsoft.Extensions.DependencyInjection;
public void ConfigureServicesWithPolly(IServiceCollection services)
{
services.AddMemoryCache();
// リトライポリシーとキャッシュの組み合わせ
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
services.AddHttpClient("ResilientCachedClient")
.AddHttpMessageHandler<CachingDelegatingHandler>()
.AddPolicyHandler(retryPolicy);
}
// 使用例
public class ResilientApiService
{
private readonly HttpClient _httpClient;
public ResilientApiService(IHttpClientFactory factory)
{
_httpClient = factory.CreateClient("ResilientCachedClient");
}
public async Task<ApiData> GetApiDataAsync()
{
var response = await _httpClient.GetAsync("api/data");
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ApiData>(json);
}
}
WebRequestのCachePolicy設定
using System.Net;
using System.Net.Cache;
public class WebRequestCacheExample
{
public void ConfigureWebRequestCache()
{
// グローバルなデフォルトキャッシュポリシーを設定
HttpWebRequest.DefaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.Default);
// または特定のリクエストに対してポリシーを設定
var request = (HttpWebRequest)WebRequest.Create("https://api.example.com/data");
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable);
// キャッシュを完全に無効化
var noCache = new RequestCachePolicy(RequestCacheLevel.BypassCache);
request.CachePolicy = noCache;
}
public async Task<string> GetWithCachePolicy()
{
var request = (HttpWebRequest)WebRequest.Create("https://api.example.com/data");
// 期限切れでない限りキャッシュから取得
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheOnly);
try
{
using var response = (HttpWebResponse)await request.GetResponseAsync();
using var reader = new StreamReader(response.GetResponseStream());
return await reader.ReadToEndAsync();
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.CacheEntryNotFound)
{
// キャッシュにない場合の処理
return "No cached data available";
}
}
}