しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET Core MVC の Content Negotiation を活用する

ASP.NET Web API 2 でも Content Negotiation は実装されていましたが、Core MVC では進化してさらに便利な機能が追加されていたので、簡単にですが調べてみました。

Formatting Response Data — ASP.NET documentation

ドキュメントも割と揃ってきているので、実際に開発を始められそうな雰囲気がしてきましたね。

Content Negotiation が有効になる条件

ASP.NET Core MVC では Web API 2 の書き方を受け継いでいるので、アクションの戻り値をいろんな型で書けるようになっていますが、Content Negotiation が有効になるのは ObjectResult が使われる場合だけです。

ObjectResult はコントローラヘルパーの Ok や NotFound などを使った時に返される型です。

public IActionResult Get()
{
    var data = new ResponseData { Name = "kazuakix", Age = 50 };

    // 戻り値の型は OkObjectResult になる
    return Ok(data);
}

Ok や NotFound などのコントローラヘルパーを使う方法は、HTTP のステータスコードと一対一になっているため、分かりやすいコードが書けるので個人的にはかなりおすすめです。

例外としては任意のオブジェクト型を返すアクションの場合は、自動的に ObjectResult でラップされるため Content Negotiation が有効になります。

public ResponseData Get()
{
    var data = new ResponseData { Name = "kazuakix", Age = 50 };

    // ObjectResult で自動的にラップされるので OK
    return data;
}

これでリクエストの Accept ヘッダーの値によって、自動的にレスポンスの Content-Type が選択されるようになります。とは言え、このままだと JSON しか返らないので、この後で Formatter を追加します。

Formatter を追加する

Core MVC ではデフォルトで JSON の Formatter が参照されていますが、公式のパッケージとして XML の Formatter も提供されているので、JSON と XML の両方を扱えるようにしておきます。

MVC 5 では JsonValueProviderFactory などの ValueProvider を拡張して各シリアライズ形式に対応していましたが、Core MVC では Input / Output Formatter という新しい機能が追加されました。

f:id:shiba-yan:20160712032941p:plain

ASP.NET Core ではパッケージ名の規約が分かりやすくなっているので、NuGet で Formatters.Xml と検索すると簡単にヒットします。

当然ながら今後 MessagePack や Protocol Buffers を扱う Formatter が出てきたとしても、NuGet からインストールするだけになるはずです。

f:id:shiba-yan:20160712032749p:plain

パッケージをインストールすると拡張メソッドが追加されるので、AddMvc の呼び出し後にメソッドチェーンで AddXmlSerializerFormatters を呼び出すだけで準備は完了します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
            .AddXmlSerializerFormatters();
}

ちなみに、Roslyn CodeFix の謎テクノロジーによって拡張メソッドを先に書くと、該当の NuGet パッケージのインストールを行うことも出来るようになっています。

f:id:shiba-yan:20160712032637p:plain

コードをコピーして持ってきた場合には、こっちの方が簡単に追加できると思います。

リクエストの Content-Type を制限する

これまでの MVC や Web API では Content-Type をあまり気にせずに、とりあえずモデルバインダが処理をしてきましたが、Core MVC では Consumes 属性を使って、許可する Content-Type を簡単に制限できます。

[Consumes("application/json")]
public IActionResult Post([FromBody] RequestData data)
{
    return Ok();
}

この場合、リクエストの Content-Type が application/json の場合のみアクションが実行され、それ以外の場合は 415 Unsupported Media Type が返されます。

意図しないフォーマットからのバインディングを防ぐ事が出来ます。

レスポンスの Content-Type を強制する

今度は Consumes とは逆になりまうが、Produces 属性を使うとレスポンスの Content-Type を指定することが出来ます。Content Negotiation を無効にするような設定です。

[Produces("application/xml")]
public IActionResult Get()
{
    var data = new ResponseData { Name = "kazuakix", Age = 50 };

    return Ok(data);
}

この例では Accept ヘッダーの値に依存せず、常に XML としてレスポンスを返すようになります。

動的にレスポンスの Content-Type を切り替える

個人的に Core MVC でも気に入っている機能が、この FormatFilter です。簡単に説明をすると RouteData やクエリ文字列に format というパラメータがあれば、そのフォーマットを選択して返す機能です。

Twitter API のように拡張子でフォーマットを切り替える処理が簡単に実装できるようになっています。format で指定する値は FormatterMappings で自由に定義できるので、独自フォーマットにも対応できます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.FormatterMappings.SetMediaTypeMappingForFormat("xml", "application/xml");
    }).AddXmlSerializerFormatters();
}

実際に使う場合には FormatFilter をコントローラかアクションに付けて、ルーティングパラメータに format を含めるようにします。これだけで拡張子のようにフォーマットを指定できます。

[FormatFilter]
[Route("api/[controller]")]
public class ValuesController : Controller
{
    [HttpGet("{name}.{format?}")]
    public IActionResult Get(string name)
    {
        var data = new ResponseData { Name = name, Age = 50 };

        return Ok(data);
    }
}

ちなみにこの書き方で 3 種類の URL に対応できます。未定義の値の場合は 404 が返るようになってます。

/api/values/kazuakix
/api/values/kazuakix.xml
/api/values/kazuakix?format=json

Content Negotiation 1 つを取り上げてみてもこれぐらいの機能があるので、思ったより全体を追いかけていくのは大変ですが、MVC 5 の頃にあった不満点がかなり解消されているので良い感じです。