打造支援 OpenAPI 標準的 Minimal API
2 |
OpenAPI 已成 Web API 的業界標準,背後有強大的生態體系,豐富的文件/程式碼產生器以測試工具,這些好處過去我已有所體會。(參考:再探 WebAPI 客戶端自動產生器 - AutoRest、NSwag 與 .NET 3.5 支援問題) 而隨著我的專案大多改用 ASP.NET Core Minimal API 開發,這篇就來看看如何讓 Minimal API 也支援 OpenAPI 規格。
MS Learn 有篇官方教學是最權威的指南,這裡簡單整理重點。
程式庫方面,.NET 7 起有官方提供的 Microsoft.AspNetCore.OpenApi,另外一般會搭配 Swashbuckle.AspNetCore(6.4+) 提供 API 文件產生、API 檢視及測試網頁功能。
參考文件,我用 Minimal API 實作一個支援 OpenAI 規格及 Swagger 介面的 Web API。練習自訂 Swagger 文件標題及說明、用 ExcludeFromDescription() 排除非 Web API 方法、WithName()/OperationId 指定作業識別碼、WitTags() 分群、定義參數說明、Produces() 指定回應型別、檢核錯誤回傳 HTTP 400 及錯誤訊息物件... 等技巧。
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(opt => {
opt.SwaggerDoc("v1", new OpenApiInfo {
Title = "Minimal API Demo",
Description = "Minimal API OpenAPI 整合範例"
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger(); // 開發環境下使用 Swagger
app.UseSwaggerUI(); // 啟用 Swagger 網頁介面
}
app.UseFileServer(); // 啟用 wwwroot 靜態檔案伺服器
app.MapGet("/", () => "Hello World!")
.ExcludeFromDescription(); // 從 OpenAPI 文件排除
app.MapGet("/guid", () => {
return Guid.NewGuid();
}).WithName("NewGuid").WithOpenApi()
// 指定作業識別碼為 NewGuid,並用預設值產生 OpenAPI 文件
.WithTags("生成函數");
app.MapGet("/randoms", (int count, int range = int.MaxValue) => {
var random = new Random();
var randoms = Enumerable.Range(0, count).Select(_ => random.Next(range));
return randoms;
}).WithOpenApi(op => {
op.OperationId = "GetRandoms"; // 另一種指定作業識別碼的方式
op.Summary = "產生隨機數"; // 摘要說明
op.Description = "產生指定數量的隨機數"; // 詳細說明
// 提供參數說明
var pCount = op.Parameters[0];
pCount.Description = "隨機數的數量";
var pRange = op.Parameters[1];
pRange.Description = "隨機數範圍(0 ~ range-1)";
return op;
}).WithTags("數學").WithTags("生成函數"); // 指定標籤
// 複雜一點的範例
// 參數及回應皆為自訂型別,檢核失敗回應 HTTP 400 並回傳 ApiError 物件
app.MapPost("workdays", (DateRange range) => {
if (range.Start > range.End)
{
return Results.BadRequest(new ApiError {
Code = 1487,
Message = "起始日期不可大於結束日期"
});
}
var workDays = new List<DateTime>();
for (var date = range.Start.Date; date <= range.End.Date; date = date.AddDays(1))
{
if (date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday)
{
workDays.Add(date);
}
}
// 傳回 HTTP 200 並回傳 WorkDaysInfo 物件
return Results.Ok(new WorkDaysInfo
{
Start = range.Start,
End = range.End,
WorkDays = workDays.ToArray()
});
})
// 列舉回應的型別
.Produces<WorkDaysInfo>()
.Produces<ApiError>(StatusCodes.Status400BadRequest)
.WithOpenApi(op => {
op.OperationId = "GetWorkDays";
op.Summary = "取得工作日";
op.Description = "取得指定日期區間內的工作日";
// 提供參數說明
op.RequestBody.Description = "日期區間";
// 提供回應說明
var r200 = op.Responses["200"];
r200.Description = "成功取得工作日";
var r400 = op.Responses["400"];
r400.Description = "請求失敗";
return op;
}).WithTags("生成函數");
app.Run();
public class DateRange {
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
public class WorkDaysInfo : DateRange {
public DateTime[] WorkDays { get; set; } = [];
}
public class ApiError {
public int Code { get; set;}
public string Message { get; set; } = string.Empty;
}
就醬,就算是用 Minimal API 寫 WebAPI,照樣能產生有模有樣的 Swagger 介面,半點不馬虎。
另外值得一提是 .NET 7 起加入 TypedResults。主打自動依回傳型別產生 OpenAPI Metadata (可省去 Produces<T>()),並且能在編譯階段檢核回傳型別是否吻合。因此 app.MapPost("workdays",...)
可改寫如下:
// 宣告回應型別共有 Ok<WorkDaysInfo> 及 BadRequest<ApiError> 兩種
app.MapPost("workdays", Results<Ok<WorkDaysInfo>, BadRequest<ApiError>> (DateRange range) => {
if (range.Start > range.End)
{
// 改 TypedRequests.BadRequest
return TypedResults.BadRequest(new ApiError {
Code = 1487,
Message = "起始日期不可大於結束日期"
});
}
var workDays = new List<DateTime>();
for (var date = range.Start.Date; date <= range.End.Date; date = date.AddDays(1))
{
if (date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday)
{
workDays.Add(date);
}
}
// 改
return TypedResults.Ok(new WorkDaysInfo
{
Start = range.Start,
End = range.End,
WorkDays = workDays.ToArray()
});
})
// 此處省略 Produces() 回應型別宣告
.WithOpenApi(op => {
//...
}).WithTags("生成函數");
如此,若回傳型別與宣告不一致,編譯時就會出錯。
第一次在 API 端嘗試 return TypedResults.BadRequest(new ApiError { Code = 1487, Message = "起始日期不可大於結束日期" });
,伺服器端會得到 HTTP Status 400,但 Content-Type application/json 且 Response 內容為 JSON 的回應。
試寫了用 JavaScript fetch 接收回應的做法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Api Test</title>
</head>
<body>
<div id="app">
<div>
<input type="date" v-model="start" placeholder="start date" />
<input type="date" v-model="end" placeholder="end date" />
<button @click="getWorkDays">Get Workdays</button>
</div>
<div>
<ul>
<li v-for="workday in workdays" :key="workday">{{ workday }}</li>
</ul>
<div v-if="apiError">
<p>Error: {{ apiError.code }} - {{ apiError.message }}</p>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@3"></script>
<script>
const start = new Date();
const end = new Date();
end.setDate(new Date().getDate() + 7);
const app = Vue.createApp({
data() {
return {
start: start.toISOString().split('T')[0],
end: end.toISOString().split('T')[0],
workdays: [],
apiError: null
}
},
methods: {
async getWorkDays() {
this.apiError = null;
try {
const response = await fetch('workdays', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start: this.start, end: this.end })
});
if (!response.headers.get('content-type')?.includes('application/json')) {
throw { code: response.status, message: await response.text() }
}
const data = await response.json();
if (!response.ok) throw data;
this.workdays = data.workDays;
} catch (error) {
this.workdays = [];
this.apiError = error.code && error.message ? error :
{ code: '?', message: error.toString() };
}
}
}
});
app.mount('#app');
</script>
</body>
</html>
練習完畢。
範例專案我上傳到 Github 了,想動手玩看看的同學可自取。
Through examples, this article introduces how to enable Minimal API to support OpenAPI and Swagger.
Comments
# by yoyo
.NET 9後 web API template 將不再使用 Swashbuckle.AspNetCore 請問有建議的替代套件,或繼續使用Swashbuckle.AspNetCore嗎? Swashbuckle.AspNetCore is being removed in .NET 9 https://github.com/dotnet/aspnetcore/issues/54599
# by Jeffrey
to yoyo, .NET 9 會有建議的替代方案 (目前看起來 ASP.NET Core 會內建),若沒其他考量,拿香跟著拜就好了。