Skip to content

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、TypedResultsHttpContextValidationFailureProblemDetailsを扱わない。
  • [Required] 同じ失敗をFluentValidation、ErrorOr、例外の複数で表現しない。

Request Validation

  • [Required] Request DTOごとに{Area}{UseCase}RequestValidatorをuse case directoryへ置く。
  • [Required] request validatorはFailFastValidator<TRequest>のような共通基底型を継承し、RuleLevelCascadeModeClassLevelCascadeModeを両方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直下のcode extensionへ設定する。
  • [Required] validation responseにerrors配列、property path、pointerを追加しない。
  • [Required] endpoint lambdaはvalidation errorの詳細を組み立てない。

RFC 9457 Problem Details

  • [Required] JSON responseはASP.NET CoreのProblemDetailsTypedResults.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を読む。