noxi雑記

.NET、Angularまわりの小ネタブログ

ASP.NET CoreのWebAPI規約を使用する

ASP.NET Core では WebAPI を実装する際に、どの HTTP ステータスが返るのかを [ProducesResponseType] 属性を使用して設定することができます。しかしながらこの機能、一つ一つの アクションメソッドに対して設定しなければならないため大変面倒でした。

ASP.NET Core 2.2 ではこの機能が強化され、アクションメソッドから返る一般的な HTTP ステータスを定義できる WebAPI 規約機能が実装されました。この WebAPI 規約機能を試してみます。


試した環境

今回のソースコードはこちらにおいてあります。

github.com

プロジェクトの作成と Swagger の導入

まずは標準設定で ASP.NET Core の WebAPI プロジェクトを作成し、 Swashbuckle を利用して Swagger を導入します。

プロジェクトの作成は Visual Studio のプロジェクトウィザードから .NET Core > ASP.NET Core Web Application を選択します。また次に表示される Web アプリの設定画面では API を選択します。

f:id:noxi515:20190105031836p:plain
新規プロジェクトウィザード

f:id:noxi515:20190105031915p:plain
Webアプリ設定

2つ目の Web アプリの設定画面で SDK のバージョンが 2.2.100 よりも小さい場合は先に .NET Core 2.2 に対応した SDK のインストールが必要です。

プロジェクトを作成したら Swashbuckle を NuGet からインストールします。プロジェクトの NuGet 管理画面を開き Swashbuckle.AspNetCore を検索してインストールします。

f:id:noxi515:20190105032651p:plain
NuGetからSwashbuckleをインストール

Swashbuckle をインストールしたら Swagger を使用するために Startup.cs を修正します。 ConfigureServices メソッド内に services.AddSwaggerGen() を追加します。また個人的好みなのですが生成される URL を小文字としたいので services.AddRouting() も併せて追加します。

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddRouting(options => options.LowercaseUrls = true);

    services.AddSwaggerGen(options =>
    {
        options.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
    });
}

ConfigureServices に追加したら次は Configure メソッド内に app.UseSwagger()app.UseSwaggerUI() を追加します。

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    });

    app.UseMvc();
}

この修正を行いアプリを起動し、 https://localhost:5001/swagger にアクセスすると SwaggerUI が表示されます。

f:id:noxi515:20190105041001p:plain
初期設定時のSwaggerUI

WebAPI 規約の適用

Swagger が表示されたら次に新機能の WebAPI 規約を適用してみます。

docs.microsoft.com

ドキュメントに記載されている適用方法は

  • アクションへの適用(アクションメソッドに属性を設定)
  • コントローラーへの適用(コントローラークラスに属性を設定)
  • アプリ全体への適用(アセンブリに属性を設定)

の3種類です。

アクションメソッドに WebAPI 規約を適用する

アクションメソッドに WebAPI 規約を適用するには、対象のアクションメソッドに [ApiConventionMethod] 属性を追加します。この属性の最初の引数には WebAPI 規約が記述されているクラスを、2番目の引数にはどの規約を使用するか、メソッドを選択します。

ASP.NET Core の WebAPI 標準規約として DefaultApiConventions が実装されていますので、試しにこれを適用してみます。例えば ValuesControllerPost メソッドに対して WebAPI 規約を適用するには、最初の引数に typeof(DefaultApiConventions) を、2番目の引数には Post に対応する規約を適用したいので nameof(DefaultApiConventions.Post) を指定します。

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    ...

    // POST api/values
    [HttpPost]
    [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
    public void Post([FromBody] string value)
    {
    }

    ...
}

この変更後にアプリを起動して SwaggerUI から確認すると、 Post のレスポンスが大きく変わります。

f:id:noxi515:20190105044110p:plain
WebAPI規約をメソッドに適用した結果

POST のアクションメソッドに WebAPI 規約が適用され、 [ProducesResponseType] を設定しなくても API 呼び出し結果として HTTP ステータスコードの201または400が返却されることがドキュメント化されました。
WebAPI 規約を使用すると個々のアクションメソッドに [ProducesResponseType] を返却する HTTP ステータスコードの数だけ記述するよりも確実に、漏れなく設定できそうな気がします。

コントローラーに WebAPI 規約を使用する

次にコントローラークラスに WebAPI 規約を使用した時の挙動を確認してみます。コントローラークラスに使用するにはコントローラークラスに [ApiConventionType] 属性を設定します。設定する際の引数は規約が記述されている静的クラスです。

ValuesControllerASP.NET Core 標準 WebAPI 規約の DefaultApiConventions を設定してみます。

[Route("api/[controller]")]
[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
public class ValuesController : ControllerBase
{
    ...
}

この状態でアプリを起動して Swagger を表示すると、スクリーンショットは省略しますが、先ほど個別にアクションメソッドに WebAPI 規約を設定した Post 以外にも Put や Delete メソッドにも HTTP ステータスコードの情報が増えていることが確認できます。

個々のアクションメソッドに対して適用するのは面倒で漏れも心配なため、一括で設定できるのはとても頼りになります。

アプリ全体に WebAPI 規約を適用する

最後に Web アプリ全体に対して WebAPI 規約を適用してみます。アプリ全体に対して使用するにはアセンブリ[ApiConventionType] 属性を設定します。

アプリ全体に DefaultApiConventions を設定するには Startup.cs を開き、次のようにアセンブリに対して属性を設定します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Swagger;

[assembly:ApiConventionType(typeof(DefaultApiConventions))]

namespace WebApiApp
{
    public class Startup
    {
        ...
    {
}

この状態でアプリを起動すると、アプリに含まれる全てのコントローラーに対して WebAPI 規約が使用されます。アクションメソッドやコントローラー個々に設定するのは特別な場合として、通常はアプリ全体に対して適用しておくのが良いでしょう。

WebAPI 規約を作成する

これまでは WebAPI 規約を適用する方法について試してみましたが、次に ASP.NET Core 標準のものでは無く独自の WebAPI 規約を作成してみます。

docs.microsoft.com

WebAPI 規約の実装は

  • 静的クラスに静的メソッドを実装する
  • その静的メソッドに [ProducesResponseType] 属性を付与する

これを満たすものであれば良いようです。

アクションメソッドに適用するための WebAPI 規約

まずは最もシンプルな、静的メソッドに [ProducesResponseType] を付与しただけのものを実装してみます。適用先は新規に CustomController を追加して行います。

using Microsoft.AspNetCore.Mvc;

namespace WebApiApp.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CustomController : ControllerBase
    {
        /// <summary>
        /// 新規でモデルを登録するアクションメソッド<br />
        /// `POST api/custom`
        /// </summary>
        [HttpPost]
        public ActionResult<CustomModel> Create([FromBody] CustomModel model)
        {
            return Created($"api/custom/{model.Id}", model);
        }
    }

    /// <summary>
    /// 対象のモデル
    /// </summary>
    public class CustomModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

とてもシンプルに実装したコントローラーです。 api/customCustomModel を POST すると、登録されたモデルと URL が 201 ステータスで返ってきます。 Swagger上で見るとこんな感じになっています。

f:id:noxi515:20190105171019p:plain
追加したCustomControllerのCreateメソッド

先ほどアプリ全体に対して DefaultApiConventions を適用しているため、このコントローラー、アクションメソッドには何も設定していませんが 201 と 400 がレスポンスとして返るようになっていました。
これで十分な気もしますが、アプリに認証とロールの機能が存在していて未認証の場合は 401 、ロールが不足している場合は 403 を返したいとします。そんなときは独自の WebAPI 規約を実装します。

プロジェクトのルート階層に CustomApiConventions クラスを作成し、試しに 201/400/401/403 を返すような規約を実装してみます。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace WebApiApp
{
    /// <summary>
    /// 独自のWebAPI規約クラス
    /// </summary>
    public static class CustomApiConventions
    {
        /// <summary>
        /// 作成するメソッドのWebAPI規約
        /// </summary>
        [ProducesResponseType(StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
        [ProducesResponseType(StatusCodes.Status403Forbidden)]
        [ProducesDefaultResponseType]
        public static void Create()
        {
        }
    }
}

これを先ほど作成した CustomControllerCreate メソッドに対して適用します。

/// <summary>
/// 新規でモデルを登録するアクションメソッド<br />
/// `POST api/custom`
/// </summary>
[HttpPost]
[ApiConventionMethod(typeof(CustomApiConventions), nameof(CustomApiConventions.Create))]
public ActionResult<CustomModel> Create([FromBody] CustomModel model)
{
    return Created($"api/custom/{model.Id}", model);
}

この結果、 Swagger ではこのように表示されます。期待通りの結果が得られました。長いので見切れてはいますが、、、

f:id:noxi515:20190105173157p:plain
201/400/401/403を返すWebAPI規約の結果

コントローラーやアプリ全体に対して適用できる WebAPI 規約を実装する

先の実装は CustomControllerCreate メソッドに対して直接 WebAPI 規約を適用する方法をとりました。しかし全てのアクションメソッドに対して個別に設定するのはあまり現実的では無いため、コントローラーやアプリ全体に対して適応できるような実装をしてみたいと思います。

コントローラーやアプリ全体に対して WebAPI 規約を適用するには次の2つの方法があります。

命名規則による自動適用を追加するには [ApiConventionNameMatch] を WebAPI の静的メソッドに付与します。引数は ApiConventionNameMatchBehavior 列挙型で、

  • Exact: メソッド名やパラメーター名の完全一致
  • Prefix: メソッド名やパラメーター名の前方一致
  • Suffix: メソッド名やパラメーター名の後方一致
  • Any: メソッド名やパラメーター名に関係無く何にでも一致

の4通りがあります。

Prefix は主にメソッド名に使用することになると思います。 MS のドキュメントにもありますが Find という名前の規約に対して Prefix を使用すると FindByIdFindPet といった目的別のアクションメソッドに対して適用することができます。また非同期メソッドは XxxAsync と Async を後ろに付けることが多いため、 FindAsync にも適用されます。
Suffix は主にパラメーター名に使用するでしょう。パラメーター名はいわゆる変数なので、パラメーターとして受けるクラスの名前にも影響されます。 model に対して Suffix を使用することで modelsampleModel といった名前に対応できます。
また Any も主にパラメーター名に使用するでしょう。アクションメソッドにパラメーターは必要だが名前に縛りをかけたくない場合に有効です。
Exact はいまいち使い道がピンと来ません。。

先ほど実装した CustomApiConventionsCreate WebAPI 規約に命名規則による前方一致の自動適用を追加します。

/// <summary>
/// 作成するメソッドのWebAPI規約
/// </summary>
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Create()
{
}

そして CustomControllerCreate メソッドから個別に設定した WebAPI 規約を削除し、コントローラーに対して WebAPI 規約を適用します。

[Route("api/[controller]")]
[ApiController]
[Produces("application/json")]
[ApiConventionType(typeof(CustomApiConventions))]
public class CustomController : ControllerBase
{
    /// <summary>
    /// 新規でモデルを登録するアクションメソッド<br />
    /// `POST api/custom`
    /// </summary>
    [HttpPost]
    public ActionResult<CustomModel> Create([FromBody] CustomModel model)
    {
        return Created($"api/custom/{model.Id}", model);
    }
}

Swagger で確認すると、、、適用されていません、、、

f:id:noxi515:20190105181958p:plain
ApiConventionNameMatchだけを追加した状態

適用されなかった原因はアクションメソッドのパラメーターにあります。 Create アクションメソッドは model というパラメーターが存在していたため、パラメーターを全く指定していない規約には引っかからなかったようです。

では WebAPI 規約にパラメーターを追加します。パラメーターの型は特に指定をしませんが、命名規則だけ後方一致条件を設定します。なおパラメーターの型の条件は指定した型にキャスト可能な AssignableFrom か、何でもよい Any の二択です。

/// <summary>
/// 作成するメソッドのWebAPI規約
/// </summary>
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Create(
    [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
    [ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
    object model)
{
}

スクリーンショットは省略しますがここまで実装してようやく、アクションメソッドに対して個別に設定したのと同じ状態になることが Swagger から確認できます。

まとめ

ASP.NET Core 2.2 で実装された WebAPI 規約を使用すると、アプリ全体、コントローラー、アクションメソッドに対して既定のステータスコードとレスポンスモデルの型を指定することができます。個人的な体験ですが、アクションメソッドに対して個々に [ProducesResponseType] を追加するとどうしてもコピペされて嘘や不足が多くなってしまうため、一括で指定できる WebAPI 規約を使用することで過不足が少ない、実装にあった API ドキュメントが出力できるでしょう。