如何在基本 API 應用程式中建立回應

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

基本端點支援下列型別的傳回值:

  1. string - 這包括 Task<string>ValueTask<string>
  2. T (任何其他型別) - 這包括 Task<T>ValueTask<T>
  3. IResult 為基礎 - 這包括 Task<IResult>ValueTask<IResult>

string 傳回值

行為 內容-類型
架構會將字串直接寫入回應。 text/plain

請考慮下列會傳回 Hello world 文字的路由處理常式。

app.MapGet("/hello", () => "Hello World");

200 狀態碼會以 text/plain Content-Type 標頭和下列內容傳回。

Hello World

T (任何其他型別) 傳回值

行為 內容-類型
架構 JSON 序列化回應。 application/json

請考慮下列會傳回包含 Message 字串屬性之匿名型別的路由處理常式。

app.MapGet("/hello", () => new { Message = "Hello World" });

200 狀態碼會以 application/json Content-Type 標頭和下列內容傳回。

{"message":"Hello World"}

IResult 傳回值

行為 內容-類型
架構會呼叫 IResult.ExecuteAsync IResult 實作決定。

IResult 介面會定義代表 HTTP 端點結果的合約。 靜態 Results 類別和靜態 TypedResults 可用來建立代表不同回應型別的各種 IResult 物件。

TypedResults 與 Results 的比較

ResultsTypedResults 靜態類別提供類似的結果協助程式集合。 TypedResults 類別是 Results 類別的同等型別。 不過,Results 協助程式的傳回型別是 IResult,而每個 TypedResults 協助程式的傳回型別則是其中一種 IResult 實作型別。 差異意味著對於 Results 協助程式而言,當需要實體型別時則需要轉換,例如適用於單元測試。 實作型別會在 Microsoft.AspNetCore.Http.HttpResults 命名空間中定義。

傳回 TypedResults 而非 Results 具有下列優勢:

請考慮下列端點,其中會產生具有預期 JSON 回應的 200 OK 狀態碼。

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

為了正確記錄此端點,會呼叫擴充方法 Produces。 不過,如果是使用 TypedResults 而非 Results,則不一定需要呼叫 Produces (如下程式碼所示)。 TypedResults 會自動提供此端點的中繼資料。

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

如需描述回應型別的詳細資訊,請參閱基本 API 中的 OpenAPI 支援

如先前所述,使用 TypedResults 時則不需要轉換。 請考慮下列會傳回 TypedResults 類別的基本 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,編譯器會在從單一端點傳回不同的結果時,自動將其推斷為要求委派傳回型別。 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>

下列情況時,請使用 Results<TResult1, TResultN> 作為端點處理常式傳回型別,而非 IResult

  • 從端點處理常式傳回多個 IResult 實作型別。
  • 使用靜態 TypedResult 類別來建立 IResult 物件。

這個替代方法比傳回 IResult 還好,因為泛型等位型別會自動保留端點中繼資料。 而且由於 Results<TResult1, TResultN> 等位型別會實作隱含轉換運算子,編譯器可以自動將泛型引數中指定的型別轉換成等位型別的執行個體。

這會有的附加優點為提供編譯時間檢查,確定路由處理常式實際上只會傳回其宣告會執行的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 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)));

內建結果

常見的結果協助程式存在於 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 儲存體串流映像:

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

如需詳細資訊,請參閱最小 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 };
}

替代方法是使用 Microsoft.AspNetCore.Mvc.ProducesAttribute 來描述產生的回應。 下列程式碼會將 PopulateMetadata 方法變更為使用 ProducesAttribute

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

設定 JSON 序列化選項

根據預設,基本 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
// }

由於有包含欄位,上述程式碼會讀取 NameField 並將其包含在輸出 JSON 中。

設定端點的 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 傳回值

行為 內容-類型
架構會將字串直接寫入回應。 text/plain

請考慮下列會傳回 Hello world 文字的路由處理常式。

app.MapGet("/hello", () => "Hello World");

200 狀態碼會以 text/plain Content-Type 標頭和下列內容傳回。

Hello World

T (任何其他型別) 傳回值

行為 內容-類型
架構 JSON 序列化回應。 application/json

請考慮下列會傳回包含 Message 字串屬性之匿名型別的路由處理常式。

app.MapGet("/hello", () => new { Message = "Hello World" });

200 狀態碼會以 application/json Content-Type 標頭和下列內容傳回。

{"message":"Hello World"}

IResult 傳回值

行為 內容-類型
架構會呼叫 IResult.ExecuteAsync IResult 實作決定。

IResult 介面會定義代表 HTTP 端點結果的合約。 靜態 Results 類別和靜態 TypedResults 可用來建立代表不同回應型別的各種 IResult 物件。

TypedResults 與 Results 的比較

ResultsTypedResults 靜態類別提供類似的結果協助程式集合。 TypedResults 類別是 Results 類別的同等型別。 不過,Results 協助程式的傳回型別是 IResult,而每個 TypedResults 協助程式的傳回型別則是其中一種 IResult 實作型別。 差異意味著對於 Results 協助程式而言,當需要實體型別時則需要轉換,例如適用於單元測試。 實作型別會在 Microsoft.AspNetCore.Http.HttpResults 命名空間中定義。

傳回 TypedResults 而非 Results 具有下列優勢:

請考慮下列端點,其中會產生具有預期 JSON 回應的 200 OK 狀態碼。

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

為了正確記錄此端點,會呼叫擴充方法 Produces。 不過,如果是使用 TypedResults 而非 Results,則不一定需要呼叫 Produces (如下程式碼所示)。 TypedResults 會自動提供此端點的中繼資料。

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

如需描述回應型別的詳細資訊,請參閱基本 API 中的 OpenAPI 支援

如先前所述,使用 TypedResults 時則不需要轉換。 請考慮下列會傳回 TypedResults 類別的基本 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,編譯器會在從單一端點傳回不同的結果時,自動將其推斷為要求委派傳回型別。 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>

下列情況時,請使用 Results<TResult1, TResultN> 作為端點處理常式傳回型別,而非 IResult

  • 從端點處理常式傳回多個 IResult 實作型別。
  • 使用靜態 TypedResult 類別來建立 IResult 物件。

這個替代方法比傳回 IResult 還好,因為泛型等位型別會自動保留端點中繼資料。 而且由於 Results<TResult1, TResultN> 等位型別會實作隱含轉換運算子,編譯器可以自動將泛型引數中指定的型別轉換成等位型別的執行個體。

這會有的附加優點為提供編譯時間檢查,確定路由處理常式實際上只會傳回其宣告會執行的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 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)));

內建結果

常見的結果協助程式存在於 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 儲存體串流映像:

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

如需詳細資訊,請參閱最小 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 };
}

替代方法是使用 Microsoft.AspNetCore.Mvc.ProducesAttribute 來描述產生的回應。 下列程式碼會將 PopulateMetadata 方法變更為使用 ProducesAttribute

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

設定 JSON 序列化選項

根據預設,基本 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
// }

由於有包含欄位,上述程式碼會讀取 NameField 並將其包含在輸出 JSON 中。

設定端點的 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
// }

其他資源