ASP.NET CoreでSwaggerを使う

ASP.NET Core向けのSwaggerライブラリが新年早々RC1としてリリースされていたので早速使ってみました。

Swaggerとは何か、みたいなのは以下のリンクを参照してみて下さい。

* [Swagger 公式サイト](http://swagger.io/)
* [swaggerの基礎。swaggerの設定ファイルの書き方とか](http://qiita.com/magaya0403/items/0419d84d8df7784ac465)
* [SwaggerでRESTful APIの管理を楽にする](http://qiita.com/disc99/items/37228f5d687ad2969aa2)

# ASP.NET Web API向けSwaggerライブラリ

ASP.NETでもSwaggerのライブラリが用意されていて、NuGetパッケージとして公開されています。導入手順が書かれたエントリもあったので合わせてリンクを貼っておきます。

* [Swashbuckle](https://www.nuget.org/packages/Swashbuckle)
* [domaindrivendev/Swashbuckle](https://github.com/domaindrivendev/Swashbuckle)
* [Swagger 2.0 に対応した ASP.NET Web API のドキュメントを作成する](http://gooner.hateblo.jp/entry/2015/12/08/011159)

2017/01/16時点での最新の安定版のバージョンは5.5.3で、6.0.0がベータ版で開発されています。

で、この6.0.0系のバージョンがASP.NET Coreに対応しており(DNX時代から対応を初めていたっぽい…)、以下のようにオプション引数を付けるとパッケージのインストールが可能です。

```cmd
PM > Install Swashbuckle -Pre
```

このバージョンを使用すればASP.NET CoreでもSwaggerが使えるのですが、どうやらこれとは別系統で開発が進んでいたようです。

# ASP.NET Core向けSwaggerライブラリ

ASP.NET Core向けのSwaggerライブラリはどうやら以下のようです。

* [Swashbuckle.AspNetCore](https://www.nuget.org/packages/Swashbuckle.AspNetCore)
* [domaindrivendev/Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)

2017/01/03にRC1がリリースされています。

それでは以下にUIからWeb APIを実行できるようにするまでの基本的な使い方を書いていきます。

# Swashbuckle.AspNetCoreの使い方

## パッケージのインストール

これまで通りNuGetからパッケージをインストールします。Visual StudioのGUI上から入れるか、コマンドを実行するか、project.jsonを直接弄ります。(個人的にはproject.jsonの直弄りがやりやすいです。)

```cmd
PM > Install Swashbuckle.AspNetCore -Pre
```

```json:project.json
{
    "dependencies": {
        …
        "Swashbuckle.AspNetCore": "1.0.0-rc1" // コレを追加する
    }
}
```

Swashbuckle.AspNetCoreのパッケージをインストールすると、参照依存の関係で以下の3つのライブラリが使えるようになります。

* Swashbuckle.AspNetCore.Swagger
    * SwaggerドキュメントをJSON形式で出力するためのライブラリ
* Swashbuckle.AspNetCore.SwaggerGen
    * ルート、コントローラ、モデルの情報をSwaggerドキュメントとして生成するためのライブラリ
* Swashbuckle.AspNetCore.SwaggerUi
    * JSON形式のSwaggerドキュメントを元にUIからAPIを使用するためのライブラリ

## Swaggerの設定

パッケージのインストールと復元が完了したらStartupクラスを編集します。

ConfigureServicesメソッド内でIServiceCollectionにSwaggerGenのサービスを登録し、Confiureメソッド内でIApplicationBuilderにSwaggerとSwaggerUIのミドルウェアを登録します。

それぞれ拡張メソッドが用意されています。

```csharp:Startup.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        // SwaggerGenサービスの登録
        services.AddSwaggerGen(option =>
        {
            option.SwaggerDoc("orders", new Title { Name = "Order APIs", Version = "v1" });
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment environment)
    {
        app.UseMvc();

        // Swaggerミドルウェアの登録
        app.UseSwagger();
        // SwaggerUIミドルウェアの登録
        app.UseSwaggerUi(option =>
        {
            option.SwaggerEndpoint("/swagger/orders/swagger.json", "Order APIs sandbox.");
        });
    }
}
```

SwaggerGenサービスの登録ですが、引数でオプションのSwaggerDocメソッドを呼び出して出力するJSON形式のSwaggerドキュメントの設定を行っています。

設定内容は、第1引数がJSONファイルを出力するパス名で第2引数がSwaggerドキュメントのタイトル情報となっています。

Swaggerミドルウェアを登録することで×××出来るようにし、SwaggerUIのミドルウェアを登録することでAPIを実行するためのUI画面を生成するようにしています。

SwaggerUIのミドルウェア登録時に表示するSwaggerドキュメントのJSONファイルのパスを指定しています。

## Controllerの作成

次にSwaggerドキュメントを生成するコントローラを作成します。

今回はSQL ServerのサンプルDBであるNorthwindから、Ordersテーブルを操作するコントローラを作成することにします。(下の例ではリポジトリパターンを適用しています。)

```csharp:OrderController.cs
/// <summary>
/// 注文管理を行うコントローラです。
/// </summary>
[Route("api/[controller]/[action]")]
public class OrderController : Controller
{
    private readonly IOrderRepository _repository;

    public OrderController()
    {
        _repository = new OrderRepository(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=True");
    }

    /// <summary>
    /// 全注文情報を取得します。
    /// </summary>
    /// <returns>取得した全注文情報。</returns>
    [HttpGet]
    public IEnumerable<Orders> GetAll()
    {
        return _repository.SelectAll();
    }

    /// <summary>
    /// 指定した ID に紐付く注文情報を取得します。
    /// </summary>
    /// <param name="orderId">取得する注文情報の ID。</param>
    /// <returns>取得した注文情報。</returns>
    [HttpGet("{orderId}")]
    public Orders GetById(int orderId)
    {
        return _repository.SelectByKey(orderId);
    }

    /// <summary>
    /// 注文情報を登録します。
    /// </summary>
    /// <param name="order">登録する注文情報。</param>
    /// <returns>登録結果。成功した場合は true、そうでない場合は false。</returns>
    [HttpPost]
    public bool Register([FromBody]Orders order)
    {
        return _repository.Insert(order);
    }

    /// <summary>
    /// 指定した ID の注文情報を編集します。
    /// </summary>
    /// <param name="orderId">編集する注文情報の ID。</param>
    /// <param name="order">編集する注文情報。</param>
    /// <returns>編集結果。成功した場合は true、そうでない場合は false。</returns>
    [HttpPut("{orderId}")]
    public bool Edit(int orderId, [FromBody]Orders order)
    {
        return _repository.UpdateByKey(orderId, order);
    }

    /// <summary>
    /// 指定した ID の注文情報を削除します。
    /// </summary>
    /// <param name="orderId">削除する注文情報の ID。</param>
    /// <returns>削除結果。成功した場合は true、そうでない場合は false。</returns>
    [HttpDelete("{orderId}")]
    public bool Remove(int orderId)
    {
        return _repository.DeleteByKey(orderId);
    }
}
```

## アプリケーションの実行

ここまでの作業が完了したらアプリケーションを実行します。

ブラウザが立ち上がったら、http://{ベースURL}/swagger に遷移します。

そうすると {ベースURL}/swagger/index.html にリダイレクトして以下の画面が出てきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39403/4e89eb0c-f405-6016-933c-7e00cf81c596.png)

この画面が表示されればこれまで通り、サンドボックス環境としてAPIの実行を行うことが出来ます。

ちなみに {ベースURL}/{SwaggerドキュメントのJSONファイルのパス} のURLにアクセスすると、JSON形式で出力されたSwaggerドキュメントを参照することが出来ます。

```json:swagger.json
{
    "basePath": "/", 
    "definitions": {
        "Orders": {
            "properties": {
                "customerID": {
                    "type": "string"
                }, 
                "employeeID": {
                    "format": "int32", 
                    "type": "integer"
                }, 
                "freight": {
                    "format": "double", 
                    "type": "number"
                }, 
                "orderDate": {
                    "format": "date-time", 
                    "type": "string"
                }, 
                "orderID": {
                    "format": "int32", 
                    "type": "integer"
                }, 
                "requiredDate": {
                    "format": "date-time", 
                    "type": "string"
                }, 
                "shipAddress": {
                    "type": "string"
                }, 
                "shipCity": {
                    "type": "string"
                }, 
                "shipCountry": {
                    "type": "string"
                }, 
                "shipName": {
                    "type": "string"
                }, 
                "shipPostalCode": {
                    "type": "string"
                }, 
                "shipRegion": {
                    "type": "string"
                }, 
                "shipVia": {
                    "format": "int32", 
                    "type": "integer"
                }, 
                "shippedDate": {
                    "format": "date-time", 
                    "type": "string"
                }
            }, 
            "type": "object"
        }
    }, 
    "info": {
        "title": "Order APIs", 
        "version": "v1"
    }, 
    "paths": {
        "/api/Order/Edit/{orderId}": {
            "put": {
                "consumes": [
                    "application/json", 
                    "text/json", 
                    "application/json-patch+json"
                ], 
                "operationId": "ApiOrderEditByOrderIdPut", 
                "parameters": [
                    {
                        "format": "int32", 
                        "in": "path", 
                        "name": "orderId", 
                        "required": true, 
                        "type": "integer"
                    }, 
                    {
                        "in": "body", 
                        "name": "order", 
                        "required": false, 
                        "schema": {
                            "$ref": "#/definitions/Orders"
                        }
                    }
                ], 
                "produces": [
                    "text/plain", 
                    "application/json", 
                    "text/json"
                ], 
                "responses": {
                    "200": {
                        "description": "Success", 
                        "schema": {
                            "type": "boolean"
                        }
                    }
                }, 
                "tags": [
                    "Order"
                ]
            }
        }, 
        "/api/Order/GetAll": {
            "get": {
                "consumes": [], 
                "operationId": "ApiOrderGetAllGet", 
                "produces": [
                    "text/plain", 
                    "application/json", 
                    "text/json"
                ], 
                "responses": {
                    "200": {
                        "description": "Success", 
                        "schema": {
                            "items": {
                                "$ref": "#/definitions/Orders"
                            }, 
                            "type": "array"
                        }
                    }
                }, 
                "tags": [
                    "Order"
                ]
            }
        }, 
        "/api/Order/GetById/{orderId}": {
            "get": {
                "consumes": [], 
                "operationId": "ApiOrderGetByIdByOrderIdGet", 
                "parameters": [
                    {
                        "format": "int32", 
                        "in": "path", 
                        "name": "orderId", 
                        "required": true, 
                        "type": "integer"
                    }
                ], 
                "produces": [
                    "text/plain", 
                    "application/json", 
                    "text/json"
                ], 
                "responses": {
                    "200": {
                        "description": "Success", 
                        "schema": {
                            "$ref": "#/definitions/Orders"
                        }
                    }
                }, 
                "tags": [
                    "Order"
                ]
            }
        }, 
        "/api/Order/Register": {
            "post": {
                "consumes": [
                    "application/json", 
                    "text/json", 
                    "application/json-patch+json"
                ], 
                "operationId": "ApiOrderRegisterPost", 
                "parameters": [
                    {
                        "in": "body", 
                        "name": "order", 
                        "required": false, 
                        "schema": {
                            "$ref": "#/definitions/Orders"
                        }
                    }
                ], 
                "produces": [
                    "text/plain", 
                    "application/json", 
                    "text/json"
                ], 
                "responses": {
                    "200": {
                        "description": "Success", 
                        "schema": {
                            "type": "boolean"
                        }
                    }
                }, 
                "tags": [
                    "Order"
                ]
            }
        }, 
        "/api/Order/Remove/{orderId}": {
            "delete": {
                "consumes": [], 
                "operationId": "ApiOrderRemoveByOrderIdDelete", 
                "parameters": [
                    {
                        "format": "int32", 
                        "in": "path", 
                        "name": "orderId", 
                        "required": true, 
                        "type": "integer"
                    }
                ], 
                "produces": [
                    "text/plain", 
                    "application/json", 
                    "text/json"
                ], 
                "responses": {
                    "200": {
                        "description": "Success", 
                        "schema": {
                            "type": "boolean"
                        }
                    }
                }, 
                "tags": [
                    "Order"
                ]
            }
        }
    }, 
    "securityDefinitions": {}, 
    "swagger": "2.0"
}
```

# ちょっとした応用と注意点

ここからはSwashbuckle.AspNetCoreを使う上でのちょっとした応用と注意すべき点(と言うより自分がハマったところ)について書いていきます。

ちなみに応用については[GithubのREADME](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#configuration--customization)にほとんど書かれているので、そちらも参照してみて下さい。

## ドキュメントコメントを出力する

これまで載せてきた例だとコントローラやモデルのドキュメントコメントが表示されていません。

まずはproject.jsonに以下の項目を追加してドキュメントコメントを含むXMLファイルを出力するようにします。

```json:project.json
{
    "buildOptions": {
        …
        "xmlDoc": true // コレを追加する
    }
}
```

こうすることでビルド時にbinフォルダ内にXMLファイルが出力されるようになります。

次にStartupクラスでSwaggerGenのサービス登録を行うときに以下の設定の追加を行います。

```csharp:Startup.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddSwaggerGen(option =>
        {
            // XMLファイルのパスを取得
            var xmlPath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "AspNetCoreSwagger.SampleApp.xml");

            // XMLファイルをドキュメントコメントとして登録
            option.IncludeXmlComments(xmlPath);
            option.SwaggerDoc("orders", new Title { Name = "Order APIs", Version = "v1" });
        });
    }
}
```

上の設定が完了したらアプリケーションを実行して先程のURLにアクセスすると、各APIのコメントが表示されているのが確認できます。(下の例では /api/Order/GetById のAPIを選択した状態になっています。)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39403/e314f8e9-6d52-a010-d1fd-06d8ceae7dff.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39403/ca49ccdc-e16c-ffd4-9629-6162109f71b8.png)

## 特定のコントローラ/アクションのドキュメントを出力する or しない

これまでのASP.NETはMVCとWeb APIで名前空間が異なっていたため、Web APIだけのコントローラとアクションをSwaggerの対象にすることが出来ましたが、ASP.NET Coreではフレームワークだけでなく統合されて基底クラスも統合されたため、MVCとWeb APIが相乗りしたアプリケーションでSwashbuckle.AspNetCoreを使用するとMVCのコントローラとアクションも表示されるようになっています。

が、SwaggerGenのサービス登録時の設定で特定のコントローラやアクションだけを出力する、またはしないようにすることも出来ます。

```csharp:Startup.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddSwaggerGen(option =>
        {
            option.DocInclusionPredicate((name, apiDescription) =>
            {
                var controllerActionDescription = apiDescription.ActionDescriptor as ControllerActionDescriptor;

                // コントローラ名に Api が含まれていたらSwaggerの対象にする
                return controllerDescription?.ControllerName.Contains("Api") ?? false;
            });
            option.SwaggerDoc("orders", new Title { Name = "Order APIs", Version = "v1" });
        });
    }
}
```

オプションのDocInclusionPredicateで対象のコントローラやアクションを絞り込むことが出来ます。

上記の例ではコントローラ名にApiが含まれている場合だけ、Swaggerの対象になるように設定しています。

## IActionResultを返す時のレスポンス情報出力

これまでの例ではコントローラのアクションが特定の戻り値を持つように設計されていましたが、ASP.NET CoreではMVC / Web API共通でIActionResultを返せるようになりました。

MVCの場合はビューのオブジェクトを返し、Web APIの場合は特定のステータスコードを含むオブジェクトを返すように設計出来ます。

ただアクションの戻り値をIActionResultにしてしまうとswagger.jsonでレスポンスの方の情報が出力出来なくなってしまいます。

そういった時はコントローラのアクションにProducesResponseTypeという属性を付与します。(ASP.NET Core MVC標準の属性です。)

```csharp:OrderController.cs
/// <summary>
/// 注文管理を行うコントローラです。
/// </summary>
[Route("api/[controller]/[action]")]
public class OrderController : Controller
{
    private readonly IOrderRepository _repository;

    public OrderController()
    {
        _repository = new OrderRepository(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=True");
    }

    /// <summary>
    /// 全注文情報を取得します。
    /// </summary>
    /// <returns>取得した全注文情報。</returns>
    [HttpGet]
    [ProducesResponseType(typeof(IEnumerable<Orders>), 200)]
    public IActionResult GetAll()
    {
        var orders = _repository.SelectAll();

        return Ok(orders);
    }

    /// <summary>
    /// 指定した ID に紐付く注文情報を取得します。
    /// </summary>
    /// <param name="orderId">取得する注文情報の ID。</param>
    /// <returns>取得した注文情報。</returns>
    [HttpGet("{orderId}")]
    [ProducesResponseType(typeof(Orders), 200)]
    public IActionResult GetById(int orderId)
    {
        var order = _repository.SelectByKey(orderId);

        return Ok(order);
    }

    /// <summary>
    /// 注文情報を登録します。
    /// </summary>
    /// <param name="order">登録する注文情報。</param>
    /// <returns>登録結果。成功した場合は true、そうでない場合は false。</returns>
    [HttpPost]
    [ProducesResponseType(typeof(bool), 200)]
    public IActionResult Register([FromBody]Orders order)
    {
        var isSuccess = _repository.Insert(order);

        return Ok(isSuccess);
    }

    /// <summary>
    /// 指定した ID の注文情報を編集します。
    /// </summary>
    /// <param name="orderId">編集する注文情報の ID。</param>
    /// <param name="order">編集する注文情報。</param>
    /// <returns>編集結果。成功した場合は true、そうでない場合は false。</returns>
    [HttpPut("{orderId}")]
    [ProducesResponseType(typeof(bool), 200)]
    public IActionResult Edit(int orderId, [FromBody]Orders order)
    {
        var isSuccess = _repository.UpdateByKey(orderId, order);

        return Ok(isSuccess);
    }

    /// <summary>
    /// 指定した ID の注文情報を削除します。
    /// </summary>
    /// <param name="orderId">削除する注文情報の ID。</param>
    /// <returns>削除結果。成功した場合は true、そうでない場合は false。</returns>
    [HttpDelete("{orderId}")]
    [ProducesResponseType(typeof(bool), 200)]
    public IActionResult Remove(int orderId)
    {
        var isSuccess = _repository.DeleteByKey(orderId);

        return Ok(isSuccess);
    }
}
```

第1引数に戻り値の型、第2引数にその時のHTTPコードを渡します。

この属性を使用すればHTTPコード単位でレスポンス情報のドキュメントを出力することも出来ます。

## アクションの厳密な属性付与

ここは自分が結構ハマったところなんですが、Swaggerでドキュメントを生成しようとすると結構厳密な開発が求められます。

例えばURLにパラメータを埋め込む場合、上のソースのようにURLの一部として埋め込むケースとクエリ文字列としてURLの最後に```?```マークを付けるケースがあります。

URLに埋め込む場合はHttpXxx属性にテンプレートとしてアクションの変数名を記述する必要があります。

また、クエリ文字列の場合はアクションの引数事態にFromQuery属性を付ける必要があります。

両方ともない場合はSwagger配下のURLにアクセスしたときにエラーになります。

```csharp
// Order/GetById/1 で取得
[HttpGet("{orderId}")]
public Orders GetById(int orderId) { }

// Order/GetById?orderId=1 で取得
[HttpGet]
public Orders GetById([FromQuery]int orderId) { }

// Order/GetById?orderId=1 で取得できるがSwaggerとしてはエラーになる
[HttpGet]
public Orders GetById(int orderId) { }
```

他にもPOSTやPUTのアクションでもFromBodyやFromFormを付けないと、通常のWeb APIとしては動きますがSwagger配下のURLにアクセスしたときにエラーになります。

```csharp
// 上3つはSwaggerドキュメントとして認識されるが…

[HttpPost]
public bool Register([FromForm]Orders order) { }

[HttpPut("{orderId}")]
public bool Edit(int orderId, [FromBody]Orders order) { }

[HttpPut]
public bool Edit([FromQuery]int orderId, [FromBody]Orders order) { }

// 以下はエラーになる

[HttpPost]
public bool Register(Orders order) { }

[HttpPut("{orderId}")]
public bool Edit(int orderId, Orders order) { }

[HttpPut]
public bool Edit(int orderId, [FromBody]Orders order) { }
```

* URLに埋め込むとき
    * HttpXxx属性のコンストラクタに引数名を渡す
    * クエリ文字列にするなら引数にFromQuery属性を付ける
* それ以外のアクションの引数にもFromXxx属性は必ず付ける

以上のことに気を付けないと、Swaggerのドキュメントが生成されないので注意が必要です。

# まとめ

Web APIのドキュメント生成やサンドボックス環境としてSwaggerは非常に強力です。

ASP.NETに限らず、Web APIの開発を行っているのであればSwaggerは入れた方が良いと思います。(実際クロスプラットフォーム向けにライブラリが公開されています。)

冒頭でも書きましたが、ASP.NET Core向けのSwaggerライブラリは2017/01/16時点ではまだRC1です。

なので今後正式リリースされたときにインターフェース等が変わる可能性はあります。

正式リリースされて、何か変更点があった場合はまた何らかのかたちでお知らせしていこうと思います。