Appearance
Validation and API Errors
.NET backendでFluentValidation、ErrorOr、RFC 9457 Problem Detailsを組み合わせるときの詳細規約。
[Required]HTTP入力検証、domain invariant、application handlerの業務結果、API error responseへの変換を分離し、予期できる失敗を例外で流さない。[Required]すべてのAPI error responseをRFC 9457 Problem Detailsとしてapplication/problem+jsonで返す。[Required]APIは一度に最も優先度の高い問題を1件だけ返し、validation failureやErrorOr errorの配列を公開しない。
Responsibility Boundary
[Required]Value Object / Entity / feature-local modelはdomain invariantと状態遷移ruleを所有する。[Required]FluentValidationはHTTP request DTOの入力検証に使い、domain ruleを再実装せずfactoryを呼び出すadapterとして扱う。[Required]ErrorOrはhandler / application層の成功値と業務エラーを表す。[Required]例外は到達不能な状態、外部依存の障害、永続化contract違反など、通常フローで回復しない失敗に使う。[Required]endpointはvalidation filterとhandler resultをProblem Detailsへ変換するだけにする。[Required]handlerはHTTP status、TypedResults、HttpContext、ValidationFailure、ProblemDetailsを扱わない。[Required]同じ失敗をFluentValidation、ErrorOr、例外の複数で表現しない。
Request Validation
[Required]Request DTOごとに{Area}{UseCase}RequestValidatorをuse case directoryへ置く。[Required]request validatorはFailFastValidator<TRequest>のような共通基底型を継承し、RuleLevelCascadeModeとClassLevelCascadeModeを両方CascadeMode.Stopにする。[Required]validatorは宣言順で最初に失敗したruleの最初のfailureだけを返し、後続validatorと後続ruleを評価しない。[Required]ruleの宣言順は利用者へ返す問題の優先順位になるため、required、shape、domain valueの順など、修正可能性を考慮して意図的に並べる。[Required]validatorはDTOの構文的な正しさとrequest境界で確定できる制約だけを見る。[Contextual]制約がdomain conceptに属する場合はvalidatorへ条件を直書きせず、Value Object / feature-local modelのfactoryを呼ぶ。
csharp
public abstract class FailFastValidator<T> : AbstractValidator<T>
{
protected FailFastValidator()
{
RuleLevelCascadeMode = CascadeMode.Stop;
ClassLevelCascadeMode = CascadeMode.Stop;
}
}
public sealed class RoomCreateRequestValidator : FailFastValidator<RoomCreateRequest>
{
public RoomCreateRequestValidator()
{
RuleFor(request => request.WolfCount).ValidateWith(WolfCount.Create);
RuleFor(request => request.DiscussionMinutes).ValidateWith(DiscussionMinutes.Create);
RuleFor(request => request.VoteSeconds).ValidateWith(VoteSeconds.Create);
}
}[Required]validatorはrepository、clock、外部serviceに依存しない。[Required]DB照会が必要な一意性、権限、状態遷移可否はvalidatorではなくhandlerのErrorOrで表す。[Required]domain conceptとして名前を持つ制約はValue Object / feature-local modelに置く。[Contextual]DTO都合のnullability、required、配列件数など、domain objectを作る前に必要なshape検査はvalidatorに置いてよい。[Required]endpoint lambda内でIValidator<T>を直接呼ばない。[Required]request bodyのJSON不正やmodel binding不足もProblem Detailsの400にする。
Domain Rule Ownership
[Required]validatorはValue Object / Entity / feature-local modelのCreate/ factory / rule methodを利用し、正規表現、文字数、format、状態条件を二重実装しない。[Required]単一fieldのValue Objectは対象fieldのRuleForで検証する。[Required]複数fieldを組み合わせるValue Objectはrequest全体のruleで検証する。[Required]Entityの状態遷移可否はEntity methodに置き、handlerがmethodを呼んでErrorOr<T>として返す。[Required]Entity methodはHTTPやFluentValidationの型を返さない。[Required]HTTP境界はhandlerを呼ぶ前にrequest DTO、route parameter、認証contextのprimitive値をValue Objectへ変換する。[Required]route parameterやrequest body由来のValue Object生成失敗はHTTP入力検証の失敗として400に変換する。[Contextual]認証filter通過後の契約済みUIDをValue Objectへ変換するときはEnsure.Valid(...)を使い、失敗はfilter / contextの契約違反として扱う。[Contextual]validation filter通過後に同じValue Object作成が失敗する場合は実装バグとして扱う。
Validation Filter
[Required]FluentValidationはendpoint filterで実行し、失敗時はendpoint本体を呼ばずProblem Detailsを返す。[Required]request DTOを受けるendpointにはRequestValidationFilter<TRequest>を付ける。[Required]validation filterはIValidator<TRequest>をDIから取得する。[Required]validatorが未登録のrequestではfilterが黙って成功しない設計にする。[Required]filterは最初のValidationFailureだけを使い、ErrorCodeをProblem Details直下のcodeextensionへ設定する。[Required]validation responseにerrors配列、property path、pointerを追加しない。[Required]endpoint lambdaはvalidation errorの詳細を組み立てない。
RFC 9457 Problem Details
[Required]JSON responseはASP.NET CoreのProblemDetailsとTypedResults.Problem(...)を使い、ProblemHttpResultとして返す。[Required]responseのContent-Typeをapplication/problem+jsonにする。通常のTypedResults.Json(...)でProblem Details shapeを模倣しない。[Required]typeは問題種別を識別する安定した絶対HTTPS URIにする。[Required]titleは同じproblem typeで発生ごとに変えず、短い人向けsummaryにする。[Required]statusは実際のHTTP status codeと一致させる。[Required]detailはその発生固有の説明にし、修正方法の理解に必要な情報だけを含める。stack trace、秘密情報、provider内部情報を含めない。[Contextual]specific occurrenceを識別する安定したURIがある場合だけinstanceを設定し、単なるrequest pathを発生識別子として流用しない。[Required]機械判定用の拡張memberは単数のcodeだけにし、clientはdetailを解析しない。[Required]problem type URIのbaseは設定から注入し、絶対HTTPS URIであることを起動時に検証する。
ErrorOr Mapping
[Required]ErrorOrのerrorはApiProblemResults.FromErrors(...)のような共通mappingへ通す。[Required]ErrorOrが複数errorを保持していてもHTTP responseには先頭の1件だけを使う。handlerの通常の業務分岐は原則1件のerrorを返す。[Required]ErrorType.Validationは400、Unauthorizedは401、Forbiddenは403、NotFoundは404、Conflictは409、Unexpectedまたは未知typeは500にする。[Required]500ではError.Descriptionを公開せず、安全な固定detailへ置き換える。[Required]API response shapeとstatus mappingをendpointごとに手書きしない。[Required]endpointの戻り値unionは原則Results<TSuccess, ProblemHttpResult>にする。[Required]想定外例外はglobal exception handlerに任せ、endpoint lambdaでcatchして業務responseへ変換しない。
FluentValidation、ErrorOr、Endpoint Filter、Problem Detailsを通したcompile可能な全体例が必要な場合だけ../samples/rfc9457-errors/README.mdを読む。