Minimal API アプリで応答を作成する方法
Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
最小限のエンドポイントは、次の型の戻り値をサポートしています。
string
- これには、Task<string>
とValueTask<string>
が含まれます。T
(その他の型) - これにはTask<T>
とValueTask<T>
が含まれます。IResult
ベース - これにはTask<IResult>
とValueTask<IResult>
が含まれます。
string
の戻り値
動作 | Content-Type |
---|---|
フレームワークは、文字列を直接応答に書き込みます。 | text/plain |
Hello world
というテキストを返す次のようなルート ハンドラーを考えてみてください。
app.MapGet("/hello", () => "Hello World");
text/plain
Content-Type ヘッダーおよび次の内容と共に、200
状態コードが返されます。
Hello World
T
(その他の型) の戻り値
動作 | コンテンツタイプ |
---|---|
フレームワークは応答を JSON シリアル化します。 | application/json |
Message
文字列プロパティを含む匿名型を返す次のようなルート ハンドラーを考えてみてください。
app.MapGet("/hello", () => new { Message = "Hello World" });
application/json
Content-Type ヘッダーおよび次の内容と共に、200
状態コードが返されます。
{"message":"Hello World"}
IResult
の戻り値
動作 | Content-Type |
---|---|
フレームワークは IResult.ExecuteAsync を呼び出します。 | IResult の実装によって決まります。 |
IResult
インターフェイスでは、HTTP エンドポイントの結果を表すコントラクトが定義されています。 静的な Results クラスと静的な TypedResults は、さまざまな型の応答を表すさまざまな IResult
オブジェクトを作成するために使われます。
TypedResults と Results
静的クラス Results と TypedResults は、同様の結果ヘルパーのセットを提供します。 TypedResults
クラスは Results
クラスと同じものですが、"型指定" されています。 ただし、Results
ヘルパーの戻り値の型が IResult であるのに対し、各 TypedResults
ヘルパーの戻り値の型は IResult
実装型の 1 つです。 この違いは、Results
ヘルパーの場合、単体テストなどで具象型が必要な場合は、変換が必要であることを意味します。 実装型は、Microsoft.AspNetCore.Http.HttpResults 名前空間で定義されています。
Results
ではなく TypedResults
を返すと、次のような利点があります。
TypedResults
ヘルパーは厳密に型指定されたオブジェクトを返すので、コードの読みやすさと単体テストが向上し、実行時エラーの可能性が少なくなります。- 実装型で、エンドポイントを記述するための OpenAPI の応答型メタデータが自動的に提供されます。
次のようなエンドポイントを考えてみてください。この場合、200 OK
状態コードと想定される JSON 応答が生成されます。
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
このエンドポイントを正しく文書化するため、拡張メソッド Produces
が呼び出されます。 ただし、次のコードで示すように、Results
の代わりに TypedResults
が使われている場合は、Produces
を呼び出す必要はありません。 TypedResults
は、エンドポイントのメタデータを自動的に提供します。
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
応答の型の記述について詳しくは、「Minimal API での OpenAPI のサポート」をご覧ください。
前に説明したように、TypedResults
を使うときは、変換は必要ありません。 TypedResults
クラスを返す次のような Minimal API について考えます
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
次のテストでは、完全な具象型がチェックされます。
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}
Results
のすべてのメソッドはシグネチャで IResult
を返すため、1 つのエンドポイントから異なる複数の結果を返すとき、コンパイラはそれを要求デリゲートの戻り値の型として自動的に推論します。 TypedResults
では、そのようなデリゲートから Results<T1, TN>
を使う必要があります。
返されるオブジェクトの実際の具象型は異なりますが、Results.Ok
と Results.NotFound
の両方とも IResult
を返すように宣言されているため、次のメソッドはコンパイルされます。
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
TypedResults.Ok
と TypedResults.NotFound
は異なる型を返すように宣言されており、コンパイラは最もよく一致する型の推論を試みないため、次のメソッドはコンパイルされません。
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
TypedResults
を使うには、戻り値の型を完全に宣言する必要があり、非同期のときは Task<>
ラッパーが必要です。 TypedResults
を使うと冗長さが増しますが、これは型情報を静的に使用できるようにし、OpenAPI に自己記述できるようにすることとのトレードオフです。
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Results<TResult1, TResultN>
次の場合、エンドポイント ハンドラーの戻り値の型として、IResult
ではなく Results<TResult1, TResultN>
を使います。
- 複数の
IResult
実装型がエンドポイント ハンドラーから返されます。 IResult
オブジェクトを作成するために、静的TypedResult
クラスが使用われています。
ジェネリック共用体型はエンドポイント メタデータを自動的に保持するため、IResult
を返すより、この代替方法の方が優れています。 また、Results<TResult1, TResultN>
共用体型では暗黙的なキャスト演算子が実装されているため、コンパイラはジェネリック引数で指定されている型を共用体型のインスタンスに自動的に変換できます。
これにはさらに、ルート ハンドラーから宣言されている結果のみが実際に返されることがコンパイル時にチェックされるという利点があります。 ジェネリック引数の 1 つとして宣言されていない型を Results<>
に返そうとすると、コンパイル エラーが発生します。
次のようなエンドポイントを考えてみてください。この場合、orderId
が 999
より大きいと、400 BadRequest
状態コードが返されます。 それ以外の場合は、200 OK
と期待される内容が生成されます。
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
このエンドポイントを正しく文書化するため、拡張メソッド Produces
が呼び出されます。 ただし、TypedResults
ヘルパーにはエンドポイントのメタデータが自動的に含まれるため、次のコードで示すように、代わりに Results<T1, Tn>
共用体型を返すことができます。
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
組み込みの結果
共通の結果ヘルパーは、Results と TypedResults 静的クラスに含まれます。 Results
を返すより、TypedResults
を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。
次のセクションでは、一般的な結果ヘルパーの使用方法を示します。
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync は、JSON を返す別の方法です。
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
カスタムの状態コード
app.MapGet("/405", () => Results.StatusCode(405));
サーバー内部エラー
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
上記の例では、状態コード 500 が返されます。
Text
app.MapGet("/text", () => Results.Text("This is some text"));
ストリーム
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream
オーバーロードを使用すると、バッファーリングなしで基になる HTTP 応答ストリームにアクセスできます。 次の例では、ImageSharp を使って、指定した画像の縮小サイズを返します。
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
次の例では、Azure Blob Storage から画像をストリーミングします。
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
次の例では、Azure Blob からビデオをストリーミングします。
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
リダイレクト
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
ファイル
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult インターフェイス
Microsoft.AspNetCore.Http 名前空間の次のインターフェイスは、実行時に IResult
型を検出する方法を提供します。これは、フィルター実装の一般的なパターンです。
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
これらのインターフェイスのいずれかを使用するフィルターの例を次に示します。
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
詳細については、「Minimal API アプリのフィルター」および「IResult 実装の種類」を参照してください。
応答のカスタマイズ
アプリケーションは、カスタムの IResult 型を実装することにより、応答を制御します。 次のコードは、HTML 結果型の例です。
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
拡張メソッドを Microsoft.AspNetCore.Http.IResultExtensions に追加して、これらのカスタムの結果をより見つけやすくすることを推奨します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
また、カスタム IResult
型は、IEndpointMetadataProvider インターフェイスを実装することで、独自の注釈を提供できます。 たとえば、次のコードは、エンドポイントによって生成される応答を説明する注釈を、前述の HtmlResult
型に追加します。
class HtmlResult : IResult, IEndpointMetadataProvider
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}
ProducesHtmlMetadata
は IProducesResponseTypeMetadata の実装であり、生成される応答コンテンツ タイプ text/html
と状態コード 200 OK
を定義しています。
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
もう 1 つの方法として、Microsoft.AspNetCore.Mvc.ProducesAttribute を使って生成される応答を説明します。 次のコードでは、ProducesAttribute
を使用するように PopulateMetadata
メソッドを変更しています。
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
JSON シリアル化オプションを構成する
既定では、Minimal API アプリによって、JSON のシリアル化と逆シリアル化の間に Web defaults
オプションが使用されます。
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
とインクルードが行われます。
エンドポイントの JSON シリアル化オプションを構成する
エンドポイントのシリアル化オプションを構成するには、次の例に示すように、Results.Json を呼び出して JsonSerializerOptions オブジェクトに渡します。
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
別の方法としては、JsonSerializerOptions オブジェクトを受け入れる WriteAsJsonAsync のオーバーロードを使用します。 次の例では、このオーバーロードを使用して出力 JSON の書式を設定しています。
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
その他のリソース
最小限のエンドポイントは、次の型の戻り値をサポートしています。
string
- これには、Task<string>
とValueTask<string>
が含まれます。T
(その他の型) - これにはTask<T>
とValueTask<T>
が含まれます。IResult
ベース - これにはTask<IResult>
とValueTask<IResult>
が含まれます。
string
の戻り値
動作 | Content-Type |
---|---|
フレームワークは、文字列を直接応答に書き込みます。 | text/plain |
Hello world
というテキストを返す次のようなルート ハンドラーを考えてみてください。
app.MapGet("/hello", () => "Hello World");
text/plain
Content-Type ヘッダーおよび次の内容と共に、200
状態コードが返されます。
Hello World
T
(その他の型) の戻り値
動作 | コンテンツタイプ |
---|---|
フレームワークは応答を JSON シリアル化します。 | application/json |
Message
文字列プロパティを含む匿名型を返す次のようなルート ハンドラーを考えてみてください。
app.MapGet("/hello", () => new { Message = "Hello World" });
application/json
Content-Type ヘッダーおよび次の内容と共に、200
状態コードが返されます。
{"message":"Hello World"}
IResult
の戻り値
動作 | Content-Type |
---|---|
フレームワークは IResult.ExecuteAsync を呼び出します。 | IResult の実装によって決まります。 |
IResult
インターフェイスでは、HTTP エンドポイントの結果を表すコントラクトが定義されています。 静的な Results クラスと静的な TypedResults は、さまざまな型の応答を表すさまざまな IResult
オブジェクトを作成するために使われます。
TypedResults と Results
静的クラス Results と TypedResults は、同様の結果ヘルパーのセットを提供します。 TypedResults
クラスは Results
クラスと同じものですが、"型指定" されています。 ただし、Results
ヘルパーの戻り値の型が IResult であるのに対し、各 TypedResults
ヘルパーの戻り値の型は IResult
実装型の 1 つです。 この違いは、Results
ヘルパーの場合、単体テストなどで具象型が必要な場合は、変換が必要であることを意味します。 実装型は、Microsoft.AspNetCore.Http.HttpResults 名前空間で定義されています。
Results
ではなく TypedResults
を返すと、次のような利点があります。
TypedResults
ヘルパーは厳密に型指定されたオブジェクトを返すので、コードの読みやすさと単体テストが向上し、実行時エラーの可能性が少なくなります。- 実装型で、エンドポイントを記述するための OpenAPI の応答型メタデータが自動的に提供されます。
次のようなエンドポイントを考えてみてください。この場合、200 OK
状態コードと想定される JSON 応答が生成されます。
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
このエンドポイントを正しく文書化するため、拡張メソッド Produces
が呼び出されます。 ただし、次のコードで示すように、Results
の代わりに TypedResults
が使われている場合は、Produces
を呼び出す必要はありません。 TypedResults
は、エンドポイントのメタデータを自動的に提供します。
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
応答の型の記述について詳しくは、「Minimal API での OpenAPI のサポート」をご覧ください。
前に説明したように、TypedResults
を使うときは、変換は必要ありません。 TypedResults
クラスを返す次のような Minimal API について考えます
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
次のテストでは、完全な具象型がチェックされます。
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}
Results
のすべてのメソッドはシグネチャで IResult
を返すため、1 つのエンドポイントから異なる複数の結果を返すとき、コンパイラはそれを要求デリゲートの戻り値の型として自動的に推論します。 TypedResults
では、そのようなデリゲートから Results<T1, TN>
を使う必要があります。
返されるオブジェクトの実際の具象型は異なりますが、Results.Ok
と Results.NotFound
の両方とも IResult
を返すように宣言されているため、次のメソッドはコンパイルされます。
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
TypedResults.Ok
と TypedResults.NotFound
は異なる型を返すように宣言されており、コンパイラは最もよく一致する型の推論を試みないため、次のメソッドはコンパイルされません。
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
TypedResults
を使うには、戻り値の型を完全に宣言する必要があり、非同期のときは Task<>
ラッパーが必要です。 TypedResults
を使うと冗長さが増しますが、これは型情報を静的に使用できるようにし、OpenAPI に自己記述できるようにすることとのトレードオフです。
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Results<TResult1, TResultN>
次の場合、エンドポイント ハンドラーの戻り値の型として、IResult
ではなく Results<TResult1, TResultN>
を使います。
- 複数の
IResult
実装型がエンドポイント ハンドラーから返されます。 IResult
オブジェクトを作成するために、静的TypedResult
クラスが使用われています。
ジェネリック共用体型はエンドポイント メタデータを自動的に保持するため、IResult
を返すより、この代替方法の方が優れています。 また、Results<TResult1, TResultN>
共用体型では暗黙的なキャスト演算子が実装されているため、コンパイラはジェネリック引数で指定されている型を共用体型のインスタンスに自動的に変換できます。
これにはさらに、ルート ハンドラーから宣言されている結果のみが実際に返されることがコンパイル時にチェックされるという利点があります。 ジェネリック引数の 1 つとして宣言されていない型を Results<>
に返そうとすると、コンパイル エラーが発生します。
次のようなエンドポイントを考えてみてください。この場合、orderId
が 999
より大きいと、400 BadRequest
状態コードが返されます。 それ以外の場合は、200 OK
と期待される内容が生成されます。
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
このエンドポイントを正しく文書化するため、拡張メソッド Produces
が呼び出されます。 ただし、TypedResults
ヘルパーにはエンドポイントのメタデータが自動的に含まれるため、次のコードで示すように、代わりに Results<T1, Tn>
共用体型を返すことができます。
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
組み込みの結果
共通の結果ヘルパーは、Results と TypedResults 静的クラスに含まれます。 Results
を返すより、TypedResults
を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。
次のセクションでは、一般的な結果ヘルパーの使用方法を示します。
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync は、JSON を返す別の方法です。
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
カスタムの状態コード
app.MapGet("/405", () => Results.StatusCode(405));
Text
app.MapGet("/text", () => Results.Text("This is some text"));
ストリーム
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream
オーバーロードを使用すると、バッファーリングなしで基になる HTTP 応答ストリームにアクセスできます。 次の例では、ImageSharp を使って、指定した画像の縮小サイズを返します。
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
次の例では、Azure Blob Storage から画像をストリーミングします。
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
次の例では、Azure Blob からビデオをストリーミングします。
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
リダイレクト
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
ファイル
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult インターフェイス
Microsoft.AspNetCore.Http 名前空間の次のインターフェイスは、実行時に IResult
型を検出する方法を提供します。これは、フィルター実装の一般的なパターンです。
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
これらのインターフェイスのいずれかを使用するフィルターの例を次に示します。
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
詳細については、「Minimal API アプリのフィルター」および「IResult 実装の種類」を参照してください。
応答のカスタマイズ
アプリケーションは、カスタムの IResult 型を実装することにより、応答を制御します。 次のコードは、HTML 結果型の例です。
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
拡張メソッドを Microsoft.AspNetCore.Http.IResultExtensions に追加して、これらのカスタムの結果をより見つけやすくすることを推奨します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
また、カスタム IResult
型は、IEndpointMetadataProvider インターフェイスを実装することで、独自の注釈を提供できます。 たとえば、次のコードは、エンドポイントによって生成される応答を説明する注釈を、前述の HtmlResult
型に追加します。
class HtmlResult : IResult, IEndpointMetadataProvider
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}
ProducesHtmlMetadata
は IProducesResponseTypeMetadata の実装であり、生成される応答コンテンツ タイプ text/html
と状態コード 200 OK
を定義しています。
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
もう 1 つの方法として、Microsoft.AspNetCore.Mvc.ProducesAttribute を使って生成される応答を説明します。 次のコードでは、ProducesAttribute
を使用するように PopulateMetadata
メソッドを変更しています。
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
JSON シリアル化オプションを構成する
既定では、Minimal API アプリによって、JSON のシリアル化と逆シリアル化の間に Web defaults
オプションが使用されます。
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
とインクルードが行われます。
エンドポイントの JSON シリアル化オプションを構成する
エンドポイントのシリアル化オプションを構成するには、次の例に示すように、Results.Json を呼び出して JsonSerializerOptions オブジェクトに渡します。
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
別の方法としては、JsonSerializerOptions オブジェクトを受け入れる WriteAsJsonAsync のオーバーロードを使用します。 次の例では、このオーバーロードを使用して出力 JSON の書式を設定しています。
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
その他のリソース
ASP.NET Core