ASP.NET APIのエラーをよりスムーズに処理

結果.問題は、ASP.NET Coreが提供する標準化されたエラー応答生成メソッドで、RFC 7807規格に準拠したエラー応答（問題の詳細）を作成します。これは、Web APIで構造化されたエラー情報を返すために使用され、タイプ、タイトル、ステータス、および詳細などのフィールドを含み、クライアントがエラーを理解し処理するのを助けます。パラメーターカスタマイズを通じて、結果.問題はエラータイプ、ステータスコード、詳細情報、およびインスタンスパスを明確に記述でき、APIの一貫性および保守性を向上させ、クライアントがエラーをより効率的に処理できるようにします。特に、最新のRESTful APIの構築に適しています。

```csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/test", () =>
{
    return Results.Problem(type: "Bad Request",
         title: "無効な番号",
         detail: "番号形式は：N00000、N+5桁の数字",
         statusCode: StatusCodes.Status400BadRequest);
});

app.Run();
```

### 実行結果：

![結果画像](https://mmbiz.qpic.cn/sz_mmbiz_png/4CAA63nvHRCr76vFlKo3NW7BsCjmyUI80cDxFdJ3MTRh8PfBKsKwsuK61m3peETUgAAB83icqtR4xhCpTx7B2Kg/640?wx_fmt=png&from=appmsg)

## ProblemDetailsの拡張

以下のコードは、ASP.NET CoreでResults.Problemを拡張して、カスタムの標準化されたエラー応答を生成する方法を示しています。この拡張は、Problem Detailsフォーマットを使用し、エラーの詳細の処理方法をカスタマイズすることで、エラー情報の構造化とデバッグ性を強化します。

まず、builder.Services.AddProblemDetailsを通じてProblem Detailsサービスを登録し、CustomizeProblemDetails構成でProblem Detailsの内容をカスタマイズします。この構成では、エラーインスタンス（Instance）の詳細な形式が指定され、リクエストプロトコル、メソッド、およびパスを含みます。同時に、requestId（リクエストのユニークな識別子）、traceId（トレース識別子）、およびclientIP（クライアントのIPアドレス）などのクライアントリクエストのトレース情報を追加し、エラー応答にコンテキスト情報を持たせます。

カスタム例外タイプをキャッチおよび処理するために、コードはProblemExceptionという名前のカスタム例外クラスを作成しました。このクラスには、特定のエラー情報を保存するためのErrorおよびMessageプロパティがあります。続いて、IExceptionHandlerインターフェースを実装したProblemExceptionHandlerクラスを実装し、このクラスは例外をインターセプトし、ProblemDetailsServiceを通じて標準のProblem Details応答を生成します。この過程で、TryHandleAsyncメソッドはProblemExceptionの例外かどうかを確認し、それがそうであれば、ステータスを400（Bad Request）に設定し、ProblemExceptionインスタンスのErrorおよびMessageプロパティに基づいてTitleおよびDetailフィールドを埋め込んだカスタムのProblemDetailsオブジェクトを作成します。最後に、ProblemDetailsService.TryWriteAsyncメソッドを通じてProblem Detailsを応答に書き込み、クライアントが構造化されたエラー情報を確実に受信できるようにします。

アプリケーション内で、/testというルートを定義し、このルートに対するクライアントリクエスト時にProblemException例外をスローし、カスタムエラー情報を含むProblem Details応答を返します。この方法を通じて、クライアントは詳細なエラーコンテキストおよび構造化されたエラー情報を受信でき、問題を迅速に識別およびデバッグするのに役立ちます。

### 具体的なコード：

```csharp
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(opt =>
{
    opt.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Instance = $"{context.HttpContext.Request.Protocol} {context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
        context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);

        var activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
        context.ProblemDetails.Extensions.TryAdd("traceId", activity?.Id);

        if (context.HttpContext.Connection != null && context.HttpContext.Connection.RemoteIpAddress != null)
        {
            context.ProblemDetails.Extensions.TryAdd("clientIP", context.HttpContext.Connection.RemoteIpAddress?.ToString());
        }
    };
});
builder.Services.AddExceptionHandler<ProblemExceptionHandler>();

var app = builder.Build();

app.MapGet("/test", () =>
{
    throw new ProblemException(error: "無効な番号",
         message: "番号形式は：N00000、N+5桁の数字");
});
app.UseExceptionHandler();
app.Run();

public class ProblemException : Exception
{
    public string Error { get; }
    public ProblemException(string error, string message) : base(message)
    {
        Error = error;
    }
}
public class ProblemExceptionHandler : IExceptionHandler
{
    private readonly IProblemDetailsService _problemDetailsService;
    public ProblemExceptionHandler(IProblemDetailsService problemDetailsService)
    {
        _problemDetailsService = problemDetailsService;
    }
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        if (exception is not ProblemException problemException)
        {
            return true;
        }
        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status400BadRequest,
            Title = problemException.Error,
            Detail = problemException.Message,
            Type = "Bad Request"
        };
        httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = httpContext,
            ProblemDetails = problemDetails
        });
    }
}
```

### 実装の結果：

![実装画像](https://mmbiz.qpic.cn/sz_mmbiz_png/4CAA63nvHRCr76vFlKo3NW7BsCjmyUI8QujS3oqqnnovOHiaiaQ1bBbeunyPznrBKAIibmSH0YibibvK1mL3pCKsT3A/640?wx_fmt=png&from=appmsg)

実装した際の欠点は、例外がスローされると、プラットフォームが失敗のログを出力することです。

![ログ画像](https://mmbiz.qpic.cn/sz_mmbiz_png/4CAA63nvHRCr76vFlKo3NW7BsCjmyUI8lyejRrJ5gqBQLjJhUewrBs6Z0zrk16H4f2VU0OuePIYmJrLDpxU4yQ/640?wx_fmt=png&from=appmsg)

(Translated by GPT)