ぷろじぇくと、みすじら。

ASP.NET MVC Core 2.0で作ったAPIでXMLなどの形式を返したい

Created at:

ASP.NET MVC Coreはアクションの戻り値が IActionResult ではない場合、デフォルトでJSONをシリアライズしたものを返します。しかし場合によってはJSONだけでなくXMLで返したいという場合もあるかと思います。

そのようなニーズにこたえるべくフォーマッターを登録することで様々な形式でレスポンスを返すことができる仕組みがあります。ここではXMLで返す方法を説明します。

まずXML用のフォーマッターはNuGetから Microsoft.AspNetCore.Mvc.Formatters.Xml パッケージをインストールできるのでインストールします。

次にConfigureServiceメソッドのASP.NET MVC Coreの設定でXMLのフォーマッターを登録します。

services.AddMvc(options =>
{
    // XMLを返すフォーマッターとそのMIME Typeと拡張子のマッピングを登録
    // XmlDataContractSerializerOutputFormatterはASP.NET Web API相当
    options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
    // options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
    options.FormatterMappings.SetMediaTypeMappingForFormat("xml", "application/xml");
});

この状態で Accept ヘッダーフィールドに application/xml を指定してリクエストするとXMLでレスポンスが返ってくるようになります。簡単ですね。

XMLシリアライザを使うときの注意

XmlDataContractSerializerOutputFormatterでシリアライズするクラスは引数なしのコンストラクタやプロパティにsetterが必要だったりするのでご注意ください。

この辺りを忘れていると406 Not Acceptableが返ってきて悩むことになります。

URLの .json/.xml のような拡張子で出し分けたい

通常は Accept によるコンテントネゴシエーションで返すフォーマットを決定すればよいのですが、URLの一部でフォーマットを指定したいというということもあります。例えば、/People/Alice.jsonとアクセスした場合はJSONで、/People/Alice.xmlであればXMLを返すといった形です。

これを行うには FormatFilter フィルター(FormatFilterAttributeクラス)をコントローラーにつけ、ルーティングに{format}を足します。例えば以下のようになります。

[FormatFilter]
[Route("[controller]/{id}.{format?}")]
public class PeopleController : Controller
{
    [HttpGet]
    public Person Get(string id)
    {
        return /* Person のインスタンスを取得 */;
    }
}

これでフォーマットが指定された場合はそれにマッチするフォーマッターが、指定されていなければデフォルトのフォーマッターが使用されます。

$ curl http://localhost:4743/People/Alice.json
{"name":"Alice","age":17}

$ curl http://localhost:4743/People/Alice.xml
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/WebApplication19.Controllers"><Age>17</Age><Name>Alice</Name></Person>

デフォルトのフォーマッターを変更する

Acceptやフォーマット名(拡張子)によってフォーマッターを決定できない場合、フォーマッターの登録順で見て行って一番最初に利用できるもの(=必ずしも一番最初ではない)がデフォルトのフォーマッターとなります。

つまり標準では JsonOutputFormatter が選択されてJSONが返されるわけですが、デフォルトをXMLにしたいという場合もあるかもしれません。そのような場合は一度 JsonOutputFormatter を抜いて、XMLのフォーマッターを登録してから再度 JsonOutputFormatter を登録します。

var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().First();
options.OutputFormatters.RemoveType<JsonOutputFormatter>();

options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());

options.OutputFormatters.Add(jsonOutputFormatter);

MessagePack-CSharpを使ってみる

折角なのでレスポンスでMessagePackを返すのも試してみましょう。まあWeb APIでMessagePackを受けたり返したりしたいシーンがどのぐらいあるのかは謎といえば謎なのですが。

C#界最速のシリアライザであるところのMessagePack-CSharpにはASP.NET MVC Core用のフォーマッターが用意されているので使うのは簡単です。

まずNuGetからMessagePackMessagePack.AspNetCoreMvcFormatterをインストールします。

インストールできたらXMLの時と同様に登録します。MessagePackのMIME Typeはapplication/x-msgpackなので拡張子をマッピングを登録する場合やリクエストのAcceptにはそれを指定してください。折角なのでInputFormatterも登録しておきます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.FormatterMappings.SetMediaTypeMappingForFormat("msgpack", "application/x-msgpack");
        options.OutputFormatters.Add(new MessagePackOutputFormatter());
        options.InputFormatters.Add(new MessagePackInputFormatter());
    });
}

あとは適当なコントローラーとデータを作ります。ここでは /People/Alice といった形でアクセスできるようなものを作ってみます。

レスポンスに使う Person クラスはMessagePackでシリアライズできるように MessagePackObject としてマークしておきます。これはMessagePack-CSharpのお約束です。

[FormatFilter]
[Route("[controller]/{id}.{format?}")]
public class PeopleController : Controller
{
    [HttpGet]
    public Person Get(string id)
    {
        switch (id)
        {
            case "Alice": return new Person("Alice", 17);
            case "Karen": return new Person("Karen", 17);
            default: return null;
        }
    }
}

[MessagePackObject]
public class Person
{
    [Key(0)]
    public string Name { get; }
    [Key(1)]
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

あとは /People/Alice.msgpack または Accept: application/x-msgpack 付きででリクエストを投げることでMessagePackで返ってくるはずです。

$ curl -H 'Accept: application/json' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000   {   "   n   a   m   e   "   :   "   A   l   i   c   e   "   ,
0000020   "   a   g   e   "   :   1   7   }
0000031

$ curl -H 'Accept: application/x-msgpack' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000 222 245   A   l   i   c   e 021
0000010

ところでMessagePackObjectとかつけるの面倒だなーMapモードでいいんだけどなーということもあるかもしれません。そんな時はContractlessStandardResolverを使用するとよいでしょう。

services.AddMvc(options =>
{
    options.FormatterMappings.SetMediaTypeMappingForFormat("msgpack", "application/x-msgpack");
    options.OutputFormatters.Add(new MessagePackOutputFormatter(ContractlessStandardResolver.Instance));
    options.InputFormatters.Add(new MessagePackInputFormatter(ContractlessStandardResolver.Instance));
});
public class Person
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}
$ curl -H 'Accept: application/x-msgpack' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000 202 244   N   a   m   e 245   A   l   i   c   e 243   A   g   e
0000020 021
0000021

このように簡単にMessagePackで返せるようになるので使えそうなときは積極的に使っていきたいですね。

余談: MvcOptions.RespectBrowserAcceptHeader とは何か

ASP.NET MVC CoreのMvcOptionsにはフォーマッターに関連したRespectBrowserAcceptHeader プロパティという設定が存在します。この設定は何をするものかというと「ブラウザのようにAcceptに*/*といったワイルドカードのメディアタイプを乗せて来た場合、デフォルトとするかどうか」です。

例えばそれぞれの設定時には以下のようになります。

つまりfalseの場合には*/*text/*のようなものを見つけると、Acceptで指定されたものを全て無視するのと同じになります。基本的にワイルドカードを指定してくるのはブラウザぐらいなのでブラウザのAcceptに敬意を払うかどうかということです。

このプロパティはデフォルトは false となっていますので、もしブラウザのようなAcceptも受け入れてその通りに判断してほしいというときは true を設定してください。