はじめに
今回はASP.NET Core
のMinimal API
で自動でOpenAPIドキュメントを生成してSwaggerUI
やReDoc
で可視化する方法を紹介したいと思います。
ちなみにMicrosoft公式が出しているMicrosoft.AspNetCore.OpenApi
というOpenAPIドキュメントを生成するパッケージがあるのですが、こちらはSwaggerUIやReDocといったOpenAPIドキュメントの可視化するための機能がありませんので注意してください。
www.nuget.org
既定では、Microsoft.AspNetCore.OpenApi パッケージには、OpenAPI ドキュメントを視覚化または操作するための組み込みサポートは付属していません。
概要
NSwag.AspNetCore
はOpenAPIドキュメントの生成やC#クライアントコード生成、SwaggerUIやReDocによる可視化をサポートしているライブラリです。
NSwag is a Swagger/OpenAPI 2.0 and 3.0 toolchain for .NET, .NET Core, Web API, ASP.NET Core, TypeScript (jQuery, AngularJS, Angular 2+, Aurelia, KnockoutJS and more) and other platforms, written in C#. The OpenAPI/Swagger specification uses JSON and JSON Schema to describe a RESTful web API. The NSwag project provides tools to generate OpenAPI specifications from existing ASP.NET Web API controllers and client code from these OpenAPI specifications.
// DeepL翻訳
NSwagは、.NET、.NET Core、Web API、ASP.NET Core、TypeScript(jQuery、AngularJS、Angular 2+、Aurelia、KnockoutJSなど)、その他のプラットフォーム用のSwagger/OpenAPI 2.0および3.0ツールチェーンで、C#で書かれています。OpenAPI/Swagger仕様は、JSONとJSON Schemaを使用してRESTfulなWeb APIを記述します。NSwagプロジェクトは、既存のASP.NET Web APIコントローラからOpenAPI仕様を生成するツールや、これらのOpenAPI仕様からクライアントコードを生成するツールを提供しています。
ちなみに公式ドキュメントではNSwag
と一緒にSwashbuckle
も紹介されていますが、こちらは.NET9以降では使用できないとのこと(検証してないので本当か怪しいが...)なのでご注意ください。
Swashbuckle は .NET 9 以降では使用できません。 別の方法については、「ASP.NET Core API アプリでの OpenAPI サポートの概要」をご覧ください。
インストール方法
NuGetからインストールします。
$ dotnet add package NSwag.AspNetCore --version 14.2.0
https://www.nuget.org/packages/NSwag.AspNetCore/#versions-body-tab
また名前の通り、ASP.NET Core
用のものなので注意してください。
# ASP.NET CoreのMinimal APIでのプロジェクト作成 $ dotnet new web -f net9.0
OpenAPIに基づいたドキュメントを生成する
var builder = WebApplication.CreateBuilder(args); // 自動でAPIを解析する (MinimalAPIのみ必要になります) // ref : https://stackoverflow.com/questions/71932980/what-is-addendpointsapiexplorer-in-asp-net-core-6/71933535#71933535 builder.Services.AddEndpointsApiExplorer(); // OpenAPI Generatorをサービスとして追加 builder.Services.AddOpenApiDocument(); var app = builder.Build(); app.MapGet("/", Results<BadRequest, Ok<string>> (string name) => { if (string.IsNullOrEmpty(name)) { return TypedResults.BadRequest(); } return TypedResults.Ok("{ \"message\": \"Hello, " + name + "\" }"); }); if(app.Environment.IsDevelopment()) { // OpenAPI に基づいたドキュメントを生成する ミドルウェア追加 // Available at: http://localhost:<port>/swagger/v1/swagger.json app.UseOpenApi(); } app.Run();
↓ 吐き出される例
{ "x-generator": "NSwag v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))", "openapi": "3.0.0", "info": { "title": "My Title", "version": "1.0.0" }, "servers": [ { "url": "http://localhost:5006" } ], "paths": { "/": { "get": { "operationId": "Get", "parameters": [ { "name": "name", "in": "query", "required": true, "schema": { "type": "string" }, "x-position": 1 } ], "responses": { "400": { "description": "" }, "200": { "description": "", "content": { "application/json": { "schema": { "type": "string" } } } } } } } }, "components": {} }
一点気をつけてほしいことは、Minimal APIのときのみAddEndpointsApiExplorer
を呼び出す必要があるということです。詳細については添付したStackOverflowを参照してください。
// APIを解析する (MinimalAPIのみ必要になります)
builder.Services.AddEndpointsApiExplorer();
c# - What is AddEndpointsApiExplorer in ASP.NET Core 6 - Stack Overflow
WebAPIのドキュメント情報を追記する
吐き出されたswagger.json
にはtitle
やversions
などの情報が含まれていたと思うのですが、これらをカスタマイズすることができます。
builder.Services.AddOpenApiDocument(options => { options.PostProcess = document => { document.Info = new OpenApiInfo { Version = "v1", Title = "MinimalAPI Sample", Description = "Sample API for MinimalAPI", TermsOfService = "https://example.com/terms", Contact = new OpenApiContact { Name = "Example Contact", Url = "https://example.com/contact" }, License = new OpenApiLicense { Name = "Example License", Url = "https://example.com/license" } }; }; });

↓OpenAPIドキュメントの一部
{ "x-generator": "NSwag v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))", "openapi": "3.0.0", "info": { "title": "MinimalAPI Sample", "description": "Sample API for MinimalAPI", "termsOfService": "https://example.com/terms", "contact": { "name": "Example Contact", "url": "https://example.com/contact" }, "license": { "name": "Example License", "url": "https://example.com/license" }, "version": "v1" }, ...
Swagger v2.0の仕様に従ったドキュメントを生成する
// OpenAPI v3.0 // builder.Services.AddOpenApiDocument(); // Swagger v2.0 builder.Services.AddSwaggerDocument();
ドキュメント名を変更する
// デフォルトのドキュメント名は "v1" services.AddOpenApiDocument(document => document.DocumentName = "a"); services.AddSwaggerDocument(document => document.DocumentName = "b");
またドキュメント名を変換するとURLも変わりますので注意してください。
http://localhost:<port>/swagger/<document-name>/swagger.json
Swagger UIを利用する
if(app.Environment.IsDevelopment()) { // OpenAPI に基づいたドキュメントを生成する ミドルウェア追加 // Available at: http://localhost:<port>/swagger/v1/swagger.json app.UseOpenApi(); // SwaggerUI を立ち上げることができるようにする ミドルウェア追加 // Available at: http://localhost:<port>/swagger app.UseSwaggerUi(); }

デフォルトで読み込むOpenAPIドキュメントは/swagger/{document-name}/swagger.json
になります。
Redocを利用する
if(app.Environment.IsDevelopment()) { // OpenAPI に基づいたドキュメントを生成する // Available at: http://localhost:<port>/swagger/v1/swagger.json app.UseOpenApi(); // ReDoc をを立ち上げることができるようにする ミドルウェア追加 app.UseReDoc(options => { options.Path = "/redoc"; }); }

コード生成する
NSwag.CodeGeneration.CSharp
を用いることでOpenAPIドキュメントからC#クライアントコードを生成することができます。
// Program.cs var document = await OpenApiDocument.FromFileAsync("openapi.json"); var clientSettings = new CSharpClientGeneratorSettings { ClassName = "MyClass", CSharpGeneratorSettings = { Namespace = "MyNamespace" } }; var clientGenerator = new CSharpClientGenerator(document, clientSettings); var code = clientGenerator.GenerateFile();
↓生成すると...
//---------------------- // <auto-generated> // Generated using the NSwag toolchain v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) // </auto-generated> //---------------------- // ... namespace MyNamespace { using System = global::System; [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class MyClass { // ... /// <exception cref="ApiException">A server side error occurred.</exception> public virtual System.Threading.Tasks.Task<string> GetAsync(string name) { return GetAsync(name, System.Threading.CancellationToken.None); } /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param> /// <exception cref="ApiException">A server side error occurred.</exception> public virtual async System.Threading.Tasks.Task<string> GetAsync(string name, System.Threading.CancellationToken cancellationToken) { if (name == null) throw new System.ArgumentNullException("name"); var client_ = _httpClient; var disposeClient_ = false; try { using (var request_ = new System.Net.Http.HttpRequestMessage()) { request_.Method = new System.Net.Http.HttpMethod("GET"); request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); var urlBuilder_ = new System.Text.StringBuilder(); if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); // Operation Path: "" urlBuilder_.Append('?'); urlBuilder_.Append(System.Uri.EscapeDataString("name")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); urlBuilder_.Length--; PrepareRequest(client_, request_, urlBuilder_); var url_ = urlBuilder_.ToString(); request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); PrepareRequest(client_, request_, url_); var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); var disposeResponse_ = true; try { var headers_ = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>(); foreach (var item_ in response_.Headers) headers_[item_.Key] = item_.Value; if (response_.Content != null && response_.Content.Headers != null) { foreach (var item_ in response_.Content.Headers) headers_[item_.Key] = item_.Value; } ProcessResponse(client_, response_); var status_ = (int)response_.StatusCode; if (status_ == 400) { string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); throw new ApiException("A server side error occurred.", status_, responseText_, headers_, null); } else if (status_ == 200) { var objectResponse_ = await ReadObjectResponseAsync<string>(response_, headers_, cancellationToken).ConfigureAwait(false); if (objectResponse_.Object == null) { throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); } return objectResponse_.Object; } else { var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); } } finally { if (disposeResponse_) response_.Dispose(); } } } finally { if (disposeClient_) client_.Dispose(); } } // ...
この生成されたコードを利用すれば、簡単にサーバーにリクエストが送れます。
MyNamespace.MyClass myClass = new MyNamespace.MyClass(new HttpClient()); var response = await myClass.GetAsync("Name"); // { "message": "Hello, Name" } Console.WriteLine(response);