共用方式為


產生 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

設定 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 文件。

自訂 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 檔

在一般 Web 應用程式中,OpenAPI 檔會在運行時間產生,並透過 HTTP 要求提供給應用程式伺服器。

在某些情況下,在應用程式的建置步驟期間產生 OpenAPI 檔會很有説明。 這些案例包括:

  • 產生認可至原始檔控制的 OpenAPI 檔。
  • 產生用於規格整合測試的 OpenAPI 檔。
  • 從網頁伺服器產生靜態提供的 OpenAPI 檔。

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

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

Install-Package Microsoft.Extensions.ApiDescription.Server

安裝時,此套件會在建置期間自動產生與應用程式相關聯的Open API檔,並將其填入應用程式的輸出目錄中。

$ dotnet build
$ cat bin/Debug/net9.0/{ProjectName}.json

自定義建置時間檔產生

修改所產生 Open API 檔案的輸出目錄

根據預設,產生的 OpenAPI 檔案會發出至應用程式的輸出目錄。 若要修改發出檔案的位置,請在 屬性中 OpenApiDocumentsDirectory 設定目標路徑。

<PropertyGroup>
  <OpenApiDocumentsDirectory>./</OpenApiDocumentsDirectory>
</PropertyGroup>

的值 OpenApiDocumentsDirectory 會相對於項目檔解析。 ./使用上述值,將會在與項目檔相同的目錄中發出 OpenAPI 檔。

修改輸出檔名

根據預設,產生的 OpenAPI 檔的名稱會與應用程式的項目檔相同。 若要修改發出的檔名,請在 屬性中OpenApiGenerateDocumentsOptions設定 --file-name 自變數。

<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--file-name my-open-api</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

選取要產生的 OpenAPI 檔

某些應用程式可能會設定為針對各種 API 版本發出多個 OpenAPI 檔,或區分公用和內部 API。 根據預設,建置時間檔產生器會針對應用程式中設定的所有檔案發出檔案。 若要只針對單一檔名稱發出,請在屬性中OpenApiGenerateDocumentsOptions設定 --document-name 自變數。

<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--document-name v2</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

在建置時間檔產生期間自定義運行時間行為

在建置時間 OpenAPI 檔案產生函式的幕後,使用插入伺服器實作啟動應用程式的進入點。 這是產生正確 OpenAPI 檔的需求,因為無法以靜態方式分析 OpenAPI 檔中的所有資訊。 由於叫用應用程式的進入點,因此會叫用應用程式啟動時的任何邏輯。 這包括將服務插入 DI 容器或從組態讀取的程式代碼。 在某些情況下,必須限制從建置時間檔產生叫用應用程式進入點時執行的程式碼路徑。 這些案例包括:

  • 未從特定組態字串讀取。
  • 未註冊資料庫相關服務。

為了限制這些程式代碼路徑不受建置時間產生管線叫用,這些程式代碼路徑可以在專案元件的檢查後方設定條件,如下所示:

using System.Reflection;

var builder = WebApplication.CreateBuilder();

if (Assembly.GetEntryAssembly()?.GetName().Name != "GetDocument.Insider")
{
  builder.Services.AddDefaults();
}

基本 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");