第35章 仕上げ:エラー・ログ・AI活用の“最低限セット”🎁✨
この章は「動くようになったアプリ」を、壊れにくく&直しやすくする最終仕上げだよ〜!🛠️💖 (しかも、ヘキサの良さ=境界で整えるがめっちゃ効くところ✨)
35-0 今日のゴール🎯✨
- エラーを「種類」で分けて、責任の場所をはっきりさせる🧯
- ログを「最小限」入れて、あとで原因追えるようにする📝
- AIに“任せる所”と“人が守る所”を分けて、事故を防ぐ🤖🚦
ちなみに今の最新だと **.NET 10 がLTS(長期サポート)**で、2025/11/11 リリース&サポートは 2028/11 頃までだよ〜📌 (Microsoft for Developers) IDEも Visual Studio 2026 が提供されてる流れだよ🪟✨ (Microsoft Learn)
35-1 エラーは3種類に分けるのが勝ち🧯✨

カフェで例えると…☕🍰
- ドメイン失敗(想定内) 🧾
- 例:数量が0、営業時間外、支払い方法NG、など
- “ルール的にダメ”だから バグじゃない🙅♀️
- 外部失敗(外が悪い) 🌩️
- 例:DB落ちた、外部APIタイムアウト、メール送信失敗
- Coreは「使いたい」だけで、壊れ方は外側の都合だよね🧊
- 想定外バグ(ほんとにヤバい) 💥
- null参照、変換ミス、前提が崩れた…みたいな
- これはログ&監視で早期発見したい😵💫
35-2 “ドメイン失敗”は Result で返すのが超ラク😊🧾
なんで例外じゃなくて Result?🤔
- 「数量0は例外!」って言われると、毎回 try/catch だらけになりがち🍝
- “想定内の失敗”は 戻り値で普通に返すと、流れが読みやすい✨
Core側に置く最小セット(エラーコード+メッセージ)📦
namespace Cafe.Core;
public enum ErrorType
{
Validation, // 入力が変
BusinessRule, // ルール違反
NotFound, // ない
Conflict, // 競合(例:二重注文)
External, // 外部都合(本当は外側で作ることが多い)
Unexpected // 想定外
}
public sealed record AppError(string Code, string Message, ErrorType Type);
public readonly record struct Result<T>(T? Value, AppError? Error)
{
public bool IsSuccess => Error is null;
public static Result<T> Ok(T value) => new(value, null);
public static Result<T> Fail(AppError error) => new(default, error);
}
使い方イメージ(ユースケース内)🧭
public Result<CreateOrderOutput> Handle(CreateOrderInput input)
{
if (input.Items.Count == 0)
{
return Result<CreateOrderOutput>.Fail(
new AppError("ORDER_EMPTY", "注文は1つ以上の商品が必要だよ🍩", ErrorType.Validation));
}
// ルールOKなら進む…
var orderId = Guid.NewGuid().ToString("N");
return Result<CreateOrderOutput>.Ok(new CreateOrderOutput(orderId));
}
ポイントはこれ👇✨
- Coreは「失敗の意味」だけ持つ(コードと分類)
- 「HTTP 400 にする? 409?」みたいな外の都合は Core に入れない🧼
35-3 “外部失敗”は Adapter で握りつぶさず、ちゃんと整形🧰🌩️
外部(DB/HTTP/メール)の例外って、めちゃくちゃ種類あるよね😵💫 だから方針はこれでOK👇
- Outbound Adapter:低レベル例外をキャッチして、意味のある例外に包む🎁
- Inbound Adapter:例外を ProblemDetails で返す🧾✨
(.NET 8+ から
IExceptionHandlerで中央集約しやすいよ) (Microsoft Learn)
Outbound Adapter(例:Repository)🗄️
public sealed class OrderRepositorySql : IOrderRepository
{
private readonly SqlConnection _conn;
public async Task SaveAsync(Order order, CancellationToken ct)
{
try
{
// DB処理…
}
catch (SqlException ex)
{
// DB都合の例外を、そのままCoreへ漏らさない
throw new RepositoryUnavailableException("DBが混み合ってるっぽい🥺", ex);
}
}
}
public sealed class RepositoryUnavailableException : Exception
{
public RepositoryUnavailableException(string message, Exception inner)
: base(message, inner) { }
}
Inbound(Web API)側:ProblemDetailsで返す🧾✨
ASP.NET Coreには ProblemDetails っていう標準エラー形式があるよ〜📌 (Microsoft Learn)
Program.cs(例外ハンドラ登録)🧩
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<ApiExceptionHandler>();
var app = builder.Build();
app.UseExceptionHandler();
app.MapPost("/orders", async (CreateOrderRequest req, ICreateOrderUseCase uc) =>
{
var result = uc.Handle(req.ToInput());
if (result.IsSuccess)
return Results.Ok(result.Value);
// Result → HTTPへ変換(境界の仕事)
return result.Error!.Type switch
{
ErrorType.Validation => Results.BadRequest(ToProblem(400, result.Error)),
ErrorType.BusinessRule => Results.Conflict(ToProblem(409, result.Error)),
ErrorType.NotFound => Results.NotFound(ToProblem(404, result.Error)),
_ => Results.BadRequest(ToProblem(400, result.Error)),
};
});
app.Run();
static ProblemDetails ToProblem(int status, AppError err) => new()
{
Status = status,
Title = err.Code,
Detail = err.Message
};
IExceptionHandler 実装(外部失敗などをここでまとめて処理)🧯
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
public sealed class ApiExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext context,
Exception exception,
CancellationToken cancellationToken)
{
if (exception is RepositoryUnavailableException)
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
var pd = new ProblemDetails
{
Status = 503,
Title = "REPOSITORY_UNAVAILABLE",
Detail = "いまDBが不安定みたい💦 ちょっと待ってもう一回やってみてね🙏"
};
await context.Response.WriteAsJsonAsync(pd, cancellationToken);
return true;
}
return false; // 次のハンドラへ
}
}
ちょい注意:.NET 10 の例外診断の扱い🧠
.NET 10 では、IExceptionHandler が「処理した」例外の診断(テレメトリ)が抑制される方向の変更が入ってるよ。必要なら SuppressDiagnosticsCallback で調整できる📌 (Microsoft Learn)
35-4 ログは「重要イベントだけ」でOK📝✨(最小で勝つ)
ログは書きすぎると読めないし、少なすぎると追えない😇 だから“最低限”のおすすめはこれ👇
✅ 残すと強いログ(例)💪
- 注文作成リクエスト受けた(OrderId/商品数)📥
- 注文作成成功(OrderId)✅
- 外部呼び出し失敗(どの外部か+例外)🌩️
- 想定外例外(スタックトレース付き)💥
.NET の ILogger は 構造化ログ(テンプレ+値)を前提にしてて高速だよ📌 (Microsoft Learn)
文字列結合じゃなくてテンプレで書く🧩
_logger.LogInformation(
"CreateOrder requested. ItemCount={ItemCount} CustomerId={CustomerId}",
req.Items.Count,
req.CustomerId);
35-5 まず動くログ設定(appsettings.json)⚙️🪟
ASP.NET Coreならこの形が基本でOK〜📝 (Microsoftのドキュメントでもこの設定構造が出てくるよ) (Microsoft Learn)
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
コツ🧠✨
- 最初は
Informationで十分 - フレームワーク系(
Microsoft.AspNetCore)はWarningくらいに落とすと読みやすい💡
35-6 “つながる”観測:OpenTelemetryは最小だけ入れる🔗📡
「このリクエストの失敗、DB?外部API?どこ?」を追うのに、 トレース(TraceId)があると一気に楽になるよ〜😆
OpenTelemetry は ログ・メトリクス・トレースの観測フレームワークだよ📌 (OpenTelemetry)
.NET でも AddOpenTelemetry().WithTracing(...) で入れられる(Microsoft側の手順例もあるよ)📌 (Microsoft Learn)
最小サンプル(まずはConsoleに出す)🧪
builder.Services.AddOpenTelemetry()
.WithTracing(t => t
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddConsoleExporter());
「まずはローカルで見える」が最強の第一歩だよ👀✨
35-7 AI活用:雛形は任せて、境界ルールは人が守る🚦🤖✨
AIはほんと便利だけど、境界を破るコードを平気で出してくることがあるのね…😇 だから、役割分担しよ〜!
🤖 AIに任せていいもの
- DTOの雛形、Mapper、単体テストのたたき台🧪
IExceptionHandlerの骨組み、ログ出力のテンプレ📝- Repository/HttpClient の実装案(ただしレビュー必須👀)
🙋♀️ 人が守るもの(ここがヘキサのキモ❤️)
- Coreが外側参照してないか(DbContext/HttpClient直参照など)🛡️
- Port(interface)の責務がブレてないか🔌
- 変換(DTO↔Domain)がAdapterに寄ってるか🔁
Copilotの“効かせ方”の基本📌
GitHub公式でも「関係あるファイルを開いて、関係ないのは閉じる」みたいな“コンテキスト整理”を推してるよ🧠✨ (GitHub Docs)
そのまま使えるお願いテンプレ(コピペ用)📌✨
- 「このプロジェクトはヘキサ。Coreは外部参照禁止。変更はAdapterで吸収。Resultでドメイン失敗、外部失敗は例外→ProblemDetails。これ守って提案して」
- 「このファイルはOutbound Adapter。低レベル例外をラップして意味のある例外にして。CoreへDB例外は漏らさないで」
35-8 最終チェックリスト✅✨(これ通ったら卒業🎓)
- ✅ Coreが外(DB/HTTP/フレームワーク)を参照してない?🛡️
- ✅ “想定内の失敗”は Result で返してる?🧾
- ✅ 外部例外は Adapter で意味ある形に整形してる?🎁
- ✅ Inboundで Result / 例外 を HTTP(ProblemDetails)へ変換してる?🌐🧾
- ✅ ログは「重要イベントだけ」&構造化で書けてる?📝
- ✅(余裕あれば)TraceIdで追える?🔗
35-9 ミニ宿題(カフェ注文アプリでやるやつ)☕🧪✨
CreateOrderで「商品0件」をORDER_EMPTYで返す🧾- Repositoryで例外をわざと投げて
503の ProblemDetails を返す🌩️ - ログで
OrderIdとItemCountを残す📝 4)(できたら)OpenTelemetryのConsoleExporterでトレース出す🔗
必要なら、いまの「カフェ注文アプリ」のコード前提で、**この章をまるごと実装した“完成形の差分”**を作って貼るよ〜😆💖(どのUI方式:Controller/Minimal API かだけ合わせて書くね)