第36章:外部サービスAdapter(HTTP等)🌍📡✨
外部API(翻訳、決済、通知、AI、天気、住所検索…)って、仕様変更・障害・遅延がつきものだよね😵 この章では、それらの「外の都合」をCoreに波及させないための作り方を、C#でやさしくまとめるよ🫶
この章のゴール🎯💖
- 外部API呼び出しを 差し替え可能にできる(実装が入れ替わってもUseCaseは無傷)🔁
- HTTPのつらさ(タイムアウト/リトライ/429/落ちる等)を Adapter側に閉じ込める🧯
- 「外部のDTO」をCoreに持ち込まず、**変換(ACL)**で吸収できる🧼✨
まず結論:外部サービスは“出口のPort”にする🚪🔌

依存の形(イメージ)🌀
- UseCase(内側):「タグ提案が欲しいな〜」→ **インターフェイス(Port)**を呼ぶ
- Adapter(外側):「HTTPで叩いて、返り値を変換して返す」
外部APIの呼び出しは、HttpClientや認証ヘッダやJSON形式など細かい事情だらけ。 だから、外の事情はAdapterに全部押し込むのが正解🙆♀️✨
HTTP呼び出しの“安全な基本セット”🧰💪
ここは最新の公式ガイドに寄せるね📘✨
-
HttpClientを雑に new しまくると ソケット枯渇しやすい😇
-
さらにDNSが変わる環境だと 古いIPを掴み続ける問題も出る😵
-
なので基本は IHttpClientFactory(AddHttpClient) を使うのが定番👍
- 内部でハンドラをプールして、ソケット問題やDNS問題を回避しやすいよ✨ (Microsoft Learn)
-
ただし注意:typed client や HttpClient を Singletonに捕獲すると、せっかくの寿命管理が効かずDNS問題が再発しうるよ⚠️ (Microsoft Learn)
例題:外部「タグ提案API」を呼ぶAdapterを作ろう🏷️🤖✨
メモ本文から「おすすめタグ」を返してくれる外部APIがある想定にするね(架空でOK)🫧 目的は “HTTPの実装詳細をCoreから隔離する” ことだよ💡
1) Core側:Port(インターフェイス)とモデルを定義する🧠✨
ポイントはこれ👇 ✅ HttpClient / HttpResponseMessage / JSON DTO をCoreに入れない ✅ Coreは「欲しい結果」と「失敗の種類」だけ知ってればOK
namespace MyApp.UseCases.External;
public interface ITagSuggestionGateway
{
Task<TagSuggestionResult> SuggestAsync(TagSuggestionRequest request, CancellationToken ct);
}
public sealed record TagSuggestionRequest(string Text);
public sealed record TagSuggestionResult(
bool IsSuccess,
IReadOnlyList<string> Tags,
ExternalServiceError? Error)
{
public static TagSuggestionResult Success(IReadOnlyList<string> tags)
=> new(true, tags, null);
public static TagSuggestionResult Fail(ExternalServiceError error)
=> new(false, Array.Empty<string>(), error);
}
public sealed record ExternalServiceError(
ExternalServiceErrorKind Kind,
string Message,
int? HttpStatusCode = null);
public enum ExternalServiceErrorKind
{
Timeout,
TransientFailure,
Unauthorized,
RateLimited,
BadRequest,
UnexpectedResponse,
Unknown
}
ここがえらい👏💕
- Coreは「HTTPの世界」を知らない🌍❌
- 外部APIの仕様変更が起きても、基本は Adapterだけ直せばOK🔧✨
- 失敗を例外で上に投げ散らかすより、意味のある失敗(Timeout/RateLimited等)に整形できる👍
2) Adapter側:HTTP実装(変換・例外整理・ログ)を書く📡🛠️
Adapterの役割はこの3つに絞るとキレイ😍
- 通信する(HTTP)
- 外部DTO ⇄ Coreモデルに変換(ACL)
- 失敗を分類してCoreに返す(例外を飼いならす)
using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
using MyApp.UseCases.External;
namespace MyApp.Adapters.External;
public sealed class TagSuggestionHttpGateway : ITagSuggestionGateway
{
private readonly HttpClient _http;
private readonly ILogger<TagSuggestionHttpGateway> _logger;
public TagSuggestionHttpGateway(HttpClient http, ILogger<TagSuggestionHttpGateway> logger)
{
_http = http;
_logger = logger;
}
public async Task<TagSuggestionResult> SuggestAsync(TagSuggestionRequest request, CancellationToken ct)
{
try
{
var dtoReq = new SuggestTagsApiRequest { text = request.Text };
using var resp = await _http.PostAsJsonAsync("v1/tags/suggest", dtoReq, ct);
// 401/403:認証系
if (resp.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
return TagSuggestionResult.Fail(new(ExternalServiceErrorKind.Unauthorized, "認証に失敗したよ😢", (int)resp.StatusCode));
// 429:レート制限
if ((int)resp.StatusCode == 429)
return TagSuggestionResult.Fail(new(ExternalServiceErrorKind.RateLimited, "混んでるみたい…少し待ってね⏳", 429));
// 400:リクエストが悪い(入力の問題)
if (resp.StatusCode == HttpStatusCode.BadRequest)
return TagSuggestionResult.Fail(new(ExternalServiceErrorKind.BadRequest, "送った内容がダメだったっぽい🙈", 400));
// その他のエラー(5xx等)
if (!resp.IsSuccessStatusCode)
{
var body = await SafeReadBodyAsync(resp, ct);
_logger.LogWarning("Tag API failed: {Status} {Body}", (int)resp.StatusCode, body);
return TagSuggestionResult.Fail(new(
ExternalServiceErrorKind.TransientFailure,
"外部サービス側でエラーが起きたよ😵",
(int)resp.StatusCode));
}
var dtoRes = await resp.Content.ReadFromJsonAsync<SuggestTagsApiResponse>(cancellationToken: ct);
if (dtoRes?.tags is null)
return TagSuggestionResult.Fail(new(ExternalServiceErrorKind.UnexpectedResponse, "返ってきた形が想定と違うよ🙈"));
// ✅ ここがACL(外部の都合をアプリの都合に変換)
var tags = dtoRes.tags
.Where(t => !string.IsNullOrWhiteSpace(t))
.Select(t => t.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
return TagSuggestionResult.Success(tags);
}
catch (TaskCanceledException) when (!ct.IsCancellationRequested)
{
// ctがキャンセルされてないのにTaskCanceled → タイムアウト扱いが多い
return TagSuggestionResult.Fail(new(ExternalServiceErrorKind.Timeout, "タイムアウトしたよ⌛️"));
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "HTTP request failed");
return TagSuggestionResult.Fail(new(ExternalServiceErrorKind.TransientFailure, "通信に失敗したよ📡"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error");
return TagSuggestionResult.Fail(new(ExternalServiceErrorKind.Unknown, "想定外の失敗だよ💥"));
}
}
private static async Task<string> SafeReadBodyAsync(HttpResponseMessage resp, CancellationToken ct)
{
try { return await resp.Content.ReadAsStringAsync(ct); }
catch { return "<unreadable>"; }
}
// 外部API用DTO(Coreに持ち込まない!)
private sealed class SuggestTagsApiRequest
{
public required string text { get; init; }
}
private sealed class SuggestTagsApiResponse
{
public string[]? tags { get; init; }
}
}
いい感じポイント💖
- 外部のJSON構造が変わっても このファイル周りだけ修正で済みやすい🔧
- 例外はCoreに投げずに 意味ある失敗に整形して返す🧠✨
- ログはAdapterで取る(Coreは静かにしておく)🔇➡️📝
3) DIで配線:AddHttpClient(typed client)を使う🧵✨
IHttpClientFactory は、DI/ログ/設定、さらにハンドラ寿命管理などに強いよ💪 (Microsoft Learn)
さらに最近は、HTTPの回復性(リトライ等)を “素で” 組みやすい公式パッケージもあるよ📦✨ Microsoft.Extensions.Http.Resilience は、HttpClient向けの回復性機構を提供してるよ (Microsoft Learn)
using MyApp.UseCases.External;
using MyApp.Adapters.External;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddHttpClient<ITagSuggestionGateway, TagSuggestionHttpGateway>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["TagApi:BaseUrl"]!);
client.Timeout = TimeSpan.FromSeconds(10); // まずは短めが安心🙆♀️
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
// 回復性:リトライ/タイムアウト/サーキット等を“いい感じ”に付与(パッケージ側の標準セット)
.AddStandardResilienceHandler();
var app = builder.Build();
app.Run();
💡 補足:AddStandardResilienceHandler は “標準の回復性セット” を付けるイメージだよ。 細かく調整したい場合も、まずこれで土台を作るのがラクちん☺️
ありがちな事故あるある😇⚠️(超だいじ)
❌ 事故1:UseCaseの中でHttpClientを直接叩く
- Coreが外部仕様に汚染される → 章の目的が崩壊💥
❌ 事故2:HttpClient(やtyped client)をSingletonに抱え込む
- IHttpClientFactoryを使ってても、寿命管理が効かずDNS問題が起きやすい⚠️ (Microsoft Learn)
❌ 事故3:タイムアウト/キャンセル無しで外部API呼び出し
- 外部が遅いと、あなたのアプリも固まる😵
- CancellationToken をちゃんと流そうね🧊
❌ 事故4:外部APIのDTOをCoreに置いちゃう
- 外部APIの都合がドメインに混入して、将来の変更が地獄になる🙈
“外部サービスAdapter”のチェックリスト✅💖
- Core側に インターフェイス(Port) がある
- Coreは HTTP型(HttpClient/HttpResponseMessage/JsonDocument等) を知らない
- Adapterで 外部DTO ⇄ Coreモデル の変換が完結してる(ACL)
- 失敗が Timeout/RateLimit/Unauthorized… みたいに分類されてる
- リトライ等の回復性が DIの設定で付与できる(コードにベタ書きしない) (Microsoft Learn)
- typed client を Singletonに捕獲してない (Microsoft Learn)
ミニ課題🎮✨(やると一気に身につくよ!)
課題A:Fake実装でUseCaseを壊さずテストしよう🧪💕
-
ITagSuggestionGateway の Fake を作って
- 成功(タグ3つ返す)
- 失敗(RateLimited返す) の2パターンでUseCaseが期待通り動くか確認✨
課題B:外部APIのエラーを“やさしい失敗”に翻訳しよう🧠
- 401 → Unauthorized
- 429 → RateLimited
- 500台 → TransientFailure
- JSONが壊れてる → UnexpectedResponse …みたいに分類を増やしてみてね💪
AIの使いどころ🤖💡(便利だけど任せすぎ注意!)
- 「この外部APIレスポンスからDTOクラス作って〜」➡️ 雛形作りは得意👏
- 「想定すべき失敗ケースを列挙して〜」➡️ 抜け漏れ防止に最高🫶
- ただし ❗ APIキーや秘密情報は貼らないでね🔐💦(そこは人間が守る!)
ちょい最新トピック🍀(バージョン選びの安心材料)
.NET 10 は 2025/11/11 リリースのLTSで、パッチも継続提供中だよ📦✨ (Microsoft) (外部通信まわりはセキュリティ修正も入りやすいので、LTS追従はかなり大事〜!🛡️)
次の章(第37章)は、このAdapter層が「変換の置き場としてちゃんと集約できてるか?」を点検する回だよ✅🔍 もし今のプロジェクト題材(メモ管理)で「外部サービス何にする?」も一緒に決めたいなら、用途に合わせて候補を3つくらい提案するよ〜🥰✨