第59章:Chain of Responsibility ④:演習(HttpClientパイプラインでログ+リトライ)📝🔁
ねらい 🎯✨
- **Chain of Responsibility(責務の連鎖)**を、C#の“現場ど真ん中”で体感するよ〜🙂
HttpClientの送受信を パイプライン(ハンドラの連鎖)で組み立てて、 ログとリトライを“後付け”できるようにするよ💪💖- 「処理の前後にちょい足ししたい」を、
if増殖なしでやる練習だよ〜🌱
到達目標 ✅🌸
DelegatingHandlerが「責務の連鎖」になっている理由を説明できる🙂- ログ用ハンドラを1個作って、
HttpClientに差し込める🧩 IHttpClientFactoryで ハンドラを合成して、呼び出し側はスッキリ✨- **標準寄りの耐障害化(resilience)**で “リトライ付き” をサクッと入れられる🔁
(
Microsoft.Extensions.Http.ResilienceがHttpClient向けの仕組みを提供してるよ)(Microsoft Learn)
手順 🧭🛠️
1) ログ用ハンドラを作る(DelegatingHandler)📝✨
- ここが CoRの主役!「本体処理(
base.SendAsync)の前後」に責務を足すよ🎁 ILoggerを使うと、IHttpClientFactoryまわりのログ設計と相性がいいよ🙂(Microsoft Learn)
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
public sealed class LoggingHandler : DelegatingHandler
{
private readonly ILogger<LoggingHandler> _logger;
public LoggingHandler(ILogger<LoggingHandler> logger)
=> _logger = logger;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
_logger.LogInformation("➡️ {Method} {Uri}", request.Method, request.RequestUri);
try
{
var response = await base.SendAsync(request, cancellationToken);
_logger.LogInformation("⬅️ {Status} {Uri} ({Elapsed}ms)",
(int)response.StatusCode, request.RequestUri, sw.ElapsedMilliseconds);
return response;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("🛑 Canceled {Method} {Uri} ({Elapsed}ms)",
request.Method, request.RequestUri, sw.ElapsedMilliseconds);
throw;
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "💥 HttpRequest failed {Method} {Uri} ({Elapsed}ms)",
request.Method, request.RequestUri, sw.ElapsedMilliseconds);
throw;
}
}
}
2) 呼び出し側(業務側)は “HttpClientを使うだけ” にする 🛒✨
- 連鎖の中身は知らない!ただ叩くだけ!がポイント💡
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public sealed class ShippingApiClient
{
private readonly HttpClient _http;
public ShippingApiClient(HttpClient http) => _http = http;
public async Task<string> GetFeeAsync(CancellationToken ct = default)
{
using var res = await _http.GetAsync("/shipping/fee", ct);
res.EnsureSuccessStatusCode();
return await res.Content.ReadAsStringAsync(ct);
}
}
3) 連鎖を組み立てる(IHttpClientFactory + 標準寄りResilience)🔗🧩

IHttpClientFactoryは “作り方・設定・ハンドラ連鎖” をまとめて管理できるよ🙂 (HttpClientを都度newして捨てるのを避けるのが定番)(Microsoft Learn)- リトライなどの耐障害化は
Microsoft.Extensions.Http.Resilienceで “HttpClient向け”に入れられるよ(Microsoft Learn) - まずは最短で:標準のResilienceハンドラを追加するところからでOK!🧡(Microsoft Learn)
下の例は「外部アクセスなし」で動くように、疑似サーバ用ハンドラを噛ませてるよ(デモ用)🙂
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Http.Resilience;
// 疑似サーバ:最初の2回は500、3回目で200を返す(リトライ確認用)
public sealed class FlakyHandler : HttpMessageHandler
{
private int _count;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var n = Interlocked.Increment(ref _count);
if (n <= 2)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("temporary fail")
});
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("fee=500")
});
}
}
public static class Demo
{
public static async Task RunAsync()
{
var services = new ServiceCollection();
services.AddLogging(b =>
{
b.AddConsole();
b.SetMinimumLevel(LogLevel.Information);
});
services.AddTransient<LoggingHandler>();
services.AddHttpClient<ShippingApiClient>(c =>
{
c.BaseAddress = new Uri("https://example.test/");
})
// ★ここが“本体”の代わり(疑似サーバ)
.ConfigurePrimaryHttpMessageHandler(() => new FlakyHandler())
// ★ログ(自作のDelegatingHandler)
.AddHttpMessageHandler<LoggingHandler>()
// ★リトライ等(標準寄り:HttpClient向けResilience)
.AddStandardResilienceHandler();
using var sp = services.BuildServiceProvider();
var api = sp.GetRequiredService<ShippingApiClient>();
var result = await api.GetFeeAsync();
Console.WriteLine($"🎉 Result: {result}");
}
}
ポイント🌟
- LoggingHandlerが「前後に処理を足す」=CoRの手触り🎁
- **Resilience側(標準)**が「失敗したら再試行」=CoRが“横断関心”に強い理由🔁(Microsoft Learn)
- リトライの最大回数などは、Retry系オプションに
MaxRetryAttemptsみたいな設定があるよ(PollyのRetryオプションの考え方)(pollydocs.org)
4) ハンドラの順番を変えて観察する 👀🔁📝
- ログを外側に置くと「1回の呼び出しとしてログ」になりやすい🙂
- ログを内側に置くと「リトライのたびにログ」になりやすい🙂
- どっちが良いかは目的次第(障害解析なら内側が便利、静かにしたいなら外側が便利)✨
落とし穴 ⚠️😵💫
-
リトライしていい処理だけにすること!
GETみたいな “安全寄り” は比較的OKPOSTは二重実行の危険があるよ(課金・注文確定とか…怖い😱)
-
ログに秘密を出さない(トークン、個人情報、本文まるごと…🙅♀️)
-
キャンセルは尊重:
CancellationTokenで止まる設計にする(止まらないリトライ地獄つらい)🛑 -
“便利そう”でハンドラを増やしすぎると、逆に追えなくなる(連鎖も肥大化する)🐣💦
演習 🧪💖
-
リトライが起きるのをログで確認しよう🔁
FlakyHandlerを「最初の3回は500」に変えて、最終的に成功するか見てみよ🙂
-
ログの位置を入れ替えて、ログの出方がどう変わるか観察しよ👀
-
キャンセルのテストをしてみよ🛑
GetFeeAsync(ct)にCancellationTokenSource(TimeSpan.FromMilliseconds(50))を渡して、ちゃんと止まるか確認✨
-
余裕があれば:**「どのHTTPだけリトライする?」**を考えて、GET以外は抑止する方針メモを書こう📝💡
チェック ✅📌
DelegatingHandlerのbase.SendAsyncは「連鎖の次へ渡す」って説明できる?🔗- ログハンドラは「業務処理を知らずに」追加できた?🧩
- リトライが起きる状況(例:一時的な失敗)と、起きてほしくない状況(例:二重課金)を分けて説明できる?💳⚠️
- “ログを外側/内側” で何が変わるか言える?📝🔁