API アプリケーションにおけるパラメーターバインディング

リクエストデータをルートハンドラで表現される強型パラメーターに変換するプロセスをバインディングと言います。 バインドソースは、どの場所からパラメーターをバインドするかを決定します。 このソースは明示的に指定されたり、HTTPメソッドやパラメーターのタイプに基づいて推測されたりします。

バインド優先順位

以下は、パラメーターからバインドソースを決定するためのルールです: 1. 次の順序でパラメーター(From*属性)に定義された明示的な属性: 1. ルート値: `[FromRoute]` 2. クエリ文字列: `[FromQuery]` 3. ヘッダー: `[FromHeader]` 4. ボディー: `[FromBody]` 5. フォーム: `[FromForm]` 6. サービス: `[FromServices]` 7. パラメーター値: `[AsParameters]` 2. 特殊なタイプ: 1. `HttpContext` 2. `HttpRequest` (`HttpContext.Request`) 3. `HttpResponse` (`HttpContext.Response`) 4. `ClaimsPrincipal` (`HttpContext.User`) 5. `CancellationToken` (`HttpContext.RequestAborted`) 6. `IFormCollection` (`HttpContext.Request.Form`) 7. `IFormFileCollection` (`HttpContext.Request.Form.Files`) 8. `IFormFile` (`HttpContext.Request.Form.Files[paramName]`) 9. `Stream` (`HttpContext.Request.Body`) 10. `PipeReader` (`HttpContext.Request.BodyReader`) 3. パラメータータイプに有効な静的`BindAsync`メソッドがある場合。 4. 文字列または有効な静的`TryParse`メソッドを持つタイプの場合。 1. ルートテンプレート内にパラメーター名が存在する場合(例:`app.Map("/todo/{id}", (int id) => {});`)、それはルートからバインドされます。 2. クエリ文字列からバインドされます。 5. パラメータータイプが依存性注入によって提供されるサービスである場合、そのサービスがソースとして使用されます。 6. パラメーターはボディーから取得されます。

バインドソース

(1) Lambdaパラメーターの自動バインド


app.MapGet("/search", (string q, int page = 1, int size = 10) =>
{
    // q, page, size はクエリ文字列から自動的にバインドされます
    return Results.Ok();
});
マッチするURL: `/serch?q='aaa'&page=2&size=10`

(2) 明示的なパラメーターのバインド

特性を使用して、バインドするパラメーターの位置を明示的に宣言できます。

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// サービスとして追加
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", ([FromRoute] int id,
                  [FromQuery(Name = "p")] int page,
                  [FromServices] Service service,
                  [FromHeader(Name = "Content-Type")] string contentType) 
                  => {});

class Service { }

record Person(string Name, int Age);
パラメーター バインドソース
`id` 名前が`id`のルート値
`page` 名前が`"p"`のクエリ文字列
`service` 依存性注入によって提供される
`contentType` 名前が`"Content-Type"`のヘッダー

(3) 依存性注入によるバインド

タイプをサービスとして設定すると、最小APIのパラメーターは依存性注入を通じてバインドされます。 `[FromServices]`属性を明示的に適用する必要はありません。次のコードでは、両方の操作が現在時刻を返します。

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton();

var app = builder.Build();

app.MapGet("/", (IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

(4) 省略可能なパラメーター `?`

ルートハンドラーで宣言されたパラメーターは必須とみなされます。

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
`/products`ルートに必要なパラメーターが提供されていない場合、エラーが発生します:`BadHttpRequestException`:「int pageNumber」のクエリ文字列が未指定です。 `pageNumber`を省略可能にするには、タイプを省略可能に定義するか、デフォルト値を提供します。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI 結果
`/products?pageNumber=3` 3が返されました
`/products` 1が返されました
`/products2` 1が返されました

(5) 特殊タイプ

以下のタイプはバインド時に明示的な属性が不要です。 - `HttpContext`: 現在のHTTPリクエストまたはレスポンスに関するすべての情報を含むコンテキスト

app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
- `HttpRequest` と `HttpResponse`: HTTPリクエストとHTTPレスポンス

app.MapGet("/", (HttpRequest request, HttpResponse response) =>
    response.WriteAsync($"Hello World {request.Query["name"]}"));
`HttpContext`や`HttpRequest`パラメーターを使用してリクエストボディーを直接読み取る:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();
- `CancellationToken`: 現在のHTTPリクエストに関連付けられたキャンセルトークン

app.MapGet("/", async (CancellationToken cancellationToken) => 
    await MakeLongRunningRequestAsync(cancellationToken));
- `ClaimsPrincipal`: リクエストに関連付けられたユーザー、`HttpContext.User`からバインドされます

app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);

(6) ストリームやパイプラードへのリクエストボディーのバインド

例えば、データを Azure Queue Storage や Azure Blob Storage に保存することができます。

app.MapPost("/register", async (HttpRequest req, Stream body,
                                Channel queue) =>{...});
データを読み取る際、`Stream`は`HttpRequest.Body`と同じオブジェクトです。

(7) IFormFile と IFormFileCollection を使用したファイルのアップロード

最小APIで`IFormFile`と`IFormFileCollection`を使用してファイルをアップロードするには、`multipart/form-data`エンコーディングが必要です。 ルートハンドラー内のパラメーター名は、リクエスト内のフォームフィールド名と一致する必要があります。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

(8) フォーム内の複雑なタイプへのバインド

コレクションや複雑なタイプ(例:`Todo`や`Project`)へのバインドもサポートされています。

app.MapPost("/todo", async Task> (
               [FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
    try
    {
        await antiforgery.ValidateRequestAsync(context);
        return TypedResults.Ok(todo);
    }
    catch (AntiforgeryValidationException e)
    {
        return TypedResults.BadRequest("Invalid antiforgery token");
    }
});

(9) 配列と文字列値のバインド

以下は、クエリ文字列を基本タイプ、文字列配列、およびStringValues配列にバインドする方法を示すコードです。

app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

タグ: ASP.NET Core Parameter Binding Lambda Expressions

7月11日 21:21 投稿