共用方式為


產生 OpenAPI 文件

Microsoft.AspNetCore.OpenApi 套件提供在 ASP.NET Core 中產生 OpenAPI 文件的內建支援。 PPL 提供下列功能:

  • 支援在執行階段產生 OpenAPI 文件,並透過應用程式上的端點加以存取。
  • 支援允許修改所產生文件的「轉換器」API。
  • 支援從單一應用程式產生多個 OpenAPI 檔。
  • 利用 System.Text.Json 所提供的 JSON 結構描述支援。
  • 與原生 AoT 相容。

套件安裝

安裝 Microsoft.AspNetCore.OpenApi 套件:

封裝管理員主控台執行下列命令:

Install-Package Microsoft.AspNetCore.OpenApi -IncludePrerelease

若要新增在組建階段產生 OpenAPI 文件的支援,請安裝 Microsoft.Extensions.ApiDescription.Server 封裝:

封裝管理員主控台執行下列命令:

Install-Package Microsoft.Extensions.ApiDescription.Server -IncludePrerelease

設定 OpenAPI 文件產生

下列程式碼範例:

  • 新增 OpenAPI 服務。
  • 啟用端點以檢視 JSON 格式的 OpenAPI 文件。
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

啟動應用程式並瀏覽至 https://localhost:<port>/openapi/v1.json,以檢視產生的 OpenAPI 文件。

在 ASP.NET Web 應用程式中包含 OpenAPI 中繼資料

包含端點的 OpenAPI 中繼資料

ASP.NET 會從 Web 應用程式的端點收集中繼資料,並用它來產生 OpenAPI 文件。 在控制器型應用程式中,會從 [EndpointDescription][HttpPost][Produces] 等屬性收集中繼資料。 在基本 API 中,您可以從屬性收集中繼資料,但也可以使用擴充方法和其他策略來設定中繼資料,例如從路由處理程式傳回 TypedResults。 下表提供所收集中繼資料的概觀,以及設定中繼資料的策略。

中繼資料 屬性 擴充方法 其他策略
摘要 [EndpointSummary] WithSummary
description [EndpointDescription] WithDescription
標記 [Tags] WithTags
operationId [EndpointName] WithName
parameters [FromQuery][FromRoute][FromHeader][FromForm]
參數描述 [Description]
requestBody [FromBody] Accepts
回覆 [Produces][ProducesProblem] ProducesProducesProblem TypedResults
排除端點 [ExcludeFromDescription] ExcludeFromDescription

ASP.NET Core 不會從 XML 文件批註收集中繼資料。

下列各節示範如何在應用程式中包含中繼資料,以自訂產生的 OpenAPI 文件。

摘要和描述

您可以使用 [EndpointSummary][EndpointDescription] 屬性,或在基本 API 中使用 WithSummaryWithDescription 擴充方法,設定端點摘要與描述。

下列範例示範設定摘要和描述的不同策略。

請注意,屬性置放在委派方法上,而不是放在 app.MapGet 方法上。

app.MapGet("/extension-methods", () => "Hello world!")
  .WithSummary("This is a summary.")
  .WithDescription("This is a description.");

app.MapGet("/attributes",
  [EndpointSummary("This is a summary.")]
  [EndpointDescription("This is a description.")]
  () => "Hello world!");

標記

OpenAPI 支援將每個端點上的標籤指定為分類形式。 在控制器型應用程式中,控制器名稱會自動新增為其每個端點上的標記,但可以使用 [Tags] 屬性來覆寫此名稱。 在基本 API 中,可以使用 [Tags] 屬性或 WithTags 擴充方法來設定標記。

下列範例示範設定標記的不同策略。

app.MapGet("/extension-methods", () => "Hello world!")
  .WithTags("todos", "projects");

app.MapGet("/attributes",
  [Tags("todos", "projects")]
  () => "Hello world!");

operationId

OpenAPI 支援每個端點上的 operationId 作為作業的唯一識別碼或名稱。 在控制器型應用程式中,可以使用 [EndpointName] 屬性來設定 operationId。 在基本 API 中,可以使用 [EndpointName] 屬性或 WithName 擴充方法來設定 operationId。

下列範例示範設定 operationId 的不同策略。

app.MapGet("/extension-methods", () => "Hello world!")
  .WithName("FromExtensionMethods");

app.MapGet("/attributes",
  [EndpointName("FromAttributes")]
  () => "Hello world!");

parameters

OpenAPI 支援 API 所使用的標註路徑、查詢字串、標頭和 cookie 參數。

架構會根據路由處理常式的簽章,自動推斷要求參數的型別。

[Description] 屬性可用來提供參數的描述。

下列範例示範如何設定參數的描述。

app.MapGet("/attributes",
  ([Description("This is a description.")] string name) => "Hello world!");

requestBody

若要定義傳送為要求本文的輸入型別,請使用 Accepts 擴充方法來定義要求處理常式所預期的物件型別和內容型別,以設定屬性。 在下列範例中,端點會接受要求本文中具有預期 application/xml 內容型別的 Todo 物件。

app.MapPost("/todos/{id}", (int id, Todo todo) => ...)
  .Accepts<Todo>("application/xml");

除了 Accepts 擴充方法之外,參數型別也可以藉由實作 IEndpointParameterMetadataProvider 介面來描述自己的註釋。 例如,下列 Todo 型別會新增需要具有 application/xml 內容型別要求本文的註釋。

public class Todo : IEndpointParameterMetadataProvider
{
    public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
    {
        builder.Metadata.Add(new AcceptsMetadata(["application/xml", "text/xml"], typeof(XmlBody)));
    }
}

如果未提供明確的註釋,如果端點處理常式中有要求主體參數,架構會嘗試判斷預設要求型別。 推斷會使用下列啟發學習法來產生註釋:

  • 透過 [FromForm] 屬性從表單讀取的要求本文參數會使用 multipart/form-data 內容型別描述。
  • 所有其他要求主體參數都會使用 application/json 內容型別來描述。
  • 如果要求主體可為 Null,或 FromBody 屬性上設定 AllowEmpty 屬性,則會將其視為選用。

描述回應類型

OpenAPI 支援提供從 API 傳回的回應描述。 基本 API 針對設定端點的回應型別支援三個策略:

Produces 擴充方法可用來將 Produces 中繼資料新增至端點。 如果未提供任何參數,擴充方法會在 200 狀態碼和 application/json 內容型別下填入目標型別的中繼資料。

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
  .Produces<IList<Todo>>();

在端點路由處理常式的實作中使用 TypedResults,會自動包含端點的回應型別中繼資料。 例如,下列程式碼會自動以具有 application/json 內容型別的 200 狀態碼的回應來註釋端點。

app.MapGet("/todos", async (TodoDb db) =>
{
    var todos = await db.Todos.ToListAsync();
    return TypedResults.Ok(todos);
});
設定 ProblemDetails 的回應

設定可能傳回 ProblemDetails 回應的端點的回應型別時, ProducesProblemProducesValidationProblem 擴充方法或 TypedResults.Problem 可用來將適當的註釋新增至端點的中繼資料。

當其中一個策略未提供明確註釋時,架構會藉由檢查回應的簽章來嘗試判斷預設回應型別。 此預設回應會填入在 OpenAPI 定義中的 200 狀態碼底下。

多個回應型別

如果端點可以在不同案例中傳回不同的回應型別,您可以透過下列方式提供中繼資料:

  • 呼叫 Produces 擴充方法多次,如下列範例所示:

    app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
             await db.Todos.FindAsync(id) 
             is Todo todo
             ? Results.Ok(todo) 
             : Results.NotFound())
       .Produces<Todo>(StatusCodes.Status200OK)
       .Produces(StatusCodes.Status404NotFound);
    
  • 在簽章和 TypedResults 處理常式主體中使用 Results<TResult1,TResult2,TResultN>,如下列範例所示:

    app.MapGet("/book{id}", Results<Ok<Book>, NotFound> (int id, List<Book> bookList) =>
    {
        return bookList.FirstOrDefault((i) => i.Id == id) is Book book
         ? TypedResults.Ok(book)
         : TypedResults.NotFound();
    });
    

    Results<TResult1,TResult2,TResultN>等位類型會宣告路由處理常式傳回多個 IResult 實作的具體類型,而實作 IEndpointMetadataProvider 的任何類型都會對端點的中繼資料造成貢獻。

    等位型別會實作隱含轉換運算子。 這些運算子可讓編譯器自動將泛型引數中指定的型別轉換成等位型別的執行個體。 此功能會有的附加優點為提供編譯時間檢查,以確定路由處理常式只會傳回其宣告會執行的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 Results<TResult1,TResult2,TResultN> 會產生編譯錯誤。

從產生的文件排除端點

根據預設,應用程式中定義的所有端點都會記錄在產生的 OpenAPI 檔案中。 最小 API 使用下列方式,支援兩種策略從 OpenAPI 文件中排除指定的端點:

下列範例示範從產生的 OpenAPI 文件中排除指定端點的不同策略。

app.MapGet("/extension-method", () => "Hello world!")
  .ExcludeFromDescription();

app.MapGet("/attributes",
  [ExcludeFromDescription]
  () => "Hello world!");

包含資料類型的 OpenAPI 中繼資料

要求或回應主體中使用的 C# 類別或記錄會以所產生 OpenAPI 文件的結構描表示。 根據預設,只有公用屬性才會在結構描述中表示,但 JsonSerializerOptions 也會建立欄位的結構描述屬性。

PropertyNamingPolicy 設定為 camel-case 時 (這是 ASP.NET Web 應用程式中的預設值),結構描述中的屬性名稱是類別或記錄屬性名稱的 camel-case 形式。 JsonPropertyNameAttribute 可用於個別屬性,以指定結構描述中的屬性名稱。

類型和格式

JSON 結構描述程式庫會將標準 C# 類型對應至 OpenAPI typeformat,如下所示:

C# 類型 OpenAPI type OpenAPI format
int 整數 int32
long 整數 int64
short 整數 int16
byte 整數 uint8
float 數值 FLOAT
double 數值 double
decimal 數值 double
bool boolean
字串 字串
char 字串 char
byte[] 字串 byte
DateTimeOffset 字串 date-time
DateOnly 字串 date
TimeOnly 字串 time
URI 字串 uri
Guid 字串 uuid
object 已省略
dynamic 已省略

請注意,物件和動態類型在 OpenAPI 中沒有定義類型,因為它們可以包含任何類型的資料,包括 int 或字串等基本類型。

typeformat 也可以使用 結構描述轉換器來設定。 例如,您可能想要十進位類型的 formatdecimal,而不是 double

使用屬性新增中繼資料

ASP.NET 使用類別或記錄屬性上屬性的中繼資料,在產生結構描述的對應屬性上設定中繼資料。

下表摘要說明 System.ComponentModel 命名空間中提供所產生結構描述中繼資料的屬性:

屬性 描述
DescriptionAttribute 設定結構描述中屬性的 description
RequiredAttribute 在結構描述中將屬性標示為 required
DefaultValueAttribute 設定結構描述中屬性的 default 值。
RangeAttribute 設定整數或數字的 minimummaximum 值。
MinLengthAttribute 設定字串的 minLength
MaxLengthAttribute 設定字串的 maxLength
RegularExpressionAttribute 設定字串的 pattern

請注意,在控制器型應用程式中,這些屬性會將篩選新增至作業,以驗證任何傳入的資料是否符合條件約束。 在基本 API 中,這些屬性會在產生的結構描述中設定中繼資料,但必須透過端點篩選、路由處理常式邏輯或透過第三方套件明確執行驗證。

所產生結構描述的其他中繼資料來源

必要

屬性也可以使用必要的修飾元標示為 required

enum

C# 中的列舉類型是以整數為基礎,但可以使用 JSON 中的 JsonConverterAttributeJsonStringEnumConverter 表示為字串。 當列舉類型在 JSON 中以字串表示時,產生的結構描述將具有列舉字串值的 enum 屬性。 不含 JsonConverterAttribute 的列舉類型會在產生的結構描述中定義為 type: integer

注意:AllowedValuesAttribute 不會設定屬性的 enum 值。

nullable

在產生的結構描述中,定義為可為 Null 的值或參考型別的屬性具有 nullable: true。 這與 System.Text.Json 還原序列化程式的預設行為一致,它接受 null 作為允許為 NULL 屬性的有效值。

additionalProperties

結構描述預設不會產生 additionalProperties 判斷提示,這表示 true 的預設值。 這與 System.Text.Json 還原序列化程式的預設行為一致,它會默默忽略 JSON 物件中的其他屬性。

如果結構描述的其他屬性應該只有特定類型的值,請將屬性或類別定義為 Dictionary<string, type>。 字典的索引鍵類型必須是 string。 這會產生結構描述,其中 additionalProperties 指定 "type" 的結構描述為必要的實值類型。

多態性類型的中繼資料

使用上層類別的 JsonPolymorphicAttributeJsonDerivedTypeAttribute 屬性來指定多型類型的鑑別子欄位與子類型。

JsonDerivedTypeAttribute 鑑別子欄位新增至每個子類別的結構描述,並使用列舉指定子類別的特定鑑別子值。 這個屬性也會修改每個衍生類別的建構函式,以設定鑑別子值。

具有 JsonPolymorphicAttribute 屬性的抽象類別具有 discriminator 結構描述中的欄位,但具有 JsonPolymorphicAttribute 屬性的實體類別沒有 discriminator 欄位。 OpenAPI 要求鑑別子屬性是結構描述中的必要屬性,但由於實體基底類別中未定義鑑別子屬性,所以結構描述不能包含 discriminator 欄位。

使用結述構描轉換器新增中繼資料

結述構描轉換器可用來覆寫任何預設中繼資料,或將其他中繼資料,例如 example 值新增至產生的結構描述。 如需詳細資訊,請參閱使用結構描述轉換器

自訂 OpenAPI 文件產生的選項

下列各節示範如何自訂 OpenAPI 文件產生。

自訂 OpenAPI 文件名稱

應用程式中的每份 OpenAPI 文件都有唯一的名稱。 註冊的預設文件名稱為 v1

builder.Services.AddOpenApi(); // Document name is v1

藉由將文件名稱當做參數傳遞至 AddOpenApi 呼叫,即可修改該名稱。

builder.Services.AddOpenApi("internal"); // Document name is internal

文件名稱會呈現在 OpenAPI 實作的數個位置中。

擷取產生的 OpenAPI 文件時,文件名稱會在要求中以 documentName 參數引數的形式提供。 下列要求會解析 v1internal 文件。

GET http://localhost:5000/openapi/v1.json
GET http://localhost:5000/openapi/internal.json

自訂所產生文件的 OpenAPI 版本

根據預設,OpenAPI 文件產生會建立一份符合 OpenAPI 規格 v3.0 的文件。 下列程式碼示範如何修改 OpenAPI 文件的預設版本:

builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0;
});

自訂 OpenAPI 端點路由

根據預設,透過呼叫 MapOpenApi 註冊的 OpenAPI 端點會在 /openapi/{documentName}.json 端點公開文件。 下列程式碼示範如何自訂 OpenAPI 文件註冊所在的路由:

app.MapOpenApi("/openapi/{documentName}/openapi.json");

從端點路由中移除 documentName 路由參數是可行的,但不建議這麼做。 當 documentName 路由參數從端點路由中移除時,架構會嘗試從查詢參數中解析文件名稱。 未在路由或查詢中提供 documentName 可能會導致非預期的行為。

自訂 OpenAPI 端點

由於 OpenAPI 文件是透過路由處理常式端點提供服務的,因此可用於標準最低端點的任何自訂也可用於 OpenAPI 端點。

限制授權使用者才能存取 OpenAPI 文件

OpenAPI 端點預設不會啟用任何授權檢查。 不過,授權檢查可以套用至 OpenAPI 文件。 例如,在下列程式碼中,限制具有 tester 角色的人員才能存取 OpenAPI 文件:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("ApiTesterPolicy", b => b.RequireRole("tester"));
});
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi()
    .RequireAuthorization("ApiTesterPolicy");

app.MapGet("/", () => "Hello world!");

app.Run();

快取產生的 OpenAPI 文件

每次向 OpenAPI 端點傳送要求時,都會重新產生 OpenAPI 文件。 重新產生可讓轉換器將動態應用程式狀態納入其作業中。 例如,使用 HTTP 內容的詳細資料重新產生要求。 如果適用,可以快取 OpenAPI 文件,以避免在每個 HTTP 要求上執行文件產生管線。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromMinutes(10)));
});
builder.Services.AddOpenApi();

var app = builder.Build();

app.UseOutputCache();

app.MapOpenApi()
    .CacheOutput();

app.MapGet("/", () => "Hello world!");

app.Run();

OpenAPI 文件轉換器

本節示範如何使用轉換器自訂 OpenAPI 文件。

使用轉換器自訂 OpenAPI 文件

轉換器會提供 API,用於透過使用者定義的自訂項目修改 OpenAPI 文件。 轉換器適用於如下案例:

  • 將參數新增至文件中的所有作業。
  • 修改參數或作業的描述。
  • 將最上層資訊新增至 OpenAPI 文件。

轉換器分為三種類別:

  • 文件轉換器可以存取整份 OpenAPI 文件。 這些可用來對文件進行全域修改。
  • 作業轉換器會套用至每個個別作業。 每個個別作業都是路徑與 HTTP 方法的組合。 這些可用來修改端點上的參數或回應。
  • 結構描述轉換器會套用至文件中每個結構描述。 這些可用來修改要求或回應主體的結構描述,或任何巢狀結構描述。

可以透過 OpenApiOptions 物件的 AddDocumentTransformer 呼叫方法,將轉換器註冊到文件上。 以下程式碼片段顯示了將轉換器註冊到文件的不同方法:

  • 使用委派註冊文件轉換器。
  • 使用 IOpenApiDocumentTransformer 的執行個體註冊文件轉換器。
  • 使用 DI 啟動的 IOpenApiDocumentTransformer 註冊文件轉換器。
  • 使用委派註冊作業轉換器。
  • 使用 IOpenApiOperationTransformer的執行個體註冊作業轉換器。
  • 使用 DI 啟動的 IOpenApiOperationTransformer註冊作業轉換器。
  • 使用委派註冊結構描述轉換器。
  • 使用 IOpenApiSchemaTransformer的執行個體註冊結構描述轉換器。
  • 使用 DI 啟動的 IOpenApiSchemaTransformer註冊結構描述轉換器。
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken)
                             => Task.CompletedTask);
    options.AddDocumentTransformer(new MyDocumentTransformer());
    options.AddDocumentTransformer<MyDocumentTransformer>();
    options.AddOperationTransformer((operation, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddOperationTransformer(new MyOperationTransformer());
    options.AddOperationTransformer<MyOperationTransformer>();
    options.AddSchemaTransformer((schema, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddSchemaTransformer(new MySchemaTransformer());
    options.AddSchemaTransformer<MySchemaTransformer>();
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

轉換器的執行順序

轉換器會根據註冊以先進先出的順序執行。 在下列程式碼片段中,文件轉換器可以存取作業轉換器所做的修改:

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken)
                                     => Task.CompletedTask);
    options.AddDocumentTransformer((document, context, cancellationToken)
                                     => Task.CompletedTask);
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

使用文件轉換器

文件轉換程式可以存取內容物件,其中包括:

  • 正要修改的文件名稱。
  • 與該文件相關聯的 ApiDescriptionGroups 清單。
  • 產生文件時使用的 IServiceProvider

文件轉換器也可變動產生的 OpenAPI 文件。 下列範例示範一個文件轉換器,其會將 API 的一些相關資訊新增至 OpenAPI 文件。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = new()
        {
            Title = "Checkout API",
            Version = "v1",
            Description = "API for processing checkouts from cart."
        };
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

服務啟動的文件轉換器可以利用 DI 中的執行個體修改應用程式。 下列範例示範一個文件轉換器使用來自驗證層的 IAuthenticationSchemeProvider 服務。 其會檢查應用程式中是否已註冊任何 JWT 持有人相關配置,並將其新增至 OpenAPI 文件的最上層:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;
        }
    }
}

文件轉換器對與其相關聯的文件執行個體來說是唯一的。 在下列範例中,轉換器:

  • 將驗證相關需求註冊至 internal 文件。
  • public 文件保持未修改狀態。
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi("internal", options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
builder.Services.AddOpenApi("public");

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/world", () => "Hello world!")
    .WithGroupName("internal");
app.MapGet("/", () => "Hello universe!")
    .WithGroupName("public");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            // Add the security scheme at the document level
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;

            // Apply it as a requirement for all operations
            foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
            {
                operation.Value.Security.Add(new OpenApiSecurityRequirement
                {
                    [new OpenApiSecurityScheme { Reference = new OpenApiReference { Id = "Bearer", Type = ReferenceType.SecurityScheme } }] = Array.Empty<string>()
                });
            }
        }
    }
}

使用作業轉換器

作業是 OpenAPI 文件中 HTTP 路徑與方法的獨特組合。 出現下列修改情況時,作業轉換器很有幫助:

  • 應該對應用程式中的每個端點進行修改,或
  • 修改有條件地套用至特定路由。

作業轉換程式可以存取內容物件,其中包含:

  • 作業所屬文件的名稱。
  • 與作業關聯的 ApiDescription
  • 產生文件時使用的 IServiceProvider

例如,下列作業轉換器會在文件中新增 500,作為所有作業支援的回應狀態碼。

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        operation.Responses.Add("500", new OpenApiResponse { Description = "Internal server error" });
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

使用結構描述轉換器

結構描述是 OpenAPI 文件中在要求和回應主體使用的資料模型。 修改時,結構描述轉換器很有用:

  • 應該對文件中每一個結構描述進行修改,或
  • 有條件地套用至特定結構描述。

結構描述轉換器可以存取內容物件,其中包含:

  • 結構描述所屬文件的名稱。
  • 與目標結構描述相關的 JSON 類型資訊。
  • 產生文件時使用的 IServiceProvider

例如,下列結構描述轉換器會將十進位類型的 format 設定為 decimal,而不是 double

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options => {
    // Schema transformer to set the format of decimal to 'decimal'
    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.JsonTypeInfo.Type == typeof(decimal))
        {
            schema.Format = "decimal";
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => new Body { Amount = 1.1m });

app.Run();

public class Body {
    public decimal Amount { get; set; }
}

其他資源

基本 API 針對透過 Microsoft.AspNetCore.OpenApi 封裝產生應用程式中端點的相關資訊提供內建支援。 透過視覺 UI 公開產生的 OpenAPI 定義需要第三方封裝。 如需支援控制器型 API 中 OpenAPI 的相關資訊,請參閱本文的 .NET 9 版本

下列程式碼是由 ASP.NET Core 基本 Web API 範本所產生,並使用 OpenAPI:

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateTime.Now.AddDays(index),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

在上述醒目提示的程式碼中:

  • 下一節會說明 Microsoft.AspNetCore.OpenApi
  • AddEndpointsApiExplorer:將應用程式設定為使用 API 總管來探索及描述具有預設註釋的端點。 WithOpenApi 會以從 Microsoft.AspNetCore.OpenApi 封裝產生的註釋來覆寫 API 總管所產生的相符預設註釋。
  • UseSwagger 會新增 Swagger 中介軟體
  • `UseSwaggerUI` 會啟用 Swagger UI 工具的內嵌版本。
  • WithName:端點上的 IEndpointNameMetadata 會用於產生連結,並視為指定端點 OpenAPI 規格中的作業識別碼。
  • 本文稍後將說明 WithOpenApi

Microsoft.AspNetCore.OpenApi NuGet 封裝

ASP.NET Core 提供 Microsoft.AspNetCore.OpenApi 封裝來與端點的 OpenAPI 規格互動。 封裝會作為在 Microsoft.AspNetCore.OpenApi 封裝中定義的 OpenAPI 模型與在基本 API 中定義的端點之間的連結。 封裝提供 API,其會檢查端點的參數、回應和中繼資料,以建構用來描述端點的 OpenAPI 註釋型別。

Microsoft.AspNetCore.OpenApi 會新增為專案檔的 PackageReference:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>    
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.*-*" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>

使用 Swashbuckle.AspNetCore 搭配 Microsoft.AspNetCore.OpenApi 時,必須使用 Swashbuckle.AspNetCore 6.4.0 或更新版本。 Microsoft.OpenApi 1.4.3 或更新版本必須用來利用 WithOpenApi 叫用中的複製建構函式。

透過 WithOpenApi 將 OpenAPI 註釋新增至端點

在端點上呼叫 WithOpenApi 會新增至端點的中繼資料。 此中繼資料可以:

  • Swashbuckle.AspNetCore 等第三方封裝中取用。
  • 顯示在 Swagger 使用者介面中,或產生 YAML 或 JSON,用來定義 API。
app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>
{
    todo.Id = id;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi();

WithOpenApi 中修改 OpenAPI 註釋

WithOpenApi 方法會接受可用來修改 OpenAPI 註釋的函式。 例如,在下列程式碼中,描述會新增至端點的第一個參數:

app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>
{
    todo.Id = id;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi(generatedOperation =>
{
    var parameter = generatedOperation.Parameters[0];
    parameter.Description = "The ID associated with the created Todo";
    return generatedOperation;
});

將作業識別碼新增至 OpenAPI

作業識別碼可用來唯一識別 OpenAPI 中的指定端點。 WithName 擴充方法可用來設定方法所使用的作業識別碼。

app.MapGet("/todoitems2", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithName("GetToDoItems");

或者,可以直接在 OpenAPI 註釋上設定 OperationId 屬性。

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        OperationId = "GetTodos"
    });

將標記新增至 OpenAPI 描述

OpenAPI 支援使用標記物件來分類作業。 這些標記通常用於群組 Swagger UI 中的作業。 藉由叫用具有所需標記的端點上的 WithTags 擴充方法,即可將這些標記新增至作業。

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithTags("TodoGroup");

或者,可以透過 OpenApiTags 擴充方法,在 OpenAPI 註釋上設定 WithOpenApi 的清單。

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Tags = new List<OpenApiTag> { new() { Name = "Todos" } }
    });

新增端點摘要或描述

透過叫用 WithOpenApi 擴充方法來新增端點摘要和描述。 在下列程式碼中,摘要會直接在 OpenAPI 註釋上設定。

app.MapGet("/todoitems2", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Summary = "This is a summary",
        Description = "This is a description"
    });

排除 OpenAPI 描述

在下列範例中,/skipme 端點會從產生 OpenAPI 描述排除:

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/swag", () => "Hello Swagger!")
    .WithOpenApi();
app.MapGet("/skipme", () => "Skipping Swagger.")
                    .ExcludeFromDescription();

app.Run();

將 API 標示為過時

若要將端點標示為過時,請在 OpenAPI 註釋上設定 Deprecated 屬性。

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Deprecated = true
    });

描述回應類型

OpenAPI 支援提供從 API 傳回的回應描述。 基本 API 針對設定端點的回應型別支援三個策略:

Produces 擴充方法可用來將 Produces 中繼資料新增至端點。 如果未提供任何參數,擴充方法會在 200 狀態碼和 application/json 內容型別下填入目標型別的中繼資料。

app
    .MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .Produces<IList<Todo>>();

在端點路由處理常式的實作中使用 TypedResults,會自動包含端點的回應型別中繼資料。 例如,下列程式碼會自動以具有 application/json 內容型別的 200 狀態碼的回應來註釋端點。

app.MapGet("/todos", async (TodoDb db) =>
{
    var todos = await db.Todos.ToListAsync());
    return TypedResults.Ok(todos);
});

設定 ProblemDetails 的回應

設定可能傳回 ProblemDetails 回應的端點的回應型別時, ProducesProblem 擴充方法、 ProducesValidationProblem,或 TypedResults.Problem 可用來將適當的註釋新增至端點的中繼資料。 請注意, ProducesProblemProducesValidationProblem 擴充方法無法與 .NET 8 及更早版本中的 路由群組 搭配使用。

當上述其中一個策略未提供明確註釋時,架構會藉由檢查回應的簽章來嘗試判斷預設回應型別。 此預設回應會填入在 OpenAPI 定義中的 200 狀態碼底下。

多個回應型別

如果端點可以在不同案例中傳回不同的回應型別,您可以透過下列方式提供中繼資料:

  • 呼叫 Produces 擴充方法多次,如下列範例所示:

    app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
             await db.Todos.FindAsync(id) 
             is Todo todo
             ? Results.Ok(todo) 
             : Results.NotFound())
       .Produces<Todo>(StatusCodes.Status200OK)
       .Produces(StatusCodes.Status404NotFound);
    
  • 在簽章和 TypedResults 處理常式主體中使用 Results<TResult1,TResult2,TResultN>,如下列範例所示:

    app.MapGet("/book{id}", Results<Ok<Book>, NotFound> (int id, List<Book> bookList) =>
    {
        return bookList.FirstOrDefault((i) => i.Id == id) is Book book
         ? TypedResults.Ok(book)
         : TypedResults.NotFound();
    });
    

    Results<TResult1,TResult2,TResultN>等位類型會宣告路由處理常式傳回多個 IResult 實作的具體類型,而實作 IEndpointMetadataProvider 的任何類型都會對端點的中繼資料造成貢獻。

    等位型別會實作隱含轉換運算子。 這些運算子可讓編譯器自動將泛型引數中指定的型別轉換成等位型別的執行個體。 此功能會有的附加優點為提供編譯時間檢查,以確定路由處理常式只會傳回其宣告會執行的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 Results<TResult1,TResult2,TResultN> 會產生編譯錯誤。

描述要求本文和參數

除了描述端點所傳回的型別之外,OpenAPI 也支援註釋 API 所取用的輸入。 這些輸入可分為兩個類別:

  • 出現在路徑、查詢字串、標頭或 Cookie 中的參數
  • 隨著要求本文傳輸的資料

架構會根據路由處理常式的簽章,自動推斷路徑、查詢和標頭字串中要求參數的型別。

若要定義傳送為要求本文的輸入型別,請使用 Accepts 擴充方法來定義要求處理常式所預期的物件型別和內容型別,以設定屬性。 在下列範例中,端點會接受要求本文中具有預期 application/xml 內容型別的 Todo 物件。

app.MapPost("/todos/{id}", (int id, Todo todo) => ...)
  .Accepts<Todo>("application/xml");

除了 Accepts 擴充方法之外,參數型別也可以藉由實作 IEndpointParameterMetadataProvider 介面來描述自己的註釋。 例如,下列 Todo 型別會新增需要具有 application/xml 內容型別要求本文的註釋。

public class Todo : IEndpointParameterMetadataProvider
{
    public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ConsumesAttribute(typeof(Todo), isOptional: false, "application/xml"));
    }
}

如果未提供明確的註釋,如果端點處理常式中有要求主體參數,架構會嘗試判斷預設要求型別。 推斷會使用下列啟發學習法來產生註釋:

  • 透過 [FromForm] 屬性從表單讀取的要求本文參數會使用 multipart/form-data 內容型別描述。
  • 所有其他要求主體參數都會使用 application/json 內容型別來描述。
  • 如果要求主體可為 Null,或 FromBody 屬性上設定 AllowEmpty 屬性,則會將其視為選用。

支援 API 版本設定

基本 API 支援透過 Asp.Versioning.Http 封裝的 API 版本設定。 您可以在 API 版本設定存放庫中找到使用基本 API 設定版本設定的範例。

GitHub 上的 ASP.NET Core OpenAPI 原始程式碼

其他資源

基本 API 可以使用 Swashbuckle 來描述路由處理常式的 OpenAPI 規格

如需支援控制器型 API 中 OpenAPI 的相關資訊,請參閱本文的 .NET 9 版本

下列程式碼是具有 OpenAPI 支援的一般 ASP.NET Core 應用程式:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = builder.Environment.ApplicationName,
                               Version = "v1" });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger(); // UseSwaggerUI Protected by if (env.IsDevelopment())
    app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
                                    $"{builder.Environment.ApplicationName} v1"));
}

app.MapGet("/swag", () => "Hello Swagger!");

app.Run();

排除 OpenAPI 描述

在下列範例中,/skipme 端點會從產生 OpenAPI 描述排除:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(); // UseSwaggerUI Protected by if (env.IsDevelopment())
}

app.MapGet("/swag", () => "Hello Swagger!");
app.MapGet("/skipme", () => "Skipping Swagger.")
                    .ExcludeFromDescription();

app.Run();

描述回應類型

下列範例會使用內建的結果型別來自訂回應:

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

將作業識別碼新增至 OpenAPI

app.MapGet("/todoitems2", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithName("GetToDoItems");

將標記新增至 OpenAPI 描述

下列程式碼使用 OpenAPI 群組標記

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithTags("TodoGroup");