產生 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] |
Produces 、ProducesProblem |
TypedResults |
排除端點 | [ExcludeFromDescription] |
ExcludeFromDescription |
ASP.NET Core 不會從 XML 文件批註收集中繼資料。
下列各節示範如何在應用程式中包含中繼資料,以自訂產生的 OpenAPI 文件。
摘要和描述
您可以使用 [EndpointSummary]
與 [EndpointDescription]
屬性,或在基本 API 中使用 WithSummary
與 WithDescription
擴充方法,設定端點摘要與描述。
[EndpointSummary]
:EndpointSummaryAttribute[EndpointDescription]
:EndpointDescriptionAttributeWithSummary
:WithSummaryWithDescription
:WithDescription
下列範例示範設定摘要和描述的不同策略。
請注意,屬性置放在委派方法上,而不是放在 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
擴充方法來設定標記。
[Tags]
:TagsAttributeWithTags
: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。
[EndpointName]
:EndpointNameAttributeWithName
:WithName
下列範例示範設定 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 擴充方法。
- 透過路由處理常式上的
ProducesResponseType
屬性。 - 透過從路由處理常式傳回 TypedResults。
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 可用來將適當的註釋新增至端點的中繼資料。
當其中一個策略未提供明確註釋時,架構會藉由檢查回應的簽章來嘗試判斷預設回應型別。 此預設回應會填入在 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 type
與 format
,如下所示:
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 或字串等基本類型。
type
和 format
也可以使用 結構描述轉換器來設定。 例如,您可能想要十進位類型的 format
為 decimal
,而不是 double
。
使用屬性新增中繼資料
ASP.NET 使用類別或記錄屬性上屬性的中繼資料,在產生結構描述的對應屬性上設定中繼資料。
下表摘要說明 System.ComponentModel
命名空間中提供所產生結構描述中繼資料的屬性:
屬性 | 描述 |
---|---|
DescriptionAttribute | 設定結構描述中屬性的 description 。 |
RequiredAttribute | 在結構描述中將屬性標示為 required 。 |
DefaultValueAttribute | 設定結構描述中屬性的 default 值。 |
RangeAttribute | 設定整數或數字的 minimum 和 maximum 值。 |
MinLengthAttribute | 設定字串的 minLength 。 |
MaxLengthAttribute | 設定字串的 maxLength 。 |
RegularExpressionAttribute | 設定字串的 pattern 。 |
請注意,在控制器型應用程式中,這些屬性會將篩選新增至作業,以驗證任何傳入的資料是否符合條件約束。 在基本 API 中,這些屬性會在產生的結構描述中設定中繼資料,但必須透過端點篩選、路由處理常式邏輯或透過第三方套件明確執行驗證。
所產生結構描述的其他中繼資料來源
必要
屬性也可以使用必要的修飾元標示為 required
。
enum
C# 中的列舉類型是以整數為基礎,但可以使用 JSON 中的 JsonConverterAttribute 和 JsonStringEnumConverter 表示為字串。 當列舉類型在 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" 的結構描述為必要的實值類型。
多態性類型的中繼資料
使用上層類別的 JsonPolymorphicAttribute 和 JsonDerivedTypeAttribute 屬性來指定多型類型的鑑別子欄位與子類型。
將 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
參數引數的形式提供。 下列要求會解析 v1
和 internal
文件。
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
擴充方法 - 透過路由處理常式上的
ProducesResponseType
屬性 - 透過從路由處理常式傳回
TypedResults
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
可用來將適當的註釋新增至端點的中繼資料。 請注意, ProducesProblem
和 ProducesValidationProblem
擴充方法無法與 .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");