Minimal API アプリで応答を作成する方法

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

最小限のエンドポイントは、次の型の戻り値をサポートしています。

  1. string - これには、Task<string>ValueTask<string> が含まれます。
  2. T (その他の型) - これには Task<T>ValueTask<T> が含まれます。
  3. 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 (その他の型) の戻り値

動作 Content-Type
フレームワークは応答を 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

静的クラス ResultsTypedResults は、同様の結果ヘルパーのセットを提供します。 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.OkResults.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.OkTypedResults.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<> に返そうとすると、コンパイル エラーが発生します。

次のようなエンドポイントを考えてみてください。この場合、orderId999 より大きいと、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)));

組み込みの結果

共通の結果ヘルパーは、ResultsTypedResults 静的クラスに含まれます。 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 型を検出する方法を提供します。これは、フィルター実装の一般的なパターンです。

これらのインターフェイスのいずれかを使用するフィルターの例を次に示します。

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());
    }
}

ProducesHtmlMetadataIProducesResponseTypeMetadata の実装であり、生成される応答コンテンツ タイプ 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
// }

その他のリソース

最小限のエンドポイントは、次の型の戻り値をサポートしています。

  1. string - これには、Task<string>ValueTask<string> が含まれます。
  2. T (その他の型) - これには Task<T>ValueTask<T> が含まれます。
  3. 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 (その他の型) の戻り値

動作 Content-Type
フレームワークは応答を 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

静的クラス ResultsTypedResults は、同様の結果ヘルパーのセットを提供します。 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.OkResults.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.OkTypedResults.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<> に返そうとすると、コンパイル エラーが発生します。

次のようなエンドポイントを考えてみてください。この場合、orderId999 より大きいと、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)));

組み込みの結果

共通の結果ヘルパーは、ResultsTypedResults 静的クラスに含まれます。 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 型を検出する方法を提供します。これは、フィルター実装の一般的なパターンです。

これらのインターフェイスのいずれかを使用するフィルターの例を次に示します。

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());
    }
}

ProducesHtmlMetadataIProducesResponseTypeMetadata の実装であり、生成される応答コンテンツ タイプ 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
// }

その他のリソース