第32章:ACL入門B(どう作る?)🔧
この章でできるようになること🎯✨
- ACL(Anti-Corruption Layer)を“部品として”組み立てられるようになる🧩
- DTO ⇔ ドメイン型の変換を、事故りにくい形で書けるようになる📦🔁🏛️
- 命名・単位・欠損値・真偽値・ステータスみたいな“揺れ”を吸収できるようになる🌪️➡️😌
1) ACLって「結局なにを作る」の?🧠🛡️

ACLは一言でいうと、外の世界(別BC/外部サービス)のクセや都合を、あなたのBCに持ち込まないための翻訳レイヤーだよ📘✨ 「呼び出し」「変換」「例外/エラーの整形」まで含めて、翻訳に必要なロジックをここへ集めるのがコツ!(Microsoft Learn)
また、DDD界隈でも “2つのドメインモデルを翻訳する層” として説明されている定番パターンだよ📌(microservices.io)
2) ACLの完成形(部品のセット)🧩✨

ACLはだいたいこの部品でできてるよ👇
- Port(ポート):あなたのBCが欲しい“機能”の口(インターフェイス)🚪
- External DTO(外部DTO):相手のAPI/イベントの形をそのまま写した入出力📦
- Translator(翻訳):External DTO ⇔ Domain(あなたの言葉)変換🔁
- Adapter/Client(接続):HTTP/メッセージングの実装(外部へつなぐ)🌐
- Resilience(回復力):リトライ/タイムアウト/遮断など(外部は落ちる前提😇)🧯
HTTP接続をするなら、.NETでは IHttpClientFactory を使うのが王道だよ。(Microsoft Learn)
さらに回復力(Resilience)を足すなら Microsoft.Extensions.Http.Resilience が用意されてるよ。(Microsoft Learn)
3) 例題:受注管理BC → 配送管理BC を呼ぶ🚚📦
状況(よくあるやつ)😵💫
受注管理(OrderManagement)BCは「配送を作ってほしい」だけ。 でも配送管理(Shipping)BCのAPIはこんなクセがある…👇
- JSONが
snake_case(例:postal_code)🐍 - 重量が グラムで来る(こっちはkgで扱いたい)⚖️
- 真偽値が
"Y"/"N"😇 - ステータスが文字列コード(
"LBL_CREATED"とか)🔤
ここを ACLで全部吸収して、受注管理BCの中は “自分の言葉” だけで生きられるようにするよ🛡️💕
4) 手順①:まず Port(欲しい機能の口)を決める🚪✨
受注管理BCが欲しいのは「配送を作る」「配送状態を取る」みたいな ユースケース。 相手APIの形じゃなくて、自分の言葉でインターフェイスを切ろう🗣️💡
public interface IShippingPort
{
Task<ShipmentResult> CreateShipmentAsync(CreateShipmentCommand command, CancellationToken ct);
}
ポイント✅
- この
IShippingPortの引数/戻り値は “ドメイン型” に寄せる(外部DTOを出さない)🙅♀️ - “配送APIがこうだから…”をここに混ぜない(混ぜたら負け😇)
5) 手順②:External DTO(相手の形を“そのまま写す”)を作る📦📝
外部DTOは割り切って 相手のJSON/契約に合わせて作るよ。
プロパティ名が合わないなら、System.Text.Json の属性や命名ポリシーで合わせられるよ📌(Microsoft Learn)
例:相手が postal_code を返す想定👇
using System.Text.Json.Serialization;
public sealed record ShippingCreateShipmentRequestDto(
[property: JsonPropertyName("order_id")] string order_id,
[property: JsonPropertyName("postal_code")] string postal_code,
[property: JsonPropertyName("weight_g")] int weight_g,
[property: JsonPropertyName("cash_on_delivery")] string cash_on_delivery // "Y" / "N"
);
public sealed record ShippingCreateShipmentResponseDto(
[property: JsonPropertyName("shipment_id")] string shipment_id,
[property: JsonPropertyName("status_code")] string status_code
);
ここでのコツ💡
- DTOは“翻訳前の生データ置き場”(ビジネスルールを入れない)🥗
string?とかint?など、欠損しそうなら素直に nullable にするのも大事🙆♀️
6) 手順③:受注管理BCのドメイン型を作る🏛️✨(外のクセ禁止)
受注管理BCは “自分の言葉” で持つよ👇
(外の "Y"/"N" とか "LBL_CREATED" とか知らなくてOK!最高!)
public sealed record ShipmentId(string Value);
public enum ShipmentStatus
{
LabelCreated,
InTransit,
Delivered,
Unknown
}
public sealed record WeightKg(decimal Value)
{
public static WeightKg FromGrams(int grams) => new(grams / 1000m);
}
public sealed record CreateShipmentCommand(
string OrderId,
string PostalCode,
WeightKg Weight,
bool CashOnDelivery
);
public sealed record ShipmentResult(
ShipmentId ShipmentId,
ShipmentStatus Status
);
7) 手順④:Translator(翻訳)を書く🔁🧠
翻訳はACLの心臓❤️ 揺れポイントをここで吸収するよ👇
- 命名:
postal_code→PostalCode🏷️ - 単位:
weight_g→WeightKg⚖️ - 真偽:
"Y"/"N"→bool✅ - ステータス:文字列コード →
enum🎛️
public static class ShippingTranslator
{
public static ShippingCreateShipmentRequestDto ToExternal(CreateShipmentCommand cmd)
=> new(
order_id: cmd.OrderId,
postal_code: cmd.PostalCode,
weight_g: (int)(cmd.Weight.Value * 1000m),
cash_on_delivery: cmd.CashOnDelivery ? "Y" : "N"
);
public static ShipmentResult ToDomain(ShippingCreateShipmentResponseDto dto)
=> new(
ShipmentId: new ShipmentId(dto.shipment_id),
Status: MapStatus(dto.status_code)
);
private static ShipmentStatus MapStatus(string? statusCode)
=> statusCode switch
{
"LBL_CREATED" => ShipmentStatus.LabelCreated,
"IN_TRANSIT" => ShipmentStatus.InTransit,
"DELIVERED" => ShipmentStatus.Delivered,
_ => ShipmentStatus.Unknown
};
}
翻訳の鉄則🔥
- 変換できない値は “Unknown” に落とす(例外で全体爆発を避ける)💣➡️🧯
- “よくわからないからとりあえずstringで持つ”を、ドメイン側に持ち込まない🙅♀️
8) 手順⑤:Adapter/Client(接続)を書く🌐🔌
ここで HttpClient を使って外部を叩くよ。
.NETでは IHttpClientFactory を使うのが推奨されてる(接続の管理やDIと相性◎)(Microsoft Learn)
using System.Net.Http.Json;
using System.Text.Json;
public sealed class ShippingAclClient : IShippingPort
{
private readonly HttpClient _http;
private static readonly JsonSerializerOptions JsonOptions = new()
{
// 必要なら命名や柔軟性をここで調整できるよ
PropertyNameCaseInsensitive = true
};
public ShippingAclClient(HttpClient http)
{
_http = http;
}
public async Task<ShipmentResult> CreateShipmentAsync(CreateShipmentCommand command, CancellationToken ct)
{
var reqDto = ShippingTranslator.ToExternal(command);
// 例:POST /shipments
using var res = await _http.PostAsJsonAsync("/shipments", reqDto, JsonOptions, ct);
// 外部エラーをドメインに持ち込まない:ここで整形する
if (!res.IsSuccessStatusCode)
{
// ここでは例として例外にしてるけど、
// 章が進むと「ドメインに合う失敗表現」に変えるのがさらに良いよ✨
throw new HttpRequestException($"Shipping API failed: {(int)res.StatusCode}");
}
var resDto = await res.Content.ReadFromJsonAsync<ShippingCreateShipmentResponseDto>(JsonOptions, ct)
?? throw new HttpRequestException("Shipping API returned empty body.");
return ShippingTranslator.ToDomain(resDto);
}
}
System.Text.Json は JsonSerializerOptions や命名ポリシー/属性で柔軟に合わせられるよ📌(Microsoft Learn)
9) 手順⑥:Resilience(回復力)を“ACL側”で持つ🧯⚡
外部は落ちる!遅れる!たまに壊れる!😇 だから リトライ・タイムアウト・遮断は、だいたいACL側で持つのが気持ちいいよ🛡️
.NETには Microsoft.Extensions.Http.Resilience が用意されてて、HttpClient 向けの回復性メカニズムを提供してるよ。(Microsoft Learn)
(章の主題はACLの形なので、ここでは“置き場所の感覚”だけ覚えよう💡)
10) “変換が肥大化”したら黄色信号🚨📈
Translatorがどんどん太ってきたら、だいたいこのどれか👇
- そもそも 境界の切り方が苦しい(責務が混ざってる)😵💫
- 相手のモデルが 頻繁に変わる(契約が安定してない)🌀
- “翻訳”じゃなくて、実は 自分の業務ルールを押し込んでる😇
こういうときは、Context Mapを見直したり、**公開する言葉(Published Language)**の導入を検討する流れになるよ📢✨(第34章につながる!)
11) ありがち事故と対策(先に踏んでおこう)🧨➡️🛡️
事故①:外部DTOがドメイン層に漏れる💧
- ❌ ドメインが
ShippingCreateShipmentResponseDtoを知ってる - ✅ IShippingPortはドメイン型だけにする
事故②:ステータスをstringで保持して地獄👹
- ✅ ドメイン側は
enumで 意味が固定されるようにする
事故③:単位変換が散らばって計算ミス⚖️💥
- ✅
WeightKg.FromGrams()みたいに 変換の置き場所を固定する
12) ミニ演習✍️🎮
お題:次のJSONをドメインに翻訳しよう🔁
外部からこう返ってくるとする👇
{
"shipment_id": "SHP_12345",
"status_code": "IN_TRANSIT"
}
やること✅
ShippingCreateShipmentResponseDtoを受け取るShipmentResult(ShipmentId, ShipmentStatus)に変換する- 未知の
status_codeはUnknownにする
13) お助けAIプロンプト🤖✨(レビュー用)
- 「この外部JSONからC# DTO(
JsonPropertyName付き)を作って」📦 - 「このDTO→ドメイン変換で、単位・null・未知値の落とし穴を指摘して」🕳️
- 「
status_codeのマッピングを enum にして、未知値の扱いも入れて」🎛️
※生成されたコードはそのまま採用せず、**“翻訳の責任がACLに閉じてるか”**だけ最終チェックしようね🔍✅
14) 今日のまとめ🌸✅
- ACLは “外のクセを中に入れない翻訳所” 🛡️(Microsoft Learn)
- 作る順番は Port → 外部DTO → ドメイン型 → Translator → Client が安定🧩
- 揺れ(命名/単位/欠損/真偽/ステータス)は Translatorに集約して勝つ🏆
- HTTP接続は
IHttpClientFactory、回復力はMicrosoft.Extensions.Http.Resilienceが定番ルートだよ🌐🧯(Microsoft Learn)
参考:この章で触れた“最新の土台”📌
.NET 10 は 2026-01-13 時点の更新(例:10.0.2)が案内されてるよ。(Microsoft) C# 14 の新機能も .NET 10 / Visual Studio 2026 で試せる、と整理されてるよ。(Microsoft Learn)