mcpField
E
error-handling

error-handling

github.com/codewithmukesh開発7913.0k 回インストール12 週間前に更新
リポジトリ

error-handling について

.NET開発で業務ロジックとAPIのエラー処理を設計するためのスキルです。想定内の失敗と例外を区別し、一貫したProblemDetails応答を推奨します。

主な機能

  1. 01想定内の失敗にResultパターン
  2. 02グローバル例外処理
  3. 03ProblemDetailsによるAPI応答
  4. 04境界でのリクエスト検証
  5. 05型付きエラーモデル

活用シーン

  1. 01検証失敗を例外なしで表現
  2. 02APIエラー形式の統一
  3. 03予期しないアプリクラッシュへの対応
  4. 04APIエンドポイントでのリクエスト検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill error-handling

インストール後、Claude Code で/error-handlingを入力して読み込まれたか確認します。

詳細

エラー処理

基本原則

  1. 想定される失敗には Result パターンを使用する — 「注文が見つからない」や「検証に失敗した」といったケースで例外をスローしないでください。これらは想定される結果であり、例外的な状況ではありません。ADR-002 を参照してください。
  2. 例外は予期しない失敗に限定する — データベース接続の切断、null 参照バグ、ネットワークタイムアウトなどは、本当に例外的な状況であり、グローバルハンドラーに伝播させる必要があります。
  3. すべての API エラーで ProblemDetails を返す — RFC 9457 が標準です。すべてのエラーレスポンスには type、title、status、detail、および任意で errors が含まれます。
  4. 境界で検証する — リクエストの検証はビジネスロジックの奥深くではなく、API 層で行います。

パターン

Result パターン

値またはエラーを保持する、シンプルな汎用 Result 型です。

public class Result
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public List<string> Errors { get; }

    protected Result(bool isSuccess, List<string>? errors = null)
    {
        IsSuccess = isSuccess;
        Errors = errors ?? [];
    }

    public static Result Success() => new(true);
    public static Result Failure(params string[] errors) => new(false, [..errors]);
    public static Result<T> Success<T>(T value) => new(value);
    public static Result<T> Failure<T>(params string[] errors) => new(errors);
}

public class Result<T> : Result
{
    public T Value { get; }

    internal Result(T value) : base(true) => Value = value;
    internal Result(IEnumerable<string> errors) : base(false, [..errors]) => Value = default!;
}
Result から ProblemDetails へのマッピング
public static class ResultExtensions
{
    public static IResult ToProblemDetails(this Result result, int statusCode = 400)
    {
        return TypedResults.Problem(
            title: "One or more errors occurred",
            statusCode: statusCode,
            extensions: new Dictionary<string, object?>
            {
                ["errors"] = result.Errors
            });
    }
}

// Usage in endpoint
group.MapPost("/", async (CreateOrder.Command command, ISender sender, CancellationToken ct) =>
{
    var result = await sender.Send(command, ct);
    return result.IsSuccess
        ? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
        : result.ToProblemDetails();
});
グローバル例外ハンドラー

予期しない例外を捕捉し、ProblemDetails に変換します。最新の IExceptionHandler を使用する方法(推奨)については、knowledge/common-infrastructure.md を参照してください。以下のインラインラムダは、シンプルなケースで使用できます。

// Program.cs
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();

        logger.LogError(exception, "Unhandled exception for {Method} {Path}",
            context.Request.Method, context.Request.Path);

        var problem = new ProblemDetails
        {
            Title = "An unexpected error occurred",
            Status = StatusCodes.Status500InternalServerError,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1"
        };

        // Don't leak details in production
        if (context.RequestServices.GetRequiredService<IHostEnvironment>().IsDevelopment())
        {
            problem.Detail = exception?.Message;
        }

        context.Response.StatusCode = problem.Status.Value;
        await context.Response.WriteAsJsonAsync(problem);
    });
});
エンドポイントフィルターを使用した FluentValidation
// Validator
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderValidator()
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty().WithMessage("Customer ID is required");

        RuleFor(x => x.Items)
            .NotEmpty().WithMessage("At least one item is required");

        RuleForEach(x => x.Items).ChildRules(item =>
        {
            item.RuleFor(x => x.ProductId).NotEmpty();
            item.RuleFor(x => x.Quantity).GreaterThan(0);
        });
    }
}

// Generic validation filter
public class ValidationFilter<TRequest> : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var validator = context.HttpContext.RequestServices.GetService<IValidator<TRequest>>();
        if (validator is null)
            return await next(context);

        var request = context.Arguments.OfType<TRequest>().FirstOrDefault();
        if (request is null)
            return await next(context);

        var result = await validator.ValidateAsync(request);
        if (!result.IsValid)
        {
            return TypedResults.ValidationProblem(result.ToDictionary());
        }

        return await next(context);
    }
}

// Registration
group.MapPost("/", CreateOrder)
    .AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
型付きエラー結果

より高度なエラー処理には、型付きエラー列挙型またはエラーオブジェクトを使用します。

public abstract record Error(string Code, string Message);
public record NotFoundError(string Entity, object Id)
    : Error("not_found", $"{Entity} with ID {Id} was not found");
public record ValidationError(string Field, string Message)
    : Error("validation", Message);
public record ConflictError(string Message)
    : Error("conflict", Message);

// Map to HTTP status codes
public static IResult ToHttpResult(this Error error) => error switch
{
    NotFoundError => TypedResults.Problem(title: error.Message, statusCode: 404),
    ValidationError => TypedResults.Problem(title: error.Message, statusCode: 400),
    ConflictError => TypedResults.Problem(title: error.Message, statusCode: 409),
    _ => TypedResults.Problem(title: error.Message, statusCode: 500)
};

アンチパターン

フロー制御に例外をスローしない
// BAD — exceptions for expected outcomes
public Order GetOrder(Guid id)
{
    var order = db.Orders.Find(id)
        ?? throw new NotFoundException($"Order {id} not found");
    return order;
}

// GOOD — Result pattern
public Result<Order> GetOrder(Guid id)
{
    var order = db.Orders.Find(id);
    return order is not null
        ? Result.Success(order)
        : Result.Failure<Order>($"Order {id} not found");
}
API から生のエラー文字列を返さない
// BAD — inconsistent error format
return Results.BadRequest("Something went wrong");
return Results.BadRequest(new { error = "Invalid input" });

// GOOD — always ProblemDetails
return TypedResults.Problem(title: "Invalid input", statusCode: 400);
return TypedResults.ValidationProblem(validationResult.ToDictionary());
例外を捕捉して握りつぶさない
// BAD — silently swallowing
try { await ProcessOrder(order); }
catch (Exception) { /* ignore */ }

// GOOD — log and handle appropriately
try { await ProcessOrder(order); }
catch (PaymentException ex)
{
    logger.LogWarning(ex, "Payment failed for order {OrderId}", order.Id);
    return Result.Failure<Order>("Payment processing failed");
}

判断ガイド

シナリオ推奨事項
想定されるビジネス上の失敗Result パターン
入力検証エンドポイントフィルターを使用した FluentValidation
予期しないクラッシュグローバル例外ハンドラー → ProblemDetails
API エラー形式RFC 9457 ProblemDetails — 常に使用
ハンドラー内の検証例外をスローせず、Result.Failure を返す
外部サービスの失敗特定の例外を捕捉し、Result.Failure を返す
エラーのログ記録相関 ID を含む構造化ログ

コメント