ASP.NET Core で Swashbuckle

はじめに

Swashbuckle はまだプレリリースの段階ではあるけど、 ASP.NET Core にも対応しているみたい。

github.com

Swashbuckle を使えば、Web API の実装から Swagger Definitions を生成できる。

温かみのある手作業で、 Web API の Swagger Definitions を書いていたけど、 もう限界なので試してみることにした。

プロジェクト新規作成

今回は Web API だけ使うので、 ASP.NET Core の Web API プロジェクトを作成しておく。

Swashbuckle をインストール

project.jsondependencies

"Swashbuckle": "6.0.0-beta902"

を追加して、パッケージをリストア。

Swagger を使うように Startup を修正

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace SwaggerSample
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();

            services.AddSwaggerGen();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseMvc();

            // Swagger Definitions を出力できるようにする
            app.UseSwagger();

            // Swagger UI を表示できるようにする
            app.UseSwaggerUi();
        }
    }
}

モデルを定義

Web API で返すモデルだけでなく、 入力用のモデルと、 エラー用のモデルも定義してみた。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace SwaggerSample.Models
{
    public class Item
    {
        public string Id { get; set; }

        public string Title { get; set; }

        public string Description { get; set; }
    }

    public class ItemInputModel
    {
        [Required]
        public string Title { get; set; }

        [Required]
        public string Description { get; set; }
    }

    public class ErrorModel
    {
        public Dictionary<string, string> Errors { get; } = new Dictionary<string, string>();
    }
}

コントローラーを作成

プロジェクトを新規作成すると ValuesController ってのが作られるけど、 そいつは削除してしまって、 ItemsController を作成。

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SwaggerSample.Models;

namespace SwaggerSample.Controllers
{
    [Route("api/[controller]")]
    public class ItemsController : Controller
    {
        private static readonly ConcurrentDictionary<string, Item> _items
            = new ConcurrentDictionary<string, Item>();

        [HttpGet]
        [ProducesResponseType(typeof(IEnumerable<Item>), 200)]
        public IActionResult Get()
        {
            return Ok(_items.Values);
        }

        [HttpGet("{id}")]
        [ProducesResponseType(typeof(Item), 200)]
        [ProducesResponseType(typeof(ErrorModel), 404)]
        public IActionResult Get(string id)
        {
            Item item;
            if (_items.TryGetValue(id, out item) == false)
            {
                return NotFound(new ErrorModel
                {
                    Errors =
                    {
                        [ "id"]= $"ID = {id} のアイテムは存在しません。"
                    }
                });
            }
            else
            {
                return Ok(item);
            }
        }

        [HttpPost]
        [ProducesResponseType(typeof(Item), 200)]
        [ProducesResponseType(typeof(ErrorModel), 400)]
        public IActionResult Post([FromBody]ItemInputModel inputModel)
        {
            if (ModelState.IsValid == false)
            {
                return BadRequest(CreateValidationError());
            }

            var item = new Item
            {
                Id = Guid.NewGuid().ToString(),
                Title = inputModel.Title,
                Description = inputModel.Description,
            };
            _items.TryAdd(item.Id, item);
            return Ok(item);
        }

        [HttpPut("{id}")]
        [ProducesResponseType(typeof(Item), 200)]
        [ProducesResponseType(typeof(ErrorModel), 400)]
        [ProducesResponseType(typeof(ErrorModel), 404)]
        public IActionResult Put(string id, [FromBody]ItemInputModel inputModel)
        {
            Item item;
            if (_items.TryGetValue(id, out item) == false)
            {
                return NotFound(new ErrorModel
                {
                    Errors =
                    {
                        ["id"] = $"ID = {id} のアイテムは存在しません。"
                    }
                });
            }

            if (ModelState.IsValid == false)
            {
                return BadRequest(CreateValidationError());
            }

            item.Title = inputModel.Title;
            item.Description = inputModel.Description;
            _items[id] = item;
            return Ok(item);
        }

        [HttpDelete("{id}")]
        [ProducesResponseType(typeof(Item), 200)]
        [ProducesResponseType(typeof(ErrorModel), 404)]
        public IActionResult Delete(string id)
        {
            Item item;
            if (_items.TryRemove(id, out item))
            {
                return Ok(item);
            }
            else
            {
                return NotFound(new ErrorModel
                {
                    Errors =
                    {
                        ["id"] = $"ID = {id} のアイテムは存在しません。"
                    }
                });
            }
        }

        private ErrorModel CreateValidationError()
        {
            var error = new ErrorModel();
            foreach (var pair in ModelState)
            {
                error.Errors.Add(
                    pair.Key,
                    string.Join(
                        Environment.NewLine,
                        pair.Value.Errors.Select(e => e.ErrorMessage)
                    )
                );
            }
            return error;
        }
    }
}

戻り値の型を IActionResult にする場合、 ProducesResponseTypeAttribute を使って、 Swashbuckle にモデルの型を教えてあげないといけない。 NotFound とか BadRequest とか扱いたいなら必須。

Swagger UI を表示

Visual Studio から実行したいところだけど、マシンパワーが貧弱で起動が遅いので、

> dotnet run

でサービスを実行。

ブラウザで localhost:5000/swagger/ui にアクセスしてみる。

f:id:griefworker:20161007165806p:plain

Swagger UI が表示された。 ここから Web API を実際に呼び出すことができて便利。

Swagger Definitions を表示

Swagger UI に Swagger Definitions の URL が表示されているので、 ブラウザでアクセスしてみる。

f:id:griefworker:20161007165821p:plain

Swagger Definitions の JSON が表示された。 モデルの情報もちゃんと含まれている。

おわりに

Swagger UI は Web APIデバッグに超便利だった。 Swagger Definitions も生成してくれるので、 Swagger Codegen に渡せばクライアントのソースコードまで生成できる。

Swagger Codegen のインストールが面倒なら、 公式がホストしている Swagger Editor に貼り付けて生成する手もある。

Swagger Definitions を YAML でゴリゴリ書いていたけど、 Web API からの自動生成を経験してしまったので、 もう戻れないな。