Minimal API アプリでのパラメーター バインド
Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
パラメーター バインドとは、要求データを、ルート ハンドラーで表現された厳密に型指定されたパラメーターに変換するプロセスです。 バインディング ソースは、パラメーターのバインド元を指定します。 バインディング ソースは明示的に指定するか、HTTP メソッドとパラメーターの型に基づいて推測できます。
サポートされているバインディング ソース:
- ルート値
- クエリ文字列
- ヘッダー
- 本文 (JSON)
- フォーム値
- 依存関係の挿入によって指定されるサービス
- Custom
次の GET
ルート ハンドラーは、これらのパラメーター バインディング ソースの一部を使用しています。
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
次の表は、前の例で使用したパラメーターと、関連付けられているバインディング ソースとの関係を示しています。
パラメーター | バインディング ソース |
---|---|
id |
ルート値 |
page |
クエリ文字列 |
customHeader |
header |
service |
依存関係の挿入によって指定 |
HTTP メソッド GET
、HEAD
、OPTIONS
、DELETE
は、本文から暗黙的にバインドしません。 これらの HTTP メソッドの本文から (JSON として) バインドするには、[FromBody]
で明示的にバインドするか、HttpRequest から読み取ります。
次の例の POST ルート ハンドラーは、person
パラメーターに本文のバインディング ソースを (JSON として) 使用しています。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
前の例のパラメーターはすべて、要求データから自動的にバインドされます。 パラメーター バインディングが提供する便利さを示すために、次のルート ハンドラーは、要求からどのように直接要求データを読み取るかを示しています。
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
明示的なパラメーター バインド
属性を使用すると、パラメーターのバインド元を明示的に宣言できます。
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
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" のヘッダー |
フォーム値からの明示的なバインド
[FromForm]
属性によってフォーム値がバインドされます。
app.MapPost("/todos", async ([FromForm] string name,
[FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
var todo = new Todo
{
Name = name,
Visibility = visibility
};
if (attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await attachment.CopyToAsync(stream);
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
別の方法には、[FromForm]
で注釈がつけられたプロパティを持つカスタム型で [AsParameters]
属性を使用する方法があります。 たとえば、次のコードによって、フォーム値から NewTodoRequest
レコード構造体のプロパティへのバインドが行われます。
app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
var todo = new Todo
{
Name = request.Name,
Visibility = request.Visibility
};
if (request.Attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await request.Attachment.CopyToAsync(stream);
todo.Attachment = attachmentName;
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
[FromForm] Visibility Visibility, IFormFile? Attachment);
詳細については、この記事で後述する AsParameters に関するセクションを参照してください。
完全なサンプル コードは、AspNetCore.Docs.Samples リポジトリにあります。
IFormFile と IFormFileCollection からのバインドをセキュリティで保護する
複雑なフォームのバインドは、[FromForm]
を使用する IFormFile と IFormFileCollection 使用してサポートされます。
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
([FromForm] FileUploadForm fileUploadForm, HttpContext context,
IAntiforgery antiforgery) =>
{
await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
fileUploadForm.Name!, app.Environment.ContentRootPath);
return TypedResults.Ok($"Your file with the description:" +
$" {fileUploadForm.Description} has been uploaded successfully");
});
app.Run();
[FromForm]
を使用して要求にバインドされたパラメーターには、偽造防止トークンが含まれます。 偽造防止トークンは、要求が処理されたときに検証されます。 詳細については、「最小限の API を使用した偽造防止」を参照してください。
詳細については、「最小限の API でのフォーム バインド」を参照してください。
完全なサンプル コードは、AspNetCore.Docs.Samples リポジトリにあります。
依存関係の挿入を使用したパラメーター バインド
Minimal API のパラメーター バインドでは、型がサービスとして構成されているときに、依存関係の挿入によってパラメーターをバインドします。 パラメーターに [FromServices]
属性を明示的に適用する必要はありません。 次のコードでは、どちらのアクションでも時刻が返されます。
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
var app = builder.Build();
app.MapGet("/", ( IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
省略可能なパラメーター
ルート ハンドラーで宣言されたパラメーターは、必要に応じて処理されます。
- 要求がルートに一致する場合、すべての必須のパラメーターが要求で指定されている場合にのみルート ハンドラーが実行されます。
- すべての必須のパラメーターが指定されていない場合は、エラーが発生します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
URI | 結果 |
---|---|
/products?pageNumber=3 |
3 が返される |
/products |
BadHttpRequestException : 必須のパラメーター "int pageNumber" が、クエリ文字列から提供されていません。 |
/products/1 |
HTTP 404 エラー、一致するルートなし |
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 が返される |
上記の null 値の許容または既定値は、すべてのソースに適用されます。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
上記のコードでは、要求本文が送信されていない場合、null 値の product でメソッドが呼び出されます。
注: 無効なデータが指定され、パラメーターが null 値を許容する場合、ルート ハンドラーは実行されません。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | 結果 |
---|---|
/products?pageNumber=3 |
3 が返される |
/products |
1 が返される |
/products?pageNumber=two |
BadHttpRequestException : "two" からパラメーター "Nullable<int> pageNumber" をバインドできませんでした。 |
/products/two |
HTTP 404 エラー、一致するルートなし |
詳細については、「バインドの失敗」セクションを参照してください。
特殊な型
次の型は、明示的な属性なしでバインドされます。
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"]}"));
CancellationToken: 現在の HTTP 要求に関連付けられているキャンセル トークン:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal: HttpContext.User からバインドされた、要求に関連付けられているユーザー:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
要求本文を Stream
または PipeReader
としてバインドする
ユーザーがデータを処理して次のようにする必要がある場合は、シナリオを効率的にサポートするために、要求本文を Stream
または PipeReader
としてバインドできます。
- データを Blob Storage に格納するか、キュー プロバイダーにデータをエンキューします。
- ワーカー プロセスまたはクラウド関数で、格納されたデータを処理します。
たとえば、データは Azure Queue Storage にエンキューされるか、Azure Blob Storage に格納される場合があります。
次のコードでは、バックグラウンド キューが実装されています。
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
次のコードでは、要求本文が Stream
にバインドされています。
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
次に示すコードは、完全な Program.cs
ファイルです。
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
- データを読み取るとき、
Stream
はHttpRequest.Body
と同じオブジェクトです。 - 要求本文は、既定ではバッファーされません。 読み取られた後の本文を巻き戻すことはできません。 ストリームを複数回読み取ることはできません。
- 基になるバッファーが破棄または再利用されるため、最小アクション ハンドラーの外部では
Stream
とPipeReader
は使用できません。
IFormFile と IFormFileCollection を使用したファイルのアップロード
次のコードでは、IFormFile と IFormFileCollection を使用して、ファイルをアップロードしています。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
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();
承認ヘッダー、クライアント証明書、または cookie ヘッダーを使用した認証されたファイルのアップロード要求がサポートされています。
IFormCollection、IFormFile、IFormFileCollection を使ったフォームへのバインディング
IFormCollection、IFormFile、IFormFileCollection を使ったフォームベースのパラメーターからのバインディングがサポートされています。 Swagger UI との統合をサポートするために、フォーム パラメーターに対して OpenAPI メタデータが推論されます。
次のコードは、IFormFile
型から推論されたバインディングを使ってファイルをアップロードします。
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
Directory.CreateDirectory(directoryPath);
return Path.Combine(directoryPath, fileName);
}
async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
var filePath = GetOrCreateFilePath(fileSaveName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
<input type="file" name="file" placeholder="Upload an image..." accept=".jpg,
.jpeg, .png" />
<input type="submit" />
</form>
</body>
</html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>,
BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await UploadFileWithName(file, fileSaveName);
return TypedResults.Ok("File uploaded successfully!");
});
app.Run();
警告: フォームを実装するときは、アプリでクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を防ぐ必要があります。 先ほどのコードでは、IAntiforgery サービスを使って、偽造防止トークンを生成して検証することで XSRF 攻撃を防いでいます。
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
Directory.CreateDirectory(directoryPath);
return Path.Combine(directoryPath, fileName);
}
async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
var filePath = GetOrCreateFilePath(fileSaveName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
<input type="file" name="file" placeholder="Upload an image..." accept=".jpg,
.jpeg, .png" />
<input type="submit" />
</form>
</body>
</html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>,
BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await UploadFileWithName(file, fileSaveName);
return TypedResults.Ok("File uploaded successfully!");
});
app.Run();
XSRF 攻撃の詳細については、「Minimal API を使用した偽造防止」を参照してください
詳細については、「最小限の API でのフォーム バインド」を参照してください。
フォームからコレクションと複合型にバインドする
バインディングは、次の場合にサポートされています。
- コレクション (例: List や Dictionary など)
- 複合型 (例:
Todo
またはProject
など)
このコードには、次の項目が示されています。
- マルチパート フォーム入力を複雑なオブジェクトにバインドする最小限のエンドポイント。
- 偽造防止サービスを使用して、偽造防止トークンの生成と検証をサポートする方法。
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html><body>
<form action="/todo" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}"
type="hidden" value="{token.RequestToken}" />
<input type="text" name="name" />
<input type="date" name="dueDate" />
<input type="checkbox" name="isCompleted" value="true" />
<input type="submit" />
<input name="isCompleted" type="hidden" value="false" />
</form>
</body></html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>>
([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");
}
});
app.Run();
class Todo
{
public string Name { get; set; } = string.Empty;
public bool IsCompleted { get; set; } = false;
public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}
上のコードでは以下の操作が行われます。
- JSON 本文から読み取る必要があるパラメーターから曖昧さを解消するには、ターゲット パラメーターに
[FromForm]
属性で注釈を付ける必要があります。 - 要求デリゲート ジェネレーターを使ってコンパイルした最小限の API の場合、複合型またはコレクション型からのバインドはサポートされていません。
- マークアップには、
isCompleted
という名前の追加の非表示入力と、false
の値が表示されます。 フォームの送信時にisCompleted
チェック ボックスをオンにすると、値true
とfalse
の両方が値として送信されます。 チェックボックスをオフにすると、非表示の入力値false
のみが送信されます。 ASP.NET Core モデルバインド プロセスは、bool
値にバインドするときに最初の値のみを読み取ります。これにより、チェックボックスがオンの場合はtrue
になり、チェックボックスがオフの場合はfalse
になります。
前のエンドポイントに送信されたフォーム データの例を次に示します。
__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false
ヘッダーとクエリ文字列から配列と文字列値をバインドする
次のコードは、クエリ文字列をプリミティブ型の配列、文字列配列、StringValues にバインドする方法を示しています。
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
クエリ文字列またはヘッダー値を複合型の配列にバインドすることは、その型で TryParse
が実装されている場合にサポートされます。 次のコードでは、文字列配列にバインドし、指定したタグを持つすべての項目を返します。
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
次のコードは、モデルと必要な TryParse
の実装を示しています。
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
次のコードでは、int
配列にバインドします。
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
上記のコードをテストするには、次のエンドポイントを追加して、データベースに Todo
項目を入力します。
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
HttpRepl
などのツールを使って、次のデータを上記のエンドポイントに渡します。
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
次のコードでは、ヘッダー キー X-Todo-Id
にバインドし、一致する Id
値を持つ Todo
項目を返します。
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
注意
クエリ文字列から string[]
をバインドするとき、一致するクエリ文字列がないと null 値ではなく空の配列になります。
[AsParameters] を使用した引数リストのパラメーター バインド
AsParametersAttribute を使うと、複雑な、または再帰的なモデル バインドではなく、型へのシンプルなパラメーター バインドが可能になります。
次のコードがあるとします。
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
次の GET
エンドポイントを考えてみます。
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
次の struct
を使って、上記の強調表示されたパラメーターを置き換えることができます。
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
リファクタリングされた GET
エンドポイントでは、上記の struct
を AsParameters 属性と共に使用します。
app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
次のコードは、アプリ内の追加のエンドポイントを示しています。
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
var todo = await Db.Todos.FindAsync(Id);
if (todo is null) return Results.NotFound();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
if (await Db.Todos.FindAsync(Id) is Todo todo)
{
Db.Todos.Remove(todo);
await Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
次のクラスは、パラメーター リストをリファクタリングするために使用されます。
class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
次のコードは、AsParameters
と上記の struct
とクラスを使ってリファクタリングされたエンドポイントを示しています。
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);
if (todo is null) return Results.NotFound();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
次の record
型を使って、上記のパラメーターを置き換えることができます。
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
struct
を AsParameters
と共に使うと、record
型を使うよりもパフォーマンスが向上します。
完全なサンプル コードは AspNetCore.Docs.Samples リポジトリにあります。
カスタム バインド
パラメーター バインドは、2 つの方法でカスタマイズできます。
- ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な
TryParse
メソッドを追加することにより、カスタムの型をバインドします。 - 型に対して
BindAsync
メソッドを実装することにより、バインディング プロセスを制御します。
TryParse
TryParse
には 2 つの API があります。
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
次のコードは、URI /map?Point=12.3,10.1
に対して Point: 12.3, 10.1
を表示します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
には次の API があります。
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
次のコードは、URI /products?SortBy=xyz&SortDir=Desc&Page=99
に対して SortBy:xyz, SortDirection:Desc, CurrentPage:99
を表示します。
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
バインドの失敗
バインドが失敗すると、フレームワークはデバッグ メッセージをログし、失敗の種類に応じてクライアントに様々さまざまな状態コードを返します。
障害モード | null 値を許容するパラメーター型 | バインディング ソース | 状態コード |
---|---|---|---|
{ParameterType}.TryParse は false を返します。 |
はい | ルート/クエリ/ヘッダー | 400 |
{ParameterType}.BindAsync は null を返します。 |
はい | custom | 400 |
{ParameterType}.BindAsync がスローされる |
問題ありません | custom | 500 |
JSON 本文を逆シリアル化できない | 問題ありません | body | 400 |
コンテンツの型が正しくない (application/json でない) |
問題ありません | body | 415 |
バインディングの優先順位
パラメーターからバインディング ソースを決定するルールは次の通りです。
- パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
- ルート値:
[FromRoute]
- クエリ文字列:
[FromQuery]
- ヘッダー:
[FromHeader]
- 本文:
[FromBody]
- フォーム:
[FromForm]
- サービス:
[FromServices]
- パラメーター値:
[AsParameters]
- ルート値:
- 特殊な型
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)IFormCollection
(HttpContext.Request.Form
)IFormFileCollection
(HttpContext.Request.Form.Files
)IFormFile
(HttpContext.Request.Form.Files[paramName]
)Stream
(HttpContext.Request.Body
)PipeReader
(HttpContext.Request.BodyReader
)
- パラメーターの型に有効な静的
BindAsync
メソッドがある。 - パラメーターの型が文字列であるか、有効な静的
TryParse
メソッドがある。- パラメーター名が
app.Map("/todo/{id}", (int id) => {});
などのルート テンプレートにある場合、ルートからバインドされる。 - クエリ文字列からバインドされる。
- パラメーター名が
- パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
- パラメーターが本文からのものである。
ボディ バインドの JSON 逆シリアル化オプションを構成する
ボディ バインド ソースでは、System.Text.Json を使用して逆シリアル化を行います。 この既定値は変更できませんが、JSON シリアル化と逆シリアル化のオプションを構成することはできます。
JSON 逆シリアル化オプションをグローバルに構成する
アプリにグローバルに適用されるオプションは、ConfigureHttpJsonOptions を呼び出すことによって構成できます。 次の例には、パブリック フィールドと JSON 出力形式が含まれています。
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
サンプル コードではシリアル化と逆シリアル化の両方を構成するため、出力 JSON に NameField
の読み取りと NameField
のインクルードを行うことができます。
エンドポイントの JSON 逆シリアル化オプションを構成する
ReadFromJsonAsync には、JsonSerializerOptions オブジェクトを受け入れるオーバーロードが用意されています。 次の例には、パブリック フィールドと JSON 出力形式が含まれています。
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});
app.Run();
class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
上記のコードでは、カスタマイズされたオプションが逆シリアル化にのみ適用されるため、出力 JSON では NameField
が除外されます。
要求本文を読み取る
要求本文を直接読み取るには、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();
上記のコードでは次の操作が行われます。
- HttpRequest.BodyReader を使用して要求本文にアクセスします。
- 要求本文をローカル ファイルにコピーします。
パラメーター バインドとは、要求データを、ルート ハンドラーで表現された厳密に型指定されたパラメーターに変換するプロセスです。 バインディング ソースは、パラメーターのバインド元を指定します。 バインディング ソースは明示的に指定するか、HTTP メソッドとパラメーターの型に基づいて推測できます。
サポートされているバインディング ソース:
- ルート値
- クエリ文字列
- ヘッダー
- 本文 (JSON)
- 依存関係の挿入によって指定されるサービス
- Custom
フォーム値からのバインドは、.NET 6 と 7 ではネイティブにサポートされて "いません"。
次の GET
ルート ハンドラーは、これらのパラメーター バインディング ソースの一部を使用しています。
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
次の表は、前の例で使用したパラメーターと、関連付けられているバインディング ソースとの関係を示しています。
パラメーター | バインディング ソース |
---|---|
id |
ルート値 |
page |
クエリ文字列 |
customHeader |
header |
service |
依存関係の挿入によって指定 |
HTTP メソッド GET
、HEAD
、OPTIONS
、DELETE
は、本文から暗黙的にバインドしません。 これらの HTTP メソッドの本文から (JSON として) バインドするには、[FromBody]
で明示的にバインドするか、HttpRequest から読み取ります。
次の例の POST ルート ハンドラーは、person
パラメーターに本文のバインディング ソースを (JSON として) 使用しています。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
前の例のパラメーターはすべて、要求データから自動的にバインドされます。 パラメーター バインディングが提供する便利さを示すために、次のルート ハンドラーは、要求からどのように直接要求データを読み取るかを示しています。
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
明示的なパラメーター バインド
属性を使用すると、パラメーターのバインド元を明示的に宣言できます。
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
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" のヘッダー |
注意
フォーム値からのバインドは、.NET 6 と 7 ではネイティブにサポートされて "いません"。
依存関係の挿入を使用したパラメーター バインド
Minimal API のパラメーター バインドでは、型がサービスとして構成されているときに、依存関係の挿入によってパラメーターをバインドします。 パラメーターに [FromServices]
属性を明示的に適用する必要はありません。 次のコードでは、どちらのアクションでも時刻が返されます。
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
var app = builder.Build();
app.MapGet("/", ( IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
省略可能なパラメーター
ルート ハンドラーで宣言されたパラメーターは、必要に応じて処理されます。
- 要求がルートに一致する場合、すべての必須のパラメーターが要求で指定されている場合にのみルート ハンドラーが実行されます。
- すべての必須のパラメーターが指定されていない場合は、エラーが発生します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
URI | 結果 |
---|---|
/products?pageNumber=3 |
3 が返される |
/products |
BadHttpRequestException : 必須パラメーター "int pageNumber" が、クエリ文字列から指定されていません |
/products/1 |
HTTP 404 エラー、一致するルートなし |
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 が返される |
上記の null 値の許容または既定値は、すべてのソースに適用されます。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
上記のコードでは、要求本文が送信されていない場合、null 値の product でメソッドが呼び出されます。
注: 無効なデータが指定され、パラメーターが null 値を許容する場合、ルート ハンドラーは実行されません。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | 結果 |
---|---|
/products?pageNumber=3 |
3 が返される |
/products |
1 が返される |
/products?pageNumber=two |
BadHttpRequestException : "two" からパラメーター "Nullable<int> pageNumber" をバインドできませんでした。 |
/products/two |
HTTP 404 エラー、一致するルートなし |
詳細については、「バインドの失敗」セクションを参照してください。
特殊な型
次の型は、明示的な属性なしでバインドされます。
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"]}"));
CancellationToken: 現在の HTTP 要求に関連付けられているキャンセル トークン:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal: HttpContext.User からバインドされた、要求に関連付けられているユーザー:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
要求本文を Stream
または PipeReader
としてバインドする
ユーザーがデータを処理して次のようにする必要がある場合は、シナリオを効率的にサポートするために、要求本文を Stream
または PipeReader
としてバインドできます。
- データを Blob Storage に格納するか、キュー プロバイダーにデータをエンキューします。
- ワーカー プロセスまたはクラウド関数で、格納されたデータを処理します。
たとえば、データは Azure Queue Storage にエンキューされるか、Azure Blob Storage に格納される場合があります。
次のコードでは、バックグラウンド キューが実装されています。
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
次のコードでは、要求本文が Stream
にバインドされています。
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
次に示すコードは、完全な Program.cs
ファイルです。
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
- データを読み取るとき、
Stream
はHttpRequest.Body
と同じオブジェクトです。 - 要求本文は、既定ではバッファーされません。 読み取られた後の本文を巻き戻すことはできません。 ストリームを複数回読み取ることはできません。
- 基になるバッファーが破棄または再利用されるため、最小アクション ハンドラーの外部では
Stream
とPipeReader
は使用できません。
IFormFile と IFormFileCollection を使用したファイルのアップロード
次のコードでは、IFormFile と IFormFileCollection を使用して、ファイルをアップロードしています。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
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();
承認ヘッダー、クライアント証明書、または cookie ヘッダーを使用した認証されたファイルのアップロード要求がサポートされています。
ASP.NET Core 7.0 には、偽造防止のサポートが組み込まれていません。 偽造防止は、ASP.NET Core 8.0 以降でのみ使用できます。 ただし、IAntiforgery
サービスを使用して実装することはできます。
ヘッダーとクエリ文字列から配列と文字列値をバインドする
次のコードは、クエリ文字列をプリミティブ型の配列、文字列配列、StringValues にバインドする方法を示しています。
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
クエリ文字列またはヘッダー値を複合型の配列にバインドすることは、その型で TryParse
が実装されている場合にサポートされます。 次のコードでは、文字列配列にバインドし、指定したタグを持つすべての項目を返します。
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
次のコードは、モデルと必要な TryParse
の実装を示しています。
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
次のコードでは、int
配列にバインドします。
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
上記のコードをテストするには、次のエンドポイントを追加して、データベースに Todo
項目を入力します。
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
HttpRepl
などの API テスト ツールを使って、次のデータを上記のエンドポイントに渡します。
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
次のコードでは、ヘッダー キー X-Todo-Id
にバインドし、一致する Id
値を持つ Todo
項目を返します。
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
注意
クエリ文字列から string[]
をバインドするとき、一致するクエリ文字列がないと null 値ではなく空の配列になります。
[AsParameters] を使用した引数リストのパラメーター バインド
AsParametersAttribute を使うと、複雑な、または再帰的なモデル バインドではなく、型へのシンプルなパラメーター バインドが可能になります。
次のコードがあるとします。
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
次の GET
エンドポイントを考えてみます。
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
次の struct
を使って、上記の強調表示されたパラメーターを置き換えることができます。
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
リファクタリングされた GET
エンドポイントでは、上記の struct
を AsParameters 属性と共に使用します。
app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
次のコードは、アプリ内の追加のエンドポイントを示しています。
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
var todo = await Db.Todos.FindAsync(Id);
if (todo is null) return Results.NotFound();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
if (await Db.Todos.FindAsync(Id) is Todo todo)
{
Db.Todos.Remove(todo);
await Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
次のクラスは、パラメーター リストをリファクタリングするために使用されます。
class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
次のコードは、AsParameters
と上記の struct
とクラスを使ってリファクタリングされたエンドポイントを示しています。
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);
if (todo is null) return Results.NotFound();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
次の record
型を使って、上記のパラメーターを置き換えることができます。
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
struct
を AsParameters
と共に使うと、record
型を使うよりもパフォーマンスが向上します。
完全なサンプル コードは AspNetCore.Docs.Samples リポジトリにあります。
カスタム バインド
パラメーター バインドは、2 つの方法でカスタマイズできます。
- ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な
TryParse
メソッドを追加することにより、カスタムの型をバインドします。 - 型に対して
BindAsync
メソッドを実装することにより、バインディング プロセスを制御します。
TryParse
TryParse
には 2 つの API があります。
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
次のコードは、URI /map?Point=12.3,10.1
に対して Point: 12.3, 10.1
を表示します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
には次の API があります。
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
次のコードは、URI /products?SortBy=xyz&SortDir=Desc&Page=99
に対して SortBy:xyz, SortDirection:Desc, CurrentPage:99
を表示します。
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
バインドの失敗
バインドが失敗すると、フレームワークはデバッグ メッセージをログし、失敗の種類に応じてクライアントに様々さまざまな状態コードを返します。
障害モード | null 値を許容するパラメーター型 | バインディング ソース | 状態コード |
---|---|---|---|
{ParameterType}.TryParse は false を返します。 |
はい | ルート/クエリ/ヘッダー | 400 |
{ParameterType}.BindAsync は null を返します。 |
はい | custom | 400 |
{ParameterType}.BindAsync がスローされる |
どちらでもよい | custom | 500 |
JSON 本文を逆シリアル化できない | どちらでもよい | body | 400 |
コンテンツの型が正しくない (application/json でない) |
どちらでもよい | body | 415 |
バインディングの優先順位
パラメーターからバインディング ソースを決定するルールは次の通りです。
- パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
- ルート値:
[FromRoute]
- クエリ文字列:
[FromQuery]
- ヘッダー:
[FromHeader]
- 本文:
[FromBody]
- サービス:
[FromServices]
- パラメーター値:
[AsParameters]
- ルート値:
- 特殊な型
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)IFormFileCollection
(HttpContext.Request.Form.Files
)IFormFile
(HttpContext.Request.Form.Files[paramName]
)Stream
(HttpContext.Request.Body
)PipeReader
(HttpContext.Request.BodyReader
)
- パラメーターの型に有効な静的
BindAsync
メソッドがある。 - パラメーターの型が文字列であるか、有効な静的
TryParse
メソッドがある。- ルート テンプレートにパラメーター名が存在する場合。
app.Map("/todo/{id}", (int id) => {});
では、id
はルートからバインドされます。 - クエリ文字列からバインドされる。
- ルート テンプレートにパラメーター名が存在する場合。
- パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
- パラメーターが本文からのものである。
ボディ バインドの JSON 逆シリアル化オプションを構成する
ボディ バインド ソースでは、System.Text.Json を使用して逆シリアル化を行います。 この既定値は変更できませんが、JSON シリアル化と逆シリアル化のオプションを構成することはできます。
JSON 逆シリアル化オプションをグローバルに構成する
アプリにグローバルに適用されるオプションは、ConfigureHttpJsonOptions を呼び出すことによって構成できます。 次の例には、パブリック フィールドと JSON 出力形式が含まれています。
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
サンプル コードではシリアル化と逆シリアル化の両方を構成するため、出力 JSON に NameField
の読み取りと NameField
のインクルードを行うことができます。
エンドポイントの JSON 逆シリアル化オプションを構成する
ReadFromJsonAsync には、JsonSerializerOptions オブジェクトを受け入れるオーバーロードが用意されています。 次の例には、パブリック フィールドと JSON 出力形式が含まれています。
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});
app.Run();
class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
上記のコードでは、カスタマイズされたオプションが逆シリアル化にのみ適用されるため、出力 JSON では NameField
が除外されます。
要求本文を読み取る
要求本文を直接読み取るには、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();
上記のコードでは次の操作が行われます。
- HttpRequest.BodyReader を使用して要求本文にアクセスします。
- 要求本文をローカル ファイルにコピーします。
ASP.NET Core